@cosmicdrift/kumiko-framework 0.269.2 → 0.273.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 (176) hide show
  1. package/README.md +5 -1
  2. package/package.json +3 -3
  3. package/src/__tests__/anonymous-access.integration.test.ts +1 -1
  4. package/src/__tests__/error-contract.integration.test.ts +1 -1
  5. package/src/__tests__/fixtures/migrate-cross-tenant/manual-only.expected.ts +44 -0
  6. package/src/__tests__/fixtures/migrate-cross-tenant/manual-only.input.ts +44 -0
  7. package/src/__tests__/fixtures/migrate-cross-tenant/rewrite.expected.ts +41 -0
  8. package/src/__tests__/fixtures/migrate-cross-tenant/rewrite.input.ts +41 -0
  9. package/src/__tests__/fixtures/migrate-open-to-all/manual-only.expected.ts +16 -0
  10. package/src/__tests__/fixtures/migrate-open-to-all/manual-only.input.ts +16 -0
  11. package/src/__tests__/fixtures/migrate-open-to-all/test-nested-shorthand.expected.ts +12 -0
  12. package/src/__tests__/fixtures/migrate-open-to-all/test-nested-shorthand.input.ts +12 -0
  13. package/src/__tests__/fixtures/migrate-open-to-all/test-simple.expected.ts +7 -0
  14. package/src/__tests__/fixtures/migrate-open-to-all/test-simple.input.ts +7 -0
  15. package/src/__tests__/full-stack.integration.test.ts +2 -2
  16. package/src/__tests__/migrate-cross-tenant-codemod.test.ts +106 -0
  17. package/src/__tests__/migrate-open-to-all-codemod.test.ts +84 -0
  18. package/src/__tests__/related-list-parent-filter.integration.test.ts +103 -0
  19. package/src/__tests__/transition-guard.integration.test.ts +7 -7
  20. package/src/api/__tests__/api.test.ts +2 -2
  21. package/src/api/__tests__/body-limit.test.ts +1 -1
  22. package/src/api/__tests__/nested-write-ownership.integration.test.ts +268 -0
  23. package/src/api/__tests__/pii-leak-guard.integration.test.ts +2 -2
  24. package/src/api/__tests__/request-locale.integration.test.ts +1 -1
  25. package/src/api/__tests__/server-error-logging.test.ts +1 -1
  26. package/src/api/server.ts +7 -20
  27. package/src/bun-db/__tests__/query-guards.test.ts +60 -3
  28. package/src/bun-db/query.ts +24 -21
  29. package/src/changes.json +99 -0
  30. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +77 -0
  31. package/src/db/__tests__/event-store-executor.integration.test.ts +1 -1
  32. package/src/db/__tests__/list-filter-field-access.integration.test.ts +1 -1
  33. package/src/db/__tests__/replay-migration-sql.test.ts +17 -0
  34. package/src/db/__tests__/tenant-db-convention-cross-tenant.test.ts +72 -0
  35. package/src/db/__tests__/tenant-db-declared-unsafe-raw.test.ts +104 -0
  36. package/src/db/__tests__/tenant-db-global.test.ts +92 -0
  37. package/src/db/__tests__/tenant-db-no-raw.test.ts +75 -0
  38. package/src/db/__tests__/tenant-db.integration.test.ts +3 -1
  39. package/src/db/__tests__/unchecked-system-db.test.ts +76 -0
  40. package/src/db/apply-entity-event.ts +1 -1
  41. package/src/db/assert-exists-in.ts +1 -1
  42. package/src/db/event-store-executor-context.ts +5 -2
  43. package/src/db/event-store-executor-read.ts +6 -4
  44. package/src/db/event-store-executor-write.ts +24 -18
  45. package/src/db/queries/shadow-swap.ts +36 -0
  46. package/src/db/replay-migration-sql.ts +5 -5
  47. package/src/db/tenant-db-runner.ts +23 -0
  48. package/src/db/tenant-db.ts +97 -24
  49. package/src/derivatives/__tests__/derivatives-context.integration.test.ts +2 -2
  50. package/src/derivatives/__tests__/derivatives-context.test.ts +0 -1
  51. package/src/engine/__tests__/boot-validator-dashboard.test.ts +5 -5
  52. package/src/engine/__tests__/boot-validator-nav-area.test.ts +116 -0
  53. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +1 -1
  54. package/src/engine/__tests__/boot-validator-projection-list.test.ts +108 -17
  55. package/src/engine/__tests__/boot-validator-query-output-schema.test.ts +161 -18
  56. package/src/engine/__tests__/boot-validator-query-refs.test.ts +3 -3
  57. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +2 -2
  58. package/src/engine/__tests__/boot-validator.test.ts +69 -18
  59. package/src/engine/__tests__/build-app-schema.test.ts +10 -6
  60. package/src/engine/__tests__/build-config-feature-schema-secrets.test.ts +48 -0
  61. package/src/engine/__tests__/build-config-feature-schema.test.ts +34 -5
  62. package/src/engine/__tests__/engine.test.ts +39 -25
  63. package/src/engine/__tests__/entity-create-caller-id.integration.test.ts +87 -0
  64. package/src/engine/__tests__/entity-handlers-escape-hatch.test.ts +91 -0
  65. package/src/engine/__tests__/entity-handlers.test.ts +47 -8
  66. package/src/engine/__tests__/entity-presave-wiring.integration.test.ts +2 -2
  67. package/src/engine/__tests__/entity-restore-tenant-isolation.integration.test.ts +30 -5
  68. package/src/engine/__tests__/entity-write-crosstenant.integration.test.ts +206 -47
  69. package/src/engine/__tests__/extends-registrar.test.ts +3 -3
  70. package/src/engine/__tests__/feature-crud-shorthand.test.ts +9 -3
  71. package/src/engine/__tests__/feature-crud-verb-access.integration.test.ts +1 -1
  72. package/src/engine/__tests__/hook-phases.test.ts +5 -5
  73. package/src/engine/__tests__/lifecycle-hooks.test.ts +9 -3
  74. package/src/engine/__tests__/multiselect-filter.integration.test.ts +1 -1
  75. package/src/engine/__tests__/post-query-hook.test.ts +6 -2
  76. package/src/engine/__tests__/projection-detail-actions.test.ts +2 -2
  77. package/src/engine/__tests__/projection-detail-singleton.test.ts +3 -3
  78. package/src/engine/__tests__/projection-detail-tabs.test.ts +3 -3
  79. package/src/engine/__tests__/projection-list-filter.integration.test.ts +1 -1
  80. package/src/engine/__tests__/read-steps-tenant-filter.integration.test.ts +215 -0
  81. package/src/engine/__tests__/registry.test.ts +19 -13
  82. package/src/engine/__tests__/required-surface-keys.test.ts +16 -0
  83. package/src/engine/__tests__/screen.test.ts +176 -20
  84. package/src/engine/__tests__/soft-delete-cleanup.test.ts +0 -1
  85. package/src/engine/__tests__/steps-read.test.ts +204 -8
  86. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +7 -4
  87. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +6 -3
  88. package/src/engine/boot-validator/__tests__/access-declarations.test.ts +5 -4
  89. package/src/engine/boot-validator/__tests__/global-tenancy.test.ts +16 -1
  90. package/src/engine/boot-validator/__tests__/parent-ref.test.ts +18 -6
  91. package/src/engine/boot-validator/__tests__/rate-limit-disabled-reason.test.ts +45 -0
  92. package/src/engine/boot-validator/access-declarations.ts +3 -5
  93. package/src/engine/boot-validator/entity-handler.ts +21 -0
  94. package/src/engine/boot-validator/global-tenancy.ts +13 -1
  95. package/src/engine/boot-validator/nav.ts +4 -3
  96. package/src/engine/boot-validator/parent-ref.ts +1 -1
  97. package/src/engine/boot-validator/projection-list-screens.ts +67 -2
  98. package/src/engine/boot-validator/query-output-columns.ts +33 -0
  99. package/src/engine/boot-validator/screens.ts +154 -31
  100. package/src/engine/build-config-feature-schema.ts +12 -2
  101. package/src/engine/entity-handlers.ts +72 -20
  102. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +3 -1
  103. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +2 -2
  104. package/src/engine/feature-ast/__tests__/parse.test.ts +106 -4
  105. package/src/engine/feature-ast/__tests__/patcher.test.ts +3 -3
  106. package/src/engine/feature-ast/__tests__/read-optional-access-rule.test.ts +4 -4
  107. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +5 -1
  108. package/src/engine/feature-ast/extractors/handlers.ts +16 -2
  109. package/src/engine/feature-ast/extractors/hooks.ts +25 -4
  110. package/src/engine/feature-ast/patcher.ts +15 -3
  111. package/src/engine/feature-ast/patterns.ts +4 -0
  112. package/src/engine/feature-ast/render.ts +3 -0
  113. package/src/engine/feature-entity-handlers.ts +4 -4
  114. package/src/engine/index.ts +5 -0
  115. package/src/engine/pattern-library/shared-fields.ts +7 -3
  116. package/src/engine/registry-validate.ts +12 -4
  117. package/src/engine/schema-builder.ts +6 -0
  118. package/src/engine/screen-helpers.ts +52 -0
  119. package/src/engine/steps/_read-source.ts +12 -0
  120. package/src/engine/steps/read-find-many.ts +8 -2
  121. package/src/engine/steps/read-find-one.ts +11 -8
  122. package/src/engine/steps/unsafe-projection-delete.ts +2 -1
  123. package/src/engine/steps/unsafe-projection-upsert.ts +2 -1
  124. package/src/engine/types/index.ts +8 -1
  125. package/src/entrypoint/__tests__/entrypoint-attach-dispatcher.integration.test.ts +1 -1
  126. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +3 -3
  127. package/src/entrypoint/__tests__/split-deploy.integration.test.ts +1 -1
  128. package/src/errors/i18n/de.yaml +14 -0
  129. package/src/errors/i18n/en.yaml +13 -0
  130. package/src/errors/reasons.ts +4 -0
  131. package/src/es-ops/__tests__/runner.integration.test.ts +2 -2
  132. package/src/i18n/required-surface-keys.ts +6 -2
  133. package/src/jobs/__tests__/job-event-trigger.integration.test.ts +2 -2
  134. package/src/jobs/__tests__/job-multi-trigger.integration.test.ts +2 -2
  135. package/src/jobs/__tests__/job-systemdb.integration.test.ts +5 -4
  136. package/src/jobs/__tests__/jobs.integration.test.ts +42 -1
  137. package/src/jobs/job-runner.ts +35 -5
  138. package/src/observability/__tests__/observability.integration.test.ts +7 -7
  139. package/src/pipeline/__tests__/archive-stream.integration.test.ts +2 -2
  140. package/src/pipeline/__tests__/dispatcher.test.ts +15 -15
  141. package/src/pipeline/__tests__/escape-hatch-audit.integration.test.ts +254 -0
  142. package/src/pipeline/__tests__/escape-hatch-report.test.ts +149 -0
  143. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +10 -10
  144. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +1 -1
  145. package/src/pipeline/__tests__/msp-rebuild.integration.test.ts +28 -0
  146. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  147. package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +94 -0
  148. package/src/pipeline/__tests__/projections-runner-systemscope.integration.test.ts +101 -0
  149. package/src/pipeline/__tests__/query-projection.integration.test.ts +172 -17
  150. package/src/pipeline/__tests__/rate-limit-default.integration.test.ts +118 -0
  151. package/src/pipeline/__tests__/rate-limit-default.test.ts +117 -0
  152. package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +5 -2
  153. package/src/pipeline/__tests__/system-identity-switch.test.ts +240 -1
  154. package/src/pipeline/dispatch-query.ts +27 -8
  155. package/src/pipeline/dispatch-shared.ts +141 -57
  156. package/src/pipeline/dispatch-stream.ts +1 -3
  157. package/src/pipeline/dispatch-write.ts +62 -14
  158. package/src/pipeline/dispatcher.ts +20 -0
  159. package/src/pipeline/escape-hatch-report.ts +127 -0
  160. package/src/pipeline/index.ts +8 -0
  161. package/src/pipeline/member-read-only-transaction.ts +75 -0
  162. package/src/pipeline/msp-rebuild.ts +2 -0
  163. package/src/pipeline/projection-rebuild.ts +2 -0
  164. package/src/pipeline/projections-runner.ts +15 -13
  165. package/src/pipeline/system-identity-switch.ts +121 -9
  166. package/src/scripts/codemod/migrate-cross-tenant.ts +335 -0
  167. package/src/scripts/codemod/migrate-db-raw.ts +19 -6
  168. package/src/scripts/codemod/migrate-open-to-all.ts +249 -0
  169. package/src/secrets/__tests__/contains-secret.test.ts +5 -5
  170. package/src/stack/__tests__/request-helper.integration.test.ts +3 -3
  171. package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +1 -1
  172. package/src/testing/__tests__/e2e-generator.test.ts +19 -5
  173. package/src/ui-types/index.ts +3 -0
  174. package/src/utils/__tests__/ids.test.ts +29 -1
  175. package/src/utils/ids.ts +10 -1
  176. package/src/utils/index.ts +1 -1
@@ -42,7 +42,7 @@ function stubListHandler(
42
42
  name: `${entityName}:list`,
43
43
  schema: z.object({}),
44
44
  handler: async () => ({ rows: [], nextCursor: null }) as never,
45
- access: { openToAll: true },
45
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
46
46
  });
47
47
  }
48
48
 
@@ -534,7 +534,7 @@ describe("boot-validator", () => {
534
534
  isSuccess: true as const,
535
535
  data: { id: "1" },
536
536
  }),
537
- { access: { openToAll: true } },
537
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
538
538
  );
539
539
  }),
540
540
  ];
@@ -566,7 +566,7 @@ describe("boot-validator", () => {
566
566
  isSuccess: true as const,
567
567
  data: {},
568
568
  }),
569
- { access: { openToAll: true } },
569
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
570
570
  );
571
571
  r.useExtension("credit-cap", "credit");
572
572
  });
@@ -631,7 +631,7 @@ describe("boot-validator", () => {
631
631
  const features = [
632
632
  defineFeature("a", (r) => {
633
633
  r.queryHandler("list", z.object({}), async () => [], {
634
- access: { openToAll: true },
634
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
635
635
  });
636
636
  }),
637
637
  ];
@@ -711,7 +711,7 @@ describe("boot-validator", () => {
711
711
  const features = [
712
712
  defineFeature("a", (r) => {
713
713
  r.streamHandler("chat:complete", z.object({}), async function* () {}, {
714
- access: { openToAll: true },
714
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
715
715
  });
716
716
  }),
717
717
  ];
@@ -2403,6 +2403,10 @@ describe("boot-validator", () => {
2403
2403
  readonly sections?: ReadonlyArray<{
2404
2404
  readonly title: string;
2405
2405
  readonly fields: readonly string[];
2406
+ readonly groups?: ReadonlyArray<{
2407
+ readonly title: string;
2408
+ readonly fields: readonly string[];
2409
+ }>;
2406
2410
  }>;
2407
2411
  readonly redirect?: string | { readonly screen: string; readonly idFrom: string };
2408
2412
  readonly cancelTarget?: string | false;
@@ -2430,7 +2434,7 @@ describe("boot-validator", () => {
2430
2434
  name: "invoice:approve",
2431
2435
  schema: { _type: "stub" } as never,
2432
2436
  handler: async () => ({ isSuccess: true, data: {} }) as never,
2433
- access: { openToAll: true },
2437
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
2434
2438
  });
2435
2439
  r.screen({
2436
2440
  id: "approve-invoice",
@@ -2501,6 +2505,53 @@ describe("boot-validator", () => {
2501
2505
  ).toThrow(/layout references unknown field "ghost"/);
2502
2506
  });
2503
2507
 
2508
+ test("section mit fields UND groups → Throw", () => {
2509
+ expect(() =>
2510
+ validateBoot([
2511
+ makeFeature({
2512
+ sections: [
2513
+ {
2514
+ title: "Approval",
2515
+ fields: ["note"],
2516
+ groups: [{ title: "Priority", fields: ["priority"] }],
2517
+ },
2518
+ ],
2519
+ }),
2520
+ ]),
2521
+ ).toThrow(/declares both fields and groups/);
2522
+ });
2523
+
2524
+ test("group referenziert unknown field → Throw", () => {
2525
+ expect(() =>
2526
+ validateBoot([
2527
+ makeFeature({
2528
+ sections: [
2529
+ { title: "Approval", fields: [], groups: [{ title: "Priority", fields: ["ghost"] }] },
2530
+ ],
2531
+ }),
2532
+ ]),
2533
+ ).toThrow(/layout references unknown field "ghost"/);
2534
+ });
2535
+
2536
+ test("valide groups → kein Throw", () => {
2537
+ expect(() =>
2538
+ validateBoot([
2539
+ makeFeature({
2540
+ sections: [
2541
+ {
2542
+ title: "Approval",
2543
+ fields: [],
2544
+ groups: [
2545
+ { title: "Basics", fields: ["note"] },
2546
+ { title: "Priority", fields: ["priority"] },
2547
+ ],
2548
+ },
2549
+ ],
2550
+ }),
2551
+ ]),
2552
+ ).not.toThrow();
2553
+ });
2554
+
2504
2555
  test("redirect → existing screen-id im selben feature → kein Throw", () => {
2505
2556
  expect(() =>
2506
2557
  validateBoot([makeFeature({ redirect: "after-form", extraScreens: ["after-form"] })]),
@@ -2713,14 +2764,14 @@ describe("boot-validator", () => {
2713
2764
  name: "token:mint",
2714
2765
  schema: { _type: "stub" } as never,
2715
2766
  handler: async () => ({ isSuccess: true, data: {} }) as never,
2716
- access: { openToAll: true },
2767
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
2717
2768
  });
2718
2769
  if (confirmOverride !== undefined) {
2719
2770
  r.writeHandler({
2720
2771
  name: "token:confirm",
2721
2772
  schema: { _type: "stub" } as never,
2722
2773
  handler: async () => ({ isSuccess: true, data: {} }) as never,
2723
- access: { openToAll: true },
2774
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
2724
2775
  });
2725
2776
  }
2726
2777
  r.screen({
@@ -4177,7 +4228,7 @@ describe("boot-validator", () => {
4177
4228
  }),
4178
4229
  );
4179
4230
  r.queryHandler("products", z.object({}), async () => ({ rows: [], nextCursor: null }), {
4180
- access: { openToAll: true },
4231
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
4181
4232
  });
4182
4233
  r.screen({
4183
4234
  id: "product-projection",
@@ -4207,7 +4258,7 @@ describe("boot-validator", () => {
4207
4258
  }),
4208
4259
  );
4209
4260
  r.queryHandler("products", z.object({}), async () => ({ rows: [], nextCursor: null }), {
4210
- access: { openToAll: true },
4261
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
4211
4262
  });
4212
4263
  r.screen({
4213
4264
  id: "product-projection",
@@ -4426,13 +4477,13 @@ describe("boot-validator", () => {
4426
4477
  test("projectionDetail relatedList defaultSort.field sortable → kein Throw", () => {
4427
4478
  const feature = defineFeature("shop", (r) => {
4428
4479
  r.queryHandler("order-detail", z.object({}), async () => ({ id: "o1" }), {
4429
- access: { openToAll: true },
4480
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
4430
4481
  });
4431
4482
  r.queryHandler(
4432
4483
  "order-positions",
4433
4484
  z.object({}),
4434
4485
  async () => ({ rows: [], nextCursor: null }),
4435
- { access: { openToAll: true } },
4486
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
4436
4487
  );
4437
4488
  r.screen({
4438
4489
  id: "order-detail",
@@ -4993,7 +5044,7 @@ describe("boot-validator", () => {
4993
5044
  const targetId = opts.targetId ?? "restock-form";
4994
5045
  return defineFeature("shop", (r) => {
4995
5046
  r.queryHandler("products", z.object({}), async () => ({ rows: [], nextCursor: null }), {
4996
- access: { openToAll: true },
5047
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
4997
5048
  });
4998
5049
  r.writeHandler(
4999
5050
  "restock",
@@ -5223,7 +5274,7 @@ describe("boot-validator — config key backing × scope", () => {
5223
5274
  ],
5224
5275
  });
5225
5276
  r.queryHandler("count", z.object({}), async () => ({ total: 0 }), {
5226
- access: { openToAll: true },
5277
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
5227
5278
  });
5228
5279
  r.screen({
5229
5280
  id: "product-dashboard",
@@ -5268,7 +5319,7 @@ describe("boot-validator — config key backing × scope", () => {
5268
5319
  ],
5269
5320
  });
5270
5321
  r.queryHandler("count", z.object({}), async () => ({ total: 0 }), {
5271
- access: { openToAll: true },
5322
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
5272
5323
  });
5273
5324
  r.screen({
5274
5325
  id: "product-dashboard",
@@ -5317,7 +5368,7 @@ describe("boot-validator — config key backing × scope", () => {
5317
5368
  ],
5318
5369
  });
5319
5370
  r.queryHandler("count", z.object({}), async () => ({ total: 0 }), {
5320
- access: { openToAll: true },
5371
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
5321
5372
  });
5322
5373
  r.screen({
5323
5374
  id: "product-dashboard",
@@ -5542,7 +5593,7 @@ describe("boot-validator — config key backing × scope", () => {
5542
5593
  }),
5543
5594
  );
5544
5595
  r.queryHandler("products", z.object({}), async () => ({ rows: [], nextCursor: null }), {
5545
- access: { openToAll: true },
5596
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
5546
5597
  });
5547
5598
  r.screen({
5548
5599
  id: "product-projection",
@@ -5624,7 +5675,7 @@ describe("boot-validator — config key backing × scope", () => {
5624
5675
  "create",
5625
5676
  z.object({ name: z.string() }),
5626
5677
  async () => ({ isSuccess: true as const, data: { id: "1" } }),
5627
- { access: { openToAll: true } },
5678
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
5628
5679
  );
5629
5680
  r.screen({
5630
5681
  id: "product-list",
@@ -209,7 +209,7 @@ describe("buildAppSchema", () => {
209
209
  r.workspace({
210
210
  id: "admin",
211
211
  label: "Admin",
212
- access: { openToAll: true },
212
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
213
213
  nav: ["orders:nav:list", "fleet:nav:vehicles"],
214
214
  default: true,
215
215
  });
@@ -832,7 +832,11 @@ describe("buildAppSchema", () => {
832
832
  const f = defineFeature("ent", (r) => {
833
833
  r.entity("thing", entity);
834
834
  r.nav({ id: "n", label: "N" });
835
- r.workspace({ id: "ws", label: "Ws", access: { openToAll: true } });
835
+ r.workspace({
836
+ id: "ws",
837
+ label: "Ws",
838
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
839
+ });
836
840
  });
837
841
  const app = buildAppSchema(createRegistry([f]));
838
842
  const json = JSON.stringify(app);
@@ -980,7 +984,7 @@ describe("buildAppSchema", () => {
980
984
  cursor: z.string().optional(),
981
985
  }),
982
986
  async () => ({ rows: [], nextCursor: null }),
983
- { access: { openToAll: true } },
987
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
984
988
  );
985
989
  r.screen({
986
990
  id: "schedule-list",
@@ -1000,7 +1004,7 @@ describe("buildAppSchema", () => {
1000
1004
  test("projectionList: a query schema without search/sort/cursor derives no capability", () => {
1001
1005
  const f = defineFeature("ledger", (r) => {
1002
1006
  r.queryHandler("schedule:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
1003
- access: { openToAll: true },
1007
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
1004
1008
  });
1005
1009
  r.screen({
1006
1010
  id: "schedule-list",
@@ -1023,7 +1027,7 @@ describe("buildAppSchema", () => {
1023
1027
  "schedule:list",
1024
1028
  z.union([z.object({ a: z.string() }), z.object({ b: z.string() })]),
1025
1029
  async () => ({ rows: [], nextCursor: null }),
1026
- { access: { openToAll: true } },
1030
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
1027
1031
  );
1028
1032
  r.screen({
1029
1033
  id: "schedule-list",
@@ -1052,7 +1056,7 @@ describe("buildAppSchema", () => {
1052
1056
  rows: [],
1053
1057
  nextCursor: null,
1054
1058
  }),
1055
- { access: { openToAll: true } },
1059
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
1056
1060
  );
1057
1061
  r.screen({
1058
1062
  id: "schedule-list",
@@ -1,8 +1,10 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { z } from "zod";
3
3
  import { buildConfigFeatureSchema } from "../build-config-feature-schema";
4
+ import { createTenantConfig } from "../config-helpers";
4
5
  import { defineFeature } from "../define-feature";
5
6
  import { createRegistry } from "../registry";
7
+ import type { AccessRule } from "../types/handlers";
6
8
  import type { NavDefinition } from "../types/nav";
7
9
  import type { ScreenDefinition, SecretsEditScreenDefinition } from "../types/screen";
8
10
 
@@ -114,6 +116,52 @@ describe("buildConfigFeatureSchema — secrets derivation", () => {
114
116
  expect(navById(schema, "audience-tenant")?.access).toEqual({ roles: customRoles });
115
117
  });
116
118
 
119
+ test("audience nav union keeps a granted openToAll's own reason, not the generic ALL_ROLE reason", () => {
120
+ const customReason = "billing key rotation is safe for any signed-in tenant member";
121
+ const secretsOpenToAll = defineFeature("secrets", (r) => {
122
+ r.writeHandler(
123
+ "set",
124
+ z.object({ key: z.string(), value: z.string() }),
125
+ async () => ({ isSuccess: true, data: null }),
126
+ { access: { openToAll: { reason: customReason } } },
127
+ );
128
+ });
129
+ const billing = defineFeature("billing", (r) => {
130
+ r.config({
131
+ keys: { apiKey: createTenantConfig("text", { mask: { title: "billing.api-key" } }) },
132
+ });
133
+ r.secret("apiKey", { label: { en: "Billing API Key" }, scope: "tenant" });
134
+ });
135
+ const schema = buildConfigFeatureSchema(createRegistry([secretsOpenToAll, billing]));
136
+
137
+ expect(navById(schema, "audience-tenant")?.access).toEqual({
138
+ openToAll: { reason: customReason },
139
+ });
140
+ });
141
+
142
+ test("audience nav union ignores a malformed openToAll — falls back to the role union", () => {
143
+ const secretsMalformedOpenToAll = defineFeature("secrets", (r) => {
144
+ r.writeHandler(
145
+ "set",
146
+ z.object({ key: z.string(), value: z.string() }),
147
+ async () => ({ isSuccess: true, data: null }),
148
+ // @cast-boundary test — simulates JSON/Designer input that doesn't match the static union
149
+ { access: { openToAll: {} } as unknown as AccessRule },
150
+ );
151
+ });
152
+ const billing = defineFeature("billing", (r) => {
153
+ r.config({
154
+ keys: { apiKey: createTenantConfig("text", { mask: { title: "billing.api-key" } }) },
155
+ });
156
+ r.secret("apiKey", { label: { en: "Billing API Key" }, scope: "tenant" });
157
+ });
158
+ const schema = buildConfigFeatureSchema(createRegistry([secretsMalformedOpenToAll, billing]));
159
+
160
+ expect(navById(schema, "audience-tenant")?.access).toEqual({
161
+ roles: ["TenantAdmin", "Admin", "SystemAdmin"],
162
+ });
163
+ });
164
+
117
165
  test("declared secrets without a mounted secrets feature yield no secretsEdit screen and no secrets nav", () => {
118
166
  const stripe = defineFeature("stripe", (r) => {
119
167
  r.secret("apiKey", { label: { en: "Stripe API Key" }, scope: "tenant" });
@@ -8,6 +8,7 @@ import {
8
8
  } from "../config-helpers";
9
9
  import { defineFeature } from "../define-feature";
10
10
  import { createRegistry } from "../registry";
11
+ import { isOpenToAllGranted } from "../types/handlers";
11
12
  import type { NavDefinition } from "../types/nav";
12
13
  import type { ConfigEditScreenDefinition, ScreenDefinition } from "../types/screen";
13
14
 
@@ -265,7 +266,15 @@ describe("buildConfigFeatureSchema — access + workspace", () => {
265
266
  roles: ["TenantAdmin", "Admin", "SystemAdmin"],
266
267
  });
267
268
  // digest is createUserConfig → write access.all (["all"]) → openToAll
268
- expect(configScreen("notify-user").access).toEqual({ openToAll: true });
269
+ const notifyUserAccess = configScreen("notify-user").access;
270
+ expect(notifyUserAccess).toEqual({
271
+ openToAll: {
272
+ reason:
273
+ 'config key declares the "all" role, so every signed-in user of the tenant may read/write it',
274
+ },
275
+ });
276
+ // The synthesized reason must actually grant access, not just look configured.
277
+ expect(notifyUserAccess !== undefined && isOpenToAllGranted(notifyUserAccess)).toBe(true);
269
278
  });
270
279
 
271
280
  test("a screen mixing an openToAll key with role-restricted keys collapses to openToAll", () => {
@@ -283,8 +292,18 @@ describe("buildConfigFeatureSchema — access + workspace", () => {
283
292
  const screen = out.screens.find((s) => s.id === "mixedwrite-tenant");
284
293
  if (screen?.type !== "configEdit")
285
294
  throw new Error('expected configEdit screen "mixedwrite-tenant"');
286
- expect(screen.access).toEqual({ openToAll: true });
287
- expect(out.navs.find((n) => n.id === "audience-tenant")?.access).toEqual({ openToAll: true });
295
+ expect(screen.access).toEqual({
296
+ openToAll: {
297
+ reason:
298
+ 'config key declares the "all" role, so every signed-in user of the tenant may read/write it',
299
+ },
300
+ });
301
+ expect(out.navs.find((n) => n.id === "audience-tenant")?.access).toEqual({
302
+ openToAll: {
303
+ reason:
304
+ 'config key declares the "all" role, so every signed-in user of the tenant may read/write it',
305
+ },
306
+ });
288
307
  });
289
308
 
290
309
  test("openToAll collapse holds regardless of key declaration order (516/2)", () => {
@@ -305,7 +324,12 @@ describe("buildConfigFeatureSchema — access + workspace", () => {
305
324
  const screen = out.screens.find((s) => s.id === "mixedwritereversed-tenant");
306
325
  if (screen?.type !== "configEdit")
307
326
  throw new Error('expected configEdit screen "mixedwritereversed-tenant"');
308
- expect(screen.access).toEqual({ openToAll: true });
327
+ expect(screen.access).toEqual({
328
+ openToAll: {
329
+ reason:
330
+ 'config key declares the "all" role, so every signed-in user of the tenant may read/write it',
331
+ },
332
+ });
309
333
  });
310
334
 
311
335
  test("returns empty (no workspace) when no key opts into the hub via mask", () => {
@@ -343,7 +367,12 @@ describe("buildConfigFeatureSchema — access + workspace", () => {
343
367
  test("workspace access is the union of write roles across all hub keys", () => {
344
368
  // billing/notify tenant keys → admin write; notify-user digest → write all.
345
369
  // Any `all`-writable key collapses the whole switcher entry to openToAll.
346
- expect(schema.workspace?.definition.access).toEqual({ openToAll: true });
370
+ expect(schema.workspace?.definition.access).toEqual({
371
+ openToAll: {
372
+ reason:
373
+ 'config key declares the "all" role, so every signed-in user of the tenant may read/write it',
374
+ },
375
+ });
347
376
  });
348
377
 
349
378
  test("workspace access stays role-gated when no hub key is world-writable", () => {
@@ -87,7 +87,7 @@ describe("defineFeature", () => {
87
87
  const _email: string = event.payload.email;
88
88
  return { isSuccess: true, data: { id: 1 } };
89
89
  },
90
- { access: { openToAll: true } },
90
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
91
91
  );
92
92
  });
93
93
 
@@ -105,7 +105,7 @@ describe("defineFeature", () => {
105
105
  isSuccess: true,
106
106
  data: null,
107
107
  }),
108
- { access: { openToAll: true } },
108
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
109
109
  );
110
110
  });
111
111
  expect(ref?.name).toBe("order:create");
@@ -115,7 +115,7 @@ describe("defineFeature", () => {
115
115
  let ref: { name: string } | undefined;
116
116
  defineFeature("test", (r) => {
117
117
  ref = r.queryHandler("order:list", z.object({}), async () => [], {
118
- access: { openToAll: true },
118
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
119
119
  });
120
120
  });
121
121
  expect(ref?.name).toBe("order:list");
@@ -130,13 +130,15 @@ describe("defineFeature", () => {
130
130
  async function* (query) {
131
131
  yield `echo:${query.payload.prompt}`;
132
132
  },
133
- { access: { openToAll: true } },
133
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
134
134
  );
135
135
  });
136
136
  expect(ref?.name).toBe("chat:complete");
137
137
 
138
138
  const stored = feature.streamHandlers["chat:complete"];
139
- expect(stored?.access).toEqual({ openToAll: true });
139
+ expect(stored?.access).toEqual({
140
+ openToAll: { reason: "test handler callable by any signed-in test user" },
141
+ });
140
142
 
141
143
  const chunks: string[] = [];
142
144
  for await (const chunk of stored!.handler(
@@ -225,7 +227,7 @@ describe("defineFeature", () => {
225
227
  const _id: number = query.payload.userId;
226
228
  return { id: _id, email: "test@test.de" };
227
229
  },
228
- { access: { openToAll: true } },
230
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
229
231
  );
230
232
  });
231
233
 
@@ -239,7 +241,7 @@ describe("defineFeature", () => {
239
241
  handler: async () => {
240
242
  return [{ id: 1, email: "test@test.de" }];
241
243
  },
242
- access: { openToAll: true },
244
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
243
245
  });
244
246
 
245
247
  const feature = defineFeature("test", (r) => {
@@ -402,12 +404,12 @@ describe("createRegistry", () => {
402
404
  test("looks up handlers across features", () => {
403
405
  const f1 = defineFeature("admin", (r) => {
404
406
  r.writeHandler("user:invite", z.object({}), async () => ({ isSuccess: true, data: null }), {
405
- access: { openToAll: true },
407
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
406
408
  });
407
409
  });
408
410
  const f2 = defineFeature("profile", (r) => {
409
411
  r.queryHandler("profile:me", z.object({}), async () => ({ id: 1 }), {
410
- access: { openToAll: true },
412
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
411
413
  });
412
414
  });
413
415
 
@@ -442,12 +444,12 @@ describe("createRegistry", () => {
442
444
  test("different features can have same handler short name (prefixed differently)", () => {
443
445
  const f1 = defineFeature("a", (r) => {
444
446
  r.writeHandler("user:invite", z.object({}), async () => ({ isSuccess: true, data: null }), {
445
- access: { openToAll: true },
447
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
446
448
  });
447
449
  });
448
450
  const f2 = defineFeature("b", (r) => {
449
451
  r.writeHandler("user:invite", z.object({}), async () => ({ isSuccess: true, data: null }), {
450
- access: { openToAll: true },
452
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
451
453
  });
452
454
  });
453
455
 
@@ -513,7 +515,7 @@ describe("createRegistry", () => {
513
515
  );
514
516
  // Handler name "promote" has no entity prefix → can't be mapped
515
517
  r.writeHandler("promote", z.object({}), async () => ({ isSuccess: true, data: null }), {
516
- access: { openToAll: true },
518
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
517
519
  });
518
520
  });
519
521
 
@@ -531,7 +533,7 @@ describe("createRegistry", () => {
531
533
  );
532
534
  // No field-access rules on entity → "reset" without entity prefix is fine
533
535
  r.writeHandler("reset", z.object({}), async () => ({ isSuccess: true, data: null }), {
534
- access: { openToAll: true },
536
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
535
537
  });
536
538
  });
537
539
 
@@ -562,7 +564,7 @@ describe("createRegistry", () => {
562
564
  isSuccess: true,
563
565
  data: null,
564
566
  }),
565
- { access: { openToAll: true } },
567
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
566
568
  );
567
569
  });
568
570
 
@@ -591,10 +593,12 @@ describe("createRegistry", () => {
591
593
  isSuccess: true,
592
594
  data: null,
593
595
  }),
594
- { access: { openToAll: true } },
596
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
595
597
  );
596
598
  // Typo: "emp" instead of "employee"
597
- r.queryHandler("emp:list", z.object({}), async () => [], { access: { openToAll: true } });
599
+ r.queryHandler("emp:list", z.object({}), async () => [], {
600
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
601
+ });
598
602
  });
599
603
 
600
604
  expect(() => createRegistry([feature])).toThrow(/emp:list.*entity-bound.*no matching entity/i);
@@ -622,13 +626,15 @@ describe("createRegistry", () => {
622
626
  isSuccess: true,
623
627
  data: null,
624
628
  }),
625
- { access: { openToAll: true } },
629
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
626
630
  );
627
631
  // Standalone queries — no dot, intentionally not entity-bound
628
632
  r.queryHandler("dashboard", z.object({}), async () => ({ total: 42 }), {
629
- access: { openToAll: true },
633
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
634
+ });
635
+ r.queryHandler("orgChart", z.object({}), async () => [], {
636
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
630
637
  });
631
- r.queryHandler("orgChart", z.object({}), async () => [], { access: { openToAll: true } });
632
638
  });
633
639
 
634
640
  expect(() => createRegistry([feature])).not.toThrow();
@@ -924,12 +930,20 @@ describe("hasAccess", () => {
924
930
 
925
931
  test("openToAll grants access to any authenticated user", () => {
926
932
  const user = createTestUser({ roles: ["Employee"] });
927
- expect(hasAccess(user, { openToAll: true })).toBe(true);
933
+ expect(
934
+ hasAccess(user, {
935
+ openToAll: { reason: "test handler callable by any signed-in test user" },
936
+ }),
937
+ ).toBe(true);
928
938
  });
929
939
 
930
940
  test("openToAll grants access even to user with no roles", () => {
931
941
  const user = createTestUser({ roles: [] });
932
- expect(hasAccess(user, { openToAll: true })).toBe(true);
942
+ expect(
943
+ hasAccess(user, {
944
+ openToAll: { reason: "test handler callable by any signed-in test user" },
945
+ }),
946
+ ).toBe(true);
933
947
  });
934
948
 
935
949
  test.each([{ openToAll: false }, { openToAll: {} }])(
@@ -2105,10 +2119,10 @@ describe("registry boot validation", () => {
2105
2119
  const feature = defineFeature("shop", (r) => {
2106
2120
  r.entity("order", createEntity({ table: "Orders", fields: {} }));
2107
2121
  r.writeHandler("order:create", z.object({}), async () => ({ isSuccess: true, data: null }), {
2108
- access: { openToAll: true },
2122
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
2109
2123
  });
2110
2124
  r.writeHandler("order:cancel", z.object({}), async () => ({ isSuccess: true, data: null }), {
2111
- access: { openToAll: true },
2125
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
2112
2126
  });
2113
2127
  // Ein Job-Body, zwei Trigger — DRY-Pattern für Fanout-Cases.
2114
2128
  r.job(
@@ -2129,7 +2143,7 @@ describe("registry boot validation", () => {
2129
2143
  const feature = defineFeature("shop", (r) => {
2130
2144
  r.entity("order", createEntity({ table: "Orders", fields: {} }));
2131
2145
  r.writeHandler("order:create", z.object({}), async () => ({ isSuccess: true, data: null }), {
2132
- access: { openToAll: true },
2146
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
2133
2147
  });
2134
2148
  // create existiert, cancel nicht — zweiter Trigger ist Geist
2135
2149
  r.job(
@@ -2153,7 +2167,7 @@ describe("registry boot validation", () => {
2153
2167
  const feature = defineFeature("test", (r) => {
2154
2168
  r.entity("item", createEntity({ table: "Items", fields: {} }));
2155
2169
  r.writeHandler("item.create", z.object({}), async () => ({ isSuccess: true, data: null }), {
2156
- access: { openToAll: true },
2170
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
2157
2171
  });
2158
2172
  r.hook("postSave", "item.create", async () => {});
2159
2173
  });