@cosmicdrift/kumiko-bundled-features 0.289.0 → 0.290.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.289.0",
3
+ "version": "0.290.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.289.0",
137
- "@cosmicdrift/kumiko-framework": "0.289.0",
138
- "@cosmicdrift/kumiko-headless": "0.289.0",
139
- "@cosmicdrift/kumiko-renderer": "0.289.0",
140
- "@cosmicdrift/kumiko-renderer-web": "0.289.0",
141
- "@cosmicdrift/kumiko-types": "0.289.0",
136
+ "@cosmicdrift/kumiko-dispatcher-live": "0.290.0",
137
+ "@cosmicdrift/kumiko-framework": "0.290.0",
138
+ "@cosmicdrift/kumiko-headless": "0.290.0",
139
+ "@cosmicdrift/kumiko-renderer": "0.290.0",
140
+ "@cosmicdrift/kumiko-renderer-web": "0.290.0",
141
+ "@cosmicdrift/kumiko-types": "0.290.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.289.0",
171
- "@cosmicdrift/kumiko-locale-es": "0.289.0",
170
+ "@cosmicdrift/kumiko-locale-de": "0.290.0",
171
+ "@cosmicdrift/kumiko-locale-es": "0.290.0",
172
172
  "jsqr": "^1.4.0"
173
173
  }
174
174
  }
@@ -563,6 +563,19 @@ async function readAsMemberErr(
563
563
  );
564
564
  }
565
565
 
566
+ // The request-level teardown gate (auth-middleware) answers 410 before a
567
+ // handler runs, so a teardown tenant can only be probed for the dispatcher's
568
+ // own active-membership rejection through the stack's dispatcher.
569
+ async function readAsMemberErrViaDispatcher(userId: string, targetQn: string) {
570
+ const result = await stack.dispatcher.write(
571
+ "queryasmemberprobe:write:read-as-member",
572
+ { userId, targetQn, payload: {} },
573
+ admin,
574
+ );
575
+ if (result.isSuccess) throw new Error("expected read-as-member to fail");
576
+ return result.error;
577
+ }
578
+
566
579
  async function createNote(ownerId: string, body: string): Promise<void> {
567
580
  await stack.http.writeOk("queryasmemberprobe:write:note:create", { ownerId, body }, admin);
568
581
  }
@@ -693,7 +706,7 @@ describe("ctx.queryAsMember — AccessDenied, same generic error for every rejec
693
706
  await addMembership(userId, TENANT_A);
694
707
  await setTenantStatus(TENANT_A, "destroying");
695
708
 
696
- const err = await readAsMemberErr(userId, WHOAMI_QN);
709
+ const err = await readAsMemberErrViaDispatcher(userId, WHOAMI_QN);
697
710
  expect(err.code).toBe("access_denied");
698
711
  expect(err.message).toBe(GENERIC_MESSAGE);
699
712
  expect(err.details).toEqual(GENERIC_DETAILS);
@@ -707,7 +720,7 @@ describe("ctx.queryAsMember — AccessDenied, same generic error for every rejec
707
720
  await addMembership(userId, TENANT_A);
708
721
  await setTenantStatus(TENANT_A, "destroyRequested");
709
722
 
710
- const err = await readAsMemberErr(userId, WHOAMI_QN);
723
+ const err = await readAsMemberErrViaDispatcher(userId, WHOAMI_QN);
711
724
  expect(err.code).toBe("access_denied");
712
725
  expect(err.message).toBe(GENERIC_MESSAGE);
713
726
  expect(err.details).toEqual(GENERIC_DETAILS);
@@ -375,12 +375,16 @@ describe("mfa verify — re-checks account state the way login.write.ts does", (
375
375
  resetTenantLifecycleGateCacheForTests();
376
376
 
377
377
  try {
378
- const err = await stack.http.writeErr(
378
+ // Via the dispatcher, not HTTP: the request-level teardown gate answers
379
+ // 410 before /api/write reaches the handler, and this test asserts the
380
+ // handler's own challenge-token gate.
381
+ const result = await stack.dispatcher.write(
379
382
  AuthMfaHandlers.verify,
380
383
  { challengeToken, code: currentTotpCode(secret) },
381
384
  GUEST,
382
385
  );
383
- expectErrorIncludes(err, "invalid_challenge_token");
386
+ if (result.isSuccess) throw new Error("expected mfa verify to fail");
387
+ expectErrorIncludes(result.error, "invalid_challenge_token");
384
388
  } finally {
385
389
  await asRawClient(stack.db).unsafe(
386
390
  `UPDATE "${tenantTable.tableName}" SET status = $1 WHERE id = $2`,
@@ -44,6 +44,7 @@ import {
44
44
  } from "../../compliance-profiles";
45
45
  import { createConfigFeature } from "../../config";
46
46
  import { createTenantFeature } from "../../tenant/feature";
47
+ import { tenantEntity } from "../../tenant/schema/tenant";
47
48
  import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
48
49
  import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id";
49
50
  import {
@@ -147,6 +148,7 @@ beforeAll(async () => {
147
148
  // subscriptionsProjectionTable wird von setupTestStack automatisch
148
149
  // gepusht (r.projection mit `table`-Property → auto-push).
149
150
  await createEventsTable(db);
151
+ await unsafeCreateEntityTable(db, tenantEntity);
150
152
  await unsafeCreateEntityTable(db, tenantComplianceProfileEntity);
151
153
  // providerCustomerId/providerSubscriptionId are `personal: "tenant"`
152
154
  // PII-subject fields — no executor wires the KMS automatically here (raw r.projection,
@@ -30,6 +30,7 @@ import {
30
30
  } from "../../compliance-profiles";
31
31
  import { createConfigFeature } from "../../config";
32
32
  import { createTenantFeature } from "../../tenant/feature";
33
+ import { tenantEntity } from "../../tenant/schema/tenant";
33
34
  import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
34
35
  import type { TierMap } from "../../tier-engine/compose-app";
35
36
  import { TierEngineHandlers } from "../../tier-engine/constants";
@@ -112,6 +113,7 @@ beforeAll(async () => {
112
113
  ],
113
114
  });
114
115
  await createEventsTable(stack.db);
116
+ await unsafeCreateEntityTable(stack.db, tenantEntity);
115
117
  await unsafeCreateEntityTable(stack.db, tenantComplianceProfileEntity);
116
118
  await unsafePushTables(stack.db, { tier_assignments: tierAssignmentTable });
117
119
  configurePiiSubjectKms(new InMemoryKmsAdapter());
@@ -10,10 +10,16 @@ import {
10
10
  createInMemoryFileProvider,
11
11
  type InMemoryFileProvider,
12
12
  } from "@cosmicdrift/kumiko-framework/files";
13
- import { setupTestStack, type TestStack, TestUsers } from "@cosmicdrift/kumiko-framework/stack";
13
+ import {
14
+ setupTestStack,
15
+ type TestStack,
16
+ TestUsers,
17
+ unsafeCreateEntityTable,
18
+ } from "@cosmicdrift/kumiko-framework/stack";
14
19
  import { createComplianceProfilesFeature } from "../../compliance-profiles";
15
20
  import { createConfigFeature } from "../../config";
16
21
  import { createTenantFeature } from "../../tenant/feature";
22
+ import { tenantEntity } from "../../tenant/schema/tenant";
17
23
  import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
18
24
  import { documentIngestFoundationFeature } from "../feature";
19
25
 
@@ -48,6 +54,7 @@ beforeAll(async () => {
48
54
  ],
49
55
  files: { storageProvider: provider },
50
56
  });
57
+ await unsafeCreateEntityTable(stack.db, tenantEntity);
51
58
  });
52
59
 
53
60
  afterAll(async () => {
@@ -46,6 +46,7 @@ import {
46
46
  import { createConfigFeature } from "../../config";
47
47
  import { inboundProviderInMemoryFeature } from "../../inbound-provider-inmemory";
48
48
  import { createTenantFeature } from "../../tenant/feature";
49
+ import { tenantEntity } from "../../tenant/schema/tenant";
49
50
  import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
50
51
  import { inboundMessageAggregateId, mailThreadAggregateId } from "../aggregate-id";
51
52
  import { InboundMailFoundationHandlers, InboundMailFoundationQueries } from "../constants";
@@ -83,6 +84,7 @@ beforeAll(async () => {
83
84
  });
84
85
  db = stack.db;
85
86
  await createEventsTable(db);
87
+ await unsafeCreateEntityTable(db, tenantEntity);
86
88
  await unsafeCreateEntityTable(db, tenantComplianceProfileEntity);
87
89
  // Unmanaged direct-write stores — kein r.entity, kein Auto-Push.
88
90
  await unsafeCreateEntityTable(db, syncCursorEntity);
@@ -27,6 +27,7 @@ import {
27
27
  import { createConfigFeature } from "../../config";
28
28
  import { inboundProviderInMemoryFeature } from "../../inbound-provider-inmemory";
29
29
  import { createTenantFeature } from "../../tenant/feature";
30
+ import { tenantEntity } from "../../tenant/schema/tenant";
30
31
  import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
31
32
  import { inboundMessageAggregateId } from "../aggregate-id";
32
33
  import { InboundMailFoundationHandlers } from "../constants";
@@ -51,6 +52,7 @@ beforeAll(async () => {
51
52
  });
52
53
  db = stack.db;
53
54
  await createEventsTable(db);
55
+ await unsafeCreateEntityTable(db, tenantEntity);
54
56
  await unsafeCreateEntityTable(db, tenantComplianceProfileEntity);
55
57
  await unsafeCreateEntityTable(db, syncCursorEntity);
56
58
  await unsafeCreateEntityTable(db, seenMessageEntity);
@@ -33,6 +33,7 @@ import {
33
33
  seedInboundMessage,
34
34
  } from "../../inbound-provider-inmemory/feature";
35
35
  import { createTenantFeature } from "../../tenant/feature";
36
+ import { tenantEntity } from "../../tenant/schema/tenant";
36
37
  import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
37
38
  import {
38
39
  createInboundMailSupervisor,
@@ -65,6 +66,7 @@ beforeAll(async () => {
65
66
  });
66
67
  db = stack.db;
67
68
  await createEventsTable(db);
69
+ await unsafeCreateEntityTable(db, tenantEntity);
68
70
  await unsafeCreateEntityTable(db, tenantComplianceProfileEntity);
69
71
  await unsafeCreateEntityTable(db, syncCursorEntity);
70
72
  await unsafeCreateEntityTable(db, seenMessageEntity);
@@ -33,6 +33,7 @@ import {
33
33
  setupTestStack,
34
34
  type TestStack,
35
35
  testTenantId,
36
+ unsafeCreateEntityTable,
36
37
  } from "@cosmicdrift/kumiko-framework/stack";
37
38
  import type {
38
39
  Payment as MolliePayment,
@@ -42,6 +43,7 @@ import { Hono } from "hono";
42
43
  import { createComplianceProfilesFeature } from "../../compliance-profiles";
43
44
  import { createConfigFeature } from "../../config";
44
45
  import { createTenantFeature } from "../../tenant/feature";
46
+ import { tenantEntity } from "../../tenant/schema/tenant";
45
47
  import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
46
48
  import { createSubscriptionMollieFeature } from "../feature";
47
49
  import type { MollieClientShape } from "../verify-webhook";
@@ -150,6 +152,7 @@ beforeAll(async () => {
150
152
  // subscriptionsProjectionTable wird von setupTestStack automatisch
151
153
  // gepusht (r.projection mit `table`-Property → auto-push).
152
154
  await createEventsTable(db);
155
+ await unsafeCreateEntityTable(db, tenantEntity);
153
156
 
154
157
  webhookApp = new Hono();
155
158
  webhookApp.post(
@@ -29,6 +29,7 @@ import {
29
29
  setupTestStack,
30
30
  type TestStack,
31
31
  testTenantId,
32
+ unsafeCreateEntityTable,
32
33
  unsafePushTables,
33
34
  } from "@cosmicdrift/kumiko-framework/stack";
34
35
  import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-framework/testing";
@@ -45,6 +46,7 @@ import {
45
46
  tenantSecretsTable,
46
47
  } from "../../secrets";
47
48
  import { createTenantFeature } from "../../tenant/feature";
49
+ import { tenantEntity } from "../../tenant/schema/tenant";
48
50
  import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
49
51
  import { createSubscriptionStripeFeature } from "../feature";
50
52
 
@@ -115,6 +117,7 @@ beforeAll(async () => {
115
117
  // gepusht (r.projection mit `table`-Property → auto-push). config +
116
118
  // secrets brauchen ihre Tabellen explizit.
117
119
  await createEventsTable(db);
120
+ await unsafeCreateEntityTable(db, tenantEntity);
118
121
  await unsafePushTables(db, { configValuesTable, tenant_secrets: tenantSecretsTable });
119
122
  // Standalone-secrets-context (gleiche KEK) zum direkten Seeden +
120
123
  // als systemSecrets für die zweite webhook-app.
@@ -504,6 +507,7 @@ describe("scenario 6: billing-live gate end-to-end (#104)", () => {
504
507
  }),
505
508
  });
506
509
  await createEventsTable(gateStack.db);
510
+ await unsafeCreateEntityTable(gateStack.db, tenantEntity);
507
511
  await unsafePushTables(gateStack.db, {
508
512
  configValuesTable,
509
513
  tenant_secrets: tenantSecretsTable,
@@ -469,3 +469,111 @@ describe("tenant-lifecycle :: sweep isolates one tenant's failure from another's
469
469
  expect(eventsB.some((e) => e.type.endsWith("tenant-destruction-stage-succeeded"))).toBe(true);
470
470
  });
471
471
  });
472
+
473
+ // No authConfig here on purpose: buildServer derives the 410 gate from the
474
+ // mounted tenantLifecycleStatus provider. The stacks above keep the explicit
475
+ // auth.resolveTenantLifecycleStatus override covered.
476
+ describe("tenant-lifecycle :: 410 gate derived from the mounted provider", () => {
477
+ let derivedStack: TestStack;
478
+
479
+ beforeAll(async () => {
480
+ const encryption = createTestEnvelopeCipher(randomBytes(32).toString("base64"));
481
+ const resolver = createConfigResolver({ cipher: encryption });
482
+ derivedStack = await setupTestStack({
483
+ features: [
484
+ createConfigFeature(),
485
+ createUserFeature(),
486
+ createTenantFeature(),
487
+ createComplianceProfilesFeature(),
488
+ authFoundationFeature,
489
+ createSessionsFeature(),
490
+ createTenantLifecycleFeature(),
491
+ ],
492
+ extraContext: { configResolver: resolver, configEncryption: encryption },
493
+ });
494
+ await unsafeCreateEntityTable(derivedStack.db, tenantEntity);
495
+ await unsafeCreateEntityTable(derivedStack.db, userSessionEntity);
496
+ await unsafeCreateEntityTable(derivedStack.db, tenantComplianceProfileEntity);
497
+ await unsafeCreateEntityTable(derivedStack.db, tenantMembershipEntity);
498
+ await createEventsTable(derivedStack.db);
499
+ await unsafePushTables(derivedStack.db, { configValuesTable });
500
+ });
501
+
502
+ afterAll(async () => {
503
+ await derivedStack.cleanup();
504
+ });
505
+
506
+ beforeEach(async () => {
507
+ derivedStack.events.reset();
508
+ resetTenantLifecycleGateCacheForTests();
509
+ await resetTestTables(derivedStack.db, [
510
+ tenantTable,
511
+ tenantComplianceProfileTable,
512
+ userSessionTable,
513
+ tenantMembershipsTable,
514
+ eventsTable,
515
+ ]);
516
+ await derivedStack.http.writeOk(
517
+ TenantHandlers.create,
518
+ { id: tenantAdmin.tenantId, key: "acme", name: "ACME Corp" },
519
+ TestUsers.systemAdmin,
520
+ );
521
+ await derivedStack.http.writeOk(SET_PROFILE, { profileKey: "eu-dsgvo" }, tenantAdmin);
522
+ });
523
+
524
+ async function rejectionCode(res: Response): Promise<string | undefined> {
525
+ const body = (await res.json()) as { error?: { code?: string } };
526
+ return body.error?.code;
527
+ }
528
+
529
+ test("active tenant passes the gate", async () => {
530
+ const res = await derivedStack.http.query(TenantQueries.me, {}, tenantAdmin);
531
+ expect(res.status).toBe(200);
532
+ });
533
+
534
+ test("destroyRequested tenant gets 410 tenant_unavailable without any hand-wiring", async () => {
535
+ await derivedStack.http.writeOk(REQUEST, {}, tenantAdmin);
536
+ resetTenantLifecycleGateCacheForTests();
537
+
538
+ const res = await derivedStack.http.query(TenantQueries.me, {}, tenantAdmin);
539
+ expect(res.status).toBe(410);
540
+ expect(await rejectionCode(res)).toBe("tenant_unavailable");
541
+ });
542
+
543
+ test("cancel-destruction stays exempt while destroyRequested", async () => {
544
+ await derivedStack.http.writeOk(REQUEST, {}, tenantAdmin);
545
+ resetTenantLifecycleGateCacheForTests();
546
+
547
+ const cancelled = await derivedStack.http.writeOk<{ status: string }>(CANCEL, {}, tenantAdmin);
548
+ expect(cancelled.status).toBe("active");
549
+ });
550
+
551
+ test("batch mixing cancel-destruction with another write gets 410", async () => {
552
+ await derivedStack.http.writeOk(REQUEST, {}, tenantAdmin);
553
+ resetTenantLifecycleGateCacheForTests();
554
+
555
+ const res = await derivedStack.http.batch(
556
+ [
557
+ { type: CANCEL, payload: {} },
558
+ { type: SET_PROFILE, payload: { profileKey: "eu-dsgvo" } },
559
+ ],
560
+ tenantAdmin,
561
+ );
562
+ expect(res.status).toBe(410);
563
+ expect(await rejectionCode(res)).toBe("tenant_unavailable");
564
+ });
565
+
566
+ test("destroying tenant has no cancel exemption", async () => {
567
+ await updateRows(
568
+ derivedStack.db,
569
+ tenantTable,
570
+ { status: "destroying" },
571
+ { id: tenantAdmin.tenantId },
572
+ );
573
+ resetTenantLifecycleGateCacheForTests();
574
+
575
+ const res = await derivedStack.http.write(CANCEL, {}, tenantAdmin);
576
+ expect(res.status).toBe(410);
577
+ expect(await rejectionCode(res)).toBe("tenant_unavailable");
578
+ });
579
+ });