@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.
Files changed (81) hide show
  1. package/dist/ids.d.ts +6 -0
  2. package/dist/ids.d.ts.map +1 -1
  3. package/dist/ids.js +14 -0
  4. package/dist/ids.js.map +1 -1
  5. package/dist/migrations/0014_public_surface.d.ts +40 -0
  6. package/dist/migrations/0014_public_surface.d.ts.map +1 -0
  7. package/dist/migrations/0014_public_surface.js +186 -0
  8. package/dist/migrations/0014_public_surface.js.map +1 -0
  9. package/dist/migrations/0015_connection_tenant_config.d.ts +42 -0
  10. package/dist/migrations/0015_connection_tenant_config.d.ts.map +1 -0
  11. package/dist/migrations/0015_connection_tenant_config.js +53 -0
  12. package/dist/migrations/0015_connection_tenant_config.js.map +1 -0
  13. package/dist/migrations/0016_audit_entity.d.ts +33 -0
  14. package/dist/migrations/0016_audit_entity.d.ts.map +1 -0
  15. package/dist/migrations/0016_audit_entity.js +137 -0
  16. package/dist/migrations/0016_audit_entity.js.map +1 -0
  17. package/dist/migrations/0017_surface_binding.d.ts +23 -0
  18. package/dist/migrations/0017_surface_binding.d.ts.map +1 -0
  19. package/dist/migrations/0017_surface_binding.js +37 -0
  20. package/dist/migrations/0017_surface_binding.js.map +1 -0
  21. package/dist/migrations/0018_connection_timezone_source.d.ts +44 -0
  22. package/dist/migrations/0018_connection_timezone_source.d.ts.map +1 -0
  23. package/dist/migrations/0018_connection_timezone_source.js +51 -0
  24. package/dist/migrations/0018_connection_timezone_source.js.map +1 -0
  25. package/dist/migrations/0019_connection_disabled.d.ts +38 -0
  26. package/dist/migrations/0019_connection_disabled.d.ts.map +1 -0
  27. package/dist/migrations/0019_connection_disabled.js +43 -0
  28. package/dist/migrations/0019_connection_disabled.js.map +1 -0
  29. package/dist/migrations/0020_manifests_add_on.d.ts +71 -0
  30. package/dist/migrations/0020_manifests_add_on.d.ts.map +1 -0
  31. package/dist/migrations/0020_manifests_add_on.js +111 -0
  32. package/dist/migrations/0020_manifests_add_on.js.map +1 -0
  33. package/dist/migrations/0021_add_on_credentials.d.ts +50 -0
  34. package/dist/migrations/0021_add_on_credentials.d.ts.map +1 -0
  35. package/dist/migrations/0021_add_on_credentials.js +80 -0
  36. package/dist/migrations/0021_add_on_credentials.js.map +1 -0
  37. package/dist/migrations/0022_studio_namespace.d.ts +44 -0
  38. package/dist/migrations/0022_studio_namespace.d.ts.map +1 -0
  39. package/dist/migrations/0022_studio_namespace.js +113 -0
  40. package/dist/migrations/0022_studio_namespace.js.map +1 -0
  41. package/dist/migrations/index.d.ts.map +1 -1
  42. package/dist/migrations/index.js +18 -0
  43. package/dist/migrations/index.js.map +1 -1
  44. package/dist/relocate.d.ts +1 -1
  45. package/dist/relocate.js +1 -1
  46. package/dist/repos/audit.d.ts.map +1 -1
  47. package/dist/repos/audit.js +8 -2
  48. package/dist/repos/audit.js.map +1 -1
  49. package/dist/repos/connections.d.ts +62 -0
  50. package/dist/repos/connections.d.ts.map +1 -1
  51. package/dist/repos/connections.js +104 -0
  52. package/dist/repos/connections.js.map +1 -1
  53. package/dist/repos/index.d.ts +2 -0
  54. package/dist/repos/index.d.ts.map +1 -1
  55. package/dist/repos/index.js +2 -0
  56. package/dist/repos/index.js.map +1 -1
  57. package/dist/repos/manifests.d.ts +139 -0
  58. package/dist/repos/manifests.d.ts.map +1 -0
  59. package/dist/repos/manifests.js +279 -0
  60. package/dist/repos/manifests.js.map +1 -0
  61. package/dist/repos/pages.d.ts +7 -0
  62. package/dist/repos/pages.d.ts.map +1 -1
  63. package/dist/repos/pages.js +39 -8
  64. package/dist/repos/pages.js.map +1 -1
  65. package/dist/repos/public-api.d.ts +140 -0
  66. package/dist/repos/public-api.d.ts.map +1 -0
  67. package/dist/repos/public-api.js +299 -0
  68. package/dist/repos/public-api.js.map +1 -0
  69. package/dist/schema/json-payloads.d.ts +27 -2
  70. package/dist/schema/json-payloads.d.ts.map +1 -1
  71. package/dist/schema/json-payloads.js +41 -2
  72. package/dist/schema/json-payloads.js.map +1 -1
  73. package/dist/schema/settings-registry.d.ts +65 -0
  74. package/dist/schema/settings-registry.d.ts.map +1 -1
  75. package/dist/schema/settings-registry.js +183 -0
  76. package/dist/schema/settings-registry.js.map +1 -1
  77. package/dist/schema/tables.d.ts +177 -1
  78. package/dist/schema/tables.d.ts.map +1 -1
  79. package/dist/schema/tables.js +6 -0
  80. package/dist/schema/tables.js.map +1 -1
  81. 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"}