@endora-commerce/mod-api-keys 0.100.0

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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/admin/index.d.ts +37 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +43 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/pages/ApiKeysPage.d.ts +3 -0
  8. package/dist/admin/pages/ApiKeysPage.d.ts.map +1 -0
  9. package/dist/admin/pages/ApiKeysPage.js +182 -0
  10. package/dist/admin/pages/ApiKeysPage.js.map +1 -0
  11. package/dist/backend/entities/api-key.entity.d.ts +33 -0
  12. package/dist/backend/entities/api-key.entity.d.ts.map +1 -0
  13. package/dist/backend/entities/api-key.entity.js +118 -0
  14. package/dist/backend/entities/api-key.entity.js.map +1 -0
  15. package/dist/backend/index.d.ts +71 -0
  16. package/dist/backend/index.d.ts.map +1 -0
  17. package/dist/backend/index.js +57 -0
  18. package/dist/backend/index.js.map +1 -0
  19. package/dist/backend/plugin.d.ts +65 -0
  20. package/dist/backend/plugin.d.ts.map +1 -0
  21. package/dist/backend/plugin.js +75 -0
  22. package/dist/backend/plugin.js.map +1 -0
  23. package/dist/backend/routes.d.ts +26 -0
  24. package/dist/backend/routes.d.ts.map +1 -0
  25. package/dist/backend/routes.js +51 -0
  26. package/dist/backend/routes.js.map +1 -0
  27. package/dist/backend/services/api-key-service.d.ts +72 -0
  28. package/dist/backend/services/api-key-service.d.ts.map +1 -0
  29. package/dist/backend/services/api-key-service.js +186 -0
  30. package/dist/backend/services/api-key-service.js.map +1 -0
  31. package/dist/manifest.d.ts +169 -0
  32. package/dist/manifest.d.ts.map +1 -0
  33. package/dist/manifest.js +155 -0
  34. package/dist/manifest.js.map +1 -0
  35. package/dist/migrations/20260724T173916_api_keys_distributor_binding.d.ts +50 -0
  36. package/dist/migrations/20260724T173916_api_keys_distributor_binding.d.ts.map +1 -0
  37. package/dist/migrations/20260724T173916_api_keys_distributor_binding.js +117 -0
  38. package/dist/migrations/20260724T173916_api_keys_distributor_binding.js.map +1 -0
  39. package/dist/migrations/index.d.ts +27 -0
  40. package/dist/migrations/index.d.ts.map +1 -0
  41. package/dist/migrations/index.js +29 -0
  42. package/dist/migrations/index.js.map +1 -0
  43. package/docs/api_keys.md +89 -0
  44. package/i18n/en.json +5 -0
  45. package/i18n/pl.json +5 -0
  46. package/package.json +93 -0
  47. package/tailwind.css +14 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * API Keys module — manifest backfill (Module Lifecycle, feature 018).
3
+ *
4
+ * Predates the lifecycle system; this manifest is the static record
5
+ * required so the module participates in the registry. No install /
6
+ * uninstall hook today — the module's schema is owned by earlier
7
+ * platform-wide migrations.
8
+ */
9
+ export declare const manifest: {
10
+ id: string;
11
+ name: string;
12
+ version: string;
13
+ dependencies: string[];
14
+ description?: string | undefined;
15
+ acknowledgedDependencies?: {
16
+ moduleId: string;
17
+ port: string;
18
+ reason: string;
19
+ }[] | undefined;
20
+ nonBindingDependencies?: {
21
+ moduleId: string;
22
+ name: string;
23
+ kind: "contributes-to" | "degrades-without" | "refuses-without";
24
+ reason: string;
25
+ whenAbsent?: string | undefined;
26
+ }[] | undefined;
27
+ activation?: {
28
+ settingCode: string;
29
+ default: boolean;
30
+ } | {
31
+ nonDeactivatable: true;
32
+ reason: string;
33
+ } | undefined;
34
+ settings?: {
35
+ moduleCode: string;
36
+ groups: {
37
+ code: string;
38
+ name: string;
39
+ salesChannelCodes?: string[] | undefined;
40
+ isSystemProtected?: boolean | undefined;
41
+ }[];
42
+ settings: {
43
+ code: string;
44
+ name: string;
45
+ valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
46
+ defaultValue: unknown;
47
+ description?: string | undefined;
48
+ groupCode?: string | undefined;
49
+ previousDefaultValues?: unknown[] | undefined;
50
+ salesChannelCodes?: string[] | undefined;
51
+ enumOptions?: string[] | undefined;
52
+ configurationType?: string | undefined;
53
+ hidden?: boolean | undefined;
54
+ }[];
55
+ } | undefined;
56
+ i18n?: {
57
+ bundlesDir: string;
58
+ } | undefined;
59
+ docs?: false | {
60
+ dir: string;
61
+ } | undefined;
62
+ demo?: false | {
63
+ summary: string;
64
+ seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
65
+ reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
66
+ after?: readonly string[] | undefined;
67
+ package?: string | undefined;
68
+ } | undefined;
69
+ actions?: {
70
+ id: string;
71
+ labelKey: string;
72
+ icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
73
+ targetRoute: string;
74
+ keywords: string[];
75
+ weight: number;
76
+ descriptionKey?: string | undefined;
77
+ requiredPermission?: string | undefined;
78
+ }[] | undefined;
79
+ permissions?: {
80
+ code: string;
81
+ label: string;
82
+ module?: string | undefined;
83
+ description?: string | undefined;
84
+ requires?: string[] | undefined;
85
+ }[] | undefined;
86
+ transactionalEmails?: {
87
+ code: string;
88
+ name: string;
89
+ variables: {
90
+ key: string;
91
+ label: string;
92
+ sampleValue?: string | undefined;
93
+ description?: string | undefined;
94
+ }[];
95
+ description?: string | undefined;
96
+ group?: string | undefined;
97
+ }[] | undefined;
98
+ capabilities?: string[] | undefined;
99
+ exclusiveCapabilities?: {
100
+ key: string;
101
+ errorCode: string;
102
+ }[] | undefined;
103
+ errorCodes?: {
104
+ code: string;
105
+ tokens?: string[] | undefined;
106
+ }[] | undefined;
107
+ blocks?: {
108
+ name: string;
109
+ labelKey: string;
110
+ category: string;
111
+ contexts: ("invoice" | "email" | "cms" | "newsletter")[];
112
+ fields: Record<string, {
113
+ type: "number" | "object" | "array" | "text" | "textarea" | "select" | "radio" | "external" | "uuid" | "richtext";
114
+ label?: string | undefined;
115
+ required?: boolean | undefined;
116
+ options?: {
117
+ label: string;
118
+ value: string | number;
119
+ }[] | undefined;
120
+ refKind?: string | undefined;
121
+ }>;
122
+ descriptionKey?: string | undefined;
123
+ defaultProps?: Record<string, unknown> | undefined;
124
+ responsiveFields?: string[] | undefined;
125
+ previewIcon?: string | undefined;
126
+ weight?: number | undefined;
127
+ }[] | undefined;
128
+ blockCategories?: {
129
+ key: string;
130
+ titleKey: string;
131
+ contexts: ("invoice" | "email" | "cms" | "newsletter")[];
132
+ weight?: number | undefined;
133
+ visible?: boolean | undefined;
134
+ }[] | undefined;
135
+ env?: {
136
+ name: string;
137
+ describes: {
138
+ en: string;
139
+ pl: string;
140
+ };
141
+ requirement: {
142
+ kind: "required";
143
+ } | {
144
+ kind: "requiredWhen";
145
+ input: string;
146
+ equals: string;
147
+ } | {
148
+ kind: "optional";
149
+ without: {
150
+ en: string;
151
+ pl: string;
152
+ };
153
+ };
154
+ secret: boolean;
155
+ generable: boolean;
156
+ owner: {
157
+ kind: "platform";
158
+ } | {
159
+ kind: "application";
160
+ application: "admin" | "backend" | "storefront";
161
+ } | {
162
+ kind: "module";
163
+ moduleId: string;
164
+ };
165
+ consumers: ("admin" | "backend" | "storefront")[];
166
+ addressOf: "admin" | "backend" | "storefront" | null;
167
+ }[] | undefined;
168
+ };
169
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkJnB,CAAC"}
@@ -0,0 +1,155 @@
1
+ import { defineModuleManifest } from '@endora-commerce/contracts';
2
+ /**
3
+ * API Keys module — manifest backfill (Module Lifecycle, feature 018).
4
+ *
5
+ * Predates the lifecycle system; this manifest is the static record
6
+ * required so the module participates in the registry. No install /
7
+ * uninstall hook today — the module's schema is owned by earlier
8
+ * platform-wide migrations.
9
+ */
10
+ export const manifest = defineModuleManifest({
11
+ id: 'api_keys',
12
+ name: 'API Keys',
13
+ description: 'Programmatic API keys (Bearer tokens) used by integrations and webhooks.',
14
+ version: '1.0.0',
15
+ // `auth` owns the `requireAdmin` port the admin routes are gated by; feature
16
+ // 072 made it a container resolution rather than a constructor argument.
17
+ dependencies: ['customer_accounts', 'organizations', 'sales_channels', 'auth'],
18
+ settings: {
19
+ moduleCode: 'api_keys',
20
+ groups: [{ code: 'api_keys', name: 'API keys' }],
21
+ settings: [
22
+ {
23
+ // Feature 073 — the operator's activation control. Platform-wide.
24
+ code: 'api_keys.enabled',
25
+ name: 'API keys enabled',
26
+ description: 'Switches machine-to-machine API keys on or off: the admin screens that issue them and the gates that authenticate them on the external catalog namespace. Switched off, existing keys stop authenticating but are not revoked — they work again exactly as issued when you switch it back on.',
27
+ groupCode: 'api_keys',
28
+ valueType: 'boolean',
29
+ defaultValue: true,
30
+ },
31
+ ],
32
+ },
33
+ // Issue #213 — `integrations:manage` is a **shared** gate: it guards this
34
+ // module's admin surface and `webhooks`', and the core `PERMISSION_CATALOGUE`
35
+ // row that carries its label can name only one module. Both owners declare it,
36
+ // so the presence filter on `/admin-roles` keeps the code grantable while
37
+ // either surface is on. This half is what the core row already says; it is
38
+ // written out anyway, because a shared code owned by one manifest and one
39
+ // hard-coded core row is the arrangement that produced the asymmetry.
40
+ permissions: [{ code: 'integrations:manage', label: 'Manage API keys + webhooks' }],
41
+ /**
42
+ * The module's command-palette entry — feature 091, Phase 4 (the plan's
43
+ * batch 6), and one of the fifteen Principle XVI entries
44
+ * `specs/deferred-defects.md` records as owed.
45
+ *
46
+ * It arrives with the drain rather than before it, by the mechanism that
47
+ * register predicts: the batch owes an off-state proof over every surface the
48
+ * module contributes, and until this declaration existed the `AppShell.tsx`
49
+ * `PALETTE_ITEMS` row that advertised /api-keys was the admin's own — a
50
+ * hand-written copy no server-side presence check was ever asked about, so it
51
+ * went on offering the screen to an operator who had switched the module off.
52
+ * The Actions group is resolved by `AdminActionsService` against the effective
53
+ * enabled-set, which is what makes the withdrawal real.
54
+ *
55
+ * `requiredPermission` is the code the target route enforces, which
56
+ * `check:action-route-permissions` compares against the registration on
57
+ * `GET /api/v1/admin/api-keys` itself. It is `integrations:manage` and not a
58
+ * `api_keys:`-prefixed code because that is the gate the module actually
59
+ * has: issue #213 records why both owners declare it.
60
+ */
61
+ actions: [
62
+ {
63
+ id: 'open-api-keys',
64
+ labelKey: 'actions.openApiKeys.label',
65
+ descriptionKey: 'actions.openApiKeys.description',
66
+ icon: 'KeyRound',
67
+ targetRoute: '/api-keys',
68
+ requiredPermission: 'integrations:manage',
69
+ keywords: ['api', 'api keys', 'klucze api', 'bearer', 'token', 'integration', 'integracja'],
70
+ weight: 700,
71
+ },
72
+ ],
73
+ activation: { settingCode: 'api_keys.enabled', default: true },
74
+ /**
75
+ * This module's first i18n bundle — D-129's remaining sweep, MR 5 — and it
76
+ * installs **no entries**.
77
+ *
78
+ * It exists because a **declaring** module that contributes no bundle file is
79
+ * `check:error-translations` exit 2 for the whole tree rather than a finding
80
+ * (`specs/090-module-owned-error-codes/contracts/error-translation-population.md`
81
+ * §2.1, §2.3; `d129-sweep.md` §3.4), and all three codes below are ledgered
82
+ * rather than written — so `{}` is what the two files hold. That is the
83
+ * default §5.4 describes for this position, and it is the first time the
84
+ * platform has carried it. What a module in that state does at boot is not
85
+ * decidable from a source-tree check, so the merge request that shipped it
86
+ * ran `bash scripts/boot-gate.sh --with-negatives` and read the answer off a
87
+ * real image: **`installed`**, with two `translation_bundles` rows carrying
88
+ * zero entries — `[i18n] reconcile complete — installed=54 skipped=15
89
+ * failed=0` over the 69 modules the platform composes, none left over.
90
+ * `loadModuleBundles` returns a `Map` of both languages here, not an empty
91
+ * one; §5.4's `{"byLanguage":{}}` is what `JSON.stringify` prints for any
92
+ * `Map` and says nothing about its size.
93
+ *
94
+ * The bundle lives at the **package root**, not under `dist`:
95
+ * `manifest-locations.ts` resolves a packaged module's `manifestPath` to its
96
+ * `package.json`, so `dirname` is the package directory. When a sentence is
97
+ * written here it must be a **flat** `{"a.b.c": "text"}` map, because a nested
98
+ * object fails `TranslationBundleEntriesSchema` and the boot reconciler logs
99
+ * and skips it — silently.
100
+ */
101
+ i18n: { bundlesDir: 'i18n' },
102
+ docs: { dir: 'docs' },
103
+ /**
104
+ * The `API_KEY_*` codes — D-129's remaining sweep, Tier B
105
+ * (`specs/090-module-owned-error-codes/d129-sweep.md` §5.2, Appendix A;
106
+ * MR 5).
107
+ *
108
+ * All three were declared by `_i18n` until this merge request, not because
109
+ * anybody judged them the platform's but because the deleted prefix chain had
110
+ * no rule for them and its last line was `return 'core'`. **D-121 T1 puts
111
+ * them here**: the noun in each is the API key, which is this module's
112
+ * entity, and its scopes and its distributor binding are columns on that row.
113
+ * T1 does not ask who throws, which matters twice over here — `API_KEY_NOT_BOUND`
114
+ * is raised by three modules (this one, `catalog` and `orders`, each gating
115
+ * its own external namespace) and `API_KEY_CHANNEL_MISMATCH` is raised by the
116
+ * **platform**, in the sales-channel resolver middleware.
117
+ *
118
+ * **That last one is D-186 §3, taken deliberately and with a condition.** A
119
+ * platform raise site now depends on a switchable module's bundle surviving,
120
+ * which is a coupling Phase 3 never created. The ruling accepts it so the
121
+ * family lands in one bundle, and the merge request carries the assertion that
122
+ * makes it safe: the raise is unreachable while `api_keys` is absent, because
123
+ * it needs `request.actor.kind === 'api_key'` and the only thing that produces
124
+ * such an actor is `auth`'s request hook calling this module's gated
125
+ * `apiKeyResolver` — which its own presence probe answers `null` for while
126
+ * this module is off. `test/integration/_lifecycle/non-binding-degradation.integration.test.ts`
127
+ * asserts it with a bound key and a mismatching `x-sales-channel` header.
128
+ *
129
+ * **None of the three arrives with a sentence.** `API_KEY_CHANNEL_MISMATCH`
130
+ * and `API_KEY_NOT_BOUND` never had one and stay on
131
+ * `UNTRANSLATED_ERROR_CODES`, under this module instead of under `_i18n`.
132
+ * `API_KEY_OUT_OF_SCOPE` carried a placeholder — `"Api Key Out Of Scope."` /
133
+ * `"Błąd: api key out of scope."` — which D-186 §2 deletes rather than
134
+ * carries, and it is **not** rewritten: it is the first code in the sweep
135
+ * ledgered despite a live raise site. Two reasons, and the second is the one
136
+ * that decides it. Its reader is an integration rather than a person, so a
137
+ * translation is answered to a program. And the raise names the scope the key
138
+ * is missing — `API key lacks the required scope: <scope>.` — which a fixed
139
+ * sentence would replace with a vaguer one, because
140
+ * `localizeErrorEnvelope` substitutes the message wholesale and the raise
141
+ * passes no `details` for a placeholder to be filled from. Writing one here
142
+ * would take information away from the only audience that meets it.
143
+ *
144
+ * **`tokens` is derived from the raise sites, not from the bundle**
145
+ * (runbook §5), and there are none: every raise is a bare
146
+ * `HttpError(403, code, message)` with no `details` argument, measured by
147
+ * balanced-paren extraction of each call's own arguments.
148
+ */
149
+ errorCodes: [
150
+ { code: 'API_KEY_CHANNEL_MISMATCH' },
151
+ { code: 'API_KEY_NOT_BOUND' },
152
+ { code: 'API_KEY_OUT_OF_SCOPE' },
153
+ ],
154
+ });
155
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,WAAW,EACT,0EAA0E;IAC5E,OAAO,EAAE,OAAO;IAChB,6EAA6E;IAC7E,yEAAyE;IACzE,YAAY,EAAE,CAAC,mBAAmB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,CAAC;IAC9E,QAAQ,EAAE;QACR,UAAU,EAAE,UAAU;QACtB,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;QAChD,QAAQ,EAAE;YACR;gBACE,kEAAkE;gBAClE,IAAI,EAAE,kBAAkB;gBACxB,IAAI,EAAE,kBAAkB;gBACxB,WAAW,EACT,+RAA+R;gBACjS,SAAS,EAAE,UAAU;gBACrB,SAAS,EAAE,SAAS;gBACpB,YAAY,EAAE,IAAI;aACnB;SACF;KACF;IACD,0EAA0E;IAC1E,8EAA8E;IAC9E,+EAA+E;IAC/E,0EAA0E;IAC1E,2EAA2E;IAC3E,0EAA0E;IAC1E,sEAAsE;IACtE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAC;IACnF;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,EAAE;QACP;YACE,EAAE,EAAE,eAAe;YACnB,QAAQ,EAAE,2BAA2B;YACrC,cAAc,EAAE,iCAAiC;YACjD,IAAI,EAAE,UAAU;YAChB,WAAW,EAAE,WAAW;YACxB,kBAAkB,EAAE,qBAAqB;YACzC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,CAAC;YAC3F,MAAM,EAAE,GAAG;SACZ;KACF;IACD,UAAU,EAAE,EAAE,WAAW,EAAE,kBAAkB,EAAE,OAAO,EAAE,IAAI,EAAE;IAC9D;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;IAC5B,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;IACrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6CG;IACH,UAAU,EAAE;QACV,EAAE,IAAI,EAAE,0BAA0B,EAAE;QACpC,EAAE,IAAI,EAAE,mBAAmB,EAAE;QAC7B,EAAE,IAAI,EAAE,sBAAsB,EAAE;KACjC;CACF,CAAC,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Feature 062 — Distributor API key binding (data-model.md §1), and since
4
+ * `specs/120-migration-closure-bridge-ownership/` Phase 3 the **creation** of
5
+ * `api_keys` itself.
6
+ *
7
+ * The table was created by `webhooks`' frozen
8
+ * `Migration20260425T091359WebhooksUs7Init`: the two modules were one surface
9
+ * until the US7 split, and the creation stayed behind while the entity, the
10
+ * service and every other statement came here. An instance installing
11
+ * `api_keys` without `webhooks` therefore had no migration that builds the
12
+ * table, and this very migration named a table outside its own closure (D-226).
13
+ *
14
+ * **It moved from one frozen body into another, and that is the whole of
15
+ * FR-014.** D-226's prescribed repair — subtract the statement and re-add it
16
+ * above `BASELINE_THROUGH` — is right for a *reference*, which can only move
17
+ * strictly later in the computed order. It is wrong for a *creation*: an
18
+ * above-watermark migration runs after the entire frozen prefix, and two frozen
19
+ * migrations reference `api_keys` — this one's own `alter table` below, and
20
+ * `Migration20260724T193611OrdersOrderPlacementIntents`' foreign key. The
21
+ * creation would have landed after both and broken a fresh database. This
22
+ * migration's historical position precedes both, so it is where the creation
23
+ * goes. Both class names are unchanged and `BASELINE_MIGRATIONS` is untouched:
24
+ * `mikro_orm_migrations` persists the class name and no checksum, so neither
25
+ * edited body is re-offered to a database that has applied it.
26
+ *
27
+ * **`if not exists`, and it is load-bearing rather than defensive.** A database
28
+ * that applied the April migration and has not yet reached this one — anything
29
+ * behind by a release — already has `api_keys` while this migration is pending.
30
+ * A verbatim `create table` would fail there. With the guard it is a no-op on
31
+ * that database and the creation on a fresh one.
32
+ *
33
+ * Adds the distributor binding columns to `api_keys`:
34
+ * - `organization_id` / `sales_channel_id` / `customer_account_id` — the
35
+ * all-or-none binding that pins a key to one org, one channel, and one
36
+ * designated service customer account. `ON DELETE RESTRICT`: revoke keys
37
+ * before removing the bound rows.
38
+ * - `expires_at` — optional expiry; NULL = non-expiring. `authenticate()`
39
+ * refuses past instants.
40
+ *
41
+ * Existing rows keep all four columns NULL ⇒ unbound legacy keys, behavior
42
+ * unchanged (FR-020). The cross-row rule "service account belongs to the bound
43
+ * org" is enforced at creation in `ApiKeyService.create` (research §R4), not
44
+ * as a DB constraint.
45
+ */
46
+ export declare class Migration20260724T173916ApiKeysDistributorBinding extends Migration {
47
+ up(): Promise<void>;
48
+ down(): Promise<void>;
49
+ }
50
+ //# sourceMappingURL=20260724T173916_api_keys_distributor_binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260724T173916_api_keys_distributor_binding.d.ts","sourceRoot":"","sources":["../../src/migrations/20260724T173916_api_keys_distributor_binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,qBAAa,iDAAkD,SAAQ,SAAS;IAC/D,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IA4DnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAqBrC"}
@@ -0,0 +1,117 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Feature 062 — Distributor API key binding (data-model.md §1), and since
4
+ * `specs/120-migration-closure-bridge-ownership/` Phase 3 the **creation** of
5
+ * `api_keys` itself.
6
+ *
7
+ * The table was created by `webhooks`' frozen
8
+ * `Migration20260425T091359WebhooksUs7Init`: the two modules were one surface
9
+ * until the US7 split, and the creation stayed behind while the entity, the
10
+ * service and every other statement came here. An instance installing
11
+ * `api_keys` without `webhooks` therefore had no migration that builds the
12
+ * table, and this very migration named a table outside its own closure (D-226).
13
+ *
14
+ * **It moved from one frozen body into another, and that is the whole of
15
+ * FR-014.** D-226's prescribed repair — subtract the statement and re-add it
16
+ * above `BASELINE_THROUGH` — is right for a *reference*, which can only move
17
+ * strictly later in the computed order. It is wrong for a *creation*: an
18
+ * above-watermark migration runs after the entire frozen prefix, and two frozen
19
+ * migrations reference `api_keys` — this one's own `alter table` below, and
20
+ * `Migration20260724T193611OrdersOrderPlacementIntents`' foreign key. The
21
+ * creation would have landed after both and broken a fresh database. This
22
+ * migration's historical position precedes both, so it is where the creation
23
+ * goes. Both class names are unchanged and `BASELINE_MIGRATIONS` is untouched:
24
+ * `mikro_orm_migrations` persists the class name and no checksum, so neither
25
+ * edited body is re-offered to a database that has applied it.
26
+ *
27
+ * **`if not exists`, and it is load-bearing rather than defensive.** A database
28
+ * that applied the April migration and has not yet reached this one — anything
29
+ * behind by a release — already has `api_keys` while this migration is pending.
30
+ * A verbatim `create table` would fail there. With the guard it is a no-op on
31
+ * that database and the creation on a fresh one.
32
+ *
33
+ * Adds the distributor binding columns to `api_keys`:
34
+ * - `organization_id` / `sales_channel_id` / `customer_account_id` — the
35
+ * all-or-none binding that pins a key to one org, one channel, and one
36
+ * designated service customer account. `ON DELETE RESTRICT`: revoke keys
37
+ * before removing the bound rows.
38
+ * - `expires_at` — optional expiry; NULL = non-expiring. `authenticate()`
39
+ * refuses past instants.
40
+ *
41
+ * Existing rows keep all four columns NULL ⇒ unbound legacy keys, behavior
42
+ * unchanged (FR-020). The cross-row rule "service account belongs to the bound
43
+ * org" is enforced at creation in `ApiKeyService.create` (research §R4), not
44
+ * as a DB constraint.
45
+ */
46
+ export class Migration20260724T173916ApiKeysDistributorBinding extends Migration {
47
+ async up() {
48
+ this.addSql(`
49
+ create table if not exists "api_keys" (
50
+ "id" uuid not null,
51
+ "name" varchar(160) not null,
52
+ "key_hash" varchar(128) not null,
53
+ "last_four" varchar(8) not null,
54
+ "scopes" jsonb not null default '[]'::jsonb,
55
+ "status" varchar(16) not null default 'active',
56
+ "last_used_at" timestamptz null,
57
+ "revoked_at" timestamptz null,
58
+ "created_by_admin_user_id" uuid null,
59
+ "created_at" timestamptz not null,
60
+ "updated_at" timestamptz not null,
61
+ constraint "api_keys_pkey" primary key ("id"),
62
+ constraint "api_keys_key_hash_unique" unique ("key_hash")
63
+ );
64
+ `);
65
+ this.addSql('create index if not exists "api_keys_key_hash_index" on "api_keys" ("key_hash");');
66
+ this.addSql(`
67
+ alter table "api_keys"
68
+ add column "organization_id" uuid null,
69
+ add column "sales_channel_id" uuid null,
70
+ add column "customer_account_id" uuid null,
71
+ add column "expires_at" timestamptz null;
72
+ `);
73
+ this.addSql(`
74
+ alter table "api_keys"
75
+ add constraint "api_keys_organization_id_foreign"
76
+ foreign key ("organization_id") references "organizations" ("id")
77
+ on update cascade on delete restrict;
78
+ `);
79
+ this.addSql(`
80
+ alter table "api_keys"
81
+ add constraint "api_keys_sales_channel_id_foreign"
82
+ foreign key ("sales_channel_id") references "sales_channels" ("id")
83
+ on update cascade on delete restrict;
84
+ `);
85
+ this.addSql(`
86
+ alter table "api_keys"
87
+ add constraint "api_keys_customer_account_id_foreign"
88
+ foreign key ("customer_account_id") references "customer_accounts" ("id")
89
+ on update cascade on delete restrict;
90
+ `);
91
+ this.addSql(`
92
+ alter table "api_keys"
93
+ add constraint "api_keys_binding_all_or_none" check (
94
+ ("organization_id" is null and "sales_channel_id" is null and "customer_account_id" is null)
95
+ or
96
+ ("organization_id" is not null and "sales_channel_id" is not null and "customer_account_id" is not null)
97
+ );
98
+ `);
99
+ this.addSql(`create index "api_keys_organization_id_index" on "api_keys" ("organization_id");`);
100
+ }
101
+ async down() {
102
+ this.addSql(`drop index if exists "api_keys_organization_id_index";`);
103
+ this.addSql(`alter table "api_keys" drop constraint if exists "api_keys_binding_all_or_none";`);
104
+ this.addSql(`alter table "api_keys" drop constraint if exists "api_keys_customer_account_id_foreign";`);
105
+ this.addSql(`alter table "api_keys" drop constraint if exists "api_keys_sales_channel_id_foreign";`);
106
+ this.addSql(`alter table "api_keys" drop constraint if exists "api_keys_organization_id_foreign";`);
107
+ this.addSql(`
108
+ alter table "api_keys"
109
+ drop column "organization_id",
110
+ drop column "sales_channel_id",
111
+ drop column "customer_account_id",
112
+ drop column "expires_at";
113
+ `);
114
+ this.addSql('drop table if exists "api_keys" cascade;');
115
+ }
116
+ }
117
+ //# sourceMappingURL=20260724T173916_api_keys_distributor_binding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260724T173916_api_keys_distributor_binding.js","sourceRoot":"","sources":["../../src/migrations/20260724T173916_api_keys_distributor_binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,OAAO,iDAAkD,SAAQ,SAAS;IACrE,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;KAgBX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,kFAAkF,CACnF,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;KAMX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC;;;;;KAKX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC;;;;;KAKX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC;;;;;KAKX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC;;;;;;;KAOX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,kFAAkF,CACnF,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,wDAAwD,CAAC,CAAC;QACtE,IAAI,CAAC,MAAM,CAAC,kFAAkF,CAAC,CAAC;QAChG,IAAI,CAAC,MAAM,CACT,0FAA0F,CAC3F,CAAC;QACF,IAAI,CAAC,MAAM,CACT,uFAAuF,CACxF,CAAC;QACF,IAAI,CAAC,MAAM,CACT,sFAAsF,CACvF,CAAC;QACF,IAAI,CAAC,MAAM,CAAC;;;;;;KAMX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,0CAA0C,CAAC,CAAC;IAC1D,CAAC;CACF"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's.
12
+ *
13
+ * The **named** exports stay beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports each class by name
16
+ * from this specifier, and a migration class name is contract in a way an entity
17
+ * class name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom is
22
+ * a query against a table nobody created.
23
+ */
24
+ import { Migration20260724T173916ApiKeysDistributorBinding } from './20260724T173916_api_keys_distributor_binding.js';
25
+ export declare const migrations: (typeof Migration20260724T173916ApiKeysDistributorBinding)[];
26
+ export { Migration20260724T173916ApiKeysDistributorBinding, };
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,iDAAiD,EAAE,MAAM,mDAAmD,CAAC;AAEtH,eAAO,MAAM,UAAU,8DAEtB,CAAC;AAEF,OAAO,EACL,iDAAiD,GAClD,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's.
12
+ *
13
+ * The **named** exports stay beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports each class by name
16
+ * from this specifier, and a migration class name is contract in a way an entity
17
+ * class name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom is
22
+ * a query against a table nobody created.
23
+ */
24
+ import { Migration20260724T173916ApiKeysDistributorBinding } from './20260724T173916_api_keys_distributor_binding.js';
25
+ export const migrations = [
26
+ Migration20260724T173916ApiKeysDistributorBinding,
27
+ ];
28
+ export { Migration20260724T173916ApiKeysDistributorBinding, };
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,iDAAiD,EAAE,MAAM,mDAAmD,CAAC;AAEtH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,iDAAiD;CAClD,CAAC;AAEF,OAAO,EACL,iDAAiD,GAClD,CAAC"}