@cosmicdrift/kumiko-framework 0.269.1 → 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-db-raw/global-fetch.expected.ts +9 -0
- package/src/__tests__/fixtures/migrate-db-raw/global-fetch.input.ts +10 -0
- package/src/__tests__/fixtures/migrate-db-raw/global-select.expected.ts +6 -0
- package/src/__tests__/fixtures/migrate-db-raw/global-select.input.ts +12 -0
- package/src/__tests__/fixtures/migrate-db-raw/import-pruning.expected.ts +12 -0
- package/src/__tests__/fixtures/migrate-db-raw/import-pruning.input.ts +12 -0
- package/src/__tests__/fixtures/migrate-db-raw/manual-only.expected.ts +26 -0
- package/src/__tests__/fixtures/migrate-db-raw/manual-only.input.ts +26 -0
- package/src/__tests__/fixtures/migrate-db-raw/own-tenant-const-shorthand.expected.ts +10 -0
- package/src/__tests__/fixtures/migrate-db-raw/own-tenant-const-shorthand.input.ts +11 -0
- package/src/__tests__/fixtures/migrate-db-raw/own-tenant-event.expected.ts +9 -0
- package/src/__tests__/fixtures/migrate-db-raw/own-tenant-event.input.ts +15 -0
- 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-db-raw-codemod.test.ts +132 -0
- package/src/__tests__/migrate-open-to-all-codemod.test.ts +84 -0
- package/src/__tests__/transition-guard.integration.test.ts +7 -7
- package/src/__tests__/upgrade-cli.test.ts +54 -0
- 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 +10 -2
- 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-db-raw.ts +397 -0
- 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
|
@@ -42,7 +42,7 @@ function stubListHandler(
|
|
|
42
42
|
name: `${entityName}:list`,
|
|
43
43
|
schema: z.object({}),
|
|
44
44
|
handler: async () => ({ rows: [], nextCursor: null }) as never,
|
|
45
|
-
access: { openToAll:
|
|
45
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -534,7 +534,7 @@ describe("boot-validator", () => {
|
|
|
534
534
|
isSuccess: true as const,
|
|
535
535
|
data: { id: "1" },
|
|
536
536
|
}),
|
|
537
|
-
{ access: { openToAll:
|
|
537
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
538
538
|
);
|
|
539
539
|
}),
|
|
540
540
|
];
|
|
@@ -566,7 +566,7 @@ describe("boot-validator", () => {
|
|
|
566
566
|
isSuccess: true as const,
|
|
567
567
|
data: {},
|
|
568
568
|
}),
|
|
569
|
-
{ access: { openToAll:
|
|
569
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
570
570
|
);
|
|
571
571
|
r.useExtension("credit-cap", "credit");
|
|
572
572
|
});
|
|
@@ -631,7 +631,7 @@ describe("boot-validator", () => {
|
|
|
631
631
|
const features = [
|
|
632
632
|
defineFeature("a", (r) => {
|
|
633
633
|
r.queryHandler("list", z.object({}), async () => [], {
|
|
634
|
-
access: { openToAll:
|
|
634
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
635
635
|
});
|
|
636
636
|
}),
|
|
637
637
|
];
|
|
@@ -711,7 +711,7 @@ describe("boot-validator", () => {
|
|
|
711
711
|
const features = [
|
|
712
712
|
defineFeature("a", (r) => {
|
|
713
713
|
r.streamHandler("chat:complete", z.object({}), async function* () {}, {
|
|
714
|
-
access: { openToAll:
|
|
714
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
715
715
|
});
|
|
716
716
|
}),
|
|
717
717
|
];
|
|
@@ -2430,7 +2430,7 @@ describe("boot-validator", () => {
|
|
|
2430
2430
|
name: "invoice:approve",
|
|
2431
2431
|
schema: { _type: "stub" } as never,
|
|
2432
2432
|
handler: async () => ({ isSuccess: true, data: {} }) as never,
|
|
2433
|
-
access: { openToAll:
|
|
2433
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
2434
2434
|
});
|
|
2435
2435
|
r.screen({
|
|
2436
2436
|
id: "approve-invoice",
|
|
@@ -2713,14 +2713,14 @@ describe("boot-validator", () => {
|
|
|
2713
2713
|
name: "token:mint",
|
|
2714
2714
|
schema: { _type: "stub" } as never,
|
|
2715
2715
|
handler: async () => ({ isSuccess: true, data: {} }) as never,
|
|
2716
|
-
access: { openToAll:
|
|
2716
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
2717
2717
|
});
|
|
2718
2718
|
if (confirmOverride !== undefined) {
|
|
2719
2719
|
r.writeHandler({
|
|
2720
2720
|
name: "token:confirm",
|
|
2721
2721
|
schema: { _type: "stub" } as never,
|
|
2722
2722
|
handler: async () => ({ isSuccess: true, data: {} }) as never,
|
|
2723
|
-
access: { openToAll:
|
|
2723
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
2724
2724
|
});
|
|
2725
2725
|
}
|
|
2726
2726
|
r.screen({
|
|
@@ -4177,7 +4177,7 @@ describe("boot-validator", () => {
|
|
|
4177
4177
|
}),
|
|
4178
4178
|
);
|
|
4179
4179
|
r.queryHandler("products", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
4180
|
-
access: { openToAll:
|
|
4180
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
4181
4181
|
});
|
|
4182
4182
|
r.screen({
|
|
4183
4183
|
id: "product-projection",
|
|
@@ -4207,7 +4207,7 @@ describe("boot-validator", () => {
|
|
|
4207
4207
|
}),
|
|
4208
4208
|
);
|
|
4209
4209
|
r.queryHandler("products", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
4210
|
-
access: { openToAll:
|
|
4210
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
4211
4211
|
});
|
|
4212
4212
|
r.screen({
|
|
4213
4213
|
id: "product-projection",
|
|
@@ -4426,13 +4426,13 @@ describe("boot-validator", () => {
|
|
|
4426
4426
|
test("projectionDetail relatedList defaultSort.field sortable → kein Throw", () => {
|
|
4427
4427
|
const feature = defineFeature("shop", (r) => {
|
|
4428
4428
|
r.queryHandler("order-detail", z.object({}), async () => ({ id: "o1" }), {
|
|
4429
|
-
access: { openToAll:
|
|
4429
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
4430
4430
|
});
|
|
4431
4431
|
r.queryHandler(
|
|
4432
4432
|
"order-positions",
|
|
4433
4433
|
z.object({}),
|
|
4434
4434
|
async () => ({ rows: [], nextCursor: null }),
|
|
4435
|
-
{ access: { openToAll:
|
|
4435
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
4436
4436
|
);
|
|
4437
4437
|
r.screen({
|
|
4438
4438
|
id: "order-detail",
|
|
@@ -4993,7 +4993,7 @@ describe("boot-validator", () => {
|
|
|
4993
4993
|
const targetId = opts.targetId ?? "restock-form";
|
|
4994
4994
|
return defineFeature("shop", (r) => {
|
|
4995
4995
|
r.queryHandler("products", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
4996
|
-
access: { openToAll:
|
|
4996
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
4997
4997
|
});
|
|
4998
4998
|
r.writeHandler(
|
|
4999
4999
|
"restock",
|
|
@@ -5223,7 +5223,7 @@ describe("boot-validator — config key backing × scope", () => {
|
|
|
5223
5223
|
],
|
|
5224
5224
|
});
|
|
5225
5225
|
r.queryHandler("count", z.object({}), async () => ({ total: 0 }), {
|
|
5226
|
-
access: { openToAll:
|
|
5226
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
5227
5227
|
});
|
|
5228
5228
|
r.screen({
|
|
5229
5229
|
id: "product-dashboard",
|
|
@@ -5268,7 +5268,7 @@ describe("boot-validator — config key backing × scope", () => {
|
|
|
5268
5268
|
],
|
|
5269
5269
|
});
|
|
5270
5270
|
r.queryHandler("count", z.object({}), async () => ({ total: 0 }), {
|
|
5271
|
-
access: { openToAll:
|
|
5271
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
5272
5272
|
});
|
|
5273
5273
|
r.screen({
|
|
5274
5274
|
id: "product-dashboard",
|
|
@@ -5317,7 +5317,7 @@ describe("boot-validator — config key backing × scope", () => {
|
|
|
5317
5317
|
],
|
|
5318
5318
|
});
|
|
5319
5319
|
r.queryHandler("count", z.object({}), async () => ({ total: 0 }), {
|
|
5320
|
-
access: { openToAll:
|
|
5320
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
5321
5321
|
});
|
|
5322
5322
|
r.screen({
|
|
5323
5323
|
id: "product-dashboard",
|
|
@@ -5542,7 +5542,7 @@ describe("boot-validator — config key backing × scope", () => {
|
|
|
5542
5542
|
}),
|
|
5543
5543
|
);
|
|
5544
5544
|
r.queryHandler("products", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
5545
|
-
access: { openToAll:
|
|
5545
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
5546
5546
|
});
|
|
5547
5547
|
r.screen({
|
|
5548
5548
|
id: "product-projection",
|
|
@@ -5624,7 +5624,7 @@ describe("boot-validator — config key backing × scope", () => {
|
|
|
5624
5624
|
"create",
|
|
5625
5625
|
z.object({ name: z.string() }),
|
|
5626
5626
|
async () => ({ isSuccess: true as const, data: { id: "1" } }),
|
|
5627
|
-
{ access: { openToAll:
|
|
5627
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
5628
5628
|
);
|
|
5629
5629
|
r.screen({
|
|
5630
5630
|
id: "product-list",
|
|
@@ -209,7 +209,7 @@ describe("buildAppSchema", () => {
|
|
|
209
209
|
r.workspace({
|
|
210
210
|
id: "admin",
|
|
211
211
|
label: "Admin",
|
|
212
|
-
access: { openToAll:
|
|
212
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
213
213
|
nav: ["orders:nav:list", "fleet:nav:vehicles"],
|
|
214
214
|
default: true,
|
|
215
215
|
});
|
|
@@ -832,7 +832,11 @@ describe("buildAppSchema", () => {
|
|
|
832
832
|
const f = defineFeature("ent", (r) => {
|
|
833
833
|
r.entity("thing", entity);
|
|
834
834
|
r.nav({ id: "n", label: "N" });
|
|
835
|
-
r.workspace({
|
|
835
|
+
r.workspace({
|
|
836
|
+
id: "ws",
|
|
837
|
+
label: "Ws",
|
|
838
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
839
|
+
});
|
|
836
840
|
});
|
|
837
841
|
const app = buildAppSchema(createRegistry([f]));
|
|
838
842
|
const json = JSON.stringify(app);
|
|
@@ -980,7 +984,7 @@ describe("buildAppSchema", () => {
|
|
|
980
984
|
cursor: z.string().optional(),
|
|
981
985
|
}),
|
|
982
986
|
async () => ({ rows: [], nextCursor: null }),
|
|
983
|
-
{ access: { openToAll:
|
|
987
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
984
988
|
);
|
|
985
989
|
r.screen({
|
|
986
990
|
id: "schedule-list",
|
|
@@ -1000,7 +1004,7 @@ describe("buildAppSchema", () => {
|
|
|
1000
1004
|
test("projectionList: a query schema without search/sort/cursor derives no capability", () => {
|
|
1001
1005
|
const f = defineFeature("ledger", (r) => {
|
|
1002
1006
|
r.queryHandler("schedule:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
1003
|
-
access: { openToAll:
|
|
1007
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
1004
1008
|
});
|
|
1005
1009
|
r.screen({
|
|
1006
1010
|
id: "schedule-list",
|
|
@@ -1023,7 +1027,7 @@ describe("buildAppSchema", () => {
|
|
|
1023
1027
|
"schedule:list",
|
|
1024
1028
|
z.union([z.object({ a: z.string() }), z.object({ b: z.string() })]),
|
|
1025
1029
|
async () => ({ rows: [], nextCursor: null }),
|
|
1026
|
-
{ access: { openToAll:
|
|
1030
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
1027
1031
|
);
|
|
1028
1032
|
r.screen({
|
|
1029
1033
|
id: "schedule-list",
|
|
@@ -1052,7 +1056,7 @@ describe("buildAppSchema", () => {
|
|
|
1052
1056
|
rows: [],
|
|
1053
1057
|
nextCursor: null,
|
|
1054
1058
|
}),
|
|
1055
|
-
{ access: { openToAll:
|
|
1059
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
1056
1060
|
);
|
|
1057
1061
|
r.screen({
|
|
1058
1062
|
id: "schedule-list",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { buildConfigFeatureSchema } from "../build-config-feature-schema";
|
|
4
|
+
import { createTenantConfig } from "../config-helpers";
|
|
4
5
|
import { defineFeature } from "../define-feature";
|
|
5
6
|
import { createRegistry } from "../registry";
|
|
7
|
+
import type { AccessRule } from "../types/handlers";
|
|
6
8
|
import type { NavDefinition } from "../types/nav";
|
|
7
9
|
import type { ScreenDefinition, SecretsEditScreenDefinition } from "../types/screen";
|
|
8
10
|
|
|
@@ -114,6 +116,52 @@ describe("buildConfigFeatureSchema — secrets derivation", () => {
|
|
|
114
116
|
expect(navById(schema, "audience-tenant")?.access).toEqual({ roles: customRoles });
|
|
115
117
|
});
|
|
116
118
|
|
|
119
|
+
test("audience nav union keeps a granted openToAll's own reason, not the generic ALL_ROLE reason", () => {
|
|
120
|
+
const customReason = "billing key rotation is safe for any signed-in tenant member";
|
|
121
|
+
const secretsOpenToAll = defineFeature("secrets", (r) => {
|
|
122
|
+
r.writeHandler(
|
|
123
|
+
"set",
|
|
124
|
+
z.object({ key: z.string(), value: z.string() }),
|
|
125
|
+
async () => ({ isSuccess: true, data: null }),
|
|
126
|
+
{ access: { openToAll: { reason: customReason } } },
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
const billing = defineFeature("billing", (r) => {
|
|
130
|
+
r.config({
|
|
131
|
+
keys: { apiKey: createTenantConfig("text", { mask: { title: "billing.api-key" } }) },
|
|
132
|
+
});
|
|
133
|
+
r.secret("apiKey", { label: { en: "Billing API Key" }, scope: "tenant" });
|
|
134
|
+
});
|
|
135
|
+
const schema = buildConfigFeatureSchema(createRegistry([secretsOpenToAll, billing]));
|
|
136
|
+
|
|
137
|
+
expect(navById(schema, "audience-tenant")?.access).toEqual({
|
|
138
|
+
openToAll: { reason: customReason },
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("audience nav union ignores a malformed openToAll — falls back to the role union", () => {
|
|
143
|
+
const secretsMalformedOpenToAll = defineFeature("secrets", (r) => {
|
|
144
|
+
r.writeHandler(
|
|
145
|
+
"set",
|
|
146
|
+
z.object({ key: z.string(), value: z.string() }),
|
|
147
|
+
async () => ({ isSuccess: true, data: null }),
|
|
148
|
+
// @cast-boundary test — simulates JSON/Designer input that doesn't match the static union
|
|
149
|
+
{ access: { openToAll: {} } as unknown as AccessRule },
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
const billing = defineFeature("billing", (r) => {
|
|
153
|
+
r.config({
|
|
154
|
+
keys: { apiKey: createTenantConfig("text", { mask: { title: "billing.api-key" } }) },
|
|
155
|
+
});
|
|
156
|
+
r.secret("apiKey", { label: { en: "Billing API Key" }, scope: "tenant" });
|
|
157
|
+
});
|
|
158
|
+
const schema = buildConfigFeatureSchema(createRegistry([secretsMalformedOpenToAll, billing]));
|
|
159
|
+
|
|
160
|
+
expect(navById(schema, "audience-tenant")?.access).toEqual({
|
|
161
|
+
roles: ["TenantAdmin", "Admin", "SystemAdmin"],
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
117
165
|
test("declared secrets without a mounted secrets feature yield no secretsEdit screen and no secrets nav", () => {
|
|
118
166
|
const stripe = defineFeature("stripe", (r) => {
|
|
119
167
|
r.secret("apiKey", { label: { en: "Stripe API Key" }, scope: "tenant" });
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../config-helpers";
|
|
9
9
|
import { defineFeature } from "../define-feature";
|
|
10
10
|
import { createRegistry } from "../registry";
|
|
11
|
+
import { isOpenToAllGranted } from "../types/handlers";
|
|
11
12
|
import type { NavDefinition } from "../types/nav";
|
|
12
13
|
import type { ConfigEditScreenDefinition, ScreenDefinition } from "../types/screen";
|
|
13
14
|
|
|
@@ -265,7 +266,15 @@ describe("buildConfigFeatureSchema — access + workspace", () => {
|
|
|
265
266
|
roles: ["TenantAdmin", "Admin", "SystemAdmin"],
|
|
266
267
|
});
|
|
267
268
|
// digest is createUserConfig → write access.all (["all"]) → openToAll
|
|
268
|
-
|
|
269
|
+
const notifyUserAccess = configScreen("notify-user").access;
|
|
270
|
+
expect(notifyUserAccess).toEqual({
|
|
271
|
+
openToAll: {
|
|
272
|
+
reason:
|
|
273
|
+
'config key declares the "all" role, so every signed-in user of the tenant may read/write it',
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
// The synthesized reason must actually grant access, not just look configured.
|
|
277
|
+
expect(notifyUserAccess !== undefined && isOpenToAllGranted(notifyUserAccess)).toBe(true);
|
|
269
278
|
});
|
|
270
279
|
|
|
271
280
|
test("a screen mixing an openToAll key with role-restricted keys collapses to openToAll", () => {
|
|
@@ -283,8 +292,18 @@ describe("buildConfigFeatureSchema — access + workspace", () => {
|
|
|
283
292
|
const screen = out.screens.find((s) => s.id === "mixedwrite-tenant");
|
|
284
293
|
if (screen?.type !== "configEdit")
|
|
285
294
|
throw new Error('expected configEdit screen "mixedwrite-tenant"');
|
|
286
|
-
expect(screen.access).toEqual({
|
|
287
|
-
|
|
295
|
+
expect(screen.access).toEqual({
|
|
296
|
+
openToAll: {
|
|
297
|
+
reason:
|
|
298
|
+
'config key declares the "all" role, so every signed-in user of the tenant may read/write it',
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
expect(out.navs.find((n) => n.id === "audience-tenant")?.access).toEqual({
|
|
302
|
+
openToAll: {
|
|
303
|
+
reason:
|
|
304
|
+
'config key declares the "all" role, so every signed-in user of the tenant may read/write it',
|
|
305
|
+
},
|
|
306
|
+
});
|
|
288
307
|
});
|
|
289
308
|
|
|
290
309
|
test("openToAll collapse holds regardless of key declaration order (516/2)", () => {
|
|
@@ -305,7 +324,12 @@ describe("buildConfigFeatureSchema — access + workspace", () => {
|
|
|
305
324
|
const screen = out.screens.find((s) => s.id === "mixedwritereversed-tenant");
|
|
306
325
|
if (screen?.type !== "configEdit")
|
|
307
326
|
throw new Error('expected configEdit screen "mixedwritereversed-tenant"');
|
|
308
|
-
expect(screen.access).toEqual({
|
|
327
|
+
expect(screen.access).toEqual({
|
|
328
|
+
openToAll: {
|
|
329
|
+
reason:
|
|
330
|
+
'config key declares the "all" role, so every signed-in user of the tenant may read/write it',
|
|
331
|
+
},
|
|
332
|
+
});
|
|
309
333
|
});
|
|
310
334
|
|
|
311
335
|
test("returns empty (no workspace) when no key opts into the hub via mask", () => {
|
|
@@ -343,7 +367,12 @@ describe("buildConfigFeatureSchema — access + workspace", () => {
|
|
|
343
367
|
test("workspace access is the union of write roles across all hub keys", () => {
|
|
344
368
|
// billing/notify tenant keys → admin write; notify-user digest → write all.
|
|
345
369
|
// Any `all`-writable key collapses the whole switcher entry to openToAll.
|
|
346
|
-
expect(schema.workspace?.definition.access).toEqual({
|
|
370
|
+
expect(schema.workspace?.definition.access).toEqual({
|
|
371
|
+
openToAll: {
|
|
372
|
+
reason:
|
|
373
|
+
'config key declares the "all" role, so every signed-in user of the tenant may read/write it',
|
|
374
|
+
},
|
|
375
|
+
});
|
|
347
376
|
});
|
|
348
377
|
|
|
349
378
|
test("workspace access stays role-gated when no hub key is world-writable", () => {
|
|
@@ -87,7 +87,7 @@ describe("defineFeature", () => {
|
|
|
87
87
|
const _email: string = event.payload.email;
|
|
88
88
|
return { isSuccess: true, data: { id: 1 } };
|
|
89
89
|
},
|
|
90
|
-
{ access: { openToAll:
|
|
90
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
91
91
|
);
|
|
92
92
|
});
|
|
93
93
|
|
|
@@ -105,7 +105,7 @@ describe("defineFeature", () => {
|
|
|
105
105
|
isSuccess: true,
|
|
106
106
|
data: null,
|
|
107
107
|
}),
|
|
108
|
-
{ access: { openToAll:
|
|
108
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
109
109
|
);
|
|
110
110
|
});
|
|
111
111
|
expect(ref?.name).toBe("order:create");
|
|
@@ -115,7 +115,7 @@ describe("defineFeature", () => {
|
|
|
115
115
|
let ref: { name: string } | undefined;
|
|
116
116
|
defineFeature("test", (r) => {
|
|
117
117
|
ref = r.queryHandler("order:list", z.object({}), async () => [], {
|
|
118
|
-
access: { openToAll:
|
|
118
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
119
119
|
});
|
|
120
120
|
});
|
|
121
121
|
expect(ref?.name).toBe("order:list");
|
|
@@ -130,13 +130,15 @@ describe("defineFeature", () => {
|
|
|
130
130
|
async function* (query) {
|
|
131
131
|
yield `echo:${query.payload.prompt}`;
|
|
132
132
|
},
|
|
133
|
-
{ access: { openToAll:
|
|
133
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
134
134
|
);
|
|
135
135
|
});
|
|
136
136
|
expect(ref?.name).toBe("chat:complete");
|
|
137
137
|
|
|
138
138
|
const stored = feature.streamHandlers["chat:complete"];
|
|
139
|
-
expect(stored?.access).toEqual({
|
|
139
|
+
expect(stored?.access).toEqual({
|
|
140
|
+
openToAll: { reason: "test handler callable by any signed-in test user" },
|
|
141
|
+
});
|
|
140
142
|
|
|
141
143
|
const chunks: string[] = [];
|
|
142
144
|
for await (const chunk of stored!.handler(
|
|
@@ -225,7 +227,7 @@ describe("defineFeature", () => {
|
|
|
225
227
|
const _id: number = query.payload.userId;
|
|
226
228
|
return { id: _id, email: "test@test.de" };
|
|
227
229
|
},
|
|
228
|
-
{ access: { openToAll:
|
|
230
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
229
231
|
);
|
|
230
232
|
});
|
|
231
233
|
|
|
@@ -239,7 +241,7 @@ describe("defineFeature", () => {
|
|
|
239
241
|
handler: async () => {
|
|
240
242
|
return [{ id: 1, email: "test@test.de" }];
|
|
241
243
|
},
|
|
242
|
-
access: { openToAll:
|
|
244
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
243
245
|
});
|
|
244
246
|
|
|
245
247
|
const feature = defineFeature("test", (r) => {
|
|
@@ -402,12 +404,12 @@ describe("createRegistry", () => {
|
|
|
402
404
|
test("looks up handlers across features", () => {
|
|
403
405
|
const f1 = defineFeature("admin", (r) => {
|
|
404
406
|
r.writeHandler("user:invite", z.object({}), async () => ({ isSuccess: true, data: null }), {
|
|
405
|
-
access: { openToAll:
|
|
407
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
406
408
|
});
|
|
407
409
|
});
|
|
408
410
|
const f2 = defineFeature("profile", (r) => {
|
|
409
411
|
r.queryHandler("profile:me", z.object({}), async () => ({ id: 1 }), {
|
|
410
|
-
access: { openToAll:
|
|
412
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
411
413
|
});
|
|
412
414
|
});
|
|
413
415
|
|
|
@@ -442,12 +444,12 @@ describe("createRegistry", () => {
|
|
|
442
444
|
test("different features can have same handler short name (prefixed differently)", () => {
|
|
443
445
|
const f1 = defineFeature("a", (r) => {
|
|
444
446
|
r.writeHandler("user:invite", z.object({}), async () => ({ isSuccess: true, data: null }), {
|
|
445
|
-
access: { openToAll:
|
|
447
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
446
448
|
});
|
|
447
449
|
});
|
|
448
450
|
const f2 = defineFeature("b", (r) => {
|
|
449
451
|
r.writeHandler("user:invite", z.object({}), async () => ({ isSuccess: true, data: null }), {
|
|
450
|
-
access: { openToAll:
|
|
452
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
451
453
|
});
|
|
452
454
|
});
|
|
453
455
|
|
|
@@ -513,7 +515,7 @@ describe("createRegistry", () => {
|
|
|
513
515
|
);
|
|
514
516
|
// Handler name "promote" has no entity prefix → can't be mapped
|
|
515
517
|
r.writeHandler("promote", z.object({}), async () => ({ isSuccess: true, data: null }), {
|
|
516
|
-
access: { openToAll:
|
|
518
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
517
519
|
});
|
|
518
520
|
});
|
|
519
521
|
|
|
@@ -531,7 +533,7 @@ describe("createRegistry", () => {
|
|
|
531
533
|
);
|
|
532
534
|
// No field-access rules on entity → "reset" without entity prefix is fine
|
|
533
535
|
r.writeHandler("reset", z.object({}), async () => ({ isSuccess: true, data: null }), {
|
|
534
|
-
access: { openToAll:
|
|
536
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
535
537
|
});
|
|
536
538
|
});
|
|
537
539
|
|
|
@@ -562,7 +564,7 @@ describe("createRegistry", () => {
|
|
|
562
564
|
isSuccess: true,
|
|
563
565
|
data: null,
|
|
564
566
|
}),
|
|
565
|
-
{ access: { openToAll:
|
|
567
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
566
568
|
);
|
|
567
569
|
});
|
|
568
570
|
|
|
@@ -591,10 +593,12 @@ describe("createRegistry", () => {
|
|
|
591
593
|
isSuccess: true,
|
|
592
594
|
data: null,
|
|
593
595
|
}),
|
|
594
|
-
{ access: { openToAll:
|
|
596
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
595
597
|
);
|
|
596
598
|
// Typo: "emp" instead of "employee"
|
|
597
|
-
r.queryHandler("emp:list", z.object({}), async () => [], {
|
|
599
|
+
r.queryHandler("emp:list", z.object({}), async () => [], {
|
|
600
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
601
|
+
});
|
|
598
602
|
});
|
|
599
603
|
|
|
600
604
|
expect(() => createRegistry([feature])).toThrow(/emp:list.*entity-bound.*no matching entity/i);
|
|
@@ -622,13 +626,15 @@ describe("createRegistry", () => {
|
|
|
622
626
|
isSuccess: true,
|
|
623
627
|
data: null,
|
|
624
628
|
}),
|
|
625
|
-
{ access: { openToAll:
|
|
629
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
626
630
|
);
|
|
627
631
|
// Standalone queries — no dot, intentionally not entity-bound
|
|
628
632
|
r.queryHandler("dashboard", z.object({}), async () => ({ total: 42 }), {
|
|
629
|
-
access: { openToAll:
|
|
633
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
634
|
+
});
|
|
635
|
+
r.queryHandler("orgChart", z.object({}), async () => [], {
|
|
636
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
630
637
|
});
|
|
631
|
-
r.queryHandler("orgChart", z.object({}), async () => [], { access: { openToAll: true } });
|
|
632
638
|
});
|
|
633
639
|
|
|
634
640
|
expect(() => createRegistry([feature])).not.toThrow();
|
|
@@ -924,12 +930,20 @@ describe("hasAccess", () => {
|
|
|
924
930
|
|
|
925
931
|
test("openToAll grants access to any authenticated user", () => {
|
|
926
932
|
const user = createTestUser({ roles: ["Employee"] });
|
|
927
|
-
expect(
|
|
933
|
+
expect(
|
|
934
|
+
hasAccess(user, {
|
|
935
|
+
openToAll: { reason: "test handler callable by any signed-in test user" },
|
|
936
|
+
}),
|
|
937
|
+
).toBe(true);
|
|
928
938
|
});
|
|
929
939
|
|
|
930
940
|
test("openToAll grants access even to user with no roles", () => {
|
|
931
941
|
const user = createTestUser({ roles: [] });
|
|
932
|
-
expect(
|
|
942
|
+
expect(
|
|
943
|
+
hasAccess(user, {
|
|
944
|
+
openToAll: { reason: "test handler callable by any signed-in test user" },
|
|
945
|
+
}),
|
|
946
|
+
).toBe(true);
|
|
933
947
|
});
|
|
934
948
|
|
|
935
949
|
test.each([{ openToAll: false }, { openToAll: {} }])(
|
|
@@ -2105,10 +2119,10 @@ describe("registry boot validation", () => {
|
|
|
2105
2119
|
const feature = defineFeature("shop", (r) => {
|
|
2106
2120
|
r.entity("order", createEntity({ table: "Orders", fields: {} }));
|
|
2107
2121
|
r.writeHandler("order:create", z.object({}), async () => ({ isSuccess: true, data: null }), {
|
|
2108
|
-
access: { openToAll:
|
|
2122
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
2109
2123
|
});
|
|
2110
2124
|
r.writeHandler("order:cancel", z.object({}), async () => ({ isSuccess: true, data: null }), {
|
|
2111
|
-
access: { openToAll:
|
|
2125
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
2112
2126
|
});
|
|
2113
2127
|
// Ein Job-Body, zwei Trigger — DRY-Pattern für Fanout-Cases.
|
|
2114
2128
|
r.job(
|
|
@@ -2129,7 +2143,7 @@ describe("registry boot validation", () => {
|
|
|
2129
2143
|
const feature = defineFeature("shop", (r) => {
|
|
2130
2144
|
r.entity("order", createEntity({ table: "Orders", fields: {} }));
|
|
2131
2145
|
r.writeHandler("order:create", z.object({}), async () => ({ isSuccess: true, data: null }), {
|
|
2132
|
-
access: { openToAll:
|
|
2146
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
2133
2147
|
});
|
|
2134
2148
|
// create existiert, cancel nicht — zweiter Trigger ist Geist
|
|
2135
2149
|
r.job(
|
|
@@ -2153,7 +2167,7 @@ describe("registry boot validation", () => {
|
|
|
2153
2167
|
const feature = defineFeature("test", (r) => {
|
|
2154
2168
|
r.entity("item", createEntity({ table: "Items", fields: {} }));
|
|
2155
2169
|
r.writeHandler("item.create", z.object({}), async () => ({ isSuccess: true, data: null }), {
|
|
2156
|
-
access: { openToAll:
|
|
2170
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
2157
2171
|
});
|
|
2158
2172
|
r.hook("postSave", "item.create", async () => {});
|
|
2159
2173
|
});
|
|
@@ -149,9 +149,11 @@ describe("defineEntityQueryHandler", () => {
|
|
|
149
149
|
|
|
150
150
|
test("access option is forwarded", () => {
|
|
151
151
|
const def = defineEntityListHandler("note", noteEntity, {
|
|
152
|
-
access: { openToAll:
|
|
152
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
153
|
+
});
|
|
154
|
+
expect(def.access).toEqual({
|
|
155
|
+
openToAll: { reason: "test handler callable by any signed-in test user" },
|
|
153
156
|
});
|
|
154
|
-
expect(def.access).toEqual({ openToAll: true });
|
|
155
157
|
});
|
|
156
158
|
});
|
|
157
159
|
|
|
@@ -180,10 +182,12 @@ describe("defineProjectionQueryHandler", () => {
|
|
|
180
182
|
const def = defineProjectionQueryHandler(
|
|
181
183
|
"revenue:list",
|
|
182
184
|
"showcase:projection:customer-revenue",
|
|
183
|
-
{ access: { openToAll:
|
|
185
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
184
186
|
);
|
|
185
187
|
expect(def.name).toBe("revenue:list");
|
|
186
|
-
expect(def.access).toEqual({
|
|
188
|
+
expect(def.access).toEqual({
|
|
189
|
+
openToAll: { reason: "test handler callable by any signed-in test user" },
|
|
190
|
+
});
|
|
187
191
|
// Empty-object schema — handler takes no payload fields.
|
|
188
192
|
expect(def.schema.safeParse({}).success).toBe(true);
|
|
189
193
|
});
|
|
@@ -192,7 +196,7 @@ describe("defineProjectionQueryHandler", () => {
|
|
|
192
196
|
const def = defineProjectionQueryHandler(
|
|
193
197
|
"revenue:list",
|
|
194
198
|
"showcase:projection:customer-revenue",
|
|
195
|
-
{ access: { openToAll:
|
|
199
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
196
200
|
);
|
|
197
201
|
const fakeRows = [{ customer: "a", totalCents: 100 }];
|
|
198
202
|
const ctx = {
|
|
@@ -215,7 +219,10 @@ describe("defineProjectionQueryHandler", () => {
|
|
|
215
219
|
const def = defineProjectionQueryHandler(
|
|
216
220
|
"revenue:list",
|
|
217
221
|
"showcase:projection:customer-revenue",
|
|
218
|
-
{
|
|
222
|
+
{
|
|
223
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
224
|
+
unsafeAllTenants: true,
|
|
225
|
+
},
|
|
219
226
|
);
|
|
220
227
|
const ctx = { queryProjection: mock().mockResolvedValue([]) };
|
|
221
228
|
await def.handler(
|
|
@@ -328,7 +335,9 @@ describe("registerEntityCrud", () => {
|
|
|
328
335
|
const { r, entities, writes, queries } = createCrudRegistrarMock();
|
|
329
336
|
registerEntityCrud(r, "note", noteEntitySoftDelete, {
|
|
330
337
|
write: { access: { roles: ["Admin"] } },
|
|
331
|
-
read: {
|
|
338
|
+
read: {
|
|
339
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
340
|
+
},
|
|
332
341
|
});
|
|
333
342
|
expect(entities["note"]).toBe(noteEntitySoftDelete);
|
|
334
343
|
expect(writes.map((w) => w.name)).toEqual([
|
|
@@ -339,7 +348,9 @@ describe("registerEntityCrud", () => {
|
|
|
339
348
|
]);
|
|
340
349
|
expect(queries.map((q) => q.name)).toEqual(["note:list", "note:detail"]);
|
|
341
350
|
expect(writes[0]?.access).toEqual({ roles: ["Admin"] });
|
|
342
|
-
expect(queries[0]?.access).toEqual({
|
|
351
|
+
expect(queries[0]?.access).toEqual({
|
|
352
|
+
openToAll: { reason: "test handler callable by any signed-in test user" },
|
|
353
|
+
});
|
|
343
354
|
});
|
|
344
355
|
|
|
345
356
|
test("verbs.delete: false skips delete handler", () => {
|
|
@@ -50,7 +50,7 @@ const throwOnPreSave: import("../types").PreSaveHookFn = async () => {
|
|
|
50
50
|
const contactFeature = defineFeature("presave-wiring", (r) => {
|
|
51
51
|
r.crud("contact", contactEntity, {
|
|
52
52
|
write: { access: { roles: ["User"] } },
|
|
53
|
-
read: { access: { openToAll:
|
|
53
|
+
read: { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
// preSave has no entity-wide `{ allOf }` shorthand (unlike postSave/
|
|
@@ -70,7 +70,7 @@ const throwingEntity = createEntity({
|
|
|
70
70
|
const throwingFeature = defineFeature("presave-wiring-throw", (r) => {
|
|
71
71
|
r.crud("thing", throwingEntity, {
|
|
72
72
|
write: { access: { roles: ["User"] } },
|
|
73
|
-
read: { access: { openToAll:
|
|
73
|
+
read: { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
74
74
|
});
|
|
75
75
|
r.hook("preSave", "thing:create", throwOnPreSave);
|
|
76
76
|
});
|