import { sdk } from '../sdk' /** * toggle-signups — StartOS Package Action. * * Sets `InstanceSettings.signupsOpen` (the multi-user signup gate). * When `true`, anyone with the URL can create an account from the app's * /auth/signup page. New users start with no admin privileges and the * full curated exercise library. * * The same toggle is also available in-app at Settings -> Instance * Settings (admin only). Both write to the same singleton row, so * either path works. This StartOS action is the safety hatch for * operators who don't have a working admin login (or aren't logged in * yet on first install). * * Design notes: * - allowedStatuses: 'only-running'. The app must be running for the * write to be visible without restart, and the subcontainer needs * /data mounted writable. We don't require a stop because the * UPDATE is a single-row write that can't conflict with the * long-running Next.js server. * - Single explicit boolean input. Avoids the "I clicked it but did it * turn on or off?" ambiguity of toggle-style actions. * - The action does NOT report the current state in `getInput`. It's a * setter, not a viewer; the in-app Settings page is the dashboard. */ export const toggleSignups = sdk.Action.withInput( 'toggle-signups', async () => ({ name: 'Set new signups', description: 'Allow or disallow anyone with the URL to create a Proof of Work account on this instance. The same toggle exists at in-app Settings -> Instance Settings (admin only).', warning: 'When sign-ups are open, anyone who can reach the URL can create an account. Make sure the instance is on a network you trust (LAN, Tor, VPN) before enabling.', visibility: 'enabled', allowedStatuses: 'only-running', group: null, }), sdk.InputSpec.of({ signupsOpen: sdk.Value.toggle({ name: 'Allow new signups', description: 'On = anyone with the URL can register. Off = closed.', default: false, }), }), async () => null, async ({ effects, input }) => { const flag = input.signupsOpen ? 1 : 0 await sdk.SubContainer.withTemp( effects, { imageId: 'main' }, sdk.Mounts.of().mountVolume({ volumeId: 'main', subpath: null, mountpoint: '/data', readonly: false, }), 'toggle-signups', async (sc) => { // Defensive: make sure the table exists. The boot-time compat ALTERs // create it, but if this action runs before a first proper boot we // want it to still succeed. const sql = [ `CREATE TABLE IF NOT EXISTS InstanceSettings (`, ` id INTEGER PRIMARY KEY DEFAULT 1,`, ` signupsOpen INTEGER NOT NULL DEFAULT 0,`, ` updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP`, `);`, `INSERT OR IGNORE INTO InstanceSettings (id, signupsOpen) VALUES (1, ${flag});`, `UPDATE InstanceSettings SET signupsOpen = ${flag}, updatedAt = CURRENT_TIMESTAMP WHERE id = 1;`, `SELECT signupsOpen FROM InstanceSettings WHERE id = 1;`, ].join('\n') const res = await sc.execFail( ['sqlite3', '/data/app.db'], { input: sql }, 30_000, ) const observed = res.stdout .toString() .split('\n') .map((s) => s.trim()) .filter(Boolean) .pop() if (observed !== String(flag)) { throw new Error( `Aborting: wrote signupsOpen=${flag} but read back ${observed}. /data/app.db may be corrupt.`, ) } }, ) return { version: '1', title: input.signupsOpen ? 'Sign-ups enabled' : 'Sign-ups disabled', message: input.signupsOpen ? 'New visitors can now create accounts at /auth/signup.' : 'New sign-ups are now closed. Existing users can still sign in.', result: { type: 'group', value: [ { type: 'single', name: 'signupsOpen', description: 'Current value of InstanceSettings.signupsOpen', value: String(input.signupsOpen), copyable: false, qr: false, masked: false, }, ], }, } }, )