@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
package/README.md
CHANGED
|
@@ -45,7 +45,11 @@ export const taskFeature = defineFeature("tasks", (r) => {
|
|
|
45
45
|
// control, all explicit via the options below.
|
|
46
46
|
r.crud("task", taskEntity, {
|
|
47
47
|
write: { access: { roles: ["User"] } },
|
|
48
|
-
read: {
|
|
48
|
+
read: {
|
|
49
|
+
access: {
|
|
50
|
+
openToAll: { reason: "any signed-in user may list and view tasks; only writes are role-gated" },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
49
53
|
});
|
|
50
54
|
|
|
51
55
|
// Read-model fed from task events, rebuildable via the CLI
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.270.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"./package.json": "./package.json"
|
|
199
199
|
},
|
|
200
200
|
"dependencies": {
|
|
201
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
201
|
+
"@cosmicdrift/kumiko-types": "0.270.0",
|
|
202
202
|
"bullmq": "^5.76.7",
|
|
203
203
|
"bun-types": "^1.3.13",
|
|
204
204
|
"hono": "^4.13.1",
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
"zod": "^4.4.3"
|
|
215
215
|
},
|
|
216
216
|
"devDependencies": {
|
|
217
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
217
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.270.0",
|
|
218
218
|
"bun-types": "^1.3.13",
|
|
219
219
|
"pino-pretty": "^13.1.3"
|
|
220
220
|
},
|
|
@@ -66,7 +66,7 @@ const shopFeature = defineFeature("anonshop", (r) => {
|
|
|
66
66
|
const rows = await selectMany(ctx.db, productTable);
|
|
67
67
|
return rows;
|
|
68
68
|
},
|
|
69
|
-
{ access: { openToAll:
|
|
69
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
70
70
|
);
|
|
71
71
|
|
|
72
72
|
// Anonymous can place a guest order.
|
|
@@ -149,7 +149,7 @@ const errorFeature = defineFeature("errctr", (r) => {
|
|
|
149
149
|
async (event) => {
|
|
150
150
|
throw new NotFoundError("item", event.payload.id);
|
|
151
151
|
},
|
|
152
|
-
{ access: { openToAll:
|
|
152
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
153
153
|
);
|
|
154
154
|
|
|
155
155
|
// InternalError auto-wrap: handler raises an unexpected TypeError; the
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const listWidgets = {
|
|
4
|
+
name: "widget:list",
|
|
5
|
+
schema: z.object({}),
|
|
6
|
+
description: "Lists all widgets; visible to everyone",
|
|
7
|
+
access: { openToAll: true },
|
|
8
|
+
handler: async () => [],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const legacyPing = {
|
|
12
|
+
name: "legacy:ping",
|
|
13
|
+
schema: z.object({}),
|
|
14
|
+
access: { openToAll: true },
|
|
15
|
+
handler: async () => "pong",
|
|
16
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const listWidgets = {
|
|
4
|
+
name: "widget:list",
|
|
5
|
+
schema: z.object({}),
|
|
6
|
+
description: "Lists all widgets; visible to everyone",
|
|
7
|
+
access: { openToAll: true },
|
|
8
|
+
handler: async () => [],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const legacyPing = {
|
|
12
|
+
name: "legacy:ping",
|
|
13
|
+
schema: z.object({}),
|
|
14
|
+
access: { openToAll: true },
|
|
15
|
+
handler: async () => "pong",
|
|
16
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const config = {
|
|
2
|
+
verbAccess: {
|
|
3
|
+
create: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
4
|
+
list: { roles: ["admin"] },
|
|
5
|
+
},
|
|
6
|
+
read: {
|
|
7
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
8
|
+
},
|
|
9
|
+
write: {
|
|
10
|
+
access: { roles: ["admin"] },
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -174,14 +174,14 @@ const userFeature = defineFeature("users", (r) => {
|
|
|
174
174
|
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
175
175
|
}),
|
|
176
176
|
async (query, ctx) => userExecutor(ctx).list(query.payload, query.user, ctx.db),
|
|
177
|
-
{ access: { openToAll:
|
|
177
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
178
178
|
);
|
|
179
179
|
|
|
180
180
|
r.queryHandler(
|
|
181
181
|
"user:detail",
|
|
182
182
|
z.object({ id: z.uuid() }),
|
|
183
183
|
async (query, ctx) => userExecutor(ctx).detail(query.payload, query.user, ctx.db),
|
|
184
|
-
{ access: { openToAll:
|
|
184
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
185
185
|
);
|
|
186
186
|
|
|
187
187
|
r.hook("postSave", { allOf: user }, async (result) => {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { migrateOpenToAllSource } from "../scripts/codemod/migrate-open-to-all";
|
|
5
|
+
|
|
6
|
+
const FIXTURES_DIR = join(import.meta.dir, "fixtures", "migrate-open-to-all");
|
|
7
|
+
const DEFAULT_REASON = "test handler callable by any signed-in test user";
|
|
8
|
+
|
|
9
|
+
function readFixture(name: string): { input: string; expected: string } {
|
|
10
|
+
return {
|
|
11
|
+
input: readFileSync(join(FIXTURES_DIR, `${name}.input.ts`), "utf8"),
|
|
12
|
+
expected: readFileSync(join(FIXTURES_DIR, `${name}.expected.ts`), "utf8"),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("migrateOpenToAllSource", () => {
|
|
17
|
+
it("rewrites a top-level openToAll: true in a *.test.ts file to the default test reason", () => {
|
|
18
|
+
const { input, expected } = readFixture("test-simple");
|
|
19
|
+
const result = migrateOpenToAllSource(input, "widget-list.test.ts", {
|
|
20
|
+
testReason: DEFAULT_REASON,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(result.output).toBe(expected);
|
|
24
|
+
expect(result.rewrites).toHaveLength(1);
|
|
25
|
+
expect(result.rewrites[0]?.after).toBe(`openToAll: { reason: "${DEFAULT_REASON}" }`);
|
|
26
|
+
expect(result.manual).toHaveLength(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("rewrites openToAll: true under a __tests__/ path even without a .test.ts suffix", () => {
|
|
30
|
+
const { input, expected } = readFixture("test-simple");
|
|
31
|
+
const result = migrateOpenToAllSource(input, "packages/framework/__tests__/widget-list.ts", {
|
|
32
|
+
testReason: DEFAULT_REASON,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(result.output).toBe(expected);
|
|
36
|
+
expect(result.rewrites).toHaveLength(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("honors a custom --test-reason for test files", () => {
|
|
40
|
+
const { input } = readFixture("test-simple");
|
|
41
|
+
const result = migrateOpenToAllSource(input, "widget-list.test.ts", {
|
|
42
|
+
testReason: "custom reason for this suite",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result.output).toContain(
|
|
46
|
+
'access: { openToAll: { reason: "custom reason for this suite" } }',
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rewrites nested shorthand openToAll: true (verbAccess, read.access) in test files", () => {
|
|
51
|
+
const { input, expected } = readFixture("test-nested-shorthand");
|
|
52
|
+
const result = migrateOpenToAllSource(input, "config.test.ts", { testReason: DEFAULT_REASON });
|
|
53
|
+
|
|
54
|
+
expect(result.output).toBe(expected);
|
|
55
|
+
expect(result.rewrites).toHaveLength(2);
|
|
56
|
+
expect(result.manual).toHaveLength(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("leaves non-test files untouched and reports manual sites with derived handler name + description", () => {
|
|
60
|
+
const { input, expected } = readFixture("manual-only");
|
|
61
|
+
const result = migrateOpenToAllSource(input, "handlers.ts", { testReason: DEFAULT_REASON });
|
|
62
|
+
|
|
63
|
+
expect(result.output).toBe(expected);
|
|
64
|
+
expect(result.rewrites).toHaveLength(0);
|
|
65
|
+
expect(result.manual).toHaveLength(2);
|
|
66
|
+
|
|
67
|
+
const withDescription = result.manual.find((m) => m.handlerName === "widget:list");
|
|
68
|
+
expect(withDescription).toBeDefined();
|
|
69
|
+
expect(withDescription?.description).toBe("Lists all widgets; visible to everyone");
|
|
70
|
+
expect(withDescription?.line).toBe(7);
|
|
71
|
+
|
|
72
|
+
const withoutDescription = result.manual.find((m) => m.handlerName === "legacy:ping");
|
|
73
|
+
expect(withoutDescription).toBeDefined();
|
|
74
|
+
expect(withoutDescription?.description).toBeUndefined();
|
|
75
|
+
expect(withoutDescription?.line).toBe(14);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("never rewrites in a non-test file, even when a description is present", () => {
|
|
79
|
+
const { input } = readFixture("manual-only");
|
|
80
|
+
const result = migrateOpenToAllSource(input, "handlers.ts", { testReason: DEFAULT_REASON });
|
|
81
|
+
|
|
82
|
+
expect(result.output).toContain("access: { openToAll: true }");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -86,7 +86,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
86
86
|
event.user,
|
|
87
87
|
ctx.db,
|
|
88
88
|
),
|
|
89
|
-
{ access: { openToAll:
|
|
89
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
90
90
|
);
|
|
91
91
|
|
|
92
92
|
r.writeHandler(
|
|
@@ -102,7 +102,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
102
102
|
event.user,
|
|
103
103
|
ctx.db,
|
|
104
104
|
),
|
|
105
|
-
{ access: { openToAll:
|
|
105
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
106
106
|
);
|
|
107
107
|
|
|
108
108
|
r.writeHandler(
|
|
@@ -114,7 +114,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
114
114
|
event.user,
|
|
115
115
|
ctx.db,
|
|
116
116
|
),
|
|
117
|
-
{ access: { openToAll:
|
|
117
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
118
118
|
);
|
|
119
119
|
|
|
120
120
|
r.writeHandler(
|
|
@@ -130,7 +130,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
130
130
|
event.user,
|
|
131
131
|
ctx.db,
|
|
132
132
|
),
|
|
133
|
-
{ access: { openToAll:
|
|
133
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
134
134
|
);
|
|
135
135
|
|
|
136
136
|
r.writeHandler(
|
|
@@ -142,7 +142,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
142
142
|
event.user,
|
|
143
143
|
ctx.db,
|
|
144
144
|
),
|
|
145
|
-
{ access: { openToAll:
|
|
145
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
146
146
|
);
|
|
147
147
|
|
|
148
148
|
r.writeHandler(
|
|
@@ -154,7 +154,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
154
154
|
event.user,
|
|
155
155
|
ctx.db,
|
|
156
156
|
),
|
|
157
|
-
{ access: { openToAll:
|
|
157
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
158
158
|
);
|
|
159
159
|
|
|
160
160
|
r.writeHandler(
|
|
@@ -170,7 +170,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
170
170
|
event.user,
|
|
171
171
|
ctx.db,
|
|
172
172
|
),
|
|
173
|
-
{ access: { openToAll:
|
|
173
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
174
174
|
);
|
|
175
175
|
});
|
|
176
176
|
|
|
@@ -37,7 +37,7 @@ const testFeature = defineFeature("test", (r) => {
|
|
|
37
37
|
"item:list",
|
|
38
38
|
z.object({ search: z.string().optional() }),
|
|
39
39
|
async () => [{ id: 1, name: "Test" }],
|
|
40
|
-
{ access: { openToAll:
|
|
40
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
41
41
|
);
|
|
42
42
|
|
|
43
43
|
r.streamHandler(
|
|
@@ -750,7 +750,7 @@ describe("feature-declared HTTP routes (r.httpRoute)", () => {
|
|
|
750
750
|
}),
|
|
751
751
|
);
|
|
752
752
|
r.queryHandler("item:list", z.object({}), async () => [{ id: 7 }], {
|
|
753
|
-
access: { openToAll:
|
|
753
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
754
754
|
});
|
|
755
755
|
r.httpRoute({
|
|
756
756
|
method: "GET",
|
|
@@ -18,7 +18,7 @@ const testFeature = defineFeature("blob", (r) => {
|
|
|
18
18
|
"note:create",
|
|
19
19
|
z.object({ body: z.string() }),
|
|
20
20
|
async (event) => ({ isSuccess: true, data: { body: event.payload.body } }),
|
|
21
|
-
{ access: { openToAll:
|
|
21
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
22
22
|
);
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -18,7 +18,7 @@ const leakyFeature = defineFeature("leaky", (r) => {
|
|
|
18
18
|
defineQueryHandler({
|
|
19
19
|
name: "raw",
|
|
20
20
|
schema: z.object({}),
|
|
21
|
-
access: { openToAll:
|
|
21
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
22
22
|
handler: async () => ({ email: CIPHERTEXT, note: "plain" }),
|
|
23
23
|
}),
|
|
24
24
|
);
|
|
@@ -26,7 +26,7 @@ const leakyFeature = defineFeature("leaky", (r) => {
|
|
|
26
26
|
defineQueryHandler({
|
|
27
27
|
name: "clean",
|
|
28
28
|
schema: z.object({}),
|
|
29
|
-
access: { openToAll:
|
|
29
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
30
30
|
handler: async () => ({ email: "marc@example.com" }),
|
|
31
31
|
}),
|
|
32
32
|
);
|
|
@@ -18,7 +18,7 @@ const localeProbe = defineFeature("locale-probe", (r) => {
|
|
|
18
18
|
"read-locale",
|
|
19
19
|
z.object({}),
|
|
20
20
|
async (_event, ctx) => ({ isSuccess: true, data: { locale: ctx.locale } }),
|
|
21
|
-
{ access: { openToAll:
|
|
21
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
22
22
|
);
|
|
23
23
|
});
|
|
24
24
|
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.269.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "`openToAll: true` is removed — every openToAll grant now requires { reason } (fw#2858).",
|
|
6
|
+
"detail": "`OpenToAllAccessRule` (`@cosmicdrift/kumiko-types/handlers`) no longer accepts `true`; only `OpenToAllDeclaration` (`{ reason: string; personalData?: \"tenant-members\" }`) satisfies it, so `{ openToAll: true }` no longer type-checks. `isOpenToAllGranted` denies `true` from untyped sources (pattern JSON, Designer) the same way it already denied a malformed object — a bare `true` reaching it now returns `false` instead of granting access. The boot validator (`engine/boot-validator/access-declarations.ts`) rejects an untyped `openToAll: true` access declaration with an error pointing at `{ reason }` instead of registering the handler. The pattern-library Designer access field (`shared-fields.ts` `accessRuleField`) is now a required text input on `access.openToAll.reason` instead of a boolean toggle, so the Designer can only ever produce the object form. `build-config-feature-schema.ts` synthesizes `{ openToAll: { reason: \"config key declares the \\\"all\\\" role, so every signed-in user of the tenant may read/write it\" } }` for a config key whose roles include `\"all\"`, instead of `{ openToAll: true }`. `boot-validator/nav.ts` and `renderer-web/app/create-app.tsx` switched from `\"openToAll\" in access` to `isOpenToAllGranted(access)`, so a malformed or `true` rule is no longer treated as a visible screen/workspace. The feature-AST extractor now also extracts `escapeHatch: { reason }` on write/query handlers (`extractors/handlers.ts`) and on `r.hook` options (`extractors/hooks.ts`), round-tripped through render/patch the same way `access`/`rateLimit` already were.",
|
|
7
|
+
"migration": "Replace every `openToAll: true` with `openToAll: { reason: \"<who may call it and why that is safe without a role>\" }`, adding `personalData: \"tenant-members\"` where the boot validator requires it for a write handler accepting unbound personal data. Pattern-library or Designer-authored JSON that still has `openToAll: true` must be re-saved with a reason before it type-checks or passes boot. `kumiko-upgrade --apply` runs the migrate-open-to-all codemod automatically, rewriting test files to the default reason and listing every non-test site for manual review; run `bun packages/framework/src/scripts/codemod/migrate-open-to-all.ts --test-reason \"<text>\" <paths>` directly to override the default test reason.",
|
|
8
|
+
"codemod": "scripts/codemod/migrate-open-to-all.ts"
|
|
9
|
+
},
|
|
2
10
|
{
|
|
3
11
|
"version": "0.269.0",
|
|
4
12
|
"type": "breaking",
|
|
@@ -36,7 +36,7 @@ const LIST_QN = "filternotes:query:note:list";
|
|
|
36
36
|
const filterNotesFeature = defineFeature("filternotes", (r) => {
|
|
37
37
|
r.crud("note", filterNoteEntity, {
|
|
38
38
|
write: { access: { roles: ["Admin"] } },
|
|
39
|
-
read: { access: { openToAll:
|
|
39
|
+
read: { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -38,7 +38,7 @@ const derivativesTestFeature = defineFeature("derivativestest", (r) => {
|
|
|
38
38
|
);
|
|
39
39
|
return { isSuccess: true as const, data: result };
|
|
40
40
|
},
|
|
41
|
-
{ access: { openToAll:
|
|
41
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
// Catches the error INSIDE the handler (not via HTTP serialization) so the
|
|
@@ -69,7 +69,7 @@ const derivativesTestFeature = defineFeature("derivativestest", (r) => {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
|
-
{ access: { openToAll:
|
|
72
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
73
73
|
);
|
|
74
74
|
|
|
75
75
|
r.job("record", { trigger: { manual: true }, runIn: "worker" }, async (payload, ctx) => {
|
|
@@ -19,10 +19,10 @@ function dashboardFeature(
|
|
|
19
19
|
) {
|
|
20
20
|
return defineFeature("demo", (r) => {
|
|
21
21
|
r.queryHandler("incident:open-count", z.object({}), async () => ({ count: 3 }), {
|
|
22
|
-
access: { openToAll:
|
|
22
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
23
23
|
});
|
|
24
24
|
r.queryHandler("incident:latest", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
25
|
-
access: { openToAll:
|
|
25
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
26
26
|
});
|
|
27
27
|
r.screen({
|
|
28
28
|
id: "overview",
|
|
@@ -245,10 +245,10 @@ describe("validateBoot — dashboard screens", () => {
|
|
|
245
245
|
|
|
246
246
|
const catalogFeature = defineFeature("catalog", (r) => {
|
|
247
247
|
r.queryHandler("items:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
248
|
-
access: { openToAll:
|
|
248
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
249
249
|
});
|
|
250
250
|
r.queryHandler("items:status", z.object({}), async () => ({ enabled: true }), {
|
|
251
|
-
access: { openToAll:
|
|
251
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
252
252
|
});
|
|
253
253
|
r.screen({
|
|
254
254
|
id: "items",
|
|
@@ -289,7 +289,7 @@ describe("validateBoot — dashboard screen panels (fw#2841)", () => {
|
|
|
289
289
|
test("accepts a same-feature short id", () => {
|
|
290
290
|
const feature = defineFeature("demo", (r) => {
|
|
291
291
|
r.queryHandler("items:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
292
|
-
access: { openToAll:
|
|
292
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
293
293
|
});
|
|
294
294
|
r.screen({
|
|
295
295
|
id: "items",
|
|
@@ -50,7 +50,7 @@ function stubListHandler(r: any, entityName: string): void {
|
|
|
50
50
|
name: `${entityName}:list`,
|
|
51
51
|
schema: z.object({}),
|
|
52
52
|
handler: async () => ({ rows: [], nextCursor: null }) as never,
|
|
53
|
-
access: { openToAll:
|
|
53
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -7,7 +7,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
7
7
|
test("rejects hand-written searchable:true when the query schema has no search param (3a)", () => {
|
|
8
8
|
const feature = defineFeature("ledger", (r) => {
|
|
9
9
|
r.queryHandler("schedule:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
10
|
-
access: { openToAll:
|
|
10
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
11
11
|
});
|
|
12
12
|
r.screen({
|
|
13
13
|
id: "schedule-list",
|
|
@@ -29,7 +29,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
29
29
|
"schedule:list",
|
|
30
30
|
z.object({ search: z.string().optional() }),
|
|
31
31
|
async () => ({ rows: [], nextCursor: null }),
|
|
32
|
-
{ access: { openToAll:
|
|
32
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
33
33
|
);
|
|
34
34
|
r.screen({
|
|
35
35
|
id: "schedule-list",
|
|
@@ -50,7 +50,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
50
50
|
"schedule:list",
|
|
51
51
|
z.object({ sort: z.string().optional() }),
|
|
52
52
|
async () => ({ rows: [], nextCursor: null }),
|
|
53
|
-
{ access: { openToAll:
|
|
53
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
54
54
|
);
|
|
55
55
|
r.screen({
|
|
56
56
|
id: "schedule-list",
|
|
@@ -71,7 +71,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
71
71
|
"schedule:list",
|
|
72
72
|
z.object({ search: z.string().optional(), sort: z.string().optional() }),
|
|
73
73
|
async () => ({ rows: [], nextCursor: null }),
|
|
74
|
-
{ access: { openToAll:
|
|
74
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
75
75
|
);
|
|
76
76
|
r.screen({
|
|
77
77
|
id: "schedule-list",
|
|
@@ -91,7 +91,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
91
91
|
test("passes when the schema offers neither search nor sort and no defaultSort is set", () => {
|
|
92
92
|
const feature = defineFeature("ledger", (r) => {
|
|
93
93
|
r.queryHandler("schedule:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
94
|
-
access: { openToAll:
|
|
94
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
95
95
|
});
|
|
96
96
|
r.screen({
|
|
97
97
|
id: "schedule-list",
|
|
@@ -112,7 +112,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
112
112
|
"schedule:list",
|
|
113
113
|
z.object({ search: z.string().optional() }),
|
|
114
114
|
async () => ({ rows: [], nextCursor: null }),
|
|
115
|
-
{ access: { openToAll:
|
|
115
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
116
116
|
);
|
|
117
117
|
r.screen({
|
|
118
118
|
id: "schedule-list",
|
|
@@ -135,7 +135,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
135
135
|
"schedule:list",
|
|
136
136
|
z.object({ search: z.string().optional() }),
|
|
137
137
|
async () => ({ rows: [], nextCursor: null }),
|
|
138
|
-
{ access: { openToAll:
|
|
138
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
139
139
|
);
|
|
140
140
|
r.screen({
|
|
141
141
|
id: "download-attempt-list",
|
|
@@ -154,7 +154,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
154
154
|
test("rejects hand-authored sortable on a projectionList screen (fw#2165 review)", () => {
|
|
155
155
|
const feature = defineFeature("ledger", (r) => {
|
|
156
156
|
r.queryHandler("schedule:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
157
|
-
access: { openToAll:
|
|
157
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
158
158
|
});
|
|
159
159
|
r.screen({
|
|
160
160
|
id: "schedule-list",
|
|
@@ -173,7 +173,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
173
173
|
test("rejects hand-authored paginated on a projectionList screen (fw#2165 review)", () => {
|
|
174
174
|
const feature = defineFeature("ledger", (r) => {
|
|
175
175
|
r.queryHandler("schedule:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
176
|
-
access: { openToAll:
|
|
176
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
177
177
|
});
|
|
178
178
|
r.screen({
|
|
179
179
|
id: "schedule-list",
|
|
@@ -202,7 +202,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
202
202
|
z.object({}),
|
|
203
203
|
async () => ({ rows: [], nextCursor: null }),
|
|
204
204
|
{
|
|
205
|
-
access: { openToAll:
|
|
205
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
206
206
|
},
|
|
207
207
|
);
|
|
208
208
|
r.screen({ id: "schedule-detail", type: "custom", renderer: { react: "stub" } });
|
|
@@ -251,7 +251,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
251
251
|
"schedule:list",
|
|
252
252
|
z.object({ filter: z.unknown().optional() }),
|
|
253
253
|
async () => ({ rows: [], nextCursor: null }),
|
|
254
|
-
{ access: { openToAll:
|
|
254
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
255
255
|
);
|
|
256
256
|
r.screen({
|
|
257
257
|
id: "schedule-list",
|
|
@@ -274,7 +274,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
274
274
|
z.object({}),
|
|
275
275
|
async () => ({ rows: [], nextCursor: null }),
|
|
276
276
|
{
|
|
277
|
-
access: { openToAll:
|
|
277
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
278
278
|
},
|
|
279
279
|
);
|
|
280
280
|
r.screen({
|
|
@@ -297,7 +297,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
297
297
|
"schedule:list",
|
|
298
298
|
z.object({ filters: z.unknown().optional() }),
|
|
299
299
|
async () => ({ rows: [], nextCursor: null }),
|
|
300
|
-
{ access: { openToAll:
|
|
300
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
301
301
|
);
|
|
302
302
|
r.screen({
|
|
303
303
|
id: "schedule-list",
|
|
@@ -326,7 +326,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
326
326
|
"schedule:list",
|
|
327
327
|
z.object({ filters: z.unknown().optional() }),
|
|
328
328
|
async () => ({ rows: [], nextCursor: null }),
|
|
329
|
-
{ access: { openToAll:
|
|
329
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
330
330
|
);
|
|
331
331
|
r.screen({
|
|
332
332
|
id: "schedule-list",
|
|
@@ -363,7 +363,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
363
363
|
"schedule:list",
|
|
364
364
|
z.object({ filters: z.unknown().optional() }),
|
|
365
365
|
async () => ({ rows: [], nextCursor: null }),
|
|
366
|
-
{ access: { openToAll:
|
|
366
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
367
367
|
);
|
|
368
368
|
r.screen({
|
|
369
369
|
id: "schedule-list",
|
|
@@ -386,7 +386,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
386
386
|
z.object({}),
|
|
387
387
|
async () => ({ rows: [], nextCursor: null }),
|
|
388
388
|
{
|
|
389
|
-
access: { openToAll:
|
|
389
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
390
390
|
},
|
|
391
391
|
);
|
|
392
392
|
r.screen({
|
|
@@ -417,7 +417,7 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
417
417
|
"schedule:list",
|
|
418
418
|
z.object({ filter: z.unknown().optional(), filters: z.unknown().optional() }),
|
|
419
419
|
async () => ({ rows: [], nextCursor: null }),
|
|
420
|
-
{ access: { openToAll:
|
|
420
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
421
421
|
);
|
|
422
422
|
r.screen({
|
|
423
423
|
id: "schedule-list",
|