From 5374d405537091a6fecfc3fc13f8c643deacf6f2 Mon Sep 17 00:00:00 2001 From: Claude Opus 5 Date: Tue, 18 Aug 2026 01:42:37 +0000 Subject: [PATCH] rename: OAS becomes Kiosk, everywhere officer kiosk is the product name; OAS was a placeholder that mimicked expo's application services and is gone. the rename is total because nothing has shipped: appName, permission key, all four table prefixes, every index and check constraint name, the exported drizzle symbols, and the secret-store purpose. no rows exist anywhere, so there is no migration and no compatibility window - which is exactly why doing it now cost nothing and doing it after the first publish would have cost a manual migration on every install. the one remaining mention of OAS is dated history in the manifest, naming what this was ported from. --- db/schema.ts | 42 +++++++++++++++++++++--------------------- manifest.ts | 15 +++++++++------ 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/db/schema.ts b/db/schema.ts index 3e983a6..7848e07 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -13,7 +13,7 @@ import { import { users } from 'officerdb/auth/schema'; /* - Every table here is prefixed `oas_`, and that is load-bearing rather than tidy: `purge` finds a + Every table here is prefixed `kiosk_`, and that is load-bearing rather than tidy: `purge` finds a plugin's tables by that prefix, so an unprefixed one is never cleaned up and shows forever in the orphan list. @@ -21,7 +21,7 @@ import { users } from 'officerdb/auth/schema'; There are no migrations. Install runs a generate-and-create against this schema and swallows "already exists", so a column added in a later version is SILENTLY ABSENT on every machine that installed an - earlier one. Email survived that because its table had never held a row; `oas_builds` holds real rows + earlier one. Email survived that because its table had never held a row; `kiosk_builds` holds real rows from the first publish. So the columns below are deliberately wider than today's needs, and the legal values are declared as CHECK constraints rather than left as free text — the house rule from `databases/CLAUDE.md`, which exists because rows for deleted providers kept their credentials long @@ -43,11 +43,11 @@ const oneOf = (column: unknown, values: readonly string[]) => /** * A published build. One row per app, per platform, per version. * - * The artefact itself is NOT here — it is a ~20MB file under the OAS artefact root, and this row points + * The artefact itself is NOT here — it is a ~20MB file under the Kiosk artefact root, and this row points * at it. See `artefactPath`. */ -export const oasBuilds = pgTable( - 'oas_builds', +export const kioskBuilds = pgTable( + 'kiosk_builds', { id: serial('id').primaryKey(), /** The directory name in the monorepo — `chat`. Not what the person sees. */ @@ -83,7 +83,7 @@ export const oasBuilds = pgTable( /** How many devices the signing profile covers. Null for anything not ad-hoc signed. */ deviceCount: integer('device_count'), sizeBytes: bigint('size_bytes', { mode: 'number' }).notNull(), - /** Relative to the OAS artefact root, never absolute — the root moves between machines. */ + /** Relative to the Kiosk artefact root, never absolute — the root moves between machines. */ artefactPath: text('artefact_path').notNull(), /** Survives retention. Replaces the `.keep` file the standalone service used. */ pinned: boolean('pinned').notNull().default(false), @@ -91,9 +91,9 @@ export const oasBuilds = pgTable( publishedAt: timestamp('published_at', { withTimezone: true }).notNull().defaultNow(), }, (table) => [ - uniqueIndex('uq_oas_builds_app_platform_version').on(table.appName, table.platform, table.version), - check('ck_oas_builds_platform', oneOf(table.platform, PLATFORMS)), - check('ck_oas_builds_signing', sql`${table.signing} IS NULL OR ${oneOf(table.signing, SIGNINGS)}`), + uniqueIndex('uq_kiosk_builds_app_platform_version').on(table.appName, table.platform, table.version), + check('ck_kiosk_builds_platform', oneOf(table.platform, PLATFORMS)), + check('ck_kiosk_builds_signing', sql`${table.signing} IS NULL OR ${oneOf(table.signing, SIGNINGS)}`), ], ); @@ -109,8 +109,8 @@ export const oasBuilds = pgTable( * back in order to build an install URL, which a hash cannot do — the platform's own `api_keys` needs * no such thing because nothing ever re-displays them. */ -export const oasUserKeys = pgTable( - 'oas_user_keys', +export const kioskUserKeys = pgTable( + 'kiosk_user_keys', { id: serial('id').primaryKey(), userId: integer('user_id') @@ -118,14 +118,14 @@ export const oasUserKeys = pgTable( .references(() => users.id, { onDelete: 'cascade' }), /** SHA-256 of the key. Never the key itself. */ keyHash: text('key_hash').notNull(), - /** `encryptSecret('oas', key)` — readable back, unlike the hash. */ + /** `encryptSecret('kiosk', key)` — readable back, unlike the hash. */ keyEncrypted: text('key_encrypted').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), rotatedAt: timestamp('rotated_at', { withTimezone: true }), /** Debounced to once a minute on write, the way `api_keys` does it. */ lastUsedAt: timestamp('last_used_at', { withTimezone: true }), }, - (table) => [uniqueIndex('uq_oas_user_keys_user').on(table.userId)], + (table) => [uniqueIndex('uq_kiosk_user_keys_user').on(table.userId)], ); /** @@ -136,8 +136,8 @@ export const oasUserKeys = pgTable( * them together, which is why a name cannot end up on the wrong device — asking afterwards is how you * get three devices called "iOS Device". */ -export const oasDevices = pgTable( - 'oas_devices', +export const kioskDevices = pgTable( + 'kiosk_devices', { id: serial('id').primaryKey(), userId: integer('user_id') @@ -162,9 +162,9 @@ export const oasDevices = pgTable( (table) => [ // Partial: many devices may sit at `udid IS NULL` while pending, but a captured identifier is one // physical device and must not be claimable twice. - uniqueIndex('uq_oas_devices_udid').on(table.udid).where(sql`${table.udid} IS NOT NULL`), - check('ck_oas_devices_status', oneOf(table.status, DEVICE_STATUSES)), - check('ck_oas_devices_kind', sql`${table.kind} IS NULL OR ${oneOf(table.kind, DEVICE_KINDS)}`), + uniqueIndex('uq_kiosk_devices_udid').on(table.udid).where(sql`${table.udid} IS NOT NULL`), + check('ck_kiosk_devices_status', oneOf(table.status, DEVICE_STATUSES)), + check('ck_kiosk_devices_kind', sql`${table.kind} IS NULL OR ${oneOf(table.kind, DEVICE_KINDS)}`), ], ); @@ -175,14 +175,14 @@ export const oasDevices = pgTable( * handed to the OS and the server never learns how it went. So this is best-effort by construction, and * an absent row means "not reported", never "not installed". */ -export const oasInstalls = pgTable('oas_installs', { +export const kioskInstalls = pgTable('kiosk_installs', { id: serial('id').primaryKey(), userId: integer('user_id') .notNull() .references(() => users.id, { onDelete: 'cascade' }), - deviceId: integer('device_id').references(() => oasDevices.id, { onDelete: 'set null' }), + deviceId: integer('device_id').references(() => kioskDevices.id, { onDelete: 'set null' }), buildId: integer('build_id') .notNull() - .references(() => oasBuilds.id, { onDelete: 'cascade' }), + .references(() => kioskBuilds.id, { onDelete: 'cascade' }), installedAt: timestamp('installed_at', { withTimezone: true }).notNull().defaultNow(), }); diff --git a/manifest.ts b/manifest.ts index 799e966..4b4a928 100644 --- a/manifest.ts +++ b/manifest.ts @@ -1,21 +1,24 @@ import type { PluginManifest } from '@@/plugins/manifest'; -// OAS — Officer Application Services. Build, sign and deliver iOS and Android apps to registered -// devices: what EAS does, self-hosted. +// Officer Kiosk — build, sign and deliver iOS and Android apps to your own registered devices. +// What EAS does, self-hosted. // -// Ported from the standalone Bun/Hono service in `officer-oas/`, whose access control was an invite key +// Ported from the standalone Bun/Hono service that was called OAS, whose access control was an invite key // held in a browser's local storage. The rule we actually want is "the people already registered on the // platform", which is the thing the platform already knows how to enforce — so the key stops being // something a person holds and becomes something an account has. // -// `appName` is the directory name: `oas`. +// `appName` is the directory name: `kiosk`. export const manifest: PluginManifest = { publisher: 'officerdev', version: '1.0.0', platform: '>=1.0.0', - label: 'OAS', + label: 'Kiosk', summary: 'Build, sign and install iOS and Android apps on your own devices', + // A lucide glyph until the real artwork lands. Kiosk already has an icon and a splash screen drawn — + // drop the icon at `assets/icon.png` (served as `/plugins/kiosk/icon.png`) and the tile renders the + // artwork instead, with `tile: 'bare'` in the catalogue entry so nothing paints a square behind it. icon: 'Smartphone', color: '#a78bfa', @@ -24,7 +27,7 @@ export const manifest: PluginManifest = { // devices) are enforced inside the router with `isSuperAdmin`, not by a second permission. permissions: [ { - key: 'oas', + key: 'kiosk', label: 'App install', description: 'Install Officer apps on your own devices', },