@cosmicdrift/kumiko-framework 0.269.2 → 0.273.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-cross-tenant/manual-only.expected.ts +44 -0
- package/src/__tests__/fixtures/migrate-cross-tenant/manual-only.input.ts +44 -0
- package/src/__tests__/fixtures/migrate-cross-tenant/rewrite.expected.ts +41 -0
- package/src/__tests__/fixtures/migrate-cross-tenant/rewrite.input.ts +41 -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-cross-tenant-codemod.test.ts +106 -0
- package/src/__tests__/migrate-open-to-all-codemod.test.ts +84 -0
- package/src/__tests__/related-list-parent-filter.integration.test.ts +103 -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__/nested-write-ownership.integration.test.ts +268 -0
- 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/api/server.ts +7 -20
- package/src/bun-db/__tests__/query-guards.test.ts +60 -3
- package/src/bun-db/query.ts +24 -21
- package/src/changes.json +99 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +77 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +1 -1
- package/src/db/__tests__/list-filter-field-access.integration.test.ts +1 -1
- package/src/db/__tests__/replay-migration-sql.test.ts +17 -0
- package/src/db/__tests__/tenant-db-convention-cross-tenant.test.ts +72 -0
- package/src/db/__tests__/tenant-db-declared-unsafe-raw.test.ts +104 -0
- package/src/db/__tests__/tenant-db-global.test.ts +92 -0
- package/src/db/__tests__/tenant-db-no-raw.test.ts +75 -0
- package/src/db/__tests__/tenant-db.integration.test.ts +3 -1
- package/src/db/__tests__/unchecked-system-db.test.ts +76 -0
- package/src/db/apply-entity-event.ts +1 -1
- package/src/db/assert-exists-in.ts +1 -1
- package/src/db/event-store-executor-context.ts +5 -2
- package/src/db/event-store-executor-read.ts +6 -4
- package/src/db/event-store-executor-write.ts +24 -18
- package/src/db/queries/shadow-swap.ts +36 -0
- package/src/db/replay-migration-sql.ts +5 -5
- package/src/db/tenant-db-runner.ts +23 -0
- package/src/db/tenant-db.ts +97 -24
- package/src/derivatives/__tests__/derivatives-context.integration.test.ts +2 -2
- package/src/derivatives/__tests__/derivatives-context.test.ts +0 -1
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +5 -5
- package/src/engine/__tests__/boot-validator-nav-area.test.ts +116 -0
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-projection-list.test.ts +108 -17
- package/src/engine/__tests__/boot-validator-query-output-schema.test.ts +161 -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 +69 -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-create-caller-id.integration.test.ts +87 -0
- package/src/engine/__tests__/entity-handlers-escape-hatch.test.ts +91 -0
- package/src/engine/__tests__/entity-handlers.test.ts +47 -8
- package/src/engine/__tests__/entity-presave-wiring.integration.test.ts +2 -2
- package/src/engine/__tests__/entity-restore-tenant-isolation.integration.test.ts +30 -5
- package/src/engine/__tests__/entity-write-crosstenant.integration.test.ts +206 -47
- 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__/read-steps-tenant-filter.integration.test.ts +215 -0
- package/src/engine/__tests__/registry.test.ts +19 -13
- package/src/engine/__tests__/required-surface-keys.test.ts +16 -0
- package/src/engine/__tests__/screen.test.ts +176 -20
- package/src/engine/__tests__/soft-delete-cleanup.test.ts +0 -1
- package/src/engine/__tests__/steps-read.test.ts +204 -8
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +7 -4
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +6 -3
- package/src/engine/boot-validator/__tests__/access-declarations.test.ts +5 -4
- package/src/engine/boot-validator/__tests__/global-tenancy.test.ts +16 -1
- package/src/engine/boot-validator/__tests__/parent-ref.test.ts +18 -6
- package/src/engine/boot-validator/__tests__/rate-limit-disabled-reason.test.ts +45 -0
- package/src/engine/boot-validator/access-declarations.ts +3 -5
- package/src/engine/boot-validator/entity-handler.ts +21 -0
- package/src/engine/boot-validator/global-tenancy.ts +13 -1
- package/src/engine/boot-validator/nav.ts +4 -3
- package/src/engine/boot-validator/parent-ref.ts +1 -1
- package/src/engine/boot-validator/projection-list-screens.ts +67 -2
- package/src/engine/boot-validator/query-output-columns.ts +33 -0
- package/src/engine/boot-validator/screens.ts +154 -31
- package/src/engine/build-config-feature-schema.ts +12 -2
- package/src/engine/entity-handlers.ts +72 -20
- 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/feature-entity-handlers.ts +4 -4
- package/src/engine/index.ts +5 -0
- package/src/engine/pattern-library/shared-fields.ts +7 -3
- package/src/engine/registry-validate.ts +12 -4
- package/src/engine/schema-builder.ts +6 -0
- package/src/engine/screen-helpers.ts +52 -0
- package/src/engine/steps/_read-source.ts +12 -0
- package/src/engine/steps/read-find-many.ts +8 -2
- package/src/engine/steps/read-find-one.ts +11 -8
- package/src/engine/steps/unsafe-projection-delete.ts +2 -1
- package/src/engine/steps/unsafe-projection-upsert.ts +2 -1
- package/src/engine/types/index.ts +8 -1
- 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/errors/i18n/de.yaml +14 -0
- package/src/errors/i18n/en.yaml +13 -0
- package/src/errors/reasons.ts +4 -0
- package/src/es-ops/__tests__/runner.integration.test.ts +2 -2
- package/src/i18n/required-surface-keys.ts +6 -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__/job-systemdb.integration.test.ts +5 -4
- package/src/jobs/__tests__/jobs.integration.test.ts +42 -1
- package/src/jobs/job-runner.ts +35 -5
- 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__/escape-hatch-audit.integration.test.ts +254 -0
- package/src/pipeline/__tests__/escape-hatch-report.test.ts +149 -0
- 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__/msp-rebuild.integration.test.ts +28 -0
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +94 -0
- package/src/pipeline/__tests__/projections-runner-systemscope.integration.test.ts +101 -0
- package/src/pipeline/__tests__/query-projection.integration.test.ts +172 -17
- package/src/pipeline/__tests__/rate-limit-default.integration.test.ts +118 -0
- package/src/pipeline/__tests__/rate-limit-default.test.ts +117 -0
- package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +5 -2
- package/src/pipeline/__tests__/system-identity-switch.test.ts +240 -1
- package/src/pipeline/dispatch-query.ts +27 -8
- package/src/pipeline/dispatch-shared.ts +141 -57
- package/src/pipeline/dispatch-stream.ts +1 -3
- package/src/pipeline/dispatch-write.ts +62 -14
- package/src/pipeline/dispatcher.ts +20 -0
- package/src/pipeline/escape-hatch-report.ts +127 -0
- package/src/pipeline/index.ts +8 -0
- package/src/pipeline/member-read-only-transaction.ts +75 -0
- package/src/pipeline/msp-rebuild.ts +2 -0
- package/src/pipeline/projection-rebuild.ts +2 -0
- package/src/pipeline/projections-runner.ts +15 -13
- package/src/pipeline/system-identity-switch.ts +121 -9
- package/src/scripts/codemod/migrate-cross-tenant.ts +335 -0
- package/src/scripts/codemod/migrate-db-raw.ts +19 -6
- package/src/scripts/codemod/migrate-open-to-all.ts +249 -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/src/ui-types/index.ts +3 -0
- package/src/utils/__tests__/ids.test.ts +29 -1
- package/src/utils/ids.ts +10 -1
- package/src/utils/index.ts +1 -1
|
@@ -31,7 +31,12 @@ import type {
|
|
|
31
31
|
} from "../types/config";
|
|
32
32
|
import type { MetricOptions, SecretOptions } from "../types/feature";
|
|
33
33
|
import type { EntityDefinition } from "../types/fields";
|
|
34
|
-
import type {
|
|
34
|
+
import type {
|
|
35
|
+
AccessRule,
|
|
36
|
+
ClaimKeyType,
|
|
37
|
+
EscapeHatchDeclaration,
|
|
38
|
+
RateLimitOption,
|
|
39
|
+
} from "../types/handlers";
|
|
35
40
|
import type { HookPhase } from "../types/hooks";
|
|
36
41
|
import type { HttpRouteMethod } from "../types/http-route";
|
|
37
42
|
import type { NavDefinition } from "../types/nav";
|
|
@@ -94,6 +99,7 @@ export type AddWriteHandlerArgs = {
|
|
|
94
99
|
readonly access?: AccessRule;
|
|
95
100
|
readonly rateLimit?: RateLimitOption;
|
|
96
101
|
readonly unsafeSkipTransitionGuard?: boolean;
|
|
102
|
+
readonly escapeHatch?: EscapeHatchDeclaration;
|
|
97
103
|
};
|
|
98
104
|
|
|
99
105
|
export type AddQueryHandlerArgs = {
|
|
@@ -102,6 +108,7 @@ export type AddQueryHandlerArgs = {
|
|
|
102
108
|
readonly handlerSource: string;
|
|
103
109
|
readonly access?: AccessRule;
|
|
104
110
|
readonly rateLimit?: RateLimitOption;
|
|
111
|
+
readonly escapeHatch?: EscapeHatchDeclaration;
|
|
105
112
|
};
|
|
106
113
|
|
|
107
114
|
export type AddStreamHandlerArgs = {
|
|
@@ -121,6 +128,7 @@ export type AddHookArgs = {
|
|
|
121
128
|
/** Source text of the closure, e.g. `"async (event, ctx) => { ... }"`. */
|
|
122
129
|
readonly handlerSource: string;
|
|
123
130
|
readonly phase?: HookPhase;
|
|
131
|
+
readonly escapeHatch?: EscapeHatchDeclaration;
|
|
124
132
|
};
|
|
125
133
|
|
|
126
134
|
export type AddJobArgs = {
|
|
@@ -380,6 +388,7 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
|
|
|
380
388
|
access,
|
|
381
389
|
rateLimit,
|
|
382
390
|
unsafeSkipTransitionGuard,
|
|
391
|
+
escapeHatch,
|
|
383
392
|
}) {
|
|
384
393
|
add({
|
|
385
394
|
kind: "writeHandler",
|
|
@@ -390,10 +399,11 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
|
|
|
390
399
|
...(access !== undefined && { access }),
|
|
391
400
|
...(rateLimit !== undefined && { rateLimit }),
|
|
392
401
|
...(unsafeSkipTransitionGuard === true && { unsafeSkipTransitionGuard: true }),
|
|
402
|
+
...(escapeHatch !== undefined && { escapeHatch }),
|
|
393
403
|
});
|
|
394
404
|
},
|
|
395
405
|
|
|
396
|
-
addQueryHandler({ name, schemaSource, handlerSource, access, rateLimit }) {
|
|
406
|
+
addQueryHandler({ name, schemaSource, handlerSource, access, rateLimit, escapeHatch }) {
|
|
397
407
|
add({
|
|
398
408
|
kind: "queryHandler",
|
|
399
409
|
source: SYNTHETIC_LOC,
|
|
@@ -402,6 +412,7 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
|
|
|
402
412
|
handlerBody: rawLoc(handlerSource),
|
|
403
413
|
...(access !== undefined && { access }),
|
|
404
414
|
...(rateLimit !== undefined && { rateLimit }),
|
|
415
|
+
...(escapeHatch !== undefined && { escapeHatch }),
|
|
405
416
|
});
|
|
406
417
|
},
|
|
407
418
|
|
|
@@ -417,7 +428,7 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
|
|
|
417
428
|
});
|
|
418
429
|
},
|
|
419
430
|
|
|
420
|
-
addHook({ type, target, handlerSource, phase }) {
|
|
431
|
+
addHook({ type, target, handlerSource, phase, escapeHatch }) {
|
|
421
432
|
add({
|
|
422
433
|
kind: "hook",
|
|
423
434
|
source: SYNTHETIC_LOC,
|
|
@@ -425,6 +436,7 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
|
|
|
425
436
|
target,
|
|
426
437
|
fnBody: rawLoc(handlerSource),
|
|
427
438
|
...(phase !== undefined && { phase }),
|
|
439
|
+
...(escapeHatch !== undefined && { escapeHatch }),
|
|
428
440
|
});
|
|
429
441
|
},
|
|
430
442
|
|
|
@@ -70,6 +70,7 @@ import type {
|
|
|
70
70
|
AccessRule,
|
|
71
71
|
AgentHandlerHints,
|
|
72
72
|
ClaimKeyType,
|
|
73
|
+
EscapeHatchDeclaration,
|
|
73
74
|
RateLimitOption,
|
|
74
75
|
} from "../types/handlers";
|
|
75
76
|
import type { HookPhase } from "../types/hooks";
|
|
@@ -388,6 +389,7 @@ export type WriteHandlerPattern = {
|
|
|
388
389
|
readonly agent?: AgentHandlerHints;
|
|
389
390
|
readonly rateLimit?: RateLimitOption;
|
|
390
391
|
readonly unsafeSkipTransitionGuard?: boolean;
|
|
392
|
+
readonly escapeHatch?: EscapeHatchDeclaration;
|
|
391
393
|
};
|
|
392
394
|
|
|
393
395
|
// `r.queryHandler(...)` — registers a read handler: name, Zod input schema,
|
|
@@ -410,6 +412,7 @@ export type QueryHandlerPattern = {
|
|
|
410
412
|
readonly description?: string;
|
|
411
413
|
readonly agent?: AgentHandlerHints;
|
|
412
414
|
readonly rateLimit?: RateLimitOption;
|
|
415
|
+
readonly escapeHatch?: EscapeHatchDeclaration;
|
|
413
416
|
};
|
|
414
417
|
|
|
415
418
|
// `r.streamHandler(...)` — registers a streaming read handler: name, Zod
|
|
@@ -444,6 +447,7 @@ export type HookPattern = {
|
|
|
444
447
|
readonly target: string | readonly string[] | { readonly allOf: string };
|
|
445
448
|
readonly fnBody: SourceLocation;
|
|
446
449
|
readonly phase?: HookPhase;
|
|
450
|
+
readonly escapeHatch?: EscapeHatchDeclaration;
|
|
447
451
|
};
|
|
448
452
|
|
|
449
453
|
// `r.job(name, options, handler)` — registers a background job, qualified
|
|
@@ -408,6 +408,7 @@ function renderWriteHandler(p: WriteHandlerPattern): string {
|
|
|
408
408
|
if (p.agent !== undefined) lines.push(` agent: ${renderValue(p.agent)},`);
|
|
409
409
|
if (p.rateLimit !== undefined) lines.push(` rateLimit: ${renderValue(p.rateLimit)},`);
|
|
410
410
|
if (p.unsafeSkipTransitionGuard === true) lines.push(" unsafeSkipTransitionGuard: true,");
|
|
411
|
+
if (p.escapeHatch !== undefined) lines.push(` escapeHatch: ${renderValue(p.escapeHatch)},`);
|
|
411
412
|
lines.push("});");
|
|
412
413
|
return lines.join("\n");
|
|
413
414
|
}
|
|
@@ -422,6 +423,7 @@ function renderQueryHandler(p: QueryHandlerPattern): string {
|
|
|
422
423
|
if (p.description !== undefined) lines.push(` description: ${JSON.stringify(p.description)},`);
|
|
423
424
|
if (p.agent !== undefined) lines.push(` agent: ${renderValue(p.agent)},`);
|
|
424
425
|
if (p.rateLimit !== undefined) lines.push(` rateLimit: ${renderValue(p.rateLimit)},`);
|
|
426
|
+
if (p.escapeHatch !== undefined) lines.push(` escapeHatch: ${renderValue(p.escapeHatch)},`);
|
|
425
427
|
lines.push("});");
|
|
426
428
|
return lines.join("\n");
|
|
427
429
|
}
|
|
@@ -450,6 +452,7 @@ function renderHook(p: HookPattern): string {
|
|
|
450
452
|
lines.push(` target: ${renderHookTarget(p.target)},`);
|
|
451
453
|
lines.push(` handler: ${reindentBody(p.fnBody.raw, PATTERN_INDENT)},`);
|
|
452
454
|
if (p.phase !== undefined) lines.push(` phase: ${JSON.stringify(p.phase)},`);
|
|
455
|
+
if (p.escapeHatch !== undefined) lines.push(` escapeHatch: ${renderValue(p.escapeHatch)},`);
|
|
453
456
|
lines.push("});");
|
|
454
457
|
return lines.join("\n");
|
|
455
458
|
}
|
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
HandlerRef,
|
|
24
24
|
NameOrRef,
|
|
25
25
|
QueryHandlerFn,
|
|
26
|
-
|
|
26
|
+
RateLimitDeclaration,
|
|
27
27
|
RelationDefinition,
|
|
28
28
|
StreamHandlerFn,
|
|
29
29
|
WriteHandlerFn,
|
|
@@ -124,7 +124,7 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
124
124
|
handler?: WriteHandlerFn<z.infer<TSchema>>,
|
|
125
125
|
options?: {
|
|
126
126
|
access: AccessRule;
|
|
127
|
-
rateLimit?:
|
|
127
|
+
rateLimit?: RateLimitDeclaration;
|
|
128
128
|
description?: string;
|
|
129
129
|
agent?: AgentHandlerHints;
|
|
130
130
|
escapeHatch?: EscapeHatchDeclaration;
|
|
@@ -181,7 +181,7 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
181
181
|
handler?: QueryHandlerFn<z.infer<TSchema>>,
|
|
182
182
|
options?: {
|
|
183
183
|
access: AccessRule;
|
|
184
|
-
rateLimit?:
|
|
184
|
+
rateLimit?: RateLimitDeclaration;
|
|
185
185
|
outputSchema?: ZodType;
|
|
186
186
|
description?: string;
|
|
187
187
|
agent?: AgentHandlerHints;
|
|
@@ -230,7 +230,7 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
230
230
|
handler?: StreamHandlerFn<z.infer<TSchema>>,
|
|
231
231
|
options?: {
|
|
232
232
|
access: AccessRule;
|
|
233
|
-
rateLimit?:
|
|
233
|
+
rateLimit?: RateLimitDeclaration;
|
|
234
234
|
escapeHatch?: EscapeHatchDeclaration;
|
|
235
235
|
},
|
|
236
236
|
): HandlerRef {
|
package/src/engine/index.ts
CHANGED
|
@@ -374,7 +374,12 @@ export type {
|
|
|
374
374
|
EntityListScreenDefinition,
|
|
375
375
|
EntityRef,
|
|
376
376
|
EntityRelations,
|
|
377
|
+
EscapeHatchAuditSink,
|
|
377
378
|
EscapeHatchDeclaration,
|
|
379
|
+
EscapeHatchKind,
|
|
380
|
+
EscapeHatchReporter,
|
|
381
|
+
EscapeHatchTarget,
|
|
382
|
+
EscapeHatchUseEvent,
|
|
378
383
|
EventDef,
|
|
379
384
|
FeatureDefinition,
|
|
380
385
|
FeatureRegistrar,
|
|
@@ -61,9 +61,13 @@ export const accessRuleField: FormFieldSpec = {
|
|
|
61
61
|
label: { en: "Open to all (auth still required)", de: "Für alle (Auth nötig)" },
|
|
62
62
|
fields: [
|
|
63
63
|
{
|
|
64
|
-
path: "access.openToAll",
|
|
65
|
-
label: { en: "
|
|
66
|
-
|
|
64
|
+
path: "access.openToAll.reason",
|
|
65
|
+
label: { en: "Reason", de: "Begründung" },
|
|
66
|
+
hint: {
|
|
67
|
+
en: "Who may call this, and why that is safe without a role — required, `true` alone no longer grants access.",
|
|
68
|
+
},
|
|
69
|
+
input: "text",
|
|
70
|
+
required: true,
|
|
67
71
|
},
|
|
68
72
|
],
|
|
69
73
|
},
|
|
@@ -25,7 +25,7 @@ import type {
|
|
|
25
25
|
PreSaveHookFn,
|
|
26
26
|
ReferenceFieldDef,
|
|
27
27
|
} from "./types";
|
|
28
|
-
import { HookPhases } from "./types";
|
|
28
|
+
import { HookPhases, isRateLimitDisabled } from "./types";
|
|
29
29
|
|
|
30
30
|
function allHandlerQns(state: RegistryState): ReadonlySet<string> {
|
|
31
31
|
return new Set([...state.writeHandlerMap.keys(), ...state.queryHandlerMap.keys()]);
|
|
@@ -721,10 +721,18 @@ export function validateExtensionUsageTargets(state: RegistryState): void {
|
|
|
721
721
|
export function computeHasRateLimitedHandler(state: RegistryState): void {
|
|
722
722
|
// Pre-compute: any handler with a rateLimit option? Keeps the boot
|
|
723
723
|
// path able to short-circuit the RateLimitResolver wiring (and its
|
|
724
|
-
// Lua-script registration on Redis) when nobody opted in.
|
|
724
|
+
// Lua-script registration on Redis) when nobody opted in. Deliberately
|
|
725
|
+
// stays limited to explicit, non-disabled `rateLimit` declarations —
|
|
726
|
+
// `{ disabled: true }` must not auto-wire a resolver for an app that only
|
|
727
|
+
// opted OUT, and fw#2861's systemScope default only fires once a resolver
|
|
728
|
+
// already exists, so it must not force wantsL3 either.
|
|
725
729
|
state.hasRateLimitedHandlerCached = (() => {
|
|
726
|
-
for (const h of state.writeHandlerMap.values())
|
|
727
|
-
|
|
730
|
+
for (const h of state.writeHandlerMap.values()) {
|
|
731
|
+
if (h.rateLimit !== undefined && !isRateLimitDisabled(h.rateLimit)) return true;
|
|
732
|
+
}
|
|
733
|
+
for (const h of state.queryHandlerMap.values()) {
|
|
734
|
+
if (h.rateLimit !== undefined && !isRateLimitDisabled(h.rateLimit)) return true;
|
|
735
|
+
}
|
|
728
736
|
return false;
|
|
729
737
|
})();
|
|
730
738
|
}
|
|
@@ -380,6 +380,12 @@ export function buildInsertSchema(
|
|
|
380
380
|
shape[name] = isRequired || hasDefault ? zodField : zodField.optional();
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
// Caller-chosen aggregate id for idempotent creates (deterministic-id
|
|
384
|
+
// patterns). Accepting it here only lets it survive parsing — the create
|
|
385
|
+
// handler still honors it only for a system-identity caller (jobs/hooks
|
|
386
|
+
// via createSystemUser), never for an ordinary HTTP-authenticated user.
|
|
387
|
+
shape["id"] = z.uuid().optional();
|
|
388
|
+
|
|
383
389
|
return applyTotalsMatchRefinements(entity, z.object(shape));
|
|
384
390
|
}
|
|
385
391
|
|
|
@@ -11,10 +11,62 @@ import type {
|
|
|
11
11
|
FormatSpec,
|
|
12
12
|
ListColumnSpec,
|
|
13
13
|
ProjectionDetailScreenDefinition,
|
|
14
|
+
RowAction,
|
|
14
15
|
ScreenDefinition,
|
|
15
16
|
SecretMintScreenDefinition,
|
|
17
|
+
ToolbarAction,
|
|
16
18
|
} from "./types/screen";
|
|
17
19
|
|
|
20
|
+
// Every screen type carrying `listScreenId` declares it identically; centralizing
|
|
21
|
+
// here keeps the switch the only place that grows for a new screen type.
|
|
22
|
+
export function explicitListScreenId(screen: ScreenDefinition): string | undefined {
|
|
23
|
+
switch (screen.type) {
|
|
24
|
+
case "custom":
|
|
25
|
+
case "projectionDetail":
|
|
26
|
+
case "entityEdit":
|
|
27
|
+
case "actionForm":
|
|
28
|
+
case "secretMint":
|
|
29
|
+
return screen.listScreenId;
|
|
30
|
+
default:
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Shared by the renderer breadcrumb and the boot-validator so both resolve
|
|
36
|
+
// parents the same way; `getId` lets each caller normalize `screen.id`.
|
|
37
|
+
export function resolveNavParentScreen(
|
|
38
|
+
screens: readonly ScreenDefinition[],
|
|
39
|
+
detail: ScreenDefinition,
|
|
40
|
+
getId: (screen: ScreenDefinition) => string,
|
|
41
|
+
): ScreenDefinition | undefined {
|
|
42
|
+
const detailScreenId = getId(detail);
|
|
43
|
+
|
|
44
|
+
const listFromExplicit = ((): ScreenDefinition | undefined => {
|
|
45
|
+
const explicitId = explicitListScreenId(detail);
|
|
46
|
+
return explicitId !== undefined ? screens.find((s) => getId(s) === explicitId) : undefined;
|
|
47
|
+
})();
|
|
48
|
+
|
|
49
|
+
// rowActions, toolbarActions, and drawer-kind actions (which mount inline
|
|
50
|
+
// and have no page of their own) all count as "reached from" this list.
|
|
51
|
+
const navigatesToDetail = (
|
|
52
|
+
actions: readonly (RowAction | ToolbarAction)[] | undefined,
|
|
53
|
+
): boolean =>
|
|
54
|
+
(actions ?? []).some(
|
|
55
|
+
(a) => (a.kind === "navigate" || a.kind === "drawer") && a.screen === detailScreenId,
|
|
56
|
+
);
|
|
57
|
+
const listFromRowAction = screens.find((s) => {
|
|
58
|
+
if (s.type !== "entityList" && s.type !== "projectionList") return false;
|
|
59
|
+
return navigatesToDetail(s.rowActions) || navigatesToDetail(s.toolbarActions);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const listFromEntity =
|
|
63
|
+
detail.type === "entityEdit"
|
|
64
|
+
? screens.find((s) => s.type === "entityList" && s.entity === detail.entity)
|
|
65
|
+
: undefined;
|
|
66
|
+
|
|
67
|
+
return listFromExplicit ?? listFromRowAction ?? listFromEntity;
|
|
68
|
+
}
|
|
69
|
+
|
|
18
70
|
export type EditLayoutScreen =
|
|
19
71
|
| ProjectionDetailScreenDefinition
|
|
20
72
|
| EntityEditScreenDefinition
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DbRunner } from "../../db/connection";
|
|
2
|
+
import { type TenantDb, unsafeRawForDeclaredStep } from "../../db/tenant-db";
|
|
3
|
+
import type { PipelineCtx } from "../types/step";
|
|
4
|
+
|
|
5
|
+
// unsafeRaw fails closed without the handler's escapeHatch (or systemScope) and reports the use.
|
|
6
|
+
export function readSourceFor(
|
|
7
|
+
ctx: PipelineCtx,
|
|
8
|
+
unsafeAllTenants: { readonly reason: string } | undefined,
|
|
9
|
+
): TenantDb | DbRunner {
|
|
10
|
+
if (!unsafeAllTenants) return ctx.db;
|
|
11
|
+
return unsafeRawForDeclaredStep(ctx.systemDb ?? ctx.db, unsafeAllTenants.reason);
|
|
12
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// r.step.read.findMany — load multiple rows from a projection table.
|
|
2
2
|
//
|
|
3
|
-
// Sibling to read.findOne — same tenant-
|
|
3
|
+
// Sibling to read.findOne — same tenant-filtering: own tenant +
|
|
4
|
+
// SYSTEM_TENANT_ID reference rows by default, `unsafeAllTenants: { reason }`
|
|
5
|
+
// plus `escapeHatch: { reason }` on the handler to read across tenants.
|
|
4
6
|
// Resolves to a row-array (possibly empty), landed under steps.<name>.
|
|
5
7
|
//
|
|
6
8
|
// Optional `limit` — defaults to no-limit (caller-chosen, NOT a
|
|
@@ -11,6 +13,7 @@
|
|
|
11
13
|
import { selectMany, type WhereObject } from "../../db/query";
|
|
12
14
|
import { defineStep } from "../define-step";
|
|
13
15
|
import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
|
|
16
|
+
import { readSourceFor } from "./_read-source";
|
|
14
17
|
import { resolveOptional } from "./_resolver-utils";
|
|
15
18
|
|
|
16
19
|
type ReadFindManyArgs = {
|
|
@@ -18,6 +21,7 @@ type ReadFindManyArgs = {
|
|
|
18
21
|
readonly table: unknown;
|
|
19
22
|
readonly where?: StepResolver<WhereObject | undefined>;
|
|
20
23
|
readonly limit?: number;
|
|
24
|
+
readonly unsafeAllTenants?: { readonly reason: string };
|
|
21
25
|
};
|
|
22
26
|
|
|
23
27
|
defineStep<ReadFindManyArgs, readonly Record<string, unknown>[]>({
|
|
@@ -26,8 +30,9 @@ defineStep<ReadFindManyArgs, readonly Record<string, unknown>[]>({
|
|
|
26
30
|
resultKey: (args) => args.name,
|
|
27
31
|
run: async (args, ctx: PipelineCtx) => {
|
|
28
32
|
const where = resolveOptional(args.where, ctx);
|
|
33
|
+
const source = readSourceFor(ctx, args.unsafeAllTenants);
|
|
29
34
|
const rows = await selectMany(
|
|
30
|
-
|
|
35
|
+
source,
|
|
31
36
|
args.table,
|
|
32
37
|
where,
|
|
33
38
|
args.limit !== undefined ? { limit: args.limit } : undefined,
|
|
@@ -42,6 +47,7 @@ export function buildReadFindManyStep(
|
|
|
42
47
|
readonly table: unknown;
|
|
43
48
|
readonly where?: StepResolver<WhereObject | undefined>;
|
|
44
49
|
readonly limit?: number;
|
|
50
|
+
readonly unsafeAllTenants?: { readonly reason: string };
|
|
45
51
|
},
|
|
46
52
|
): StepInstance {
|
|
47
53
|
return {
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// r.step.read.findOne — load a single row from a projection table.
|
|
2
2
|
//
|
|
3
3
|
// Thin wrapper on selectMany(db, table, where, { limit: 1 }) (bun-db).
|
|
4
|
-
// Resolves to the first row or null. Tenant-
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// who pass an explicit tenantId.
|
|
4
|
+
// Resolves to the first row or null. Tenant-filtered like ctx.db
|
|
5
|
+
// method-form reads (own tenant + SYSTEM_TENANT_ID reference rows); a
|
|
6
|
+
// foreign `where.tenantId` is narrowed to the caller's own scope.
|
|
7
|
+
// Cross-tenant reads require `unsafeAllTenants: { reason }` here AND
|
|
8
|
+
// `escapeHatch: { reason }` on the handler — reported as an "unsafe-raw"
|
|
9
|
+
// escape-hatch audit event.
|
|
11
10
|
//
|
|
12
11
|
// Use when a subsequent step needs a row from the read-side. For
|
|
13
12
|
// cross-feature reads, prefer `r.step.callFeature(...)` (M.2) so the
|
|
@@ -23,12 +22,14 @@
|
|
|
23
22
|
import { selectMany, type WhereObject } from "../../db/query";
|
|
24
23
|
import { defineStep } from "../define-step";
|
|
25
24
|
import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
|
|
25
|
+
import { readSourceFor } from "./_read-source";
|
|
26
26
|
import { resolveRequired } from "./_resolver-utils";
|
|
27
27
|
|
|
28
28
|
type ReadFindOneArgs = {
|
|
29
29
|
readonly name: string;
|
|
30
30
|
readonly table: unknown;
|
|
31
31
|
readonly where: StepResolver<WhereObject | undefined>;
|
|
32
|
+
readonly unsafeAllTenants?: { readonly reason: string };
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
defineStep<ReadFindOneArgs, Record<string, unknown> | null>({
|
|
@@ -37,7 +38,8 @@ defineStep<ReadFindOneArgs, Record<string, unknown> | null>({
|
|
|
37
38
|
resultKey: (args) => args.name,
|
|
38
39
|
run: async (args, ctx: PipelineCtx) => {
|
|
39
40
|
const where = resolveRequired(args.where, ctx);
|
|
40
|
-
const
|
|
41
|
+
const source = readSourceFor(ctx, args.unsafeAllTenants);
|
|
42
|
+
const rows = await selectMany(source, args.table, where, { limit: 1 });
|
|
41
43
|
return (rows[0] as Record<string, unknown> | undefined) ?? null;
|
|
42
44
|
},
|
|
43
45
|
});
|
|
@@ -47,6 +49,7 @@ export function buildReadFindOneStep(
|
|
|
47
49
|
opts: {
|
|
48
50
|
readonly table: unknown;
|
|
49
51
|
readonly where: StepResolver<WhereObject | undefined>;
|
|
52
|
+
readonly unsafeAllTenants?: { readonly reason: string };
|
|
50
53
|
},
|
|
51
54
|
): StepInstance {
|
|
52
55
|
return {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import type { EntityTableMeta } from "../../db/entity-table-meta";
|
|
20
20
|
import { deleteMany, type WhereObject } from "../../db/query";
|
|
21
|
+
import { tenantDbRunner } from "../../db/tenant-db-runner";
|
|
21
22
|
import { defineStep } from "../define-step";
|
|
22
23
|
import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
|
|
23
24
|
import { resolveRequired } from "./_resolver-utils";
|
|
@@ -40,7 +41,7 @@ defineStep<UnsafeProjectionDeleteArgs, void>({
|
|
|
40
41
|
// The `unsafe*`-step IS the sanctioned direct projection write — it exists
|
|
41
42
|
// to bypass the executor deliberately (boot-validated allowlist + reviewer
|
|
42
43
|
// gate). The brand-strip cast makes that explicit at the one blessed seam.
|
|
43
|
-
await deleteMany(ctx.db
|
|
44
|
+
await deleteMany(tenantDbRunner(ctx.db), args.table as EntityTableMeta, where);
|
|
44
45
|
},
|
|
45
46
|
});
|
|
46
47
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { KUMIKO_COLUMNS_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
15
15
|
import { extractTableName } from "../../db";
|
|
16
16
|
import { executeRawQuery } from "../../db/queries/raw-sql";
|
|
17
|
+
import { tenantDbRunner } from "../../db/tenant-db-runner";
|
|
17
18
|
import { defineStep } from "../define-step";
|
|
18
19
|
import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
|
|
19
20
|
import { resolveRequired } from "./_resolver-utils";
|
|
@@ -78,7 +79,7 @@ defineStep<UnsafeProjectionUpsertArgs, void>({
|
|
|
78
79
|
`INSERT INTO ${quoteIdent(tableName)} (${colNames.join(", ")}) VALUES (${placeholders.join(", ")}) ` +
|
|
79
80
|
`ON CONFLICT (${conflictCols}) DO UPDATE SET ${setClauses.join(", ")}`;
|
|
80
81
|
|
|
81
|
-
await executeRawQuery(ctx.db
|
|
82
|
+
await executeRawQuery(tenantDbRunner(ctx.db), sqlText, params);
|
|
82
83
|
},
|
|
83
84
|
});
|
|
84
85
|
|
|
@@ -159,7 +159,12 @@ export type {
|
|
|
159
159
|
DeclarativeEventMigration,
|
|
160
160
|
DispatchWriteRef,
|
|
161
161
|
EntityRef,
|
|
162
|
+
EscapeHatchAuditSink,
|
|
162
163
|
EscapeHatchDeclaration,
|
|
164
|
+
EscapeHatchKind,
|
|
165
|
+
EscapeHatchReporter,
|
|
166
|
+
EscapeHatchTarget,
|
|
167
|
+
EscapeHatchUseEvent,
|
|
163
168
|
EventDef,
|
|
164
169
|
EventMigrationDef,
|
|
165
170
|
EventPiiFields,
|
|
@@ -184,6 +189,8 @@ export type {
|
|
|
184
189
|
QueryEvent,
|
|
185
190
|
QueryHandlerDef,
|
|
186
191
|
QueryHandlerFn,
|
|
192
|
+
RateLimitDeclaration,
|
|
193
|
+
RateLimitDisabled,
|
|
187
194
|
RateLimitOption,
|
|
188
195
|
RateLimitPer,
|
|
189
196
|
SessionUser,
|
|
@@ -198,7 +205,7 @@ export type {
|
|
|
198
205
|
WriteHandlerFn,
|
|
199
206
|
WriteResult,
|
|
200
207
|
} from "@cosmicdrift/kumiko-types/handlers";
|
|
201
|
-
export { isOpenToAllGranted } from "@cosmicdrift/kumiko-types/handlers";
|
|
208
|
+
export { isOpenToAllGranted, isRateLimitDisabled } from "@cosmicdrift/kumiko-types/handlers";
|
|
202
209
|
export type {
|
|
203
210
|
DeleteContext,
|
|
204
211
|
EntityHookMap,
|
|
@@ -41,7 +41,7 @@ const writeProbeFeature = defineFeature("writeProbe", (r) => {
|
|
|
41
41
|
});
|
|
42
42
|
return { isSuccess: true as const, data: { note: event.payload.note } };
|
|
43
43
|
},
|
|
44
|
-
{ access: { openToAll:
|
|
44
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
45
45
|
);
|
|
46
46
|
r.job("write-via-job", { trigger: { manual: true }, retries: 0 }, async (payload, ctx) => {
|
|
47
47
|
try {
|
|
@@ -33,7 +33,7 @@ const wiringFeature = defineFeature("wiring", (r) => {
|
|
|
33
33
|
isSuccess: true as const,
|
|
34
34
|
data: { id: 1, sku: event.payload.sku },
|
|
35
35
|
}),
|
|
36
|
-
{ access: { openToAll:
|
|
36
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
37
37
|
);
|
|
38
38
|
r.job(
|
|
39
39
|
"record-order",
|
|
@@ -57,7 +57,7 @@ const mixedLaneFeature = defineFeature("mixed", (r) => {
|
|
|
57
57
|
isSuccess: true as const,
|
|
58
58
|
data: { id: 1, msg: event.payload.msg },
|
|
59
59
|
}),
|
|
60
|
-
{ access: { openToAll:
|
|
60
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
61
61
|
);
|
|
62
62
|
r.job(
|
|
63
63
|
"handle-on-worker",
|
|
@@ -240,7 +240,7 @@ const featureGateJobFeature = defineFeature("featuregate", (r) => {
|
|
|
240
240
|
isSuccess: true as const,
|
|
241
241
|
data: { id: 1, msg: event.payload.msg },
|
|
242
242
|
}),
|
|
243
|
-
{ access: { openToAll:
|
|
243
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
244
244
|
);
|
|
245
245
|
r.job(
|
|
246
246
|
"record-features",
|
|
@@ -57,7 +57,7 @@ const workerWriteFeature = defineFeature("workerWrite", (r) => {
|
|
|
57
57
|
});
|
|
58
58
|
return { isSuccess: true as const, data: { note: event.payload.note } };
|
|
59
59
|
},
|
|
60
|
-
{ access: { openToAll:
|
|
60
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
61
61
|
);
|
|
62
62
|
r.multiStreamProjection({
|
|
63
63
|
name: "consume-notes",
|
package/src/errors/i18n/de.yaml
CHANGED
|
@@ -113,6 +113,20 @@ identity_switch_denied:
|
|
|
113
113
|
(oder am `r.hook(...)`-Aufruf, wenn der Wechsel in einem Hook passiert),
|
|
114
114
|
oder verlagere die Logik in ein `r.systemScope()`-Feature oder einen Job.
|
|
115
115
|
|
|
116
|
+
unsafe_all_tenants_denied:
|
|
117
|
+
endUser: |
|
|
118
|
+
Diese Aktion ist nicht erlaubt.
|
|
119
|
+
Wende dich an einen Administrator wenn du glaubst, dass das ein Fehler ist.
|
|
120
|
+
developer: |
|
|
121
|
+
`AccessDeniedError`: ein Handler oder Hook hat
|
|
122
|
+
`ctx.queryProjection(qualifiedName, { unsafeAllTenants: true })`
|
|
123
|
+
aufgerufen, ohne `r.systemScope()`-Handler zu sein oder
|
|
124
|
+
`escapeHatch: { reason }` zu deklarieren.
|
|
125
|
+
|
|
126
|
+
Deklariere `escapeHatch: { reason: "..." }` am Write-, Query- oder
|
|
127
|
+
Stream-Handler (oder am `r.hook(...)`-Aufruf, wenn der Read in einem Hook
|
|
128
|
+
passiert), oder verlagere die Logik in ein `r.systemScope()`-Feature.
|
|
129
|
+
|
|
116
130
|
member_resolution_denied:
|
|
117
131
|
endUser: |
|
|
118
132
|
Diese Aktion ist nicht erlaubt.
|
package/src/errors/i18n/en.yaml
CHANGED
|
@@ -109,6 +109,19 @@ identity_switch_denied:
|
|
|
109
109
|
(or on the `r.hook(...)` call, if the switch happens inside a hook), or
|
|
110
110
|
move the logic into an `r.systemScope()` feature or a job.
|
|
111
111
|
|
|
112
|
+
unsafe_all_tenants_denied:
|
|
113
|
+
endUser: |
|
|
114
|
+
This action isn't allowed.
|
|
115
|
+
Please contact an administrator if you believe this is a mistake.
|
|
116
|
+
developer: |
|
|
117
|
+
`AccessDeniedError`: a handler or hook called
|
|
118
|
+
`ctx.queryProjection(qualifiedName, { unsafeAllTenants: true })` without
|
|
119
|
+
being an `r.systemScope()` handler or declaring `escapeHatch: { reason }`.
|
|
120
|
+
|
|
121
|
+
Declare `escapeHatch: { reason: "..." }` on the write, query or stream
|
|
122
|
+
handler (or on the `r.hook(...)` call, if the read happens inside a
|
|
123
|
+
hook), or move the logic into an `r.systemScope()` feature.
|
|
124
|
+
|
|
112
125
|
member_resolution_denied:
|
|
113
126
|
endUser: |
|
|
114
127
|
This action isn't allowed.
|
package/src/errors/reasons.ts
CHANGED
|
@@ -48,6 +48,10 @@ export const FrameworkReasons = {
|
|
|
48
48
|
// claims, origin or extra roles) without r.systemScope() or escapeHatch.
|
|
49
49
|
identitySwitchDenied: "identity_switch_denied",
|
|
50
50
|
|
|
51
|
+
// AccessDeniedError: a handler/hook called ctx.queryProjection(..., { unsafeAllTenants: true })
|
|
52
|
+
// without an r.systemScope() feature or a declared escapeHatch.
|
|
53
|
+
unsafeAllTenantsDenied: "unsafe_all_tenants_denied",
|
|
54
|
+
|
|
51
55
|
// AccessDeniedError: ctx.queryAsMember's userId is not an active member of
|
|
52
56
|
// ctx's tenant. Deliberately generic — a caller can't probe which check failed.
|
|
53
57
|
memberResolutionDenied: "member_resolution_denied",
|
|
@@ -58,7 +58,7 @@ const seedTestFeature = defineFeature("esopstest", (r) => {
|
|
|
58
58
|
"probe:create",
|
|
59
59
|
z.object({ label: z.string().min(1) }),
|
|
60
60
|
async (event, ctx) => probeExecutor.create(event.payload, event.user, ctx.db),
|
|
61
|
-
{ access: { openToAll:
|
|
61
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
62
62
|
);
|
|
63
63
|
r.writeHandler(
|
|
64
64
|
"probe:fail",
|
|
@@ -70,7 +70,7 @@ const seedTestFeature = defineFeature("esopstest", (r) => {
|
|
|
70
70
|
currentVersion: 2,
|
|
71
71
|
});
|
|
72
72
|
},
|
|
73
|
-
{ access: { openToAll:
|
|
73
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
74
74
|
);
|
|
75
75
|
// Role-gated handler (no openToAll): the bare system actor has no bypass
|
|
76
76
|
// for explicit role gates — only systemWriteAs' extraRoles reaches it.
|
|
@@ -196,7 +196,9 @@ export function requiredKeysFromScreen(
|
|
|
196
196
|
const form = screen as ActionFormScreenDefinition;
|
|
197
197
|
pushKey(out, form.submitLabel);
|
|
198
198
|
for (const fieldName of Object.keys(form.fields)) {
|
|
199
|
-
|
|
199
|
+
const override = form.fieldLabels?.[fieldName];
|
|
200
|
+
if (override !== undefined) pushKey(out, override);
|
|
201
|
+
else out.add(fieldLabelKey(featureName, ACTION_FORM_ENTITY, fieldName));
|
|
200
202
|
}
|
|
201
203
|
for (const section of form.layout.sections) {
|
|
202
204
|
if (isExtensionEditSection(section)) {
|
|
@@ -207,7 +209,9 @@ export function requiredKeysFromScreen(
|
|
|
207
209
|
pushKey(out, section.title);
|
|
208
210
|
for (const f of section.fields) {
|
|
209
211
|
const fieldName = editFieldName(f);
|
|
210
|
-
|
|
212
|
+
const override = form.fieldLabels?.[fieldName];
|
|
213
|
+
if (override !== undefined) pushKey(out, override);
|
|
214
|
+
else out.add(fieldLabelKey(featureName, ACTION_FORM_ENTITY, fieldName));
|
|
211
215
|
}
|
|
212
216
|
}
|
|
213
217
|
break;
|
|
@@ -26,7 +26,7 @@ const ordersFeature = defineFeature("orders", (r) => {
|
|
|
26
26
|
data: { id: 1, product: event.payload.product, amount: event.payload.amount },
|
|
27
27
|
};
|
|
28
28
|
},
|
|
29
|
-
{ access: { openToAll:
|
|
29
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
30
30
|
);
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -51,7 +51,7 @@ const analyticsFeature = defineFeature("analytics", (r) => {
|
|
|
51
51
|
isSuccess: true as const,
|
|
52
52
|
data: null,
|
|
53
53
|
}),
|
|
54
|
-
{ access: { openToAll:
|
|
54
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
55
55
|
);
|
|
56
56
|
|
|
57
57
|
r.job("trackOrder", { trigger: { on: "orders:write:orders:create" } }, async (payload) => {
|