@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.
- package/README.md +5 -1
- package/package.json +3 -3
- package/src/__tests__/anonymous-access.integration.test.ts +1 -1
- package/src/__tests__/error-contract.integration.test.ts +1 -1
- package/src/__tests__/fixtures/migrate-open-to-all/manual-only.expected.ts +16 -0
- package/src/__tests__/fixtures/migrate-open-to-all/manual-only.input.ts +16 -0
- package/src/__tests__/fixtures/migrate-open-to-all/test-nested-shorthand.expected.ts +12 -0
- package/src/__tests__/fixtures/migrate-open-to-all/test-nested-shorthand.input.ts +12 -0
- package/src/__tests__/fixtures/migrate-open-to-all/test-simple.expected.ts +7 -0
- package/src/__tests__/fixtures/migrate-open-to-all/test-simple.input.ts +7 -0
- package/src/__tests__/full-stack.integration.test.ts +2 -2
- package/src/__tests__/migrate-open-to-all-codemod.test.ts +84 -0
- package/src/__tests__/transition-guard.integration.test.ts +7 -7
- package/src/api/__tests__/api.test.ts +2 -2
- package/src/api/__tests__/body-limit.test.ts +1 -1
- package/src/api/__tests__/pii-leak-guard.integration.test.ts +2 -2
- package/src/api/__tests__/request-locale.integration.test.ts +1 -1
- package/src/api/__tests__/server-error-logging.test.ts +1 -1
- package/src/changes.json +8 -0
- package/src/db/__tests__/list-filter-field-access.integration.test.ts +1 -1
- package/src/derivatives/__tests__/derivatives-context.integration.test.ts +2 -2
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +5 -5
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-projection-list.test.ts +17 -17
- package/src/engine/__tests__/boot-validator-query-output-schema.test.ts +18 -18
- package/src/engine/__tests__/boot-validator-query-refs.test.ts +3 -3
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +2 -2
- package/src/engine/__tests__/boot-validator.test.ts +18 -18
- package/src/engine/__tests__/build-app-schema.test.ts +10 -6
- package/src/engine/__tests__/build-config-feature-schema-secrets.test.ts +48 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +34 -5
- package/src/engine/__tests__/engine.test.ts +39 -25
- package/src/engine/__tests__/entity-handlers.test.ts +19 -8
- package/src/engine/__tests__/entity-presave-wiring.integration.test.ts +2 -2
- package/src/engine/__tests__/extends-registrar.test.ts +3 -3
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +9 -3
- package/src/engine/__tests__/feature-crud-verb-access.integration.test.ts +1 -1
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/lifecycle-hooks.test.ts +9 -3
- package/src/engine/__tests__/multiselect-filter.integration.test.ts +1 -1
- package/src/engine/__tests__/post-query-hook.test.ts +6 -2
- package/src/engine/__tests__/projection-detail-actions.test.ts +2 -2
- package/src/engine/__tests__/projection-detail-singleton.test.ts +3 -3
- package/src/engine/__tests__/projection-detail-tabs.test.ts +3 -3
- package/src/engine/__tests__/projection-list-filter.integration.test.ts +1 -1
- package/src/engine/__tests__/registry.test.ts +19 -13
- package/src/engine/__tests__/screen.test.ts +26 -20
- package/src/engine/boot-validator/__tests__/access-declarations.test.ts +5 -4
- package/src/engine/boot-validator/__tests__/parent-ref.test.ts +18 -6
- package/src/engine/boot-validator/access-declarations.ts +3 -5
- package/src/engine/boot-validator/nav.ts +4 -3
- package/src/engine/build-config-feature-schema.ts +9 -2
- package/src/engine/entity-handlers.ts +1 -1
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +3 -1
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +2 -2
- package/src/engine/feature-ast/__tests__/parse.test.ts +106 -4
- package/src/engine/feature-ast/__tests__/patcher.test.ts +3 -3
- package/src/engine/feature-ast/__tests__/read-optional-access-rule.test.ts +4 -4
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +5 -1
- package/src/engine/feature-ast/extractors/handlers.ts +16 -2
- package/src/engine/feature-ast/extractors/hooks.ts +25 -4
- package/src/engine/feature-ast/patcher.ts +15 -3
- package/src/engine/feature-ast/patterns.ts +4 -0
- package/src/engine/feature-ast/render.ts +3 -0
- package/src/engine/pattern-library/shared-fields.ts +7 -3
- package/src/entrypoint/__tests__/entrypoint-attach-dispatcher.integration.test.ts +1 -1
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +3 -3
- package/src/entrypoint/__tests__/split-deploy.integration.test.ts +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +2 -2
- package/src/jobs/__tests__/job-event-trigger.integration.test.ts +2 -2
- package/src/jobs/__tests__/job-multi-trigger.integration.test.ts +2 -2
- package/src/jobs/__tests__/jobs.integration.test.ts +1 -1
- package/src/observability/__tests__/observability.integration.test.ts +7 -7
- package/src/pipeline/__tests__/archive-stream.integration.test.ts +2 -2
- package/src/pipeline/__tests__/dispatcher.test.ts +15 -15
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +10 -10
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +1 -1
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/query-projection.integration.test.ts +3 -3
- package/src/scripts/codemod/migrate-open-to-all.ts +248 -0
- package/src/secrets/__tests__/contains-secret.test.ts +5 -5
- package/src/stack/__tests__/request-helper.integration.test.ts +3 -3
- package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +1 -1
- 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: {
|
|
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: {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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({
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
{
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
{
|
|
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:
|
|
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
|
-
{
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
263
|
-
// @ts-expect-error
|
|
264
|
-
const access: AccessRule = { openToAll: true
|
|
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(
|
|
56
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
//
|
|
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
|
|
129
|
-
"
|
|
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
|
|
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
|
|
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)
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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", () => {
|