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';
|
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
|
plugin's tables by that prefix, so an unprefixed one is never cleaned up and shows forever in the
|
||||||
orphan list.
|
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
|
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
|
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
|
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
|
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
|
`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.
|
* 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`.
|
* at it. See `artefactPath`.
|
||||||
*/
|
*/
|
||||||
export const oasBuilds = pgTable(
|
export const kioskBuilds = pgTable(
|
||||||
'oas_builds',
|
'kiosk_builds',
|
||||||
{
|
{
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
/** The directory name in the monorepo — `chat`. Not what the person sees. */
|
/** 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. */
|
/** How many devices the signing profile covers. Null for anything not ad-hoc signed. */
|
||||||
deviceCount: integer('device_count'),
|
deviceCount: integer('device_count'),
|
||||||
sizeBytes: bigint('size_bytes', { mode: 'number' }).notNull(),
|
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(),
|
artefactPath: text('artefact_path').notNull(),
|
||||||
/** Survives retention. Replaces the `.keep` file the standalone service used. */
|
/** Survives retention. Replaces the `.keep` file the standalone service used. */
|
||||||
pinned: boolean('pinned').notNull().default(false),
|
pinned: boolean('pinned').notNull().default(false),
|
||||||
@@ -91,9 +91,9 @@ export const oasBuilds = pgTable(
|
|||||||
publishedAt: timestamp('published_at', { withTimezone: true }).notNull().defaultNow(),
|
publishedAt: timestamp('published_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
uniqueIndex('uq_oas_builds_app_platform_version').on(table.appName, table.platform, table.version),
|
uniqueIndex('uq_kiosk_builds_app_platform_version').on(table.appName, table.platform, table.version),
|
||||||
check('ck_oas_builds_platform', oneOf(table.platform, PLATFORMS)),
|
check('ck_kiosk_builds_platform', oneOf(table.platform, PLATFORMS)),
|
||||||
check('ck_oas_builds_signing', sql`${table.signing} IS NULL OR ${oneOf(table.signing, SIGNINGS)}`),
|
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
|
* 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.
|
* no such thing because nothing ever re-displays them.
|
||||||
*/
|
*/
|
||||||
export const oasUserKeys = pgTable(
|
export const kioskUserKeys = pgTable(
|
||||||
'oas_user_keys',
|
'kiosk_user_keys',
|
||||||
{
|
{
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
userId: integer('user_id')
|
userId: integer('user_id')
|
||||||
@@ -118,14 +118,14 @@ export const oasUserKeys = pgTable(
|
|||||||
.references(() => users.id, { onDelete: 'cascade' }),
|
.references(() => users.id, { onDelete: 'cascade' }),
|
||||||
/** SHA-256 of the key. Never the key itself. */
|
/** SHA-256 of the key. Never the key itself. */
|
||||||
keyHash: text('key_hash').notNull(),
|
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(),
|
keyEncrypted: text('key_encrypted').notNull(),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
rotatedAt: timestamp('rotated_at', { withTimezone: true }),
|
rotatedAt: timestamp('rotated_at', { withTimezone: true }),
|
||||||
/** Debounced to once a minute on write, the way `api_keys` does it. */
|
/** Debounced to once a minute on write, the way `api_keys` does it. */
|
||||||
lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
|
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
|
* 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".
|
* get three devices called "iOS Device".
|
||||||
*/
|
*/
|
||||||
export const oasDevices = pgTable(
|
export const kioskDevices = pgTable(
|
||||||
'oas_devices',
|
'kiosk_devices',
|
||||||
{
|
{
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
userId: integer('user_id')
|
userId: integer('user_id')
|
||||||
@@ -162,9 +162,9 @@ export const oasDevices = pgTable(
|
|||||||
(table) => [
|
(table) => [
|
||||||
// Partial: many devices may sit at `udid IS NULL` while pending, but a captured identifier is one
|
// 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.
|
// physical device and must not be claimable twice.
|
||||||
uniqueIndex('uq_oas_devices_udid').on(table.udid).where(sql`${table.udid} IS NOT NULL`),
|
uniqueIndex('uq_kiosk_devices_udid').on(table.udid).where(sql`${table.udid} IS NOT NULL`),
|
||||||
check('ck_oas_devices_status', oneOf(table.status, DEVICE_STATUSES)),
|
check('ck_kiosk_devices_status', oneOf(table.status, DEVICE_STATUSES)),
|
||||||
check('ck_oas_devices_kind', sql`${table.kind} IS NULL OR ${oneOf(table.kind, DEVICE_KINDS)}`),
|
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
|
* 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".
|
* 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(),
|
id: serial('id').primaryKey(),
|
||||||
userId: integer('user_id')
|
userId: integer('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: 'cascade' }),
|
.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')
|
buildId: integer('build_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => oasBuilds.id, { onDelete: 'cascade' }),
|
.references(() => kioskBuilds.id, { onDelete: 'cascade' }),
|
||||||
installedAt: timestamp('installed_at', { withTimezone: true }).notNull().defaultNow(),
|
installedAt: timestamp('installed_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|||||||
+9
-6
@@ -1,21 +1,24 @@
|
|||||||
import type { PluginManifest } from '@@/plugins/manifest';
|
import type { PluginManifest } from '@@/plugins/manifest';
|
||||||
|
|
||||||
// OAS — Officer Application Services. Build, sign and deliver iOS and Android apps to registered
|
// Officer Kiosk — build, sign and deliver iOS and Android apps to your own registered devices.
|
||||||
// devices: what EAS does, self-hosted.
|
// 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
|
// 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
|
// 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.
|
// 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 = {
|
export const manifest: PluginManifest = {
|
||||||
publisher: 'officerdev',
|
publisher: 'officerdev',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
platform: '>=1.0.0',
|
platform: '>=1.0.0',
|
||||||
|
|
||||||
label: 'OAS',
|
label: 'Kiosk',
|
||||||
summary: 'Build, sign and install iOS and Android apps on your own devices',
|
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',
|
icon: 'Smartphone',
|
||||||
color: '#a78bfa',
|
color: '#a78bfa',
|
||||||
|
|
||||||
@@ -24,7 +27,7 @@ export const manifest: PluginManifest = {
|
|||||||
// devices) are enforced inside the router with `isSuperAdmin`, not by a second permission.
|
// devices) are enforced inside the router with `isSuperAdmin`, not by a second permission.
|
||||||
permissions: [
|
permissions: [
|
||||||
{
|
{
|
||||||
key: 'oas',
|
key: 'kiosk',
|
||||||
label: 'App install',
|
label: 'App install',
|
||||||
description: 'Install Officer apps on your own devices',
|
description: 'Install Officer apps on your own devices',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user