@cosmicdrift/kumiko-bundled-features 0.305.0 → 0.307.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 (76) hide show
  1. package/package.json +9 -9
  2. package/src/__tests__/extension-points-typed.test.ts +135 -0
  3. package/src/__tests__/use-extension-options.test.ts +278 -0
  4. package/src/auth-email-password/changes.json +5 -0
  5. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +1 -1
  6. package/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
  7. package/src/auth-foundation/__tests__/session-store.test.ts +1 -0
  8. package/src/auth-foundation/__tests__/tenant-providers.test.ts +1 -0
  9. package/src/auth-foundation/__tests__/token-verifier.test.ts +2 -0
  10. package/src/auth-foundation/types.ts +14 -4
  11. package/src/auth-mfa/__tests__/session-scoped-sse-revoke.integration.test.ts +270 -0
  12. package/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -1
  13. package/src/auth-mfa/handlers/verify.write.ts +1 -1
  14. package/src/billing-foundation/__tests__/billing-foundation.integration.test.ts +4 -63
  15. package/src/billing-foundation/__tests__/tenant-destroy.integration.test.ts +199 -0
  16. package/src/billing-foundation/changes.json +6 -0
  17. package/src/billing-foundation/feature.ts +8 -1
  18. package/src/billing-foundation/tenant-destroy-hook.ts +20 -20
  19. package/src/billing-foundation/types.ts +13 -1
  20. package/src/cap-counter/__tests__/enforce-cap.test.ts +7 -0
  21. package/src/cap-counter/changes.json +5 -0
  22. package/src/cap-counter/enforce-cap.ts +8 -1
  23. package/src/channel-email/feature.ts +2 -1
  24. package/src/channel-in-app/feature.ts +2 -1
  25. package/src/channel-push/feature.ts +2 -1
  26. package/src/config/__tests__/reencrypt-job-concurrent-write-race.integration.test.ts +14 -3
  27. package/src/config/__tests__/reencrypt-job-kek-rotation.integration.test.ts +11 -3
  28. package/src/config/__tests__/reencrypt-job-tenant-checks.integration.test.ts +7 -3
  29. package/src/config/__tests__/require-system-db.test.ts +3 -3
  30. package/src/config/handlers/reencrypt.job.ts +4 -8
  31. package/src/custom-fields/db/queries/__tests__/{unsafe-read-retrying.test.ts → unsafe-read-retrying.integration.test.ts} +25 -24
  32. package/src/delivery/__tests__/delivery-screens.boot.test.ts +20 -2
  33. package/src/delivery/__tests__/delivery.integration.test.ts +140 -3
  34. package/src/delivery/constants.ts +4 -0
  35. package/src/delivery/delivery-service.ts +45 -29
  36. package/src/delivery/feature.ts +2 -1
  37. package/src/delivery/index.ts +14 -11
  38. package/src/delivery/types.ts +24 -0
  39. package/src/document-ingest-foundation/__tests__/pages-crypto-shredding.integration.test.ts +0 -27
  40. package/src/document-ingest-foundation/__tests__/tenant-destroy.integration.test.ts +166 -0
  41. package/src/document-ingest-foundation/changes.json +5 -0
  42. package/src/document-ingest-foundation/tenant-destroy-hook.ts +6 -17
  43. package/src/file-derivatives/handlers/public-variant.query.ts +7 -0
  44. package/src/files/__tests__/files.integration.test.ts +2 -2
  45. package/src/form-draft/db/queries/__tests__/{unsafe-read-retrying.test.ts → unsafe-read-retrying.integration.test.ts} +24 -21
  46. package/src/inbound-mail-foundation/__tests__/tenant-destroy.integration.test.ts +173 -0
  47. package/src/inbound-mail-foundation/changes.json +6 -0
  48. package/src/inbound-mail-foundation/feature.ts +4 -0
  49. package/src/inbound-mail-foundation/tenant-destroy-hook.ts +15 -20
  50. package/src/inbound-mail-foundation/types.ts +10 -3
  51. package/src/jobs/__tests__/job-system-user.integration.test.ts +3 -3
  52. package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +15 -6
  53. package/src/mail-foundation/feature.ts +15 -4
  54. package/src/mail-foundation/index.ts +1 -0
  55. package/src/mail-transport-inmemory/feature.ts +5 -4
  56. package/src/mail-transport-smtp/feature.ts +5 -4
  57. package/src/renderer-foundation/__tests__/collect-plugins.integration.test.ts +24 -2
  58. package/src/renderer-foundation/changes.json +8 -1
  59. package/src/renderer-foundation/constants.ts +4 -0
  60. package/src/renderer-foundation/feature.ts +10 -13
  61. package/src/renderer-foundation/index.ts +3 -0
  62. package/src/renderer-foundation/types.ts +23 -1
  63. package/src/renderer-simple/feature.ts +2 -1
  64. package/src/sessions/handlers/revoke-all-others.write.ts +1 -0
  65. package/src/sessions/session-callbacks.ts +1 -0
  66. package/src/sessions/session-revoked-event.ts +3 -0
  67. package/src/tenant/__tests__/multi-tenant.integration.test.ts +2 -2
  68. package/src/tenant-lifecycle/__tests__/stages.test.ts +51 -0
  69. package/src/tenant-lifecycle/stages.ts +20 -18
  70. package/src/user/db/queries/__tests__/unsafe-read-retrying.integration.test.ts +54 -0
  71. package/src/user-data-rights/__tests__/download.integration.test.ts +3 -0
  72. package/src/user-data-rights/lib/default-mailers.ts +2 -1
  73. package/src/workflow-runner/__tests__/event-wakeup.integration.test.ts +6 -2
  74. package/src/workflow-runner/__tests__/resume-loop.integration.test.ts +6 -2
  75. package/src/workflow-runner/__tests__/workflow-runner.integration.test.ts +5 -0
  76. package/src/user/db/queries/__tests__/unsafe-read-retrying.test.ts +0 -52
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.305.0",
3
+ "version": "0.307.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -133,12 +133,12 @@
133
133
  "./workflow-runner": "./src/workflow-runner/index.ts"
134
134
  },
135
135
  "dependencies": {
136
- "@cosmicdrift/kumiko-dispatcher-live": "0.305.0",
137
- "@cosmicdrift/kumiko-framework": "0.305.0",
138
- "@cosmicdrift/kumiko-headless": "0.305.0",
139
- "@cosmicdrift/kumiko-renderer": "0.305.0",
140
- "@cosmicdrift/kumiko-renderer-web": "0.305.0",
141
- "@cosmicdrift/kumiko-types": "0.305.0",
136
+ "@cosmicdrift/kumiko-dispatcher-live": "0.307.0",
137
+ "@cosmicdrift/kumiko-framework": "0.307.0",
138
+ "@cosmicdrift/kumiko-headless": "0.307.0",
139
+ "@cosmicdrift/kumiko-renderer": "0.307.0",
140
+ "@cosmicdrift/kumiko-renderer-web": "0.307.0",
141
+ "@cosmicdrift/kumiko-types": "0.307.0",
142
142
  "@mollie/api-client": "^4.5.0",
143
143
  "@node-rs/argon2": "^2.0.2",
144
144
  "@types/mailparser": "^3.4.6",
@@ -167,8 +167,8 @@
167
167
  ],
168
168
  "devDependencies": {
169
169
  "@testing-library/user-event": "^14.6.1",
170
- "@cosmicdrift/kumiko-locale-de": "0.305.0",
171
- "@cosmicdrift/kumiko-locale-es": "0.305.0",
170
+ "@cosmicdrift/kumiko-locale-de": "0.307.0",
171
+ "@cosmicdrift/kumiko-locale-es": "0.307.0",
172
172
  "jsqr": "^1.4.0"
173
173
  }
174
174
  }
@@ -0,0 +1,135 @@
1
+ // Ratchet: every extension point a bundled feature declares via
2
+ // `r.extendsRegistrar` and whose options are actually read somewhere must be
3
+ // entered into `KumikoExtensionOptionsMap` — see extension-options-map.ts.
4
+ // New points either join TYPED_EXTENSION_POINTS (with the map augmentation)
5
+ // or UNTYPED_BY_DESIGN (with a reason). Anything else fails this test.
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import { readFileSync } from "node:fs";
9
+ import type { KumikoExtensionOptionsMap } from "@cosmicdrift/kumiko-types/extension-options-map";
10
+ import { authFoundationFeature } from "../auth-foundation/feature";
11
+ import { billingFoundationFeature } from "../billing-foundation/feature";
12
+ import { createCustomFieldsFeature } from "../custom-fields/feature";
13
+ import { createDeliveryFeature } from "../delivery/feature";
14
+ import { createFileDerivativesFeature } from "../file-derivatives/feature";
15
+ import { fileFoundationFeature } from "../file-foundation/feature";
16
+ import { inboundMailFoundationFeature } from "../inbound-mail-foundation/feature";
17
+ import { mailFoundationFeature } from "../mail-foundation/feature";
18
+ import { createRendererFoundationFeature } from "../renderer-foundation/feature";
19
+ import { createTenantLifecycleFeature } from "../tenant-lifecycle/feature";
20
+ import { createTierEngineFeature } from "../tier-engine/feature";
21
+ import { createUserFeature } from "../user/feature";
22
+ import { createUserDataRightsFeature } from "../user-data-rights/feature";
23
+
24
+ const TYPED_EXTENSION_POINTS = [
25
+ "userData",
26
+ "tenantData",
27
+ "storageProvider",
28
+ "searchAdapter",
29
+ "externalResource",
30
+ "infraResource",
31
+ "fileProvider",
32
+ "derivativeRenderer",
33
+ "derivativeOverlayResolver",
34
+ "derivativePublicPredicate",
35
+ "principalStatus",
36
+ "tenantLifecycleStatus",
37
+ "tenantTierResolver",
38
+ "tokenVerifier",
39
+ "sessionStore",
40
+ "tenantResolver",
41
+ "tenantExistence",
42
+ "inboundMailProvider",
43
+ "renderer",
44
+ "deliveryChannel",
45
+ "subscriptionProvider",
46
+ "mailTransport",
47
+ ] as const satisfies readonly (keyof KumikoExtensionOptionsMap)[];
48
+
49
+ const UNTYPED_BY_DESIGN: Record<string, string> = {
50
+ customFields: "opt-in marker only — registered options are never read",
51
+ };
52
+
53
+ // Owner-directories that declare at least one extension point via
54
+ // `r.extendsRegistrar` — one entry per feature composed in
55
+ // allRegistrarExtensionNames() below. Catches a new feature that starts
56
+ // calling r.extendsRegistrar without being added to that composition list.
57
+ const DECLARING_FEATURE_DIRS = [
58
+ "auth-foundation",
59
+ "billing-foundation",
60
+ "custom-fields",
61
+ "delivery",
62
+ "file-derivatives",
63
+ "file-foundation",
64
+ "inbound-mail-foundation",
65
+ "mail-foundation",
66
+ "renderer-foundation",
67
+ "tenant-lifecycle",
68
+ "tier-engine",
69
+ "user",
70
+ "user-data-rights",
71
+ ].sort();
72
+
73
+ // Scans bundled-features source (excluding tests) for real `r.extendsRegistrar(`
74
+ // call sites, ignoring occurrences inside line comments (e.g. a doc-comment
75
+ // mentioning the call). Returns the sorted, deduped set of owning top-level
76
+ // directories under packages/bundled-features/src.
77
+ function scanExtendsRegistrarOwnerDirs(): readonly string[] {
78
+ const srcDir = new URL("..", import.meta.url).pathname;
79
+ const glob = new Bun.Glob("**/*.ts");
80
+ const owners = new Set<string>();
81
+ for (const relPath of glob.scanSync({ cwd: srcDir, onlyFiles: true })) {
82
+ if (relPath.includes("__tests__/") || relPath.endsWith(".test.ts")) continue;
83
+ const contents = readFileSync(`${srcDir}${relPath}`, "utf8");
84
+ const hasRealCall = contents
85
+ .split("\n")
86
+ .some((line) => line.split("//")[0]?.includes("r.extendsRegistrar("));
87
+ if (!hasRealCall) continue;
88
+ const topLevelDir = relPath.split("/")[0];
89
+ if (topLevelDir) owners.add(topLevelDir);
90
+ }
91
+ return [...owners].sort();
92
+ }
93
+
94
+ function allRegistrarExtensionNames(): readonly string[] {
95
+ const features = [
96
+ authFoundationFeature,
97
+ billingFoundationFeature,
98
+ createCustomFieldsFeature(),
99
+ createDeliveryFeature(),
100
+ createFileDerivativesFeature(),
101
+ fileFoundationFeature,
102
+ inboundMailFoundationFeature,
103
+ mailFoundationFeature,
104
+ createRendererFoundationFeature(),
105
+ createTenantLifecycleFeature(),
106
+ createTierEngineFeature(),
107
+ createUserFeature(),
108
+ createUserDataRightsFeature(),
109
+ ];
110
+ const names = new Set<string>();
111
+ for (const feature of features) {
112
+ for (const name of Object.keys(feature.registrarExtensions)) {
113
+ names.add(name);
114
+ }
115
+ }
116
+ return [...names];
117
+ }
118
+
119
+ describe("extension-points-typed ratchet", () => {
120
+ test("every declared extension point is either typed or explicitly untyped-by-design", () => {
121
+ const declared = allRegistrarExtensionNames();
122
+ const known = new Set<string>([...TYPED_EXTENSION_POINTS, ...Object.keys(UNTYPED_BY_DESIGN)]);
123
+ const unaccounted = declared.filter((name) => !known.has(name));
124
+ expect(unaccounted).toEqual([]);
125
+ });
126
+
127
+ test("TYPED_EXTENSION_POINTS and UNTYPED_BY_DESIGN don't overlap", () => {
128
+ const overlap = TYPED_EXTENSION_POINTS.filter((name) => name in UNTYPED_BY_DESIGN);
129
+ expect(overlap).toEqual([]);
130
+ });
131
+
132
+ test("every directory that calls r.extendsRegistrar is composed into allRegistrarExtensionNames", () => {
133
+ expect(scanExtendsRegistrarOwnerDirs()).toEqual(DECLARING_FEATURE_DIRS);
134
+ });
135
+ });
@@ -0,0 +1,278 @@
1
+ // Imports only via package specifiers so the KumikoExtensionOptionsMap augmentation is proven to reach
2
+ // consumers. Each @ts-expect-error has a positive control next to it, so an unrelated error cannot satisfy it.
3
+ import { expect, expectTypeOf, test } from "bun:test";
4
+ import { MAIL_TRANSPORT_EXTENSION } from "@cosmicdrift/kumiko-bundled-features/mail-foundation";
5
+ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
6
+ import {
7
+ defineFeature,
8
+ EXT_STORAGE_PROVIDER,
9
+ EXT_TENANT_DATA,
10
+ EXT_USER_DATA,
11
+ type ExtensionOptionsArgs,
12
+ type ExtensionOptionsFor,
13
+ type TenantDataDestroyHook,
14
+ type TenantDataHookCtx,
15
+ type TenantId,
16
+ type TenantResourceDestroyHook,
17
+ type TenantResourceHookCtx,
18
+ type UserDataDeleteHook,
19
+ type UserDataDeleteStrategy,
20
+ type UserDataExportHook,
21
+ type UserDataHookCtx,
22
+ } from "@cosmicdrift/kumiko-framework/engine";
23
+
24
+ test("useExtension accepts a well-typed tenantData destroy hook", () => {
25
+ defineFeature("probe-tenant-data-ok", (r) => {
26
+ r.useExtension(EXT_TENANT_DATA, "x", {
27
+ destroy: async (ctx: TenantDataHookCtx) => {
28
+ expectTypeOf(ctx.db).not.toBeAny();
29
+ },
30
+ });
31
+ });
32
+ });
33
+
34
+ test("useExtension rejects a tenantData destroy hook with the #3196 wrong ctx shape", () => {
35
+ defineFeature("probe-tenant-data-wrong-ctx", (r) => {
36
+ r.useExtension(EXT_TENANT_DATA, "x", {
37
+ // @ts-expect-error #3196: destroy ctx must be TenantDataHookCtx, not a hand-rolled { db: DbRunner; tenantId } — TenantDb is not a DbRunner.
38
+ destroy: async (_ctx: { readonly db: DbRunner; readonly tenantId: TenantId }) => {},
39
+ });
40
+ });
41
+ expectTypeOf<
42
+ (ctx: { readonly db: DbRunner; readonly tenantId: TenantId }) => Promise<void>
43
+ >().not.toExtend<TenantDataDestroyHook>();
44
+ });
45
+
46
+ test("useExtension requires options for a known extension name — omitting them is a type error", () => {
47
+ defineFeature("probe-tenant-data-missing-options", (r) => {
48
+ // @ts-expect-error tenantData's options are mandatory — a bare (name, entity) call registered no hook, silently.
49
+ r.useExtension(EXT_TENANT_DATA, "x");
50
+ });
51
+ expectTypeOf<[]>().not.toExtend<ExtensionOptionsArgs<typeof EXT_TENANT_DATA>>();
52
+ });
53
+
54
+ test("useExtension infers the destroy hook's ctx type from context (positional form, no annotation)", () => {
55
+ defineFeature("probe-tenant-data-contextual", (r) => {
56
+ r.useExtension(EXT_TENANT_DATA, "x", {
57
+ destroy: async (ctx) => {
58
+ expectTypeOf(ctx).toEqualTypeOf<TenantDataHookCtx>();
59
+ },
60
+ });
61
+ });
62
+ });
63
+
64
+ test("useExtension's object form accepts a well-typed tenantData destroy hook", () => {
65
+ defineFeature("probe-tenant-data-object-form-ok", (r) => {
66
+ r.useExtension({
67
+ name: EXT_TENANT_DATA,
68
+ entity: "x",
69
+ destroy: async (ctx: TenantDataHookCtx) => {
70
+ expectTypeOf(ctx.tenantId).toBeString();
71
+ },
72
+ });
73
+ });
74
+ });
75
+
76
+ test("useExtension's object form rejects the #3196 wrong ctx shape", () => {
77
+ defineFeature("probe-tenant-data-object-form-wrong-ctx", (r) => {
78
+ r.useExtension({
79
+ name: EXT_TENANT_DATA,
80
+ entity: "x",
81
+ // @ts-expect-error #3196: same mismatch as the positional form above.
82
+ destroy: async (_ctx: { readonly db: DbRunner; readonly tenantId: TenantId }) => {},
83
+ });
84
+ });
85
+ expectTypeOf<
86
+ (ctx: { readonly db: DbRunner; readonly tenantId: TenantId }) => Promise<void>
87
+ >().not.toExtend<TenantDataDestroyHook>();
88
+ });
89
+
90
+ test("useExtension's object form infers the destroy hook's ctx type from context (no annotation)", () => {
91
+ defineFeature("probe-tenant-data-object-form-contextual", (r) => {
92
+ r.useExtension({
93
+ name: EXT_TENANT_DATA,
94
+ entity: "x",
95
+ destroy: async (ctx) => {
96
+ expectTypeOf(ctx).toEqualTypeOf<TenantDataHookCtx>();
97
+ },
98
+ });
99
+ });
100
+ });
101
+
102
+ test("useExtension accepts a tenantData registration with escapeHatch", () => {
103
+ const feature = defineFeature("probe-tenant-data-escape-hatch", (r) => {
104
+ r.useExtension(EXT_TENANT_DATA, "x", {
105
+ destroy: async (_ctx: TenantDataHookCtx) => {},
106
+ escapeHatch: { reason: "needs raw SQL for a batch delete" },
107
+ });
108
+ });
109
+ expect(feature.extensionUsages).toContainEqual(
110
+ expect.objectContaining({
111
+ extensionName: EXT_TENANT_DATA,
112
+ entityName: "x",
113
+ options: expect.objectContaining({
114
+ escapeHatch: { reason: "needs raw SQL for a batch delete" },
115
+ }),
116
+ }),
117
+ );
118
+ });
119
+
120
+ test("useExtension accepts export-only userData registration (#972)", () => {
121
+ const exportHook: UserDataExportHook = async (_ctx) => null;
122
+ const feature = defineFeature("probe-user-data-export-only", (r) => {
123
+ r.useExtension(EXT_USER_DATA, "x", { export: exportHook });
124
+ });
125
+ expect(feature.extensionUsages).toContainEqual(
126
+ expect.objectContaining({
127
+ extensionName: EXT_USER_DATA,
128
+ entityName: "x",
129
+ options: expect.objectContaining({ export: exportHook }),
130
+ }),
131
+ );
132
+ });
133
+
134
+ test("useExtension rejects a userData export hook with the wrong ctx shape", () => {
135
+ defineFeature("probe-user-data-wrong-ctx", (r) => {
136
+ r.useExtension(EXT_USER_DATA, "x", {
137
+ // @ts-expect-error export ctx must be UserDataHookCtx (ctx.db: TenantDb), not a hand-rolled { db: DbRunner; userId }.
138
+ export: async (_ctx: { readonly db: DbRunner; readonly userId: string }) => null,
139
+ });
140
+ });
141
+ expectTypeOf<
142
+ (ctx: { readonly db: DbRunner; readonly userId: string }) => Promise<null>
143
+ >().not.toExtend<UserDataExportHook>();
144
+ });
145
+
146
+ test("useExtension accepts delete-only userData registration with order", () => {
147
+ const deleteHook: UserDataDeleteHook = async (_ctx, _strategy) => undefined;
148
+ const feature = defineFeature("probe-user-data-delete-only", (r) => {
149
+ r.useExtension(EXT_USER_DATA, "x", { delete: deleteHook, order: 1 });
150
+ });
151
+ expect(feature.extensionUsages).toContainEqual(
152
+ expect.objectContaining({
153
+ extensionName: EXT_USER_DATA,
154
+ entityName: "x",
155
+ options: expect.objectContaining({ delete: deleteHook, order: 1 }),
156
+ }),
157
+ );
158
+ });
159
+
160
+ test("useExtension infers the delete hook's ctx/strategy types from context (no annotation)", () => {
161
+ defineFeature("probe-user-data-delete-contextual", (r) => {
162
+ r.useExtension(EXT_USER_DATA, "x", {
163
+ delete: async (ctx, strategy) => {
164
+ expectTypeOf(ctx).toEqualTypeOf<UserDataHookCtx>();
165
+ expectTypeOf(strategy).toEqualTypeOf<UserDataDeleteStrategy>();
166
+ return undefined;
167
+ },
168
+ });
169
+ });
170
+ });
171
+
172
+ test("useExtension rejects an empty userData registration", () => {
173
+ defineFeature("probe-user-data-empty", (r) => {
174
+ // @ts-expect-error userData needs at least one of export/delete — an empty bag silently registered neither hook.
175
+ r.useExtension(EXT_USER_DATA, "x", {});
176
+ });
177
+ expectTypeOf<Record<string, never>>().not.toExtend<ExtensionOptionsFor<typeof EXT_USER_DATA>>();
178
+ });
179
+
180
+ test("useExtension rejects a userData registration with only order", () => {
181
+ defineFeature("probe-user-data-only-order", (r) => {
182
+ // @ts-expect-error same as the empty-bag case above: order alone registers no hook.
183
+ r.useExtension(EXT_USER_DATA, "x", { order: 1 });
184
+ });
185
+ expectTypeOf<{ readonly order: number }>().not.toExtend<
186
+ ExtensionOptionsFor<typeof EXT_USER_DATA>
187
+ >();
188
+ });
189
+
190
+ test("useExtension accepts an unknown extension name with an arbitrary options bag", () => {
191
+ const feature = defineFeature("probe-unknown-name", (r) => {
192
+ r.useExtension("someAppPoint", "x", { anything: 42, goes: "here" });
193
+ });
194
+ expect(feature.extensionUsages).toContainEqual(
195
+ expect.objectContaining({
196
+ extensionName: "someAppPoint",
197
+ entityName: "x",
198
+ options: { anything: 42, goes: "here" },
199
+ }),
200
+ );
201
+ });
202
+
203
+ test("useExtension accepts an unknown extension name with no options at all", () => {
204
+ const feature = defineFeature("probe-unknown-name-no-options", (r) => {
205
+ r.useExtension("someOtherAppPoint", "x");
206
+ });
207
+ expect(feature.extensionUsages).toContainEqual(
208
+ expect.objectContaining({ extensionName: "someOtherAppPoint", entityName: "x" }),
209
+ );
210
+ });
211
+
212
+ test("useExtension accepts a string-typed extension name with an arbitrary options bag", () => {
213
+ const name: string = "someAppPoint";
214
+ const feature = defineFeature("probe-string-typed-name", (r) => {
215
+ r.useExtension(name, "x", { anything: 42 });
216
+ });
217
+ expect(feature.extensionUsages).toContainEqual(
218
+ expect.objectContaining({
219
+ extensionName: "someAppPoint",
220
+ entityName: "x",
221
+ options: { anything: 42 },
222
+ }),
223
+ );
224
+ });
225
+
226
+ test("useExtension accepts a well-typed storageProvider destroyTenant hook", () => {
227
+ defineFeature("probe-storage-provider-ok", (r) => {
228
+ r.useExtension(EXT_STORAGE_PROVIDER, "x", {
229
+ destroyTenant: async (tenantId: TenantId, ctx: TenantResourceHookCtx) => {
230
+ expectTypeOf(tenantId).toBeString();
231
+ expectTypeOf(ctx.db).not.toBeAny();
232
+ },
233
+ });
234
+ });
235
+ });
236
+
237
+ test("useExtension infers the storageProvider destroyTenant hook's arg types from context", () => {
238
+ defineFeature("probe-storage-provider-contextual", (r) => {
239
+ r.useExtension(EXT_STORAGE_PROVIDER, "x", {
240
+ destroyTenant: async (tenantId, ctx) => {
241
+ expectTypeOf(tenantId).toEqualTypeOf<TenantId>();
242
+ expectTypeOf(ctx).toEqualTypeOf<TenantResourceHookCtx>();
243
+ },
244
+ });
245
+ });
246
+ });
247
+
248
+ test("useExtension rejects a storageProvider destroyTenant hook shaped like the tenantData (ctx)-only hook", () => {
249
+ defineFeature("probe-storage-provider-wrong-shape", (r) => {
250
+ r.useExtension(EXT_STORAGE_PROVIDER, "x", {
251
+ // @ts-expect-error storageProvider's destroyTenant takes (tenantId, ctx) — a tenantData-style (ctx)-only hook is missing the tenantId arg.
252
+ destroyTenant: async (_ctx: TenantDataHookCtx) => {},
253
+ });
254
+ });
255
+ expectTypeOf<
256
+ (ctx: TenantDataHookCtx) => Promise<void>
257
+ >().not.toExtend<TenantResourceDestroyHook>();
258
+ });
259
+
260
+ test("useExtension rejects a storageProvider registration without destroyTenant", () => {
261
+ defineFeature("probe-storage-provider-missing-hook", (r) => {
262
+ // @ts-expect-error storageProvider requires destroyTenant — an empty bag silently registered no cleanup hook.
263
+ r.useExtension(EXT_STORAGE_PROVIDER, "x", {});
264
+ });
265
+ expectTypeOf<Record<string, never>>().not.toExtend<
266
+ ExtensionOptionsFor<typeof EXT_STORAGE_PROVIDER>
267
+ >();
268
+ });
269
+
270
+ test("useExtension rejects a mailTransport registration without build", () => {
271
+ defineFeature("probe-mail-transport-missing-build", (r) => {
272
+ // @ts-expect-error mailTransport requires build — an empty bag silently registered no transport factory.
273
+ r.useExtension(MAIL_TRANSPORT_EXTENSION, "x", {});
274
+ });
275
+ expectTypeOf<Record<string, never>>().not.toExtend<
276
+ ExtensionOptionsFor<typeof MAIL_TRANSPORT_EXTENSION>
277
+ >();
278
+ });
@@ -1,4 +1,9 @@
1
1
  [
2
+ {
3
+ "version": "0.307.0",
4
+ "type": "improvement",
5
+ "title": "Anonymous signup-confirm and invite-signup-complete (auth-email-password) and verify / enable-confirm-preauth (auth-mfa) declare personalData: public-intake, since they write personal data through a TenantDb derived from ctx.db.unsafeRaw (fw#3185)"
6
+ },
2
7
  {
3
8
  "version": "0.305.0",
4
9
  "type": "breaking",
@@ -92,7 +92,7 @@ export function createInviteSignupCompleteHandler() {
92
92
  >({
93
93
  name: "invite-signup-complete",
94
94
  schema: InviteSignupCompleteSchema,
95
- access: { roles: ["anonymous"] },
95
+ access: { roles: ["anonymous"], personalData: "public-intake" },
96
96
  rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
97
97
  agent: { expose: false },
98
98
  escapeHatch: {
@@ -67,7 +67,7 @@ export function createSignupConfirmHandler() {
67
67
  return defineWriteHandler<"signup-confirm", typeof SignupConfirmSchema, SignupConfirmData>({
68
68
  name: "signup-confirm",
69
69
  schema: SignupConfirmSchema,
70
- access: { roles: ["anonymous"] },
70
+ access: { roles: ["anonymous"], personalData: "public-intake" },
71
71
  rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
72
72
  agent: { expose: false },
73
73
  escapeHatch: {
@@ -67,6 +67,7 @@ describe("validateSessionStoreMultiplicity", () => {
67
67
 
68
68
  test("malformed registration (no build) → throws a malformed-plugin error", () => {
69
69
  const broken = defineFeature("mock-broken", (r) => {
70
+ // @ts-expect-error malformed plugin (no build) — proves the runtime guard rejects it too.
70
71
  r.useExtension(EXT_SESSION_STORE, "broken", {});
71
72
  });
72
73
  const registry = createRegistry([authFoundationFeature, broken]);
@@ -92,6 +92,7 @@ describe("validateTenantResolverMultiplicity", () => {
92
92
 
93
93
  test("malformed (no trust) → throws", () => {
94
94
  const broken = defineFeature("mock-broken-resolver", (r) => {
95
+ // @ts-expect-error malformed plugin (no trust) — proves the runtime guard rejects it too.
95
96
  r.useExtension(EXT_TENANT_RESOLVER, "broken", { build: () => () => null });
96
97
  });
97
98
  const registry = createRegistry([authFoundationFeature, broken]);
@@ -127,6 +127,7 @@ describe("validateTokenVerifierMultiplicity", () => {
127
127
 
128
128
  test("wrong-shape registration → throws a malformed-plugin error", () => {
129
129
  const broken = defineFeature("mock-broken", (r) => {
130
+ // @ts-expect-error malformed plugin (no build) — proves the runtime guard rejects it too.
130
131
  r.useExtension(EXT_TOKEN_VERIFIER, "broken", { shape: { kind: "prefix" } });
131
132
  });
132
133
  const registry = createRegistry([authFoundationFeature, broken]);
@@ -138,6 +139,7 @@ describe("validateTokenVerifierMultiplicity", () => {
138
139
  test("malformed shape (build present, prefix missing) → throws a malformed-plugin error", () => {
139
140
  const broken = defineFeature("mock-broken-shape", (r) => {
140
141
  r.useExtension(EXT_TOKEN_VERIFIER, "broken-shape", {
142
+ // @ts-expect-error malformed shape (prefix missing) — proves the runtime guard rejects it too.
141
143
  shape: { kind: "prefix" },
142
144
  build: () => async () => null,
143
145
  });
@@ -22,7 +22,7 @@ import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
22
22
 
23
23
  export type { SessionMassRevoker };
24
24
 
25
- export const EXT_TOKEN_VERIFIER = "tokenVerifier";
25
+ export const EXT_TOKEN_VERIFIER = "tokenVerifier" as const;
26
26
 
27
27
  /**
28
28
  * Deps available to a provider's `build()`. DB access only — a hot-path
@@ -84,7 +84,7 @@ export function tokenShapeKey(shape: TokenShape): string {
84
84
  // single-provider — sessions have no per-provider shape to route on, so
85
85
  // exactly one implementation must be registered (boot-fails on 0 or ≥2).
86
86
 
87
- export const EXT_SESSION_STORE = "sessionStore";
87
+ export const EXT_SESSION_STORE = "sessionStore" as const;
88
88
 
89
89
  /** Keep current sid (if any); revoke every other live session. Used by auth-mfa. */
90
90
  export type SessionRevokeAllOthers = (
@@ -117,8 +117,8 @@ export function isSessionStoreProvider(o: unknown): o is SessionStoreProvider {
117
117
  // optional — zero registered means today's single-tenant / header-cookie
118
118
  // path (no subdomain resolver, no existence check). ≥2 fails boot.
119
119
 
120
- export const EXT_TENANT_RESOLVER = "tenantResolver";
121
- export const EXT_TENANT_EXISTENCE = "tenantExistence";
120
+ export const EXT_TENANT_RESOLVER = "tenantResolver" as const;
121
+ export const EXT_TENANT_EXISTENCE = "tenantExistence" as const;
122
122
 
123
123
  /** Trust mode for a registered tenantResolver — mirrors AnonymousAccessConfig.resolverTrust. */
124
124
  export type TenantResolverTrust = "authoritative" | "fallback-only";
@@ -168,3 +168,13 @@ export function isTenantExistenceProvider(o: unknown): o is TenantExistenceProvi
168
168
  typeof (o as { build: unknown }).build === "function"
169
169
  );
170
170
  }
171
+
172
+ // r.useExtension options-shape for auth-foundation's four extension points.
173
+ declare module "@cosmicdrift/kumiko-framework/engine" {
174
+ interface KumikoExtensionOptionsMap {
175
+ [EXT_TOKEN_VERIFIER]: AuthProviderPlugin;
176
+ [EXT_SESSION_STORE]: SessionStoreProvider;
177
+ [EXT_TENANT_RESOLVER]: TenantResolverProvider;
178
+ [EXT_TENANT_EXISTENCE]: TenantExistenceProvider;
179
+ }
180
+ }