@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
@@ -33,7 +33,7 @@ const echoFeature = defineFeature("echo", (r) => {
33
33
  items: [],
34
34
  search: query.payload.search,
35
35
  }),
36
- { access: { openToAll: true } },
36
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
37
37
  );
38
38
 
39
39
  r.streamHandler(
@@ -137,7 +137,7 @@ describe("dispatcher.write", () => {
137
137
  z.object({ name: z.string() }),
138
138
  async (event) => ({ isSuccess: true, data: { name: event.payload.name } }),
139
139
  {
140
- access: { openToAll: true },
140
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
141
141
  rateLimit: { per: "ip+handler", limit: 3, windowSeconds: 60 },
142
142
  },
143
143
  );
@@ -168,7 +168,7 @@ describe("dispatcher.write", () => {
168
168
  z.object({ name: z.string() }),
169
169
  async (event) => ({ isSuccess: true, data: { name: event.payload.name } }),
170
170
  {
171
- access: { openToAll: true },
171
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
172
172
  rateLimit: { per: "user", limit: 3, windowSeconds: 60 },
173
173
  },
174
174
  );
@@ -428,7 +428,7 @@ describe("dispatcher.stream", () => {
428
428
  yield { i };
429
429
  }
430
430
  },
431
- { access: { openToAll: true } },
431
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
432
432
  );
433
433
  });
434
434
  const dispatcher = createDispatcher(createRegistry([nestedFeature]), {
@@ -466,7 +466,7 @@ describe("dispatcher.stream", () => {
466
466
  throw new Error("close-boom");
467
467
  }
468
468
  },
469
- { access: { openToAll: true } },
469
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
470
470
  );
471
471
  });
472
472
  const dispatcher = createDispatcher(createRegistry([closeFailFeature]), {
@@ -500,7 +500,7 @@ describe("dispatcher.stream", () => {
500
500
  yield { i: 0 };
501
501
  yield { i: 1 };
502
502
  },
503
- { access: { openToAll: true } },
503
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
504
504
  );
505
505
  });
506
506
  const dispatcher = createDispatcher(createRegistry([feature]), {
@@ -536,7 +536,7 @@ describe("dispatcher.stream", () => {
536
536
  yield { i: 1 };
537
537
  yield { i: 2 };
538
538
  },
539
- { access: { openToAll: true } },
539
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
540
540
  );
541
541
  });
542
542
  const dispatcher = createDispatcher(createRegistry([feature]), {
@@ -571,7 +571,7 @@ describe("dispatcher.query postQuery hooks", () => {
571
571
  // pass on entity-existence makes such a hook register silently + never run.
572
572
  const feature = defineFeature("dash", (r) => {
573
573
  r.queryHandler("dashboard", z.object({}), async () => ({ count: 1 }), {
574
- access: { openToAll: true },
574
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
575
575
  });
576
576
  r.hook("postQuery", "dashboard", async ({ entityName, rows }) => {
577
577
  expect(entityName).toBeUndefined();
@@ -590,7 +590,7 @@ describe("dispatcher.query postQuery hooks", () => {
590
590
  // [...null]). The hook here returns the row unchanged so the shape survives.
591
591
  const feature = defineFeature("nullrows", (r) => {
592
592
  r.queryHandler("dashboard", z.object({}), async () => ({ rows: null, nextCursor: null }), {
593
- access: { openToAll: true },
593
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
594
594
  });
595
595
  r.hook("postQuery", "dashboard", async ({ rows }) => ({ rows }));
596
596
  });
@@ -603,7 +603,7 @@ describe("dispatcher.query postQuery hooks", () => {
603
603
  test("single-object postQuery hook returning ≠1 row throws", async () => {
604
604
  const feature = defineFeature("multi", (r) => {
605
605
  r.queryHandler("dashboard", z.object({}), async () => ({ count: 1 }), {
606
- access: { openToAll: true },
606
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
607
607
  });
608
608
  r.hook("postQuery", "dashboard", async ({ rows }) => ({ rows: [...rows, ...rows] }));
609
609
  });
@@ -708,7 +708,7 @@ describe("dispatcher feature-gate", () => {
708
708
  }),
709
709
  );
710
710
  r.queryHandler("widget:list", z.object({}).passthrough(), async () => ({ items: [] }), {
711
- access: { openToAll: true },
711
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
712
712
  });
713
713
  r.writeHandler(
714
714
  "widget:create",
@@ -728,7 +728,7 @@ describe("dispatcher feature-gate", () => {
728
728
  i++;
729
729
  }
730
730
  },
731
- { access: { openToAll: true } },
731
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
732
732
  );
733
733
  });
734
734
  }
@@ -900,7 +900,7 @@ describe("dispatcher feature-gate", () => {
900
900
  "check",
901
901
  z.object({}).passthrough(),
902
902
  async (_event, ctx) => ({ enabled: await ctx.hasFeature("toggled") }),
903
- { access: { openToAll: true } },
903
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
904
904
  );
905
905
  });
906
906
  const registry = createRegistry([toggled(), probe]);
@@ -940,7 +940,7 @@ describe("dispatcher feature-gate", () => {
940
940
  "check",
941
941
  z.object({}).passthrough(),
942
942
  async (_event, ctx) => ({ enabled: await ctx.hasFeature("premium-companion") }),
943
- { access: { openToAll: true } },
943
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
944
944
  );
945
945
  });
946
946
  const registry = createRegistry([probe]);
@@ -1027,7 +1027,7 @@ describe("dispatcher context.geoTzProvider (680/1)", () => {
1027
1027
  longitude: query.payload.longitude,
1028
1028
  }),
1029
1029
  }),
1030
- { access: { openToAll: true } },
1030
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
1031
1031
  );
1032
1032
  });
1033
1033
  const registry = createRegistry([geoFeature]);
@@ -24,7 +24,7 @@ function makeRegistry(hooks?: { preSave?: PreSaveHookFn[]; postSave?: PostSaveHo
24
24
  );
25
25
  // Dummy handler so hook targets resolve (boot validation requires it)
26
26
  r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
27
- access: { openToAll: true },
27
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
28
28
  });
29
29
  if (hooks?.preSave) {
30
30
  for (const h of hooks.preSave) r.hook("preSave", "user", h);
@@ -296,7 +296,7 @@ describe("runPostSave phase routing", () => {
296
296
  const feature = defineFeature("phases", (r) => {
297
297
  r.entity("user", createEntity({ table: "Users", fields: {} }));
298
298
  r.writeHandler("user", z.object({}), async () => ({ isSuccess: true, data: null }), {
299
- access: { openToAll: true },
299
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
300
300
  });
301
301
  r.hook(
302
302
  "postSave",
@@ -325,7 +325,7 @@ describe("runPostSave phase routing", () => {
325
325
  const feature = defineFeature("phases", (r) => {
326
326
  r.entity("user", createEntity({ table: "Users", fields: {} }));
327
327
  r.writeHandler("user", z.object({}), async () => ({ isSuccess: true, data: null }), {
328
- access: { openToAll: true },
328
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
329
329
  });
330
330
  r.hook(
331
331
  "postSave",
@@ -351,7 +351,7 @@ describe("runPostSave phase routing", () => {
351
351
  const feature = defineFeature("phases", (r) => {
352
352
  r.entity("user", createEntity({ table: "Users", fields: {} }));
353
353
  r.writeHandler("user", z.object({}), async () => ({ isSuccess: true, data: null }), {
354
- access: { openToAll: true },
354
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
355
355
  });
356
356
  r.hook("postSave", "user", async () => {
357
357
  throw new Error("afterCommit-boom");
@@ -378,7 +378,7 @@ describe("runPostSave phase routing", () => {
378
378
  const feature = defineFeature("phases", (r) => {
379
379
  r.entity("user", createEntity({ table: "Users", fields: {} }));
380
380
  r.writeHandler("user", z.object({}), async () => ({ isSuccess: true, data: null }), {
381
- access: { openToAll: true },
381
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
382
382
  });
383
383
  });
384
384
  const registry = createRegistry([feature]);
@@ -439,7 +439,7 @@ describe("Sprint 8a: per-tenant entity-hook filter", () => {
439
439
  "widget:create",
440
440
  z.object({ name: z.string() }),
441
441
  async () => ({ isSuccess: true as const, data: null }),
442
- { access: { openToAll: true } },
442
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
443
443
  );
444
444
  });
445
445
 
@@ -570,7 +570,7 @@ describe("runPreDelete", () => {
570
570
  }),
571
571
  );
572
572
  r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
573
- access: { openToAll: true },
573
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
574
574
  });
575
575
  r.hook("preDelete", "user", async () => {
576
576
  calls.push("handler");
@@ -606,7 +606,7 @@ describe("runPreDelete", () => {
606
606
  }),
607
607
  );
608
608
  r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
609
- access: { openToAll: true },
609
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
610
610
  });
611
611
  r.hook("preDelete", "user", async () => {
612
612
  throw new Error("blocked-delete");
@@ -652,7 +652,7 @@ describe("runPostDelete", () => {
652
652
  }),
653
653
  );
654
654
  r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
655
- access: { openToAll: true },
655
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
656
656
  });
657
657
  r.hook("postDelete", "user", async () => {
658
658
  calls.push("feature");
@@ -685,7 +685,7 @@ describe("runPostDelete", () => {
685
685
  }),
686
686
  );
687
687
  r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
688
- access: { openToAll: true },
688
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
689
689
  });
690
690
  r.hook(
691
691
  "postDelete",
@@ -133,7 +133,7 @@ const asOfFeature = defineFeature("asoftest", (r) => {
133
133
  }
134
134
  return state;
135
135
  },
136
- { access: { openToAll: true } },
136
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
137
137
  );
138
138
  });
139
139
 
@@ -58,7 +58,7 @@ const postQueryFeature = defineFeature("postquerytest", (r) => {
58
58
  ],
59
59
  nextCursor: null,
60
60
  }),
61
- { access: { openToAll: true } },
61
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
62
62
  );
63
63
 
64
64
  // Handler-keyed: fires only for widget:list
@@ -91,13 +91,13 @@ const duplicateRowHook: PostQueryHookFn = async ({ rows }) => ({ rows: [...rows,
91
91
  const singleObjectFeature = defineFeature("singleobjtest", (r) => {
92
92
  const gadget = r.entity("gadget", gadgetEntity);
93
93
  r.queryHandler("gadget:get", z.object({}), async () => ({ id: "g1", name: "Gadget" }), {
94
- access: { openToAll: true },
94
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
95
95
  });
96
96
  r.hook("postQuery", { allOf: gadget }, dropRowHook);
97
97
 
98
98
  const gizmo = r.entity("gizmo", gizmoEntity);
99
99
  r.queryHandler("gizmo:get", z.object({}), async () => ({ id: "z1", name: "Gizmo" }), {
100
- access: { openToAll: true },
100
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
101
101
  });
102
102
  r.hook("postQuery", { allOf: gizmo }, duplicateRowHook);
103
103
  });
@@ -92,7 +92,7 @@ const qpFeature = defineFeature("qp", (r) => {
92
92
  "widget:list-tenant",
93
93
  z.object({}),
94
94
  async (_query, ctx) => ctx.queryProjection("qp:projection:widget-count-tenant"),
95
- { access: { openToAll: true } },
95
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
96
96
  );
97
97
 
98
98
  r.queryHandler(
@@ -102,14 +102,14 @@ const qpFeature = defineFeature("qp", (r) => {
102
102
  ctx.queryProjection("qp:projection:widget-audit", {
103
103
  unsafeAllTenants: query.payload.unsafeAllTenants ?? false,
104
104
  }),
105
- { access: { openToAll: true } },
105
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
106
106
  );
107
107
 
108
108
  r.queryHandler(
109
109
  "widget:list-ghost",
110
110
  z.object({}),
111
111
  async (_query, ctx) => ctx.queryProjection("qp:projection:does-not-exist"),
112
- { access: { openToAll: true } },
112
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
113
113
  );
114
114
  });
115
115
 
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env bun
2
+ // migrate-open-to-all — rewrites deprecated `openToAll: true` to the required
3
+ // `{ reason }` form in test files, and reports every remaining non-test site
4
+ // for a human to pick a reason (fw#2858).
5
+ //
6
+ // Usage:
7
+ // bun scripts/codemod/migrate-open-to-all.ts [--dry-run] [--test-reason "<text>"] <path...>
8
+
9
+ import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
10
+ import { join, resolve } from "node:path";
11
+ import {
12
+ Node,
13
+ type ObjectLiteralExpression,
14
+ Project,
15
+ type PropertyAssignment,
16
+ SyntaxKind,
17
+ } from "ts-morph";
18
+
19
+ const DEFAULT_TEST_REASON = "test handler callable by any signed-in test user";
20
+
21
+ export interface Rewrite {
22
+ readonly file: string;
23
+ readonly line: number;
24
+ readonly before: string;
25
+ readonly after: string;
26
+ }
27
+
28
+ export interface ManualSite {
29
+ readonly file: string;
30
+ readonly line: number;
31
+ readonly handlerName?: string;
32
+ readonly description?: string;
33
+ }
34
+
35
+ export interface MigrateOpenToAllResult {
36
+ readonly output: string;
37
+ readonly rewrites: Rewrite[];
38
+ readonly manual: ManualSite[];
39
+ }
40
+
41
+ export interface MigrateOpenToAllOptions {
42
+ readonly testReason: string;
43
+ }
44
+
45
+ function isTestFile(fileName: string): boolean {
46
+ return /(^|\/)__tests__\//.test(fileName) || /\.test\.(ts|tsx)$/.test(fileName);
47
+ }
48
+
49
+ function isOpenToAllTrueAssignment(node: Node): node is PropertyAssignment {
50
+ if (!Node.isPropertyAssignment(node)) return false;
51
+ if (node.getName() !== "openToAll") return false;
52
+ return node.getInitializer()?.getKind() === SyntaxKind.TrueKeyword;
53
+ }
54
+
55
+ function readStringLiteralProperty(obj: ObjectLiteralExpression, name: string): string | undefined {
56
+ const prop = obj.getProperty(name);
57
+ if (!prop || !Node.isPropertyAssignment(prop)) return undefined;
58
+ const init = prop.getInitializer();
59
+ return init && Node.isStringLiteral(init) ? init.getLiteralValue() : undefined;
60
+ }
61
+
62
+ function enclosingObjectLiterals(node: Node): ObjectLiteralExpression[] {
63
+ const result: ObjectLiteralExpression[] = [];
64
+ let current: Node | undefined = node.getParent();
65
+ while (current) {
66
+ if (Node.isObjectLiteralExpression(current)) result.push(current);
67
+ current = current.getParent();
68
+ }
69
+ return result;
70
+ }
71
+
72
+ // Best-effort human-triage hint (not the rewrite's source of truth): derive a handler name/description from the enclosing object literal, or the enclosing call's first string-literal argument for the positional form.
73
+ function deriveHandlerContext(assignment: PropertyAssignment): {
74
+ readonly handlerName?: string;
75
+ readonly description?: string;
76
+ } {
77
+ let handlerName: string | undefined;
78
+ let description: string | undefined;
79
+ for (const obj of enclosingObjectLiterals(assignment)) {
80
+ if (handlerName === undefined) handlerName = readStringLiteralProperty(obj, "name");
81
+ if (description === undefined) description = readStringLiteralProperty(obj, "description");
82
+ if (handlerName !== undefined && description !== undefined) break;
83
+ }
84
+ if (handlerName === undefined) {
85
+ const call = assignment.getFirstAncestorByKind(SyntaxKind.CallExpression);
86
+ const firstArg = call?.getArguments()[0];
87
+ if (firstArg && Node.isStringLiteral(firstArg)) handlerName = firstArg.getLiteralValue();
88
+ }
89
+ return {
90
+ ...(handlerName !== undefined && { handlerName }),
91
+ ...(description !== undefined && { description }),
92
+ };
93
+ }
94
+
95
+ export function migrateOpenToAllSource(
96
+ source: string,
97
+ fileName: string,
98
+ opts: MigrateOpenToAllOptions,
99
+ ): MigrateOpenToAllResult {
100
+ const project = new Project({ useInMemoryFileSystem: true, skipFileDependencyResolution: true });
101
+ const sourceFile = project.createSourceFile(fileName, source);
102
+ const testFile = isTestFile(fileName);
103
+
104
+ const targets = sourceFile
105
+ .getDescendantsOfKind(SyntaxKind.PropertyAssignment)
106
+ .filter(isOpenToAllTrueAssignment);
107
+
108
+ const rewrites: Rewrite[] = [];
109
+ const manual: ManualSite[] = [];
110
+
111
+ for (const assignment of targets) {
112
+ if (assignment.wasForgotten()) continue;
113
+ const line = assignment.getStartLineNumber();
114
+
115
+ if (testFile) {
116
+ const before = assignment.getText();
117
+ const after = `openToAll: { reason: ${JSON.stringify(opts.testReason)} }`;
118
+ assignment.replaceWithText(after);
119
+ rewrites.push({ file: fileName, line, before, after });
120
+ continue;
121
+ }
122
+
123
+ manual.push({ file: fileName, line, ...deriveHandlerContext(assignment) });
124
+ }
125
+
126
+ return { output: sourceFile.getFullText(), rewrites, manual };
127
+ }
128
+
129
+ class CliError extends Error {}
130
+
131
+ interface CliOptions {
132
+ readonly dryRun: boolean;
133
+ readonly testReason: string;
134
+ readonly paths: string[];
135
+ }
136
+
137
+ function parseArgs(argv: string[]): CliOptions {
138
+ let dryRun = false;
139
+ let testReason = DEFAULT_TEST_REASON;
140
+ const paths: string[] = [];
141
+
142
+ for (let i = 0; i < argv.length; i++) {
143
+ const arg = argv[i]!;
144
+ if (arg === "--dry-run") {
145
+ dryRun = true;
146
+ continue;
147
+ }
148
+ if (arg === "--test-reason") {
149
+ const value = argv[++i];
150
+ if (!value) throw new CliError("--test-reason requires a value");
151
+ testReason = value;
152
+ continue;
153
+ }
154
+ if (arg.startsWith("--")) throw new CliError(`unknown flag: ${arg}`);
155
+ paths.push(arg);
156
+ }
157
+
158
+ if (paths.length === 0) throw new CliError("at least one path is required");
159
+ return { dryRun, testReason, paths };
160
+ }
161
+
162
+ function isMigratableFile(name: string): boolean {
163
+ return /\.(ts|tsx)$/.test(name);
164
+ }
165
+
166
+ function walkDir(dir: string, out: string[]): void {
167
+ for (const name of readdirSync(dir)) {
168
+ if (name === "node_modules") continue;
169
+ const full = join(dir, name);
170
+ const stat = statSync(full);
171
+ if (stat.isDirectory()) {
172
+ walkDir(full, out);
173
+ continue;
174
+ }
175
+ if (isMigratableFile(name)) out.push(full);
176
+ }
177
+ }
178
+
179
+ function collectFiles(paths: string[]): string[] {
180
+ const files: string[] = [];
181
+ for (const p of paths) {
182
+ const abs = resolve(p);
183
+ const stat = statSync(abs);
184
+ if (stat.isDirectory()) walkDir(abs, files);
185
+ else if (isMigratableFile(abs)) files.push(abs);
186
+ }
187
+ return files;
188
+ }
189
+
190
+ async function main(): Promise<void> {
191
+ let options: CliOptions;
192
+ try {
193
+ options = parseArgs(process.argv.slice(2));
194
+ } catch (err) {
195
+ if (err instanceof CliError) {
196
+ console.error(`[migrate-open-to-all] ${err.message}`);
197
+ process.exit(2);
198
+ }
199
+ throw err;
200
+ }
201
+
202
+ const files = collectFiles(options.paths);
203
+ const allManual: ManualSite[] = [];
204
+ let filesChanged = 0;
205
+ let totalRewrites = 0;
206
+
207
+ for (const file of files) {
208
+ const before = readFileSync(file, "utf8");
209
+ if (!before.includes("openToAll")) continue;
210
+ const result = migrateOpenToAllSource(before, file, { testReason: options.testReason });
211
+ allManual.push(...result.manual);
212
+
213
+ if (result.rewrites.length === 0) continue;
214
+ filesChanged += 1;
215
+ totalRewrites += result.rewrites.length;
216
+
217
+ if (options.dryRun) {
218
+ console.log(`\n=== ${file} (${result.rewrites.length} rewrite(s)) ===`);
219
+ for (const r of result.rewrites) {
220
+ console.log(`${file}:${r.line}`);
221
+ console.log(`- ${r.before}`);
222
+ console.log(`+ ${r.after}`);
223
+ }
224
+ } else {
225
+ writeFileSync(file, result.output);
226
+ }
227
+ }
228
+
229
+ console.log(
230
+ `\n[migrate-open-to-all] ${options.dryRun ? "would change" : "changed"} ${filesChanged} file(s); ` +
231
+ `rewrites: ${totalRewrites}`,
232
+ );
233
+
234
+ if (allManual.length > 0) {
235
+ console.log(`\n[migrate-open-to-all] manual review needed (${allManual.length}):`);
236
+ for (const m of allManual) {
237
+ console.log(
238
+ `${m.file}:${m.line} ${m.handlerName ?? "<unknown>"} ${m.description ?? "<no description>"}`,
239
+ );
240
+ }
241
+ }
242
+
243
+ process.exit(0);
244
+ }
245
+
246
+ if (import.meta.main) {
247
+ await main();
248
+ }
@@ -54,7 +54,7 @@ declare const aSecret: Secret<string>;
54
54
  defineQueryHandler({
55
55
  name: "t:query:leak",
56
56
  schema,
57
- access: { openToAll: true },
57
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
58
58
  handler: async () => ({ apiKey: aSecret }),
59
59
  });
60
60
 
@@ -65,7 +65,7 @@ function genericResponseHandler<K extends string>(kind: K) {
65
65
  return defineQueryHandler({
66
66
  name: "t:query:generic",
67
67
  schema,
68
- access: { openToAll: true },
68
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
69
69
  handler: async () => ({ kind, ok: true }) as { kind: K; ok: boolean },
70
70
  });
71
71
  }
@@ -83,7 +83,7 @@ declare const leakingUnion: { ok: true } | { apiKey: Secret<string> };
83
83
  defineQueryHandler({
84
84
  name: "t:query:union-leak",
85
85
  schema,
86
- access: { openToAll: true },
86
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
87
87
  handler: async () => leakingUnion,
88
88
  });
89
89
 
@@ -91,7 +91,7 @@ declare const cleanUnion: { ok: true } | { id: string };
91
91
  defineQueryHandler({
92
92
  name: "t:query:union-clean",
93
93
  schema,
94
- access: { openToAll: true },
94
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
95
95
  handler: async () => cleanUnion,
96
96
  });
97
97
 
@@ -100,7 +100,7 @@ describe("R6 ContainsSecret", () => {
100
100
  const def = defineQueryHandler({
101
101
  name: "t:query:clean",
102
102
  schema,
103
- access: { openToAll: true },
103
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
104
104
  handler: async () => ({ ok: true, value: createSecret("x").reveal() }),
105
105
  });
106
106
  expect(def.name).toBe("t:query:clean");
@@ -20,7 +20,7 @@ const pingFeature = defineFeature("reqhelp", (r) => {
20
20
  isSuccess: true as const,
21
21
  data: { note: event.payload.note, userId: event.user.id },
22
22
  }),
23
- { access: { openToAll: true } },
23
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
24
24
  );
25
25
 
26
26
  r.writeHandler(
@@ -28,7 +28,7 @@ const pingFeature = defineFeature("reqhelp", (r) => {
28
28
  z.object({}),
29
29
  async () =>
30
30
  writeFailure(new UnprocessableError("reqhelp-boom", { i18nKey: "errors.unprocessable" })),
31
- { access: { openToAll: true } },
31
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
32
32
  );
33
33
 
34
34
  r.queryHandler(
@@ -40,7 +40,7 @@ const pingFeature = defineFeature("reqhelp", (r) => {
40
40
  }
41
41
  return { id: query.payload.id, ok: true };
42
42
  },
43
- { access: { openToAll: true } },
43
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
44
44
  );
45
45
  });
46
46
 
@@ -29,7 +29,7 @@ const manualDispatchFeature = defineFeature("manualdispatch", (r) => {
29
29
  await jobRunner.dispatch("manualdispatch:job:record", { note: event.payload.note });
30
30
  return { isSuccess: true as const, data: { id: 1, note: event.payload.note } };
31
31
  },
32
- { access: { openToAll: true } },
32
+ { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
33
33
  );
34
34
  r.job("record", { trigger: { manual: true }, runIn: "worker" }, async (payload) => {
35
35
  jobRuns.push({ name: "manualdispatch:job:record", payload });
@@ -34,7 +34,11 @@ function createTasksFeature() {
34
34
  return defineFeature("tasks", (r) => {
35
35
  r.systemScope();
36
36
  r.entity("task", taskEntity);
37
- r.writeHandler(defineEntityCreateHandler("task", taskEntity, { access: { openToAll: true } }));
37
+ r.writeHandler(
38
+ defineEntityCreateHandler("task", taskEntity, {
39
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
40
+ }),
41
+ );
38
42
  r.screen({
39
43
  id: "task-list",
40
44
  type: "entityList",
@@ -105,7 +109,11 @@ describe("generateE2ESpec", () => {
105
109
  const feature = defineFeature("events", (r) => {
106
110
  r.systemScope();
107
111
  r.entity("event", entity);
108
- r.writeHandler(defineEntityCreateHandler("event", entity, { access: { openToAll: true } }));
112
+ r.writeHandler(
113
+ defineEntityCreateHandler("event", entity, {
114
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
115
+ }),
116
+ );
109
117
  r.screen({
110
118
  id: "event-edit",
111
119
  type: "entityEdit",
@@ -165,7 +173,9 @@ describe("generateE2ESpec", () => {
165
173
  r.systemScope();
166
174
  r.entity("task", taskEntity);
167
175
  r.writeHandler(
168
- defineEntityCreateHandler("task", taskEntity, { access: { openToAll: true } }),
176
+ defineEntityCreateHandler("task", taskEntity, {
177
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
178
+ }),
169
179
  );
170
180
  r.screen({
171
181
  id: "edit",
@@ -201,7 +211,9 @@ describe("generateE2ESpec", () => {
201
211
  r.systemScope();
202
212
  r.entity("contact", contactEntity);
203
213
  r.writeHandler(
204
- defineEntityCreateHandler("contact", contactEntity, { access: { openToAll: true } }),
214
+ defineEntityCreateHandler("contact", contactEntity, {
215
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
216
+ }),
205
217
  );
206
218
  r.screen({
207
219
  id: "list",
@@ -225,7 +237,9 @@ describe("generateE2ESpec", () => {
225
237
  r.systemScope();
226
238
  r.entity("task", taskEntity);
227
239
  r.writeHandler(
228
- defineEntityCreateHandler("task", taskEntity, { access: { openToAll: true } }),
240
+ defineEntityCreateHandler("task", taskEntity, {
241
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
242
+ }),
229
243
  );
230
244
  r.screen({ id: "list", type: "entityList", entity: "task", columns: ["title"] });
231
245
  r.screen({