@adminiumjs/meta 0.2.2-rc.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ids.d.ts +6 -0
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js +14 -0
- package/dist/ids.js.map +1 -1
- package/dist/migrations/0014_public_surface.d.ts +40 -0
- package/dist/migrations/0014_public_surface.d.ts.map +1 -0
- package/dist/migrations/0014_public_surface.js +186 -0
- package/dist/migrations/0014_public_surface.js.map +1 -0
- package/dist/migrations/0015_connection_tenant_config.d.ts +42 -0
- package/dist/migrations/0015_connection_tenant_config.d.ts.map +1 -0
- package/dist/migrations/0015_connection_tenant_config.js +53 -0
- package/dist/migrations/0015_connection_tenant_config.js.map +1 -0
- package/dist/migrations/0016_audit_entity.d.ts +33 -0
- package/dist/migrations/0016_audit_entity.d.ts.map +1 -0
- package/dist/migrations/0016_audit_entity.js +137 -0
- package/dist/migrations/0016_audit_entity.js.map +1 -0
- package/dist/migrations/0017_surface_binding.d.ts +23 -0
- package/dist/migrations/0017_surface_binding.d.ts.map +1 -0
- package/dist/migrations/0017_surface_binding.js +37 -0
- package/dist/migrations/0017_surface_binding.js.map +1 -0
- package/dist/migrations/0018_connection_timezone_source.d.ts +44 -0
- package/dist/migrations/0018_connection_timezone_source.d.ts.map +1 -0
- package/dist/migrations/0018_connection_timezone_source.js +51 -0
- package/dist/migrations/0018_connection_timezone_source.js.map +1 -0
- package/dist/migrations/0019_connection_disabled.d.ts +38 -0
- package/dist/migrations/0019_connection_disabled.d.ts.map +1 -0
- package/dist/migrations/0019_connection_disabled.js +43 -0
- package/dist/migrations/0019_connection_disabled.js.map +1 -0
- package/dist/migrations/0020_manifests_add_on.d.ts +71 -0
- package/dist/migrations/0020_manifests_add_on.d.ts.map +1 -0
- package/dist/migrations/0020_manifests_add_on.js +111 -0
- package/dist/migrations/0020_manifests_add_on.js.map +1 -0
- package/dist/migrations/0021_add_on_credentials.d.ts +50 -0
- package/dist/migrations/0021_add_on_credentials.d.ts.map +1 -0
- package/dist/migrations/0021_add_on_credentials.js +80 -0
- package/dist/migrations/0021_add_on_credentials.js.map +1 -0
- package/dist/migrations/0022_studio_namespace.d.ts +44 -0
- package/dist/migrations/0022_studio_namespace.d.ts.map +1 -0
- package/dist/migrations/0022_studio_namespace.js +113 -0
- package/dist/migrations/0022_studio_namespace.js.map +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +18 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/relocate.d.ts +1 -1
- package/dist/relocate.js +1 -1
- package/dist/repos/audit.d.ts.map +1 -1
- package/dist/repos/audit.js +8 -2
- package/dist/repos/audit.js.map +1 -1
- package/dist/repos/connections.d.ts +62 -0
- package/dist/repos/connections.d.ts.map +1 -1
- package/dist/repos/connections.js +104 -0
- package/dist/repos/connections.js.map +1 -1
- package/dist/repos/index.d.ts +2 -0
- package/dist/repos/index.d.ts.map +1 -1
- package/dist/repos/index.js +2 -0
- package/dist/repos/index.js.map +1 -1
- package/dist/repos/manifests.d.ts +139 -0
- package/dist/repos/manifests.d.ts.map +1 -0
- package/dist/repos/manifests.js +279 -0
- package/dist/repos/manifests.js.map +1 -0
- package/dist/repos/pages.d.ts +7 -0
- package/dist/repos/pages.d.ts.map +1 -1
- package/dist/repos/pages.js +39 -8
- package/dist/repos/pages.js.map +1 -1
- package/dist/repos/public-api.d.ts +140 -0
- package/dist/repos/public-api.d.ts.map +1 -0
- package/dist/repos/public-api.js +299 -0
- package/dist/repos/public-api.js.map +1 -0
- package/dist/schema/json-payloads.d.ts +27 -2
- package/dist/schema/json-payloads.d.ts.map +1 -1
- package/dist/schema/json-payloads.js +41 -2
- package/dist/schema/json-payloads.js.map +1 -1
- package/dist/schema/settings-registry.d.ts +65 -0
- package/dist/schema/settings-registry.d.ts.map +1 -1
- package/dist/schema/settings-registry.js +183 -0
- package/dist/schema/settings-registry.js.map +1 -1
- package/dist/schema/tables.d.ts +177 -1
- package/dist/schema/tables.d.ts.map +1 -1
- package/dist/schema/tables.js +6 -0
- package/dist/schema/tables.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
/**
|
|
3
|
+
* Wave 0017 — key ↔ app-surface binding (29-app-surfaces.md D10, 29-T15).
|
|
4
|
+
*
|
|
5
|
+
* A hosted CUSTOMER surface is configured at serve time, not bake time: the
|
|
6
|
+
* surfaces plugin answers `GET /apps/<key>/customer/surface-config.json` with
|
|
7
|
+
* the newest live publishable key bound to that app, so rotating a key is
|
|
8
|
+
* Studio + reload rather than a rebuild. "Bound to that app" needs a COLUMN —
|
|
9
|
+
* before this wave, "minted per side per app" was a naming convention on
|
|
10
|
+
* `adminium_public_keys.name`, which nothing could query.
|
|
11
|
+
*
|
|
12
|
+
* `app_key` is the manifest key of the owning app (`clients`, `clinic`, …),
|
|
13
|
+
* nullable because binding is optional: a key minted for a standalone build
|
|
14
|
+
* or a third-party integration belongs to no hosted surface and stays NULL.
|
|
15
|
+
*
|
|
16
|
+
* Planned as `0016_surface_binding` (29 D10); renumbered to 0017 because
|
|
17
|
+
* plan 30's `0016_audit_entity` shipped first and the list is append-only.
|
|
18
|
+
*/
|
|
19
|
+
import { metaTable } from '../prefix.js';
|
|
20
|
+
/** Matches the discovery layout: a surface directory name, e.g. `clients`. */
|
|
21
|
+
export const PUBLIC_KEY_APP_KEY_MAX = 64;
|
|
22
|
+
export async function up(db, c) {
|
|
23
|
+
await db.schema
|
|
24
|
+
.alterTable(metaTable('public_keys'))
|
|
25
|
+
.addColumn('app_key', c.str(PUBLIC_KEY_APP_KEY_MAX))
|
|
26
|
+
.execute();
|
|
27
|
+
// (app_key, side): the config route's exact lookup — every live key for one
|
|
28
|
+
// app's side, newest picked in the query. The table is operator-scale (tens
|
|
29
|
+
// of rows), but the read is on an anonymous, unauthenticated path, so it
|
|
30
|
+
// should not scan by policy rather than by size.
|
|
31
|
+
await db.schema
|
|
32
|
+
.createIndex('ix_adminium_public_keys_app')
|
|
33
|
+
.on(metaTable('public_keys'))
|
|
34
|
+
.columns(['app_key', 'side'])
|
|
35
|
+
.execute();
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=0017_surface_binding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0017_surface_binding.js","sourceRoot":"","sources":["../../src/migrations/0017_surface_binding.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAEzC,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC,EAAmB,EAAE,CAAgB;IAC5D,MAAM,EAAE,CAAC,MAAM;SACZ,UAAU,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;SACpC,SAAS,CAAC,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;SACnD,OAAO,EAAE,CAAC;IACb,4EAA4E;IAC5E,4EAA4E;IAC5E,yEAAyE;IACzE,iDAAiD;IACjD,MAAM,EAAE,CAAC,MAAM;SACZ,WAAW,CAAC,6BAA6B,CAAC;SAC1C,EAAE,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;SAC5B,OAAO,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;SAC5B,OAAO,EAAE,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 0018 — `adminium_connections.timezoneSource`: WHO chose the zone.
|
|
3
|
+
*
|
|
4
|
+
* ─── What changed under 0015 ───────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 0015 added `timezone` and argued for no default at all: "a wrong zone is
|
|
7
|
+
* worse than a missing one, because it looks like data". `connectionsRepo`
|
|
8
|
+
* later seeded new rows from the SERVER's zone anyway, and for a good reason —
|
|
9
|
+
* a null zone made a hosted app surface refuse to render, so the cost of the
|
|
10
|
+
* missing value landed as total unavailability rather than an hour's drift.
|
|
11
|
+
*
|
|
12
|
+
* Both positions are right about their own failure, and the column below is
|
|
13
|
+
* what lets them both hold. A seeded zone keeps surfaces rendering; recording
|
|
14
|
+
* that it was seeded keeps it from passing as a decision nobody made.
|
|
15
|
+
*
|
|
16
|
+
* ─── Why provenance and not a different default ────────────────────────────
|
|
17
|
+
*
|
|
18
|
+
* Without this column a stored zone is two very different facts wearing one
|
|
19
|
+
* value: something an operator chose for the business, or something the server
|
|
20
|
+
* guessed from the clock of whatever machine it happens to run on (a developer
|
|
21
|
+
* laptop, a container defaulting to UTC, a VM in the wrong region). Studio
|
|
22
|
+
* renders them identically, so the guess reads as a decision — reproducing in
|
|
23
|
+
* the UI precisely the failure 0015 refused in the data.
|
|
24
|
+
*
|
|
25
|
+
* ─── Values, and why NULL is not backfilled ────────────────────────────────
|
|
26
|
+
*
|
|
27
|
+
* 'operator' a human set this — the wizard, Studio, or `PATCH /connections`
|
|
28
|
+
* 'host' `connectionsRepo.create` derived it from the server's own zone
|
|
29
|
+
* NULL unknown, or no zone to attribute
|
|
30
|
+
*
|
|
31
|
+
* Rows predating this column are left NULL rather than guessed at, and NULL
|
|
32
|
+
* must render as no claim at all. The asymmetry is deliberate: under-claiming
|
|
33
|
+
* costs an operator a badge they did not need, while over-claiming tells
|
|
34
|
+
* someone their correct, deliberately chosen zone is a guess — which is how a
|
|
35
|
+
* badge earns the right to be ignored.
|
|
36
|
+
*
|
|
37
|
+
* A zone that is itself NULL carries a NULL source: there is no choice to
|
|
38
|
+
* attribute, and "the operator chose nothing" is a state `timezone` already
|
|
39
|
+
* records on its own.
|
|
40
|
+
*/
|
|
41
|
+
import type { Kysely } from 'kysely';
|
|
42
|
+
import type { ColumnHelpers } from '../columns.js';
|
|
43
|
+
export declare function up(db: Kysely<unknown>, c: ColumnHelpers): Promise<void>;
|
|
44
|
+
//# sourceMappingURL=0018_connection_timezone_source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0018_connection_timezone_source.d.ts","sourceRoot":"","sources":["../../src/migrations/0018_connection_timezone_source.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,wBAAsB,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAO7E"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
/**
|
|
3
|
+
* Wave 0018 — `adminium_connections.timezoneSource`: WHO chose the zone.
|
|
4
|
+
*
|
|
5
|
+
* ─── What changed under 0015 ───────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* 0015 added `timezone` and argued for no default at all: "a wrong zone is
|
|
8
|
+
* worse than a missing one, because it looks like data". `connectionsRepo`
|
|
9
|
+
* later seeded new rows from the SERVER's zone anyway, and for a good reason —
|
|
10
|
+
* a null zone made a hosted app surface refuse to render, so the cost of the
|
|
11
|
+
* missing value landed as total unavailability rather than an hour's drift.
|
|
12
|
+
*
|
|
13
|
+
* Both positions are right about their own failure, and the column below is
|
|
14
|
+
* what lets them both hold. A seeded zone keeps surfaces rendering; recording
|
|
15
|
+
* that it was seeded keeps it from passing as a decision nobody made.
|
|
16
|
+
*
|
|
17
|
+
* ─── Why provenance and not a different default ────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* Without this column a stored zone is two very different facts wearing one
|
|
20
|
+
* value: something an operator chose for the business, or something the server
|
|
21
|
+
* guessed from the clock of whatever machine it happens to run on (a developer
|
|
22
|
+
* laptop, a container defaulting to UTC, a VM in the wrong region). Studio
|
|
23
|
+
* renders them identically, so the guess reads as a decision — reproducing in
|
|
24
|
+
* the UI precisely the failure 0015 refused in the data.
|
|
25
|
+
*
|
|
26
|
+
* ─── Values, and why NULL is not backfilled ────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* 'operator' a human set this — the wizard, Studio, or `PATCH /connections`
|
|
29
|
+
* 'host' `connectionsRepo.create` derived it from the server's own zone
|
|
30
|
+
* NULL unknown, or no zone to attribute
|
|
31
|
+
*
|
|
32
|
+
* Rows predating this column are left NULL rather than guessed at, and NULL
|
|
33
|
+
* must render as no claim at all. The asymmetry is deliberate: under-claiming
|
|
34
|
+
* costs an operator a badge they did not need, while over-claiming tells
|
|
35
|
+
* someone their correct, deliberately chosen zone is a guess — which is how a
|
|
36
|
+
* badge earns the right to be ignored.
|
|
37
|
+
*
|
|
38
|
+
* A zone that is itself NULL carries a NULL source: there is no choice to
|
|
39
|
+
* attribute, and "the operator chose nothing" is a state `timezone` already
|
|
40
|
+
* records on its own.
|
|
41
|
+
*/
|
|
42
|
+
import { metaTable } from '../prefix.js';
|
|
43
|
+
export async function up(db, c) {
|
|
44
|
+
await db.schema
|
|
45
|
+
.alterTable(metaTable('connections'))
|
|
46
|
+
// 'operator' is the longest value at 8; 16 leaves room for a third source
|
|
47
|
+
// (an inherited or probed zone) without a second ALTER.
|
|
48
|
+
.addColumn('timezone_source', c.str(16))
|
|
49
|
+
.execute();
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=0018_connection_timezone_source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0018_connection_timezone_source.js","sourceRoot":"","sources":["../../src/migrations/0018_connection_timezone_source.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAKH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC,EAAmB,EAAE,CAAgB;IAC5D,MAAM,EAAE,CAAC,MAAM;SACZ,UAAU,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;QACrC,0EAA0E;QAC1E,wDAAwD;SACvD,SAAS,CAAC,iBAAiB,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;SACvC,OAAO,EAAE,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 0019 — `adminium_connections.disabled_at`: the operator PAUSED this
|
|
3
|
+
* source, and when.
|
|
4
|
+
*
|
|
5
|
+
* ─── Why not a `status` value ──────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* `status` is a HEALTH reading — `connected` / `error` / `unconfigured` — and
|
|
8
|
+
* every writer of it is a probe (`recordTestResult`, the create path). Folding
|
|
9
|
+
* a pause into that enum would mean the next successful test silently unpaused
|
|
10
|
+
* the connection, because a probe's whole job is to overwrite `status` with
|
|
11
|
+
* what it just observed. It would also destroy the reading underneath: an
|
|
12
|
+
* operator who pauses a source that is currently failing, fixes the database,
|
|
13
|
+
* and resumes has no way back to `error` because nothing recorded it.
|
|
14
|
+
*
|
|
15
|
+
* Health is observed; a pause is INTENDED. Two facts, two columns, and the
|
|
16
|
+
* card can say "paused, and it was failing when you paused it".
|
|
17
|
+
*
|
|
18
|
+
* ─── Why a timestamp and not a boolean ─────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* A pause is meant to be temporary, so "how long has this been off?" is the
|
|
21
|
+
* question that follows it — a source paused an hour ago during a migration
|
|
22
|
+
* and one paused five weeks ago and forgotten are the same boolean and very
|
|
23
|
+
* different situations. NULL means enabled; anything else is the epoch-ms
|
|
24
|
+
* instant it was paused (07-meta-store.md §2.1: never a native datetime).
|
|
25
|
+
*
|
|
26
|
+
* WHO paused it is not stored here. The audit log already records the actor
|
|
27
|
+
* for `connection.disable` / `connection.enable` alongside every other
|
|
28
|
+
* connection action, and a second, unjoined copy on the row would be one more
|
|
29
|
+
* thing that can disagree with it.
|
|
30
|
+
*
|
|
31
|
+
* ─── Backfill ──────────────────────────────────────────────────────────────
|
|
32
|
+
*
|
|
33
|
+
* None. Every pre-0019 row was serving, which is exactly what NULL means.
|
|
34
|
+
*/
|
|
35
|
+
import type { Kysely } from 'kysely';
|
|
36
|
+
import type { ColumnHelpers } from '../columns.js';
|
|
37
|
+
export declare function up(db: Kysely<unknown>, c: ColumnHelpers): Promise<void>;
|
|
38
|
+
//# sourceMappingURL=0019_connection_disabled.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0019_connection_disabled.d.ts","sourceRoot":"","sources":["../../src/migrations/0019_connection_disabled.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,wBAAsB,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAK7E"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
/**
|
|
3
|
+
* Wave 0019 — `adminium_connections.disabled_at`: the operator PAUSED this
|
|
4
|
+
* source, and when.
|
|
5
|
+
*
|
|
6
|
+
* ─── Why not a `status` value ──────────────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* `status` is a HEALTH reading — `connected` / `error` / `unconfigured` — and
|
|
9
|
+
* every writer of it is a probe (`recordTestResult`, the create path). Folding
|
|
10
|
+
* a pause into that enum would mean the next successful test silently unpaused
|
|
11
|
+
* the connection, because a probe's whole job is to overwrite `status` with
|
|
12
|
+
* what it just observed. It would also destroy the reading underneath: an
|
|
13
|
+
* operator who pauses a source that is currently failing, fixes the database,
|
|
14
|
+
* and resumes has no way back to `error` because nothing recorded it.
|
|
15
|
+
*
|
|
16
|
+
* Health is observed; a pause is INTENDED. Two facts, two columns, and the
|
|
17
|
+
* card can say "paused, and it was failing when you paused it".
|
|
18
|
+
*
|
|
19
|
+
* ─── Why a timestamp and not a boolean ─────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* A pause is meant to be temporary, so "how long has this been off?" is the
|
|
22
|
+
* question that follows it — a source paused an hour ago during a migration
|
|
23
|
+
* and one paused five weeks ago and forgotten are the same boolean and very
|
|
24
|
+
* different situations. NULL means enabled; anything else is the epoch-ms
|
|
25
|
+
* instant it was paused (07-meta-store.md §2.1: never a native datetime).
|
|
26
|
+
*
|
|
27
|
+
* WHO paused it is not stored here. The audit log already records the actor
|
|
28
|
+
* for `connection.disable` / `connection.enable` alongside every other
|
|
29
|
+
* connection action, and a second, unjoined copy on the row would be one more
|
|
30
|
+
* thing that can disagree with it.
|
|
31
|
+
*
|
|
32
|
+
* ─── Backfill ──────────────────────────────────────────────────────────────
|
|
33
|
+
*
|
|
34
|
+
* None. Every pre-0019 row was serving, which is exactly what NULL means.
|
|
35
|
+
*/
|
|
36
|
+
import { metaTable } from '../prefix.js';
|
|
37
|
+
export async function up(db, c) {
|
|
38
|
+
await db.schema
|
|
39
|
+
.alterTable(metaTable('connections'))
|
|
40
|
+
.addColumn('disabled_at', c.ts)
|
|
41
|
+
.execute();
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=0019_connection_disabled.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0019_connection_disabled.js","sourceRoot":"","sources":["../../src/migrations/0019_connection_disabled.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC,EAAmB,EAAE,CAAgB;IAC5D,MAAM,EAAE,CAAC,MAAM;SACZ,UAAU,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;SACpC,SAAS,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,CAAC;SAC9B,OAAO,EAAE,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 0020 — the add-on half of `adminium_manifests`, plus the attachment
|
|
3
|
+
* table an add-on needs and an app does not
|
|
4
|
+
* (26-add-on-runtime.md §4, as amended 2026-08-29).
|
|
5
|
+
*
|
|
6
|
+
* ─── This ALTERs. It does not create. ───────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* 26 §4 as first written said `adminium_manifests` "does not exist at all
|
|
9
|
+
* today, so this creates it rather than altering it", and `research/BRIEF.md`
|
|
10
|
+
* §6 and `16-milestones.md` M16-T02 repeated it. All three were wrong: the
|
|
11
|
+
* table has shipped since `0006_platform.ts:319-348`. It has no repo, no
|
|
12
|
+
* writer and zero rows, which is how three documents came to record a shipped
|
|
13
|
+
* table as absent — and it is why this migration adds two columns to something
|
|
14
|
+
* that is already there rather than creating a fourth copy of the idea.
|
|
15
|
+
*
|
|
16
|
+
* Three shipped columns this plan never accounted for come along for the ride:
|
|
17
|
+
* `connection_id`, `status`, and **`license_key_encrypted`** — a column 17
|
|
18
|
+
* defers by name. It is deliberately left alone rather than dropped: it is
|
|
19
|
+
* empty, dropping a column is the one irreversible thing a migration can do,
|
|
20
|
+
* and "17 defers licences" is an argument for never WRITING it, which costs
|
|
21
|
+
* nothing to honour. `repos/manifests.ts` never reads or writes it.
|
|
22
|
+
*
|
|
23
|
+
* ─── Why attachments are a table and not `attached_to` on the row ───────────
|
|
24
|
+
*
|
|
25
|
+
* 26 O3 asks how one add-on attaches to several hosts and recommends "two rows,
|
|
26
|
+
* keyed `(manifest_key, attached_to)`". **This does not do that**, and the
|
|
27
|
+
* reason is a cost O3 could not have weighed, because it was written believing
|
|
28
|
+
* the table did not exist yet:
|
|
29
|
+
*
|
|
30
|
+
* 1. **Two rows means two manifest documents.** `manifest` is the whole
|
|
31
|
+
* validated JSON. An add-on attached to two hosts would store it twice, and
|
|
32
|
+
* every upgrade would have to rewrite N rows atomically to avoid a
|
|
33
|
+
* deployment where one attachment is on 1.1.0 and the other on 1.0.0.
|
|
34
|
+
* 2. **It makes the credential FK ambiguous.** `adminium_add_on_credentials`
|
|
35
|
+
* (0021) is one row per CONNECTED add-on — a DHL API key belongs to the
|
|
36
|
+
* add-on, not to one of its attachments. With two manifest rows, that FK
|
|
37
|
+
* points at whichever row happened to be inserted first, and disconnecting
|
|
38
|
+
* "the other one" either orphans a secret or deletes a live one.
|
|
39
|
+
* 3. **It requires editing a shipped constraint.** `uq_adminium_manifests_manifest_key`
|
|
40
|
+
* is UNIQUE on `manifest_key` ALONE (`0006_platform.ts:344-348`). Two rows
|
|
41
|
+
* per key violates it, so O3's shape means dropping and recreating a shipped
|
|
42
|
+
* index across three dialects — against §4's own "never edit a shipped
|
|
43
|
+
* migration".
|
|
44
|
+
*
|
|
45
|
+
* An attachment is a many-to-many fact between a manifest and a host app, so it
|
|
46
|
+
* gets the table that models one. The shipped unique index stays true (one
|
|
47
|
+
* manifest, one row, one document, one credential), and `attaches` in the
|
|
48
|
+
* manifest stays what it always was: the set of hosts this add-on MAY attach to,
|
|
49
|
+
* of which the rows below record the ones it actually is attached to.
|
|
50
|
+
*
|
|
51
|
+
* ─── Why `disabled_at` and not a boolean ───────────────────────────────────
|
|
52
|
+
*
|
|
53
|
+
* §5.1's `PATCH /add-ons/:key` enables and disables per surface, and that state
|
|
54
|
+
* belongs on the attachment rather than on the manifest: an add-on can be
|
|
55
|
+
* legitimately live on one host and switched off on another. The timestamp
|
|
56
|
+
* follows 0019's discipline verbatim — NULL means enabled, and "how long has
|
|
57
|
+
* this been off" is the question that follows a disable, which a boolean cannot
|
|
58
|
+
* answer.
|
|
59
|
+
*
|
|
60
|
+
* ─── Backfill ──────────────────────────────────────────────────────────────
|
|
61
|
+
*
|
|
62
|
+
* None, and not for the usual reason: the table has zero rows in every
|
|
63
|
+
* deployment, because nothing has ever written to it. `kind` still carries a
|
|
64
|
+
* default so the column is honest about what a pre-existing row would have
|
|
65
|
+
* meant — `adminium_manifests` was introduced for installed micro-SaaS apps
|
|
66
|
+
* (13-marketplace.md), so an unlabelled row is an app.
|
|
67
|
+
*/
|
|
68
|
+
import type { Kysely } from 'kysely';
|
|
69
|
+
import type { ColumnHelpers } from '../columns.js';
|
|
70
|
+
export declare function up(db: Kysely<unknown>, c: ColumnHelpers): Promise<void>;
|
|
71
|
+
//# sourceMappingURL=0020_manifests_add_on.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0020_manifests_add_on.d.ts","sourceRoot":"","sources":["../../src/migrations/0020_manifests_add_on.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,wBAAsB,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAiD7E"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
/**
|
|
3
|
+
* Wave 0020 — the add-on half of `adminium_manifests`, plus the attachment
|
|
4
|
+
* table an add-on needs and an app does not
|
|
5
|
+
* (26-add-on-runtime.md §4, as amended 2026-08-29).
|
|
6
|
+
*
|
|
7
|
+
* ─── This ALTERs. It does not create. ───────────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* 26 §4 as first written said `adminium_manifests` "does not exist at all
|
|
10
|
+
* today, so this creates it rather than altering it", and `research/BRIEF.md`
|
|
11
|
+
* §6 and `16-milestones.md` M16-T02 repeated it. All three were wrong: the
|
|
12
|
+
* table has shipped since `0006_platform.ts:319-348`. It has no repo, no
|
|
13
|
+
* writer and zero rows, which is how three documents came to record a shipped
|
|
14
|
+
* table as absent — and it is why this migration adds two columns to something
|
|
15
|
+
* that is already there rather than creating a fourth copy of the idea.
|
|
16
|
+
*
|
|
17
|
+
* Three shipped columns this plan never accounted for come along for the ride:
|
|
18
|
+
* `connection_id`, `status`, and **`license_key_encrypted`** — a column 17
|
|
19
|
+
* defers by name. It is deliberately left alone rather than dropped: it is
|
|
20
|
+
* empty, dropping a column is the one irreversible thing a migration can do,
|
|
21
|
+
* and "17 defers licences" is an argument for never WRITING it, which costs
|
|
22
|
+
* nothing to honour. `repos/manifests.ts` never reads or writes it.
|
|
23
|
+
*
|
|
24
|
+
* ─── Why attachments are a table and not `attached_to` on the row ───────────
|
|
25
|
+
*
|
|
26
|
+
* 26 O3 asks how one add-on attaches to several hosts and recommends "two rows,
|
|
27
|
+
* keyed `(manifest_key, attached_to)`". **This does not do that**, and the
|
|
28
|
+
* reason is a cost O3 could not have weighed, because it was written believing
|
|
29
|
+
* the table did not exist yet:
|
|
30
|
+
*
|
|
31
|
+
* 1. **Two rows means two manifest documents.** `manifest` is the whole
|
|
32
|
+
* validated JSON. An add-on attached to two hosts would store it twice, and
|
|
33
|
+
* every upgrade would have to rewrite N rows atomically to avoid a
|
|
34
|
+
* deployment where one attachment is on 1.1.0 and the other on 1.0.0.
|
|
35
|
+
* 2. **It makes the credential FK ambiguous.** `adminium_add_on_credentials`
|
|
36
|
+
* (0021) is one row per CONNECTED add-on — a DHL API key belongs to the
|
|
37
|
+
* add-on, not to one of its attachments. With two manifest rows, that FK
|
|
38
|
+
* points at whichever row happened to be inserted first, and disconnecting
|
|
39
|
+
* "the other one" either orphans a secret or deletes a live one.
|
|
40
|
+
* 3. **It requires editing a shipped constraint.** `uq_adminium_manifests_manifest_key`
|
|
41
|
+
* is UNIQUE on `manifest_key` ALONE (`0006_platform.ts:344-348`). Two rows
|
|
42
|
+
* per key violates it, so O3's shape means dropping and recreating a shipped
|
|
43
|
+
* index across three dialects — against §4's own "never edit a shipped
|
|
44
|
+
* migration".
|
|
45
|
+
*
|
|
46
|
+
* An attachment is a many-to-many fact between a manifest and a host app, so it
|
|
47
|
+
* gets the table that models one. The shipped unique index stays true (one
|
|
48
|
+
* manifest, one row, one document, one credential), and `attaches` in the
|
|
49
|
+
* manifest stays what it always was: the set of hosts this add-on MAY attach to,
|
|
50
|
+
* of which the rows below record the ones it actually is attached to.
|
|
51
|
+
*
|
|
52
|
+
* ─── Why `disabled_at` and not a boolean ───────────────────────────────────
|
|
53
|
+
*
|
|
54
|
+
* §5.1's `PATCH /add-ons/:key` enables and disables per surface, and that state
|
|
55
|
+
* belongs on the attachment rather than on the manifest: an add-on can be
|
|
56
|
+
* legitimately live on one host and switched off on another. The timestamp
|
|
57
|
+
* follows 0019's discipline verbatim — NULL means enabled, and "how long has
|
|
58
|
+
* this been off" is the question that follows a disable, which a boolean cannot
|
|
59
|
+
* answer.
|
|
60
|
+
*
|
|
61
|
+
* ─── Backfill ──────────────────────────────────────────────────────────────
|
|
62
|
+
*
|
|
63
|
+
* None, and not for the usual reason: the table has zero rows in every
|
|
64
|
+
* deployment, because nothing has ever written to it. `kind` still carries a
|
|
65
|
+
* default so the column is honest about what a pre-existing row would have
|
|
66
|
+
* meant — `adminium_manifests` was introduced for installed micro-SaaS apps
|
|
67
|
+
* (13-marketplace.md), so an unlabelled row is an app.
|
|
68
|
+
*/
|
|
69
|
+
import { metaTable } from '../prefix.js';
|
|
70
|
+
export async function up(db, c) {
|
|
71
|
+
// `kind` — 'app' | 'add-on' (@adminium/manifest MANIFEST_KINDS). An add-on IS
|
|
72
|
+
// a manifest (26 §4), so install, upgrade, uninstall and the audit trail need
|
|
73
|
+
// no parallel code path; this column is what tells the two apart.
|
|
74
|
+
await db.schema
|
|
75
|
+
.alterTable(metaTable('manifests'))
|
|
76
|
+
.addColumn('kind', c.str(12), (col) => col.notNull().defaultTo('app'))
|
|
77
|
+
.execute();
|
|
78
|
+
await db.schema
|
|
79
|
+
.createTable(metaTable('manifest_attachments'))
|
|
80
|
+
.ifNotExists()
|
|
81
|
+
.addColumn('id', c.id, (col) => col.primaryKey())
|
|
82
|
+
.addColumn('manifest_id', c.id, (col) => col.notNull())
|
|
83
|
+
// The host app's `manifest_key` (24 §5.7) — 80 to match
|
|
84
|
+
// `adminium_manifests.manifest_key`, the column it points at by value.
|
|
85
|
+
.addColumn('attached_to', c.str(80), (col) => col.notNull())
|
|
86
|
+
/** NULL = enabled on this host. Epoch ms, never a native datetime. */
|
|
87
|
+
.addColumn('disabled_at', c.ts)
|
|
88
|
+
.addColumn('created_at', c.ts, (col) => col.notNull())
|
|
89
|
+
// CASCADE, unlike the shipped manifests FKs' `set null`: an attachment with
|
|
90
|
+
// no manifest is not a degraded row, it is a row that means nothing. A
|
|
91
|
+
// named table-level constraint because MySQL parses an inline column-level
|
|
92
|
+
// `REFERENCES` and silently discards it (the 2026-07-20 CI onion).
|
|
93
|
+
.addForeignKeyConstraint('fk_adminium_manifest_attachments_manifest_id', ['manifest_id'], metaTable('manifests'), ['id'], (cb) => cb.onDelete('cascade'))
|
|
94
|
+
.execute();
|
|
95
|
+
// One attachment per (manifest, host). Re-attaching is idempotent rather than
|
|
96
|
+
// a second row, and the enable/disable state has one home.
|
|
97
|
+
await db.schema
|
|
98
|
+
.createIndex('uq_adminium_manifest_attachments_pair')
|
|
99
|
+
.on(metaTable('manifest_attachments'))
|
|
100
|
+
.columns(['manifest_id', 'attached_to'])
|
|
101
|
+
.unique()
|
|
102
|
+
.execute();
|
|
103
|
+
// The read the runtime actually makes: "what is attached to this host app,
|
|
104
|
+
// and is it on?" — `GET /api/v1/add-ons` runs it on every host page load.
|
|
105
|
+
await db.schema
|
|
106
|
+
.createIndex('ix_adminium_manifest_attachments_host')
|
|
107
|
+
.on(metaTable('manifest_attachments'))
|
|
108
|
+
.columns(['attached_to'])
|
|
109
|
+
.execute();
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=0020_manifests_add_on.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0020_manifests_add_on.js","sourceRoot":"","sources":["../../src/migrations/0020_manifests_add_on.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAKH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC,EAAmB,EAAE,CAAgB;IAC5D,8EAA8E;IAC9E,8EAA8E;IAC9E,kEAAkE;IAClE,MAAM,EAAE,CAAC,MAAM;SACZ,UAAU,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;SAClC,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;SACrE,OAAO,EAAE,CAAC;IAEb,MAAM,EAAE,CAAC,MAAM;SACZ,WAAW,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;SAC9C,WAAW,EAAE;SACb,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC;SAChD,SAAS,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACvD,wDAAwD;QACxD,uEAAuE;SACtE,SAAS,CAAC,aAAa,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QAC5D,sEAAsE;SACrE,SAAS,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,CAAC;SAC9B,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACtD,4EAA4E;QAC5E,uEAAuE;QACvE,2EAA2E;QAC3E,mEAAmE;SAClE,uBAAuB,CACtB,8CAA8C,EAC9C,CAAC,aAAa,CAAC,EACf,SAAS,CAAC,WAAW,CAAC,EACtB,CAAC,IAAI,CAAC,EACN,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAC/B;SACA,OAAO,EAAE,CAAC;IAEb,8EAA8E;IAC9E,2DAA2D;IAC3D,MAAM,EAAE,CAAC,MAAM;SACZ,WAAW,CAAC,uCAAuC,CAAC;SACpD,EAAE,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;SACrC,OAAO,CAAC,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;SACvC,MAAM,EAAE;SACR,OAAO,EAAE,CAAC;IAEb,2EAA2E;IAC3E,0EAA0E;IAC1E,MAAM,EAAE,CAAC,MAAM;SACZ,WAAW,CAAC,uCAAuC,CAAC;SACpD,EAAE,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;SACrC,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC;SACxB,OAAO,EAAE,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 0021 — `adminium_add_on_credentials`: the secret a connected add-on was
|
|
3
|
+
* given, and nothing else (26-add-on-runtime.md §4, D2, D5).
|
|
4
|
+
*
|
|
5
|
+
* ─── One row per connected add-on ──────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* Not per attachment. A DHL API key belongs to the add-on the operator
|
|
8
|
+
* connected, not to each host app it happens to be mounted on — connecting it
|
|
9
|
+
* twice because it is attached twice would mean two copies of one secret, two
|
|
10
|
+
* places to revoke, and a disconnect that leaves one behind. The unique index
|
|
11
|
+
* on `manifest_id` is what makes "connected" a boolean fact about an add-on.
|
|
12
|
+
*
|
|
13
|
+
* ─── What is stored, and what is deliberately not ──────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* `payload` is AES-256-GCM ciphertext over the whole credential envelope,
|
|
16
|
+
* encrypted with a key derived from `ADMINIUM_SECRET` — the same discipline
|
|
17
|
+
* `adminium_connections` uses for DSNs, reached through the same
|
|
18
|
+
* `config/secrets.ts` helpers rather than a second crypto path. Its plaintext
|
|
19
|
+
* shape is a JSON object whose keys differ per `kind`, which is precisely why
|
|
20
|
+
* it is one opaque column and not a set of typed ones: an `api-key` credential
|
|
21
|
+
* and an `oauth2` token pair have nothing in common worth normalising, and
|
|
22
|
+
* columns named `access_token` invite a log line that prints them.
|
|
23
|
+
*
|
|
24
|
+
* `expires_at` and `scopes` sit OUTSIDE the ciphertext on purpose. Both are
|
|
25
|
+
* needed to decide whether to refresh and what the consent dialog should say,
|
|
26
|
+
* and neither is a secret — putting them inside would mean decrypting a token
|
|
27
|
+
* to find out it had expired, on every request.
|
|
28
|
+
*
|
|
29
|
+
* There is no `refresh_token` column. It lives inside `payload` with the access
|
|
30
|
+
* token, because a refresh token IS the credential once an access token lapses,
|
|
31
|
+
* and a schema that stores it beside the ciphertext rather than inside it is a
|
|
32
|
+
* schema that will eventually have it read by something that thought it was
|
|
33
|
+
* metadata.
|
|
34
|
+
*
|
|
35
|
+
* ─── D5: disconnect deletes this row, and only this row ────────────────────
|
|
36
|
+
*
|
|
37
|
+
* 24 D16 / 26 D5 — disconnecting keeps every table the add-on brought and
|
|
38
|
+
* destroys the keys. That is a DELETE here plus nothing anywhere else, which is
|
|
39
|
+
* a property the schema should make easy to get right: this table holds only
|
|
40
|
+
* secrets, so "delete the secrets" is one statement and cannot take data with
|
|
41
|
+
* it by accident.
|
|
42
|
+
*
|
|
43
|
+
* The FK cascades for the uninstall path — a credential outliving its manifest
|
|
44
|
+
* is an orphaned secret nothing can reach to revoke, which is strictly worse
|
|
45
|
+
* than one that is gone.
|
|
46
|
+
*/
|
|
47
|
+
import type { Kysely } from 'kysely';
|
|
48
|
+
import type { ColumnHelpers } from '../columns.js';
|
|
49
|
+
export declare function up(db: Kysely<unknown>, c: ColumnHelpers): Promise<void>;
|
|
50
|
+
//# sourceMappingURL=0021_add_on_credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0021_add_on_credentials.d.ts","sourceRoot":"","sources":["../../src/migrations/0021_add_on_credentials.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,wBAAsB,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAqC7E"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
/**
|
|
3
|
+
* Wave 0021 — `adminium_add_on_credentials`: the secret a connected add-on was
|
|
4
|
+
* given, and nothing else (26-add-on-runtime.md §4, D2, D5).
|
|
5
|
+
*
|
|
6
|
+
* ─── One row per connected add-on ──────────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* Not per attachment. A DHL API key belongs to the add-on the operator
|
|
9
|
+
* connected, not to each host app it happens to be mounted on — connecting it
|
|
10
|
+
* twice because it is attached twice would mean two copies of one secret, two
|
|
11
|
+
* places to revoke, and a disconnect that leaves one behind. The unique index
|
|
12
|
+
* on `manifest_id` is what makes "connected" a boolean fact about an add-on.
|
|
13
|
+
*
|
|
14
|
+
* ─── What is stored, and what is deliberately not ──────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* `payload` is AES-256-GCM ciphertext over the whole credential envelope,
|
|
17
|
+
* encrypted with a key derived from `ADMINIUM_SECRET` — the same discipline
|
|
18
|
+
* `adminium_connections` uses for DSNs, reached through the same
|
|
19
|
+
* `config/secrets.ts` helpers rather than a second crypto path. Its plaintext
|
|
20
|
+
* shape is a JSON object whose keys differ per `kind`, which is precisely why
|
|
21
|
+
* it is one opaque column and not a set of typed ones: an `api-key` credential
|
|
22
|
+
* and an `oauth2` token pair have nothing in common worth normalising, and
|
|
23
|
+
* columns named `access_token` invite a log line that prints them.
|
|
24
|
+
*
|
|
25
|
+
* `expires_at` and `scopes` sit OUTSIDE the ciphertext on purpose. Both are
|
|
26
|
+
* needed to decide whether to refresh and what the consent dialog should say,
|
|
27
|
+
* and neither is a secret — putting them inside would mean decrypting a token
|
|
28
|
+
* to find out it had expired, on every request.
|
|
29
|
+
*
|
|
30
|
+
* There is no `refresh_token` column. It lives inside `payload` with the access
|
|
31
|
+
* token, because a refresh token IS the credential once an access token lapses,
|
|
32
|
+
* and a schema that stores it beside the ciphertext rather than inside it is a
|
|
33
|
+
* schema that will eventually have it read by something that thought it was
|
|
34
|
+
* metadata.
|
|
35
|
+
*
|
|
36
|
+
* ─── D5: disconnect deletes this row, and only this row ────────────────────
|
|
37
|
+
*
|
|
38
|
+
* 24 D16 / 26 D5 — disconnecting keeps every table the add-on brought and
|
|
39
|
+
* destroys the keys. That is a DELETE here plus nothing anywhere else, which is
|
|
40
|
+
* a property the schema should make easy to get right: this table holds only
|
|
41
|
+
* secrets, so "delete the secrets" is one statement and cannot take data with
|
|
42
|
+
* it by accident.
|
|
43
|
+
*
|
|
44
|
+
* The FK cascades for the uninstall path — a credential outliving its manifest
|
|
45
|
+
* is an orphaned secret nothing can reach to revoke, which is strictly worse
|
|
46
|
+
* than one that is gone.
|
|
47
|
+
*/
|
|
48
|
+
import { metaTable } from '../prefix.js';
|
|
49
|
+
export async function up(db, c) {
|
|
50
|
+
await db.schema
|
|
51
|
+
.createTable(metaTable('add_on_credentials'))
|
|
52
|
+
.ifNotExists()
|
|
53
|
+
.addColumn('id', c.id, (col) => col.primaryKey())
|
|
54
|
+
.addColumn('manifest_id', c.id, (col) => col.notNull())
|
|
55
|
+
/** `api-key` | `oauth2` — `none` never reaches this table (D2). */
|
|
56
|
+
.addColumn('kind', c.str(12), (col) => col.notNull())
|
|
57
|
+
/** AES-256-GCM ciphertext over the whole envelope. Never logged, never served. */
|
|
58
|
+
.addColumn('payload', c.text, (col) => col.notNull())
|
|
59
|
+
/** Epoch ms; NULL for a credential that does not expire (an API key). */
|
|
60
|
+
.addColumn('expires_at', c.ts)
|
|
61
|
+
/** The granted OAuth scopes, for the consent surface. Not a secret. */
|
|
62
|
+
.addColumn('scopes', c.json)
|
|
63
|
+
.addColumn('created_at', c.ts, (col) => col.notNull())
|
|
64
|
+
.addColumn('updated_at', c.ts, (col) => col.notNull())
|
|
65
|
+
// Named and table-level: MySQL parses an inline column-level `REFERENCES`
|
|
66
|
+
// and silently discards it, which would leave the cascade below existing
|
|
67
|
+
// only in this file (the 2026-07-20 CI onion).
|
|
68
|
+
.addForeignKeyConstraint('fk_adminium_add_on_credentials_manifest_id', ['manifest_id'], metaTable('manifests'), ['id'], (cb) => cb.onDelete('cascade'))
|
|
69
|
+
.execute();
|
|
70
|
+
// One credential per add-on — see the header. Unique rather than merely
|
|
71
|
+
// indexed, so a second connect is an upsert and can never become a duplicate
|
|
72
|
+
// secret the disconnect path would miss.
|
|
73
|
+
await db.schema
|
|
74
|
+
.createIndex('uq_adminium_add_on_credentials_manifest')
|
|
75
|
+
.on(metaTable('add_on_credentials'))
|
|
76
|
+
.columns(['manifest_id'])
|
|
77
|
+
.unique()
|
|
78
|
+
.execute();
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=0021_add_on_credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0021_add_on_credentials.js","sourceRoot":"","sources":["../../src/migrations/0021_add_on_credentials.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAKH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC,EAAmB,EAAE,CAAgB;IAC5D,MAAM,EAAE,CAAC,MAAM;SACZ,WAAW,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;SAC5C,WAAW,EAAE;SACb,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC;SAChD,SAAS,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACvD,mEAAmE;SAClE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACrD,kFAAkF;SACjF,SAAS,CAAC,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACrD,yEAAyE;SACxE,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC;QAC9B,uEAAuE;SACtE,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC;SAC3B,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;SACrD,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACtD,0EAA0E;QAC1E,yEAAyE;QACzE,+CAA+C;SAC9C,uBAAuB,CACtB,4CAA4C,EAC5C,CAAC,aAAa,CAAC,EACf,SAAS,CAAC,WAAW,CAAC,EACtB,CAAC,IAAI,CAAC,EACN,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAC/B;SACA,OAAO,EAAE,CAAC;IAEb,wEAAwE;IACxE,6EAA6E;IAC7E,yCAAyC;IACzC,MAAM,EAAE,CAAC,MAAM;SACZ,WAAW,CAAC,yCAAyC,CAAC;SACtD,EAAE,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;SACnC,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC;SACxB,MAAM,EAAE;SACR,OAAO,EAAE,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 0022 — the Studio's messages moved namespace, so the operator's
|
|
3
|
+
* rewordings of them have to move with the messages (10-T06).
|
|
4
|
+
*
|
|
5
|
+
* ─── What moved, and why it is not a rename ────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* `common.studio.*` and `common.studioPages.*` — 971 keys, the whole admin
|
|
8
|
+
* console — became the `studio` namespace's `*` and `pages.*`. The point was
|
|
9
|
+
* bundle weight: `studio` is fetched when somebody opens the Studio instead of
|
|
10
|
+
* shipping in every user's first load. But `adminium_translations` files a row
|
|
11
|
+
* by `(scope, locale, namespace, key)`, so an override an admin wrote against
|
|
12
|
+
* the old address stops resolving the moment the message answers to a new one.
|
|
13
|
+
* Nothing errors. The string simply reverts to the compiled English, on the
|
|
14
|
+
* one surface whose users are the people who did the rewording.
|
|
15
|
+
*
|
|
16
|
+
* So this is a DATA move, in JavaScript rather than SQL: `substr` and string
|
|
17
|
+
* concatenation are spelled three different ways across sqlite, postgres and
|
|
18
|
+
* mysql, and the row count here is bounded by how many messages a human has
|
|
19
|
+
* personally rewritten.
|
|
20
|
+
*
|
|
21
|
+
* ─── The two carried keys ──────────────────────────────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* The topbar's Studio menu used to title its two items from `studio.hub.title`
|
|
24
|
+
* and `studio.settingsHub.title`. It paints on every route, long before any
|
|
25
|
+
* Studio chunk exists, so it now has its own `common:topbar.*` keys.
|
|
26
|
+
*
|
|
27
|
+
* An override on either old key is COPIED to the new one rather than only
|
|
28
|
+
* moved. The alternative is an operator who renamed "Data connections" to
|
|
29
|
+
* "Databases" finding the page still says Databases and the menu item that
|
|
30
|
+
* opens it saying Data connections — a split they did not ask for and cannot
|
|
31
|
+
* see the cause of. The cost is honest and small: two rows where there was
|
|
32
|
+
* one, so a later reset has to be done twice. Preserving what they configured
|
|
33
|
+
* is worth more than saving them a click they may never make.
|
|
34
|
+
*
|
|
35
|
+
* ─── No version bump ───────────────────────────────────────────────────────
|
|
36
|
+
*
|
|
37
|
+
* The i18n version counter in `adminium_settings` is what makes a warm browser
|
|
38
|
+
* refetch. It is deliberately untouched: a cached override under a key that no
|
|
39
|
+
* longer exists resolves for nothing and is inert, and the next ordinary write
|
|
40
|
+
* moves the counter anyway.
|
|
41
|
+
*/
|
|
42
|
+
import type { Kysely } from 'kysely';
|
|
43
|
+
export declare function up(db: Kysely<unknown>): Promise<void>;
|
|
44
|
+
//# sourceMappingURL=0022_studio_namespace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0022_studio_namespace.d.ts","sourceRoot":"","sources":["../../src/migrations/0022_studio_namespace.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAwCrC,wBAAsB,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAsD3D"}
|