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.
This commit is contained in:
+21
-21
@@ -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(),
|
||||
});
|
||||
|
||||
+9
-6
@@ -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',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user