@cosmicdrift/kumiko-framework 0.269.2 → 0.270.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 (84) 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-open-to-all/manual-only.expected.ts +16 -0
  6. package/src/__tests__/fixtures/migrate-open-to-all/manual-only.input.ts +16 -0
  7. package/src/__tests__/fixtures/migrate-open-to-all/test-nested-shorthand.expected.ts +12 -0
  8. package/src/__tests__/fixtures/migrate-open-to-all/test-nested-shorthand.input.ts +12 -0
  9. package/src/__tests__/fixtures/migrate-open-to-all/test-simple.expected.ts +7 -0
  10. package/src/__tests__/fixtures/migrate-open-to-all/test-simple.input.ts +7 -0
  11. package/src/__tests__/full-stack.integration.test.ts +2 -2
  12. package/src/__tests__/migrate-open-to-all-codemod.test.ts +84 -0
  13. package/src/__tests__/transition-guard.integration.test.ts +7 -7
  14. package/src/api/__tests__/api.test.ts +2 -2
  15. package/src/api/__tests__/body-limit.test.ts +1 -1
  16. package/src/api/__tests__/pii-leak-guard.integration.test.ts +2 -2
  17. package/src/api/__tests__/request-locale.integration.test.ts +1 -1
  18. package/src/api/__tests__/server-error-logging.test.ts +1 -1
  19. package/src/changes.json +8 -0
  20. package/src/db/__tests__/list-filter-field-access.integration.test.ts +1 -1
  21. package/src/derivatives/__tests__/derivatives-context.integration.test.ts +2 -2
  22. package/src/engine/__tests__/boot-validator-dashboard.test.ts +5 -5
  23. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +1 -1
  24. package/src/engine/__tests__/boot-validator-projection-list.test.ts +17 -17
  25. package/src/engine/__tests__/boot-validator-query-output-schema.test.ts +18 -18
  26. package/src/engine/__tests__/boot-validator-query-refs.test.ts +3 -3
  27. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +2 -2
  28. package/src/engine/__tests__/boot-validator.test.ts +18 -18
  29. package/src/engine/__tests__/build-app-schema.test.ts +10 -6
  30. package/src/engine/__tests__/build-config-feature-schema-secrets.test.ts +48 -0
  31. package/src/engine/__tests__/build-config-feature-schema.test.ts +34 -5
  32. package/src/engine/__tests__/engine.test.ts +39 -25
  33. package/src/engine/__tests__/entity-handlers.test.ts +19 -8
  34. package/src/engine/__tests__/entity-presave-wiring.integration.test.ts +2 -2
  35. package/src/engine/__tests__/extends-registrar.test.ts +3 -3
  36. package/src/engine/__tests__/feature-crud-shorthand.test.ts +9 -3
  37. package/src/engine/__tests__/feature-crud-verb-access.integration.test.ts +1 -1
  38. package/src/engine/__tests__/hook-phases.test.ts +5 -5
  39. package/src/engine/__tests__/lifecycle-hooks.test.ts +9 -3
  40. package/src/engine/__tests__/multiselect-filter.integration.test.ts +1 -1
  41. package/src/engine/__tests__/post-query-hook.test.ts +6 -2
  42. package/src/engine/__tests__/projection-detail-actions.test.ts +2 -2
  43. package/src/engine/__tests__/projection-detail-singleton.test.ts +3 -3
  44. package/src/engine/__tests__/projection-detail-tabs.test.ts +3 -3
  45. package/src/engine/__tests__/projection-list-filter.integration.test.ts +1 -1
  46. package/src/engine/__tests__/registry.test.ts +19 -13
  47. package/src/engine/__tests__/screen.test.ts +26 -20
  48. package/src/engine/boot-validator/__tests__/access-declarations.test.ts +5 -4
  49. package/src/engine/boot-validator/__tests__/parent-ref.test.ts +18 -6
  50. package/src/engine/boot-validator/access-declarations.ts +3 -5
  51. package/src/engine/boot-validator/nav.ts +4 -3
  52. package/src/engine/build-config-feature-schema.ts +9 -2
  53. package/src/engine/entity-handlers.ts +1 -1
  54. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +3 -1
  55. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +2 -2
  56. package/src/engine/feature-ast/__tests__/parse.test.ts +106 -4
  57. package/src/engine/feature-ast/__tests__/patcher.test.ts +3 -3
  58. package/src/engine/feature-ast/__tests__/read-optional-access-rule.test.ts +4 -4
  59. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +5 -1
  60. package/src/engine/feature-ast/extractors/handlers.ts +16 -2
  61. package/src/engine/feature-ast/extractors/hooks.ts +25 -4
  62. package/src/engine/feature-ast/patcher.ts +15 -3
  63. package/src/engine/feature-ast/patterns.ts +4 -0
  64. package/src/engine/feature-ast/render.ts +3 -0
  65. package/src/engine/pattern-library/shared-fields.ts +7 -3
  66. package/src/entrypoint/__tests__/entrypoint-attach-dispatcher.integration.test.ts +1 -1
  67. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +3 -3
  68. package/src/entrypoint/__tests__/split-deploy.integration.test.ts +1 -1
  69. package/src/es-ops/__tests__/runner.integration.test.ts +2 -2
  70. package/src/jobs/__tests__/job-event-trigger.integration.test.ts +2 -2
  71. package/src/jobs/__tests__/job-multi-trigger.integration.test.ts +2 -2
  72. package/src/jobs/__tests__/jobs.integration.test.ts +1 -1
  73. package/src/observability/__tests__/observability.integration.test.ts +7 -7
  74. package/src/pipeline/__tests__/archive-stream.integration.test.ts +2 -2
  75. package/src/pipeline/__tests__/dispatcher.test.ts +15 -15
  76. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +10 -10
  77. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +1 -1
  78. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  79. package/src/pipeline/__tests__/query-projection.integration.test.ts +3 -3
  80. package/src/scripts/codemod/migrate-open-to-all.ts +248 -0
  81. package/src/secrets/__tests__/contains-secret.test.ts +5 -5
  82. package/src/stack/__tests__/request-helper.integration.test.ts +3 -3
  83. package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +1 -1
  84. package/src/testing/__tests__/e2e-generator.test.ts +19 -5
@@ -78,13 +78,17 @@ describe("getAllQueryHandlers", () => {
78
78
  const taskFeature = defineFeature("registry-test-task", (r) => {
79
79
  r.crud("task", taskEntity, {
80
80
  write: { access: { roles: ["Admin"] } },
81
- read: { access: { openToAll: true } },
81
+ read: {
82
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
83
+ },
82
84
  });
83
85
  });
84
86
  const noteFeature = defineFeature("registry-test-note", (r) => {
85
87
  r.crud("note", noteEntity, {
86
88
  write: { access: { roles: ["Admin"] } },
87
- read: { access: { openToAll: true } },
89
+ read: {
90
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
91
+ },
88
92
  });
89
93
  });
90
94
 
@@ -109,12 +113,12 @@ describe("getAllStreamHandlers", () => {
109
113
  test("returns every registered stream handler, qualified, across multiple features", () => {
110
114
  const aiFeature = defineFeature("registry-test-ai", (r) => {
111
115
  r.streamHandler("chat:complete", z.object({ prompt: z.string() }), async function* () {}, {
112
- access: { openToAll: true },
116
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
113
117
  });
114
118
  });
115
119
  const otherFeature = defineFeature("registry-test-other", (r) => {
116
120
  r.streamHandler("chat:complete", z.object({ prompt: z.string() }), async function* () {}, {
117
- access: { openToAll: true },
121
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
118
122
  });
119
123
  });
120
124
 
@@ -137,12 +141,12 @@ describe("getAllStreamHandlers", () => {
137
141
  test("duplicate stream-handler short-name across features qualifies independently, no collision", () => {
138
142
  const aiFeature = defineFeature("registry-test-dup-a", (r) => {
139
143
  r.streamHandler("chat:complete", z.object({}), async function* () {}, {
140
- access: { openToAll: true },
144
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
141
145
  });
142
146
  });
143
147
  const otherFeature = defineFeature("registry-test-dup-b", (r) => {
144
148
  r.streamHandler("chat:complete", z.object({}), async function* () {}, {
145
- access: { openToAll: true },
149
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
146
150
  });
147
151
  });
148
152
  expect(() => createRegistry([aiFeature, otherFeature])).not.toThrow();
@@ -154,12 +158,12 @@ describe("getAllStreamHandlers", () => {
154
158
  // fire) but toKebab() collapses both to the same qualified name.
155
159
  const featureA = defineFeature("registry-test-kebab-dup", (r) => {
156
160
  r.streamHandler("chat:complete", z.object({}), async function* () {}, {
157
- access: { openToAll: true },
161
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
158
162
  });
159
163
  });
160
164
  const featureB = defineFeature("registryTestKebabDup", (r) => {
161
165
  r.streamHandler("chat:complete", z.object({}), async function* () {}, {
162
- access: { openToAll: true },
166
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
163
167
  });
164
168
  });
165
169
  expect(() => createRegistry([featureA, featureB])).toThrow(/Duplicate stream handler/);
@@ -173,7 +177,7 @@ describe("getAllStreamHandlers", () => {
173
177
  name: "chat:complete",
174
178
  schema,
175
179
  handler: handlerFn,
176
- access: { openToAll: true },
180
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
177
181
  rateLimit: { per: "ip+handler", limit: 5, windowSeconds: 60 },
178
182
  });
179
183
  });
@@ -184,7 +188,9 @@ describe("getAllStreamHandlers", () => {
184
188
  expect(registered).toBeDefined();
185
189
  expect(registered?.schema).toBe(schema);
186
190
  expect(registered?.handler).toBe(handlerFn);
187
- expect(registered?.access).toEqual({ openToAll: true });
191
+ expect(registered?.access).toEqual({
192
+ openToAll: { reason: "test handler callable by any signed-in test user" },
193
+ });
188
194
  expect(registered?.rateLimit).toEqual({ per: "ip+handler", limit: 5, windowSeconds: 60 });
189
195
  });
190
196
  });
@@ -196,7 +202,7 @@ describe("double-qualified handler names (#1991)", () => {
196
202
  "registry-test-ai-orch:query:duplicate-candidates",
197
203
  z.object({}),
198
204
  async () => ({}),
199
- { access: { openToAll: true } },
205
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
200
206
  );
201
207
  });
202
208
 
@@ -206,7 +212,7 @@ describe("double-qualified handler names (#1991)", () => {
206
212
  test("createRegistry allows a sub-structured short name whose entity prefix merely resembles the feature name", () => {
207
213
  const feature = defineFeature("registry-test-invoices", (r) => {
208
214
  r.queryHandler("invoice:mark-paid", z.object({}), async () => ({}), {
209
- access: { openToAll: true },
215
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
210
216
  });
211
217
  });
212
218
 
@@ -309,7 +315,7 @@ describe("extensionSelector boot-validation", () => {
309
315
  "doSomething",
310
316
  z.object({}),
311
317
  async () => ({ isSuccess: true as const, data: {} }),
312
- { access: { openToAll: true } },
318
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
313
319
  );
314
320
  r.useExtension("credit-cap", "credit");
315
321
  });
@@ -211,7 +211,7 @@ describe("r.screen() — registration", () => {
211
211
  const features = [
212
212
  defineFeature("app", (r) => {
213
213
  r.queryHandler("foo:detail", z.object({}), async () => ({}), {
214
- access: { openToAll: true },
214
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
215
215
  });
216
216
  r.screen({
217
217
  id: "x",
@@ -281,10 +281,10 @@ describe("r.screen() — registration", () => {
281
281
  const features = [
282
282
  defineFeature("app", (r) => {
283
283
  r.queryHandler("foo:detail", z.object({}), async () => ({}), {
284
- access: { openToAll: true },
284
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
285
285
  });
286
286
  r.queryHandler("foo:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
287
- access: { openToAll: true },
287
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
288
288
  });
289
289
  r.screen({
290
290
  id: "x",
@@ -345,10 +345,10 @@ describe("r.screen() — registration", () => {
345
345
  const features = [
346
346
  defineFeature("app", (r) => {
347
347
  r.queryHandler("foo:detail", z.object({}), async () => ({}), {
348
- access: { openToAll: true },
348
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
349
349
  });
350
350
  r.queryHandler("foo:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
351
- access: { openToAll: true },
351
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
352
352
  });
353
353
  r.screen({
354
354
  id: "x",
@@ -370,10 +370,10 @@ describe("r.screen() — registration", () => {
370
370
  const features = [
371
371
  defineFeature("app", (r) => {
372
372
  r.queryHandler("foo:detail", z.object({}), async () => ({}), {
373
- access: { openToAll: true },
373
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
374
374
  });
375
375
  r.queryHandler("foo:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
376
- access: { openToAll: true },
376
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
377
377
  });
378
378
  r.screen({
379
379
  id: "x",
@@ -400,13 +400,15 @@ describe("r.screen() — registration", () => {
400
400
  const features = [
401
401
  defineFeature("app", (r) => {
402
402
  r.queryHandler("foo:detail", z.object({}), async () => ({}), {
403
- access: { openToAll: true },
403
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
404
404
  });
405
405
  r.queryHandler(
406
406
  "foo:list",
407
407
  z.object({ search: z.string().optional() }),
408
408
  async () => ({ rows: [], nextCursor: null }),
409
- { access: { openToAll: true } },
409
+ {
410
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
411
+ },
410
412
  );
411
413
  r.screen({
412
414
  id: "x",
@@ -433,10 +435,10 @@ describe("r.screen() — registration", () => {
433
435
  const features = [
434
436
  defineFeature("app", (r) => {
435
437
  r.queryHandler("foo:detail", z.object({}), async () => ({}), {
436
- access: { openToAll: true },
438
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
437
439
  });
438
440
  r.queryHandler("foo:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
439
- access: { openToAll: true },
441
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
440
442
  });
441
443
  r.screen({
442
444
  id: "x",
@@ -470,13 +472,15 @@ describe("r.screen() — registration", () => {
470
472
  const features = [
471
473
  defineFeature("app", (r) => {
472
474
  r.queryHandler("foo:detail", z.object({}), async () => ({}), {
473
- access: { openToAll: true },
475
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
474
476
  });
475
477
  r.queryHandler(
476
478
  "foo:list",
477
479
  z.object({ filters: z.array(z.object({})).optional() }),
478
480
  async () => ({ rows: [], nextCursor: null }),
479
- { access: { openToAll: true } },
481
+ {
482
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
483
+ },
480
484
  );
481
485
  r.screen({
482
486
  id: "x",
@@ -512,13 +516,15 @@ describe("r.screen() — registration", () => {
512
516
  const features = [
513
517
  defineFeature("app", (r) => {
514
518
  r.queryHandler("foo:detail", z.object({}), async () => ({}), {
515
- access: { openToAll: true },
519
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
516
520
  });
517
521
  r.queryHandler(
518
522
  "foo:list",
519
523
  z.object({ filters: z.array(z.object({})).optional() }),
520
524
  async () => ({ rows: [], nextCursor: null }),
521
- { access: { openToAll: true } },
525
+ {
526
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
527
+ },
522
528
  );
523
529
  r.screen({
524
530
  id: "x",
@@ -660,10 +666,10 @@ describe("r.screen() — registration", () => {
660
666
  },
661
667
  );
662
668
  r.queryHandler("foo:detail", z.object({}), async () => ({}), {
663
- access: { openToAll: true },
669
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
664
670
  });
665
671
  r.queryHandler("foo:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
666
- access: { openToAll: true },
672
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
667
673
  });
668
674
  r.screen({
669
675
  id: "x",
@@ -873,7 +879,7 @@ describe("r.screen() — registration", () => {
873
879
  const features = [
874
880
  defineFeature("app", (r) => {
875
881
  r.queryHandler("foo:detail", z.object({}), async () => ({}), {
876
- access: { openToAll: true },
882
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
877
883
  });
878
884
  r.writeHandler(
879
885
  "save",
@@ -917,7 +923,7 @@ describe("r.screen() — registration", () => {
917
923
  // here so the throw below isolates the one key this test is about.
918
924
  r.translations({ keys: { "screen:x.title": { en: "X" } } });
919
925
  r.queryHandler("foo:detail", z.object({}), async () => ({}), {
920
- access: { openToAll: true },
926
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
921
927
  });
922
928
  r.writeHandler(
923
929
  "save",
@@ -959,7 +965,7 @@ describe("r.screen() — registration", () => {
959
965
  },
960
966
  });
961
967
  r.queryHandler("foo:detail", z.object({}), async () => ({}), {
962
- access: { openToAll: true },
968
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
963
969
  });
964
970
  r.writeHandler(
965
971
  "save",
@@ -55,7 +55,8 @@ describe("validateAccessDeclarations", () => {
55
55
  });
56
56
 
57
57
  // 1b. malformed openToAll (from untyped sources) counts as invalid, not a crash.
58
- test.each([{ openToAll: false }, { openToAll: {} }])(
58
+ // `{ openToAll: true }` (deprecated) must fail boot too — it never grants access per isOpenToAllGranted.
59
+ test.each([{ openToAll: false }, { openToAll: {} }, { openToAll: true }])(
59
60
  "malformed openToAll %p throws, naming the handler",
60
61
  (access) => {
61
62
  const feature = defineFeature("notes", (r) => {
@@ -259,9 +260,9 @@ describe("validateAccessDeclarations", () => {
259
260
  expect(() => validateAccessDeclarations(feature)).toThrow(/"email"/);
260
261
  });
261
262
 
262
- test("the deprecated openToAll: true form cannot carry personalData", () => {
263
- // @ts-expect-error personalData lives only on the { reason } object form
264
- const access: AccessRule = { openToAll: true, personalData: "tenant-members" };
263
+ test("the deprecated openToAll: true form no longer type-checks against AccessRule", () => {
264
+ // @ts-expect-error `openToAll: true` was removed from AccessRule — see #2858
265
+ const access: AccessRule = { openToAll: true };
265
266
  expect(access).toBeDefined();
266
267
  });
267
268
 
@@ -52,8 +52,16 @@ describe("validateParentRefs", () => {
52
52
  const entity = joinEntity(validRef);
53
53
  const feature = defineFeature("joins", (r) => {
54
54
  r.entity("join", entity);
55
- r.queryHandler(defineEntityListHandler("join", entity, { access: { openToAll: true } }));
56
- r.queryHandler(defineEntityDetailHandler("join", entity, { access: { openToAll: true } }));
55
+ r.queryHandler(
56
+ defineEntityListHandler("join", entity, {
57
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
58
+ }),
59
+ );
60
+ r.queryHandler(
61
+ defineEntityDetailHandler("join", entity, {
62
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
63
+ }),
64
+ );
57
65
  });
58
66
  expect(() => validate(feature)).not.toThrow();
59
67
  });
@@ -65,7 +73,7 @@ describe("validateParentRefs", () => {
65
73
  name: "host:list",
66
74
  schema: z.object({}),
67
75
  handler: async () => ({ rows: [], nextCursor: null }),
68
- access: { openToAll: true },
76
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
69
77
  });
70
78
  });
71
79
  expect(() => validate(feature)).not.toThrow();
@@ -99,7 +107,11 @@ describe("validateParentRefs", () => {
99
107
  const entity = joinEntity({ ...validRef, allowedTypes: ["host"] });
100
108
  const feature = defineFeature("joins", (r) => {
101
109
  r.entity("join", entity);
102
- r.queryHandler(defineEntityListHandler("join", entity, { access: { openToAll: true } }));
110
+ r.queryHandler(
111
+ defineEntityListHandler("join", entity, {
112
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
113
+ }),
114
+ );
103
115
  });
104
116
  expect(() => validate(feature, [hosts])).not.toThrow();
105
117
  });
@@ -121,7 +133,7 @@ describe("validateParentRefs", () => {
121
133
  name: "join:list",
122
134
  schema: z.object({}),
123
135
  handler: async () => ({ rows: [], nextCursor: null }),
124
- access: { openToAll: true },
136
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
125
137
  });
126
138
  });
127
139
  expect(() => validate(feature)).toThrow(/join:list/);
@@ -134,7 +146,7 @@ describe("validateParentRefs", () => {
134
146
  name: "join:detail",
135
147
  schema: z.object({ id: z.uuid() }),
136
148
  handler: async () => null,
137
- access: { openToAll: true },
149
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
138
150
  });
139
151
  });
140
152
  expect(() => validate(feature)).toThrow(/join:detail/);
@@ -88,12 +88,10 @@ function candidatePersonalFieldNames(
88
88
  return names;
89
89
  }
90
90
 
91
- // A value that is neither `true` nor an object with a non-empty string `reason`
92
- // counts as "empty" — catches malformed openToAll from untyped sources too.
91
+ // Not an object with a non-empty string `reason` (incl. deprecated `openToAll: true`) counts as "empty".
93
92
  function openToAllReasonIsEmpty(access: AccessRule): boolean {
94
93
  if (!("openToAll" in access)) return false;
95
94
  const openToAll: unknown = access.openToAll;
96
- if (openToAll === true) return false;
97
95
  if (typeof openToAll !== "object" || openToAll === null) return true;
98
96
  if (!("reason" in openToAll) || typeof openToAll.reason !== "string") return true;
99
97
  return openToAll.reason.trim().length === 0;
@@ -125,8 +123,8 @@ function validateOpenToAllReason(
125
123
  if (!openToAllReasonIsEmpty(access)) return;
126
124
  throw new Error(
127
125
  `[Feature ${feature.name}] ${kind} handler "${handlerName}" declares an invalid ` +
128
- '{ openToAll: ... } — must be `true` or { reason: "<non-empty explanation>" } ' +
129
- "for why any authenticated user may call this handler.",
126
+ '{ openToAll: ... } — must be { reason: "<non-empty explanation>" } for why any ' +
127
+ "authenticated user may call this handler. `{ openToAll: true }` is no longer accepted.",
130
128
  );
131
129
  }
132
130
 
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { qualifyEntityName } from "../qualified-name";
6
6
  import type { AccessRule, FeatureDefinition, NavDefinition, WorkspaceDefinition } from "../types";
7
+ import { isOpenToAllGranted } from "../types";
7
8
 
8
9
  export function collectWriteHandlerQns(features: readonly FeatureDefinition[]): Set<string> {
9
10
  const set = new Set<string>();
@@ -135,10 +136,10 @@ export function validateNavCycles(
135
136
  }
136
137
  }
137
138
 
138
- // undefined access or `{ openToAll: true }` both mean "visible to everyone"
139
- // — neither has a role-set an inversion check could compare against.
139
+ // undefined or granted openToAll means visible to everyone (no role-set to compare); malformed/denied openToAll returns `[]`, same as an explicit `roles: []`.
140
140
  function navViewerRoles(access: AccessRule | undefined): readonly string[] | undefined {
141
- if (access === undefined || "openToAll" in access) return undefined;
141
+ if (access === undefined) return undefined;
142
+ if ("openToAll" in access) return isOpenToAllGranted(access) ? undefined : [];
142
143
  return access.roles;
143
144
  }
144
145
 
@@ -28,6 +28,7 @@ import type { ConfigKeyDefinition, TranslationKeys } from "./types/config";
28
28
  import type { Registry, SecretKeyDefinition } from "./types/feature";
29
29
  import type { FieldDefinition } from "./types/fields";
30
30
  import type { AccessRule } from "./types/handlers";
31
+ import { isOpenToAllGranted } from "./types/handlers";
31
32
  import type { NavDefinition, NavIconKey } from "./types/nav";
32
33
  import type {
33
34
  ConfigEditScreenDefinition,
@@ -497,13 +498,19 @@ function minMaskOrder(keys: readonly MaskedKey[]): number {
497
498
  // opt-int, und zeigt user-scope (write `all`) jedem. `all` lässt sich in
498
499
  // AccessRule nur als openToAll ausdrücken; der Write bleibt server-seitig
499
500
  // per Key gegated.
501
+ const ALL_ROLE_OPEN_TO_ALL_REASON =
502
+ 'config key declares the "all" role, so every signed-in user of the tenant may read/write it';
503
+
500
504
  // Union der Navs-Access-Regeln: ein openToAll-Nav öffnet das ganze Gate, sonst
501
505
  // die Vereinigung der Rollen. undefined-access-Navs tragen nichts bei.
502
506
  function unionAccessRules(rules: readonly (AccessRule | undefined)[]): AccessRule {
503
507
  const roles: string[] = [];
504
508
  for (const rule of rules) {
505
509
  if (rule === undefined) continue;
506
- if ("openToAll" in rule) return { openToAll: true };
510
+ if ("openToAll" in rule) {
511
+ if (isOpenToAllGranted(rule)) return rule;
512
+ continue;
513
+ }
507
514
  roles.push(...rule.roles);
508
515
  }
509
516
  return rolesToAccess(roles);
@@ -512,6 +519,6 @@ function unionAccessRules(rules: readonly (AccessRule | undefined)[]): AccessRul
512
519
  // `all` lässt sich in AccessRule nur als openToAll ausdrücken; der Write bleibt
513
520
  // server-seitig per Key gegated.
514
521
  function rolesToAccess(roles: readonly string[]): AccessRule {
515
- if (roles.includes("all")) return { openToAll: true };
522
+ if (roles.includes("all")) return { openToAll: { reason: ALL_ROLE_OPEN_TO_ALL_REASON } };
516
523
  return { roles: [...new Set(roles)] };
517
524
  }
@@ -534,7 +534,7 @@ export function createEntityExecutor(
534
534
  //
535
535
  // r.queryHandler(
536
536
  // defineProjectionQueryHandler("revenue:list", "showcase:projection:customer-revenue", {
537
- // access: { openToAll: true },
537
+ // access: { openToAll: { reason: "any signed-in user reviews aggregate revenue" } },
538
538
  // }),
539
539
  // );
540
540
  //
@@ -70,13 +70,14 @@ defineFeature("todoList", (r) => {
70
70
  return { isSuccess: true, data: { id: "x" } };
71
71
  },
72
72
  access: { roles: ["user"] },
73
+ escapeHatch: { reason: "seeds default priority labels via db.global()" },
73
74
  });
74
75
 
75
76
  r.queryHandler({
76
77
  name: "task:list",
77
78
  schema: z.object({}),
78
79
  handler: async (q, ctx) => [],
79
- access: { openToAll: true },
80
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
80
81
  });
81
82
 
82
83
  r.streamHandler({
@@ -94,6 +95,7 @@ defineFeature("todoList", (r) => {
94
95
  handler: async (event, ctx) => {
95
96
  console.log("task saved");
96
97
  },
98
+ escapeHatch: { reason: "reindexes the task search doc via SYSTEM identity" },
97
99
  });
98
100
 
99
101
  r.hook({
@@ -64,7 +64,7 @@ defineFeature("todoList", (r) => {
64
64
  "task:list",
65
65
  z.object({}),
66
66
  async (q, ctx) => [],
67
- { access: { openToAll: true } },
67
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
68
68
  );
69
69
 
70
70
  r.hook("postSave", "task", async (event, ctx) => {
@@ -168,7 +168,7 @@ describe("parseSourceFile against a complete inline-form feature", () => {
168
168
 
169
169
  const queryList = result.patterns.find((p) => p.kind === "queryHandler");
170
170
  expect(queryList).toMatchObject({
171
- access: { openToAll: true },
171
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
172
172
  });
173
173
  });
174
174
 
@@ -1332,6 +1332,37 @@ defineFeature("f", (r) => {
1332
1332
  expect(result.patterns[0]).toMatchObject({ kind: "hook", phase: "afterCommit" });
1333
1333
  });
1334
1334
 
1335
+ test("captures the optional escapeHatch from the options object (positional form)", () => {
1336
+ const result = parseInline(`
1337
+ defineFeature("f", (r) => {
1338
+ r.hook("postSave", "task", (event, ctx) => {}, { escapeHatch: { reason: "reindexes via SYSTEM" } });
1339
+ });
1340
+ `);
1341
+
1342
+ expect(result.patterns[0]).toMatchObject({
1343
+ kind: "hook",
1344
+ escapeHatch: { reason: "reindexes via SYSTEM" },
1345
+ });
1346
+ });
1347
+
1348
+ test("captures the optional escapeHatch from the object form", () => {
1349
+ const result = parseInline(`
1350
+ defineFeature("f", (r) => {
1351
+ r.hook({
1352
+ type: "postSave",
1353
+ target: "task",
1354
+ handler: (event, ctx) => {},
1355
+ escapeHatch: { reason: "reindexes via SYSTEM" },
1356
+ });
1357
+ });
1358
+ `);
1359
+
1360
+ expect(result.patterns[0]).toMatchObject({
1361
+ kind: "hook",
1362
+ escapeHatch: { reason: "reindexes via SYSTEM" },
1363
+ });
1364
+ });
1365
+
1335
1366
  test("rejects an unknown hook type", () => {
1336
1367
  const result = parseInline(`
1337
1368
  defineFeature("f", (r) => {
@@ -1576,7 +1607,7 @@ defineFeature("f", (r) => {
1576
1607
  name: "task:approve",
1577
1608
  schema: z.object({ id: z.string() }),
1578
1609
  handler: async (event, ctx) => ({ isSuccess: true, data: {} }),
1579
- access: { openToAll: true },
1610
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
1580
1611
  });
1581
1612
  });
1582
1613
  `);
@@ -1584,7 +1615,7 @@ defineFeature("f", (r) => {
1584
1615
  expect(result.patterns[0]).toMatchObject({
1585
1616
  kind: "writeHandler",
1586
1617
  handlerName: "task:approve",
1587
- access: { openToAll: true },
1618
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
1588
1619
  });
1589
1620
  });
1590
1621
 
@@ -1678,6 +1709,42 @@ defineFeature("f", (r) => {
1678
1709
  });
1679
1710
  expect(result.errors).toEqual([]);
1680
1711
  });
1712
+
1713
+ test("object form: escapeHatch is extracted", () => {
1714
+ const result = parseInline(`
1715
+ defineFeature("f", (r) => {
1716
+ r.writeHandler({
1717
+ name: "task:reindex",
1718
+ schema: z.object({}),
1719
+ handler: async (event, ctx) => ({ isSuccess: true, data: {} }),
1720
+ access: { roles: ["Admin"] },
1721
+ escapeHatch: { reason: "bulk reindex writes via db.global()" },
1722
+ });
1723
+ });
1724
+ `);
1725
+
1726
+ expect(result.patterns[0]).toMatchObject({
1727
+ kind: "writeHandler",
1728
+ handlerName: "task:reindex",
1729
+ escapeHatch: { reason: "bulk reindex writes via db.global()" },
1730
+ });
1731
+ });
1732
+
1733
+ test("4-argument form: escapeHatch is read from the options object", () => {
1734
+ const result = parseInline(`
1735
+ defineFeature("f", (r) => {
1736
+ r.writeHandler("task:reindex", z.object({}), async (event, ctx) => {
1737
+ return { isSuccess: true, data: {} };
1738
+ }, { access: { roles: ["Admin"] }, escapeHatch: { reason: "bulk reindex writes via db.global()" } });
1739
+ });
1740
+ `);
1741
+
1742
+ expect(result.patterns[0]).toMatchObject({
1743
+ kind: "writeHandler",
1744
+ handlerName: "task:reindex",
1745
+ escapeHatch: { reason: "bulk reindex writes via db.global()" },
1746
+ });
1747
+ });
1681
1748
  });
1682
1749
 
1683
1750
  describe("extractQueryHandler", () => {
@@ -1714,6 +1781,26 @@ defineFeature("f", (r) => {
1714
1781
  agent: { risk: "low" },
1715
1782
  });
1716
1783
  });
1784
+
1785
+ test("object form: escapeHatch is extracted (query handlers may switch to SYSTEM via ctx.queryAs)", () => {
1786
+ const result = parseInline(`
1787
+ defineFeature("f", (r) => {
1788
+ r.queryHandler({
1789
+ name: "task:audit-list",
1790
+ schema: z.object({}),
1791
+ handler: async (q, ctx) => [],
1792
+ access: { roles: ["Admin"] },
1793
+ escapeHatch: { reason: "reads across tenants via ctx.queryAs(SYSTEM, ...)" },
1794
+ });
1795
+ });
1796
+ `);
1797
+
1798
+ expect(result.patterns[0]).toMatchObject({
1799
+ kind: "queryHandler",
1800
+ handlerName: "task:audit-list",
1801
+ escapeHatch: { reason: "reads across tenants via ctx.queryAs(SYSTEM, ...)" },
1802
+ });
1803
+ });
1717
1804
  });
1718
1805
 
1719
1806
  describe("extractStreamHandler", () => {
@@ -1740,7 +1827,7 @@ defineFeature("f", (r) => {
1740
1827
  name: "chat:complete",
1741
1828
  schema: z.object({ prompt: z.string() }),
1742
1829
  handler: async function* (input, ctx) { yield "token"; },
1743
- access: { openToAll: true },
1830
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
1744
1831
  });
1745
1832
  });
1746
1833
  `);
@@ -1748,7 +1835,7 @@ defineFeature("f", (r) => {
1748
1835
  expect(result.patterns[0]).toMatchObject({
1749
1836
  kind: "streamHandler",
1750
1837
  handlerName: "chat:complete",
1751
- access: { openToAll: true },
1838
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
1752
1839
  });
1753
1840
  });
1754
1841
 
@@ -1783,6 +1870,21 @@ defineFeature("f", (r) => {
1783
1870
  expect(result.patterns[0]).not.toHaveProperty("description");
1784
1871
  expect(result.patterns[0]).not.toHaveProperty("agent");
1785
1872
  });
1873
+
1874
+ test("escapeHatch is NOT part of StreamHandlerPattern's runtime shape (StreamHandlerDef has no escapeHatch)", () => {
1875
+ const result = parseInline(`
1876
+ defineFeature("f", (r) => {
1877
+ r.streamHandler({
1878
+ name: "chat:complete",
1879
+ schema: z.object({ prompt: z.string() }),
1880
+ handler: async function* (input, ctx) { yield "token"; },
1881
+ escapeHatch: { reason: "Should be ignored for streamHandler." },
1882
+ });
1883
+ });
1884
+ `);
1885
+
1886
+ expect(result.patterns[0]).not.toHaveProperty("escapeHatch");
1887
+ });
1786
1888
  });
1787
1889
 
1788
1890
  describe("extractJob", () => {