@cosmicdrift/kumiko-framework 0.353.0 → 0.354.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/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/redis-sse-broker.d.ts +1 -0
- package/dist/api/redis-sse-broker.js +16 -10
- package/dist/api/request-context.d.ts +1 -0
- package/dist/api/request-context.js +5 -0
- package/dist/api/routes.js +4 -2
- package/dist/api/server.js +28 -3
- package/dist/api/sse-broker.d.ts +7 -3
- package/dist/api/sse-route.js +2 -6
- package/dist/api/token-expiry-timer.d.ts +1 -0
- package/dist/api/token-expiry-timer.js +10 -0
- package/dist/bun-db/query.js +29 -2
- package/dist/crypto/event-pii.js +4 -2
- package/dist/crypto/kms-wiring.js +31 -5
- package/dist/db/eagerload.d.ts +8 -3
- package/dist/db/eagerload.js +85 -81
- package/dist/db/event-store-executor-context.d.ts +12 -0
- package/dist/db/event-store-executor-context.js +59 -18
- package/dist/db/event-store-executor-read.js +67 -25
- package/dist/db/event-store-executor-write.js +54 -3
- package/dist/db/index.d.ts +1 -1
- package/dist/db/migrate-generator.js +1 -1
- package/dist/db/queries/backfill-pii.d.ts +1 -0
- package/dist/db/queries/backfill-pii.js +11 -6
- package/dist/db/queries/event-consumer.d.ts +2 -3
- package/dist/db/queries/event-consumer.js +54 -43
- package/dist/db/queries/event-store.d.ts +4 -0
- package/dist/db/queries/event-store.js +40 -3
- package/dist/db/queries/pii-backfill-state.d.ts +8 -1
- package/dist/db/queries/pii-backfill-state.js +16 -5
- package/dist/db/queries/shadow-swap.js +32 -3
- package/dist/db/render-ddl.js +6 -6
- package/dist/db/table-builder.js +6 -7
- package/dist/db/tenant-db.d.ts +5 -2
- package/dist/db/tenant-db.js +47 -7
- package/dist/derivatives/variant-key.d.ts +1 -0
- package/dist/derivatives/variant-key.js +3 -0
- package/dist/engine/boot-validator/access-declarations.js +5 -0
- package/dist/engine/boot-validator/config-deps.js +3 -0
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +40 -0
- package/dist/engine/boot-validator/handler-field-references.d.ts +2 -0
- package/dist/engine/boot-validator/handler-field-references.js +25 -0
- package/dist/engine/boot-validator/index.js +6 -1
- package/dist/engine/boot-validator/nav.js +3 -1
- package/dist/engine/boot-validator/ownership.d.ts +2 -1
- package/dist/engine/boot-validator/ownership.js +32 -1
- package/dist/engine/boot-validator/pii-retention.js +8 -1
- package/dist/engine/boot-validator/query-output-columns.js +9 -9
- package/dist/engine/boot-validator/screens.js +98 -7
- package/dist/engine/boot-validator/transfer-graph.d.ts +2 -1
- package/dist/engine/boot-validator/transfer-graph.js +8 -63
- package/dist/engine/boot-validator/url-prefill-fields.d.ts +1 -0
- package/dist/engine/boot-validator/url-prefill-fields.js +35 -0
- package/dist/engine/build-app-schema.js +3 -0
- package/dist/engine/changeset-changes.d.ts +1 -0
- package/dist/engine/changeset-changes.js +49 -33
- package/dist/engine/define-feature.js +11 -0
- package/dist/engine/entity-handlers.d.ts +3 -3
- package/dist/engine/entity-handlers.js +55 -34
- package/dist/engine/escape-hatch-reason.d.ts +2 -0
- package/dist/engine/escape-hatch-reason.js +8 -0
- package/dist/engine/extension-names.d.ts +8 -9
- package/dist/engine/extension-names.js +8 -9
- package/dist/engine/extensions/tenant-data.d.ts +3 -1
- package/dist/engine/extensions/tenant-resource.d.ts +9 -1
- package/dist/engine/factories.d.ts +1 -1
- package/dist/engine/feature-changelog.d.ts +2 -0
- package/dist/engine/feature-changelog.js +6 -0
- package/dist/engine/feature-config-events-jobs.js +41 -4
- package/dist/engine/handler-field-references.d.ts +3 -0
- package/dist/engine/handler-field-references.js +31 -0
- package/dist/engine/i18n-key.js +2 -3
- package/dist/engine/index.d.ts +8 -2
- package/dist/engine/index.js +6 -1
- package/dist/engine/ownership.js +3 -4
- package/dist/engine/project-app-schema-for-roles.js +3 -1
- package/dist/engine/registry-ingest.d.ts +1 -0
- package/dist/engine/registry-ingest.js +8 -3
- package/dist/engine/run-pipeline.js +12 -1
- package/dist/engine/screen-helpers.d.ts +13 -0
- package/dist/engine/screen-helpers.js +31 -0
- package/dist/engine/soft-delete-cleanup.js +3 -2
- package/dist/engine/steps/_read-source.d.ts +4 -0
- package/dist/engine/steps/_read-source.js +22 -0
- package/dist/engine/steps/describe-workflow-step-error.js +3 -1
- package/dist/engine/steps/read-find-many.js +2 -1
- package/dist/engine/steps/read-find-one.js +2 -1
- package/dist/engine/steps/workflow-step-error.d.ts +5 -0
- package/dist/engine/steps/workflow-step-error.js +13 -0
- package/dist/engine/transfer-adjacency.d.ts +24 -0
- package/dist/engine/transfer-adjacency.js +107 -0
- package/dist/entrypoint/index.d.ts +2 -0
- package/dist/entrypoint/index.js +2 -0
- package/dist/env/index.js +45 -23
- package/dist/errors/classes.d.ts +8 -0
- package/dist/errors/classes.js +13 -0
- package/dist/errors/index.d.ts +2 -1
- package/dist/errors/index.js +2 -1
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +3 -0
- package/dist/errors/subject-erased.d.ts +2 -0
- package/dist/errors/subject-erased.js +9 -0
- package/dist/event-store/admin-api.js +14 -10
- package/dist/event-store/event-store.js +11 -9
- package/dist/event-store/events-schema.js +2 -1
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +30 -6
- package/dist/files/file-routes.js +10 -4
- package/dist/files/resolve-content-type.d.ts +2 -0
- package/dist/files/resolve-content-type.js +8 -0
- package/dist/i18n/required-surface-keys.d.ts +2 -0
- package/dist/i18n/required-surface-keys.js +8 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +13 -0
- package/dist/jobs/job-runner.js +128 -22
- package/dist/migrations/pii-event-backfill.js +46 -25
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/standard-metrics.d.ts +4 -0
- package/dist/observability/standard-metrics.js +12 -0
- package/dist/pipeline/dispatch-shared.d.ts +2 -0
- package/dist/pipeline/dispatch-shared.js +74 -9
- package/dist/pipeline/dispatch-stream.d.ts +5 -1
- package/dist/pipeline/dispatch-stream.js +19 -12
- package/dist/pipeline/dispatcher.d.ts +3 -1
- package/dist/pipeline/dispatcher.js +10 -15
- package/dist/pipeline/event-consumer-state.js +0 -26
- package/dist/pipeline/event-dispatcher-admin.js +17 -11
- package/dist/pipeline/event-dispatcher-delivery.d.ts +1 -1
- package/dist/pipeline/event-dispatcher-delivery.js +3 -2
- package/dist/pipeline/event-dispatcher.js +48 -7
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/member-read-only-transaction.js +25 -5
- package/dist/pipeline/msp-rebuild.js +3 -3
- package/dist/pipeline/pending-gap-ranges.d.ts +1 -0
- package/dist/pipeline/pending-gap-ranges.js +22 -0
- package/dist/pipeline/system-hooks.js +4 -4
- package/dist/pipeline/system-identity-switch.d.ts +1 -1
- package/dist/pipeline/system-identity-switch.js +5 -4
- package/dist/pipeline/tenant-timezone-cache.js +5 -3
- package/dist/redis/cache-sync-topics.d.ts +1 -0
- package/dist/redis/cache-sync-topics.js +1 -0
- package/dist/redis/client.d.ts +1 -0
- package/dist/redis/client.js +7 -0
- package/dist/redis/index.d.ts +1 -1
- package/dist/redis/index.js +1 -1
- package/dist/redis/pubsub-signal.d.ts +1 -0
- package/dist/redis/pubsub-signal.js +3 -0
- package/dist/search/meilisearch-adapter.js +34 -4
- package/dist/stack/test-stack.d.ts +4 -1
- package/dist/stack/test-stack.js +9 -2
- package/dist/testing/e2e-generator.js +7 -2
- package/dist/ui-types/index.d.ts +3 -3
- package/dist/ui-types/index.js +2 -2
- package/dist/upgrade-cli.js +7 -2
- package/package.json +12 -4
- package/src/changes.json +198 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +2 -1
package/dist/engine/ownership.js
CHANGED
|
@@ -108,10 +108,9 @@ export function userCanReadFieldRow(user, accessMap, row) {
|
|
|
108
108
|
const rule = accessMap[role];
|
|
109
109
|
if (!rule)
|
|
110
110
|
continue;
|
|
111
|
-
// where-rules are SQL predicates
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// check denies rather than crashing.
|
|
111
|
+
// where-rules are SQL predicates; matchesRule can't evaluate them in-memory.
|
|
112
|
+
// The boot validator rejects them on field maps, so this only fails closed
|
|
113
|
+
// for maps that bypassed boot validation.
|
|
115
114
|
// skip: where-rules are SQL-layer only — fail closed instead of throwing.
|
|
116
115
|
if (rule !== "all" && rule.kind === "where")
|
|
117
116
|
continue;
|
|
@@ -443,7 +443,9 @@ function dropUnreachableCreateScreen(screen, feature, keptScreenQns) {
|
|
|
443
443
|
if (isScreenTargetKept(feature.featureName, screen.createScreen, keptScreenQns))
|
|
444
444
|
return screen;
|
|
445
445
|
const { createScreen: _unreachable, ...rest } = screen;
|
|
446
|
-
|
|
446
|
+
// Marker, not a bare delete: the renderer treats a missing createScreen as
|
|
447
|
+
// "not set" and would fall back to the entity's edit form.
|
|
448
|
+
return { ...rest, createUnavailable: true };
|
|
447
449
|
}
|
|
448
450
|
function projectListScreen(original, feature, indices, keptScreenQns) {
|
|
449
451
|
const screen = dropUnreachableCreateScreen(original, feature, keptScreenQns);
|
|
@@ -7,6 +7,7 @@ export declare function populateJobsAndNotifications(state: RegistryState, featu
|
|
|
7
7
|
export declare function populateEvents(state: RegistryState, feature: FeatureDefinition): void;
|
|
8
8
|
export declare function populateSystemEvents(state: RegistryState): void;
|
|
9
9
|
export declare function populateTranslations(state: RegistryState, feature: FeatureDefinition): void;
|
|
10
|
+
export declare function qualifyTranslationKey(featureName: string, key: string): string;
|
|
10
11
|
export declare function populateHooks(state: RegistryState, feature: FeatureDefinition): void;
|
|
11
12
|
export declare function populateExtensionsAndSeeds(state: RegistryState, feature: FeatureDefinition): void;
|
|
12
13
|
export declare function populateMetricsAndSecrets(state: RegistryState, feature: FeatureDefinition): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveTableName } from "../db/entity-table-meta.js";
|
|
2
2
|
import { buildMetricName, validateMetricName } from "../observability/index.js";
|
|
3
|
+
import { toKebab } from "./qualified-name.js";
|
|
3
4
|
import { mergeHookList, mergeHookListQualified, qualify } from "./registry-state.js";
|
|
4
5
|
import { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
|
|
5
6
|
// Feature registration + entities (globally-unique, physical-table-checked) + relations
|
|
@@ -149,12 +150,16 @@ export function populateTranslations(state, feature) {
|
|
|
149
150
|
// bare and qualified spellings for the same key are accepted; if a
|
|
150
151
|
// feature defines both, whichever is declared last wins (Object.entries
|
|
151
152
|
// = insertion order, not "qualified always wins").
|
|
152
|
-
const prefix = `${feature.name}:`;
|
|
153
153
|
for (const [key, value] of Object.entries(feature.translations ?? {})) {
|
|
154
|
-
|
|
155
|
-
state.mergedTranslations[qualifiedKey] = value;
|
|
154
|
+
state.mergedTranslations[qualifyTranslationKey(feature.name, key)] = value;
|
|
156
155
|
}
|
|
157
156
|
}
|
|
157
|
+
// Handler QNs use the kebab scope, so a camelCase feature's full QN key
|
|
158
|
+
// (`my-feature:write:x:title`) must count as qualified too.
|
|
159
|
+
export function qualifyTranslationKey(featureName, key) {
|
|
160
|
+
const isQualified = key.startsWith(`${featureName}:`) || key.startsWith(`${toKebab(featureName)}:`);
|
|
161
|
+
return isQualified ? key : `${featureName}:${key}`;
|
|
162
|
+
}
|
|
158
163
|
// Lifecycle hooks (handler-targeted, qualified) + entity hooks (entity-targeted,
|
|
159
164
|
// unprefixed) + search-payload-extensions (additive per entity).
|
|
160
165
|
export function populateHooks(state, feature) {
|
|
@@ -25,6 +25,7 @@ import { getStep } from "./define-step.js";
|
|
|
25
25
|
import { buildPipelineSteps } from "./pipeline.js";
|
|
26
26
|
import { SUSPEND_SENTINEL } from "./steps/_step-dispatch-constants.js";
|
|
27
27
|
import { RETURN_RESULT_KEY } from "./steps/return.js";
|
|
28
|
+
import { unwrapWorkflowStepError, WorkflowStepError } from "./steps/workflow-step-error.js";
|
|
28
29
|
export async function runPipeline(pipelineDef, event, handlerCtx, workflow, resumeFrom) {
|
|
29
30
|
const steps = buildPipelineSteps(pipelineDef, event);
|
|
30
31
|
const stepsAcc = {};
|
|
@@ -92,7 +93,17 @@ export async function runStepList(steps, event, handlerCtx, stepsAcc, scopeAcc,
|
|
|
92
93
|
workflow: { ...workflow, stepIndex: i },
|
|
93
94
|
}),
|
|
94
95
|
};
|
|
95
|
-
|
|
96
|
+
let value;
|
|
97
|
+
try {
|
|
98
|
+
value = await stepDef.run(instance.args, pipelineCtx);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// Workflow runs only: plain handler pipelines must keep surfacing the raw error to the
|
|
102
|
+
// dispatcher's error mapping. Nested lists re-wrap, so the outermost index wins.
|
|
103
|
+
if (!workflow)
|
|
104
|
+
throw error;
|
|
105
|
+
throw new WorkflowStepError(i, unwrapWorkflowStepError(error));
|
|
106
|
+
}
|
|
96
107
|
// Tier-3 suspension: the step wrote a waiting event and returned
|
|
97
108
|
// SUSPEND_SENTINEL to signal the pipeline should stop. The caller
|
|
98
109
|
// (defineWorkflow/workflow-engine) persists the suspension state.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FieldDefinition, SelectFieldDef } from "./types/fields.js";
|
|
1
2
|
import type { ActionFormScreenDefinition, ConfigEditScreenDefinition, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, FieldCondition, FormatSpec, ListColumnSpec, ProjectionDetailScreenDefinition, ScreenDefinition, SecretMintScreenDefinition } from "./types/screen.js";
|
|
2
3
|
export declare function explicitListScreenId(screen: ScreenDefinition): string | undefined;
|
|
3
4
|
export declare function resolveNavParentScreen(screens: readonly ScreenDefinition[], detail: ScreenDefinition, getId: (screen: ScreenDefinition) => string): ScreenDefinition | undefined;
|
|
@@ -24,4 +25,16 @@ export declare function normalizeListColumn(c: ListColumnSpec): Exclude<ListColu
|
|
|
24
25
|
* (form-condition closures) reuse it; three hand-rolled copies had
|
|
25
26
|
* already drifted in shape. */
|
|
26
27
|
export declare function evalFieldCondition(cond: FieldCondition, values: Record<string, unknown>): boolean;
|
|
28
|
+
type SelectOptionRules = Pick<SelectFieldDef, "options" | "conditionalOptions">;
|
|
29
|
+
export type UnavailableSelectOption = {
|
|
30
|
+
readonly field: string;
|
|
31
|
+
readonly value: string;
|
|
32
|
+
readonly allowed: readonly string[];
|
|
33
|
+
};
|
|
34
|
+
/** Options of a select field that are offered for the given row values, in declaration order. */
|
|
35
|
+
export declare function availableSelectOptions(field: SelectOptionRules, values: Record<string, unknown>): readonly string[];
|
|
36
|
+
/** Select values that violate a `conditionalOptions` rule. With `previous`, a field is only
|
|
37
|
+
* checked when it or one of its condition fields changed, so legacy rows stay editable. */
|
|
38
|
+
export declare function findUnavailableSelectOptions(fields: Readonly<Record<string, FieldDefinition>>, values: Record<string, unknown>, previous?: Record<string, unknown>): readonly UnavailableSelectOption[];
|
|
27
39
|
export declare function normalizeEditField(f: EditFieldSpec): Exclude<EditFieldSpec, string>;
|
|
40
|
+
export {};
|
|
@@ -110,6 +110,37 @@ export function evalFieldCondition(cond, values) {
|
|
|
110
110
|
return !cond.notIn.includes(val);
|
|
111
111
|
return val !== cond.ne;
|
|
112
112
|
}
|
|
113
|
+
/** Options of a select field that are offered for the given row values, in declaration order. */
|
|
114
|
+
export function availableSelectOptions(field, values) {
|
|
115
|
+
const rules = field.conditionalOptions;
|
|
116
|
+
if (!rules || rules.length === 0)
|
|
117
|
+
return field.options;
|
|
118
|
+
return field.options.filter((option) => rules.every((rule) => !rule.options.includes(option) || evalFieldCondition(rule.when, values)));
|
|
119
|
+
}
|
|
120
|
+
function isSameValue(a, b) {
|
|
121
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
122
|
+
}
|
|
123
|
+
/** Select values that violate a `conditionalOptions` rule. With `previous`, a field is only
|
|
124
|
+
* checked when it or one of its condition fields changed, so legacy rows stay editable. */
|
|
125
|
+
export function findUnavailableSelectOptions(fields, values, previous) {
|
|
126
|
+
const unavailable = [];
|
|
127
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
128
|
+
if (def.type !== "select" || !def.conditionalOptions)
|
|
129
|
+
continue;
|
|
130
|
+
const value = values[name];
|
|
131
|
+
if (typeof value !== "string" || value === "")
|
|
132
|
+
continue;
|
|
133
|
+
if (previous) {
|
|
134
|
+
const watched = [name, ...def.conditionalOptions.map((rule) => rule.when.field)];
|
|
135
|
+
if (watched.every((key) => isSameValue(values[key], previous[key])))
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const allowed = availableSelectOptions(def, values);
|
|
139
|
+
if (!allowed.includes(value))
|
|
140
|
+
unavailable.push({ field: name, value, allowed });
|
|
141
|
+
}
|
|
142
|
+
return unavailable;
|
|
143
|
+
}
|
|
113
144
|
export function normalizeEditField(f) {
|
|
114
145
|
return typeof f === "string" ? { field: f } : f;
|
|
115
146
|
}
|
|
@@ -101,7 +101,8 @@ export const softDeleteCleanupSystemJob = async (_payload, ctx) => {
|
|
|
101
101
|
continue;
|
|
102
102
|
const where = { isDeleted: true, deletedAt: { lt: cutoff } };
|
|
103
103
|
if (isGlobalTenancyTable(proj.table)) {
|
|
104
|
-
|
|
104
|
+
// db.global() rejects writes on executor-managed tables; a retention purge has no event to append.
|
|
105
|
+
await deleteMany(db.unsafeRaw(), proj.table, where);
|
|
105
106
|
}
|
|
106
107
|
else {
|
|
107
108
|
await deleteMany(db, proj.table, where);
|
|
@@ -127,7 +128,7 @@ export function buildSoftDeleteCleanupSystemJob() {
|
|
|
127
128
|
runIn: "worker",
|
|
128
129
|
escapeHatch: {
|
|
129
130
|
reason: "hard-deletes expired soft-deleted rows of global entities (e.g. user) once, system-wide",
|
|
130
|
-
grants: ["
|
|
131
|
+
grants: ["unsafeRaw"],
|
|
131
132
|
},
|
|
132
133
|
};
|
|
133
134
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { DbRunner } from "../../db/connection.js";
|
|
2
|
+
import type { WhereObject } from "../../db/query.js";
|
|
2
3
|
import { type TenantDb } from "../../db/tenant-db.js";
|
|
3
4
|
import type { PipelineCtx } from "../types/step.js";
|
|
4
5
|
export declare function readSourceFor(ctx: PipelineCtx, unsafeAllTenants: {
|
|
5
6
|
readonly reason: string;
|
|
6
7
|
} | undefined): TenantDb | DbRunner;
|
|
8
|
+
export declare function warnOnNarrowedForeignTenantFilter(ctx: PipelineCtx, stepKind: string, stepName: string, where: WhereObject | undefined, unsafeAllTenants: {
|
|
9
|
+
readonly reason: string;
|
|
10
|
+
} | undefined): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { unsafeRawForDeclaredStep } from "../../db/tenant-db.js";
|
|
2
2
|
import { InternalError } from "../../errors/classes.js";
|
|
3
|
+
import { SYSTEM_TENANT_ID } from "../types/identifiers.js";
|
|
3
4
|
// unsafeRaw fails closed without the handler's escapeHatch (or systemScope) and reports the use.
|
|
4
5
|
export function readSourceFor(ctx, unsafeAllTenants) {
|
|
5
6
|
if (!unsafeAllTenants)
|
|
@@ -13,3 +14,24 @@ export function readSourceFor(ctx, unsafeAllTenants) {
|
|
|
13
14
|
}
|
|
14
15
|
return unsafeRawForDeclaredStep(ctx.systemDb ?? ctx.db, unsafeAllTenants.reason);
|
|
15
16
|
}
|
|
17
|
+
// TenantDb silently narrows a foreign where.tenantId to the caller's scope; the step keeps that
|
|
18
|
+
// behavior but makes the dropped filter visible, since it usually means a cross-tenant read was intended.
|
|
19
|
+
export function warnOnNarrowedForeignTenantFilter(ctx, stepKind, stepName, where, unsafeAllTenants) {
|
|
20
|
+
// skip: explicit cross-tenant read, nothing was narrowed.
|
|
21
|
+
if (unsafeAllTenants)
|
|
22
|
+
return;
|
|
23
|
+
const requested = where?.["tenantId"];
|
|
24
|
+
// skip: no tenantId filter requested.
|
|
25
|
+
if (requested === undefined)
|
|
26
|
+
return;
|
|
27
|
+
const allowed = [ctx.db.tenantId, SYSTEM_TENANT_ID];
|
|
28
|
+
const requestedList = Array.isArray(requested) ? requested : [requested];
|
|
29
|
+
// skip: every requested tenant is within the caller's scope.
|
|
30
|
+
if (requestedList.every((t) => typeof t === "string" && allowed.includes(t)))
|
|
31
|
+
return;
|
|
32
|
+
ctx.log?.warn(`${stepKind} "${stepName}": where.tenantId is outside the caller's tenant scope and was narrowed`, {
|
|
33
|
+
step: stepKind,
|
|
34
|
+
name: stepName,
|
|
35
|
+
hint: "pass unsafeAllTenants: { reason } (with an escapeHatch on the handler) to read across tenants",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Workflow events (run-failed, retry.scheduled) outlive an Art. 17 erase, and a
|
|
2
2
|
// raw error message can echo recipients or payload fragments. Events carry only
|
|
3
3
|
// this generic text; the raw error goes to the log.
|
|
4
|
+
import { unwrapWorkflowStepError } from "./workflow-step-error.js";
|
|
4
5
|
export function describeWorkflowStepError(error) {
|
|
5
|
-
|
|
6
|
+
const original = unwrapWorkflowStepError(error);
|
|
7
|
+
return `workflow step failed (${original instanceof Error ? original.name : "unknown error"})`;
|
|
6
8
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// `limit` explicitly when the row-count could grow without bound.
|
|
12
12
|
import { selectMany } from "../../db/query.js";
|
|
13
13
|
import { defineStep } from "../define-step.js";
|
|
14
|
-
import { readSourceFor } from "./_read-source.js";
|
|
14
|
+
import { readSourceFor, warnOnNarrowedForeignTenantFilter } from "./_read-source.js";
|
|
15
15
|
import { resolveOptional } from "./_resolver-utils.js";
|
|
16
16
|
defineStep({
|
|
17
17
|
kind: "read.findMany",
|
|
@@ -19,6 +19,7 @@ defineStep({
|
|
|
19
19
|
resultKey: (args) => args.name,
|
|
20
20
|
run: async (args, ctx) => {
|
|
21
21
|
const where = resolveOptional(args.where, ctx);
|
|
22
|
+
warnOnNarrowedForeignTenantFilter(ctx, "read.findMany", args.name, where, args.unsafeAllTenants);
|
|
22
23
|
const source = readSourceFor(ctx, args.unsafeAllTenants);
|
|
23
24
|
const rows = await selectMany(source, args.table, where, args.limit !== undefined ? { limit: args.limit } : undefined);
|
|
24
25
|
return rows;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// runtime check; reviewer responsibility.
|
|
21
21
|
import { selectMany } from "../../db/query.js";
|
|
22
22
|
import { defineStep } from "../define-step.js";
|
|
23
|
-
import { readSourceFor } from "./_read-source.js";
|
|
23
|
+
import { readSourceFor, warnOnNarrowedForeignTenantFilter } from "./_read-source.js";
|
|
24
24
|
import { resolveRequired } from "./_resolver-utils.js";
|
|
25
25
|
defineStep({
|
|
26
26
|
kind: "read.findOne",
|
|
@@ -28,6 +28,7 @@ defineStep({
|
|
|
28
28
|
resultKey: (args) => args.name,
|
|
29
29
|
run: async (args, ctx) => {
|
|
30
30
|
const where = resolveRequired(args.where, ctx);
|
|
31
|
+
warnOnNarrowedForeignTenantFilter(ctx, "read.findOne", args.name, where, args.unsafeAllTenants);
|
|
31
32
|
const source = readSourceFor(ctx, args.unsafeAllTenants);
|
|
32
33
|
const rows = await selectMany(source, args.table, where, { limit: 1 });
|
|
33
34
|
return rows[0] ?? null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Carries the top-level step index a workflow run failed at, so run-failed
|
|
2
|
+
// events don't have to guess it. `cause` stays the original throw.
|
|
3
|
+
export class WorkflowStepError extends Error {
|
|
4
|
+
stepIndex;
|
|
5
|
+
constructor(stepIndex, cause) {
|
|
6
|
+
super(`workflow step ${stepIndex} failed`, { cause });
|
|
7
|
+
this.stepIndex = stepIndex;
|
|
8
|
+
this.name = "WorkflowStepError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function unwrapWorkflowStepError(error) {
|
|
12
|
+
return error instanceof WorkflowStepError ? error.cause : error;
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EntityDefinition } from "./types/index.js";
|
|
2
|
+
export declare const MAX_TRANSFER_DEPTH = 5;
|
|
3
|
+
export type TransferEdgeLink = {
|
|
4
|
+
readonly kind: "parentRef";
|
|
5
|
+
readonly typeField: string;
|
|
6
|
+
readonly idField: string;
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: "reference";
|
|
9
|
+
readonly field: string;
|
|
10
|
+
};
|
|
11
|
+
export type TransferEdge = {
|
|
12
|
+
readonly entityName: string;
|
|
13
|
+
readonly entity: EntityDefinition;
|
|
14
|
+
/** The already-resolved type this edge hangs off — its moved row ids are
|
|
15
|
+
* the parent ids the mover matches this edge's rows against. */
|
|
16
|
+
readonly parentEntityName: string;
|
|
17
|
+
readonly link: TransferEdgeLink;
|
|
18
|
+
};
|
|
19
|
+
/** Outgoing edges per type: keyed by the type an edge hangs off, so the mover
|
|
20
|
+
* can ask "what hangs off the rows I just moved" for any type it reaches, at
|
|
21
|
+
* any depth, as often as it reaches it. */
|
|
22
|
+
export type TransferAdjacency = ReadonlyMap<string, readonly TransferEdge[]>;
|
|
23
|
+
export declare function resolveTransferAdjacency(entities: ReadonlyMap<string, EntityDefinition>, rootEntityType?: string): TransferAdjacency;
|
|
24
|
+
export declare function findOverDeepTransferChain(adjacency: TransferAdjacency, onPath: readonly string[]): readonly string[] | undefined;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { parseRefTargetEntityName } from "./parse-ref-target.js";
|
|
2
|
+
// The single definition of the limit. It lives here rather than next to the
|
|
3
|
+
// resolver in bundled-features because the dependency only runs that way:
|
|
4
|
+
// bundled-features imports from the framework, never the reverse.
|
|
5
|
+
//
|
|
6
|
+
// Guards against a schema whose reference edges span more levels than anyone
|
|
7
|
+
// intended — a runaway graph would move rows an operator never associated with
|
|
8
|
+
// the handover. Deliberately a constant and not per-feature config: the limit
|
|
9
|
+
// is a safety net, and a configurable one gets raised by whoever trips it
|
|
10
|
+
// instead of prompting them to reconsider their graph.
|
|
11
|
+
export const MAX_TRANSFER_DEPTH = 5;
|
|
12
|
+
// A `multiple` reference stores a jsonb array rather than a single id column,
|
|
13
|
+
// which the mover's `= ANY($ids)` match cannot address. The boot validator
|
|
14
|
+
// (boot-validator/transfer-graph.ts) rejects that combination on transferable
|
|
15
|
+
// entities, so skipping it here can only ever hit an entity that is not
|
|
16
|
+
// transferable — never a silent partial move.
|
|
17
|
+
// `handover: "stay"` is the declared opt-out: the reference crosses the tenant
|
|
18
|
+
// boundary after a handover on purpose, so the edge is not walked.
|
|
19
|
+
function referenceEdgesFrom(entityName, entity) {
|
|
20
|
+
const edges = [];
|
|
21
|
+
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
22
|
+
if (field.type !== "reference")
|
|
23
|
+
continue;
|
|
24
|
+
if (field.multiple === true)
|
|
25
|
+
continue;
|
|
26
|
+
if (field.handover === "stay")
|
|
27
|
+
continue;
|
|
28
|
+
edges.push({
|
|
29
|
+
entityName,
|
|
30
|
+
entity,
|
|
31
|
+
parentEntityName: parseRefTargetEntityName(field.entity),
|
|
32
|
+
link: { kind: "reference", field: fieldName },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return edges;
|
|
36
|
+
}
|
|
37
|
+
// A parentRef names the types it accepts; without `allowedTypes` it accepts
|
|
38
|
+
// any of them, so the edge is recorded against every registered type.
|
|
39
|
+
function parentRefEdgesFrom(entityName, entity, allEntityNames) {
|
|
40
|
+
const parentRef = entity.parentRef;
|
|
41
|
+
if (!parentRef)
|
|
42
|
+
return [];
|
|
43
|
+
const link = {
|
|
44
|
+
kind: "parentRef",
|
|
45
|
+
typeField: parentRef.entityTypeField,
|
|
46
|
+
idField: parentRef.entityIdField,
|
|
47
|
+
};
|
|
48
|
+
return (parentRef.allowedTypes ?? allEntityNames).map((parentEntityName) => ({
|
|
49
|
+
entityName,
|
|
50
|
+
entity,
|
|
51
|
+
parentEntityName,
|
|
52
|
+
link,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
// The one definition of the transfer graph, shared by the boot validator
|
|
56
|
+
// (depth limit) and the tenant-handover mover (traversal), so the limit that is
|
|
57
|
+
// checked at boot is measured over exactly the edges that are walked at claim
|
|
58
|
+
// time. Edges are kept for every entity, transferable or not: a
|
|
59
|
+
// non-transferable node in the middle of a chain is still a hop the mover
|
|
60
|
+
// takes before it fails the claim.
|
|
61
|
+
//
|
|
62
|
+
// A type can legitimately be reached by more than one edge — `note.vehicleId`
|
|
63
|
+
// and `note.campaignId` are distinct, and collapsing them by entity name would
|
|
64
|
+
// leave one edge's rows behind, the silent partial move #3088 exists to end.
|
|
65
|
+
export function resolveTransferAdjacency(entities, rootEntityType) {
|
|
66
|
+
const allEntityNames = [...entities.keys()];
|
|
67
|
+
const byParent = new Map();
|
|
68
|
+
for (const [entityName, entity] of entities) {
|
|
69
|
+
// skip: the root is never collected as someone else's descendant. A self
|
|
70
|
+
// reference would otherwise drag unrelated rows of the root's own type
|
|
71
|
+
// across the tenant boundary along with the one that was claimed.
|
|
72
|
+
if (entityName === rootEntityType)
|
|
73
|
+
continue;
|
|
74
|
+
for (const edge of [
|
|
75
|
+
...parentRefEdgesFrom(entityName, entity, allEntityNames),
|
|
76
|
+
...referenceEdgesFrom(entityName, entity),
|
|
77
|
+
]) {
|
|
78
|
+
const edges = byParent.get(edge.parentEntityName) ?? [];
|
|
79
|
+
edges.push(edge);
|
|
80
|
+
byParent.set(edge.parentEntityName, edges);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return byParent;
|
|
84
|
+
}
|
|
85
|
+
// `onPath` counts NODES, the limit counts EDGES: a chain of exactly
|
|
86
|
+
// MAX_TRANSFER_DEPTH edges holds MAX_TRANSFER_DEPTH + 1 entities, and the
|
|
87
|
+
// mover runs MAX_TRANSFER_DEPTH rounds of one hop each, so it still walks that
|
|
88
|
+
// chain whole. Returns the first path that exceeds the limit.
|
|
89
|
+
export function findOverDeepTransferChain(adjacency, onPath) {
|
|
90
|
+
if (onPath.length > MAX_TRANSFER_DEPTH + 1)
|
|
91
|
+
return onPath;
|
|
92
|
+
const startName = onPath[onPath.length - 1];
|
|
93
|
+
if (startName === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
for (const edge of adjacency.get(startName) ?? []) {
|
|
96
|
+
// skip: a cycle revisits a type already on this path, and schema depth
|
|
97
|
+
// cannot measure how far it actually runs — that depends on the rows, not
|
|
98
|
+
// the declaration. The mover carries this one instead, failing with
|
|
99
|
+
// `transfer_graph_too_deep` when its rounds run out (#3131).
|
|
100
|
+
if (onPath.includes(edge.entityName))
|
|
101
|
+
continue;
|
|
102
|
+
const deeper = findOverDeepTransferChain(adjacency, [...onPath, edge.entityName]);
|
|
103
|
+
if (deeper !== undefined)
|
|
104
|
+
return deeper;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
@@ -30,6 +30,8 @@ type JobsBlock = {
|
|
|
30
30
|
readonly onJobStart?: JobRunnerOptions["onJobStart"];
|
|
31
31
|
readonly onJobComplete?: JobRunnerOptions["onJobComplete"];
|
|
32
32
|
readonly onJobFailed?: JobRunnerOptions["onJobFailed"];
|
|
33
|
+
readonly onJobQueued?: JobRunnerOptions["onJobQueued"];
|
|
34
|
+
readonly onJobDropped?: JobRunnerOptions["onJobDropped"];
|
|
33
35
|
};
|
|
34
36
|
export type ApiEntrypointOptions = BaseEntrypointOptions & {
|
|
35
37
|
readonly auth?: AuthRoutesConfig;
|
package/dist/entrypoint/index.js
CHANGED
|
@@ -176,6 +176,8 @@ function buildJobRunnerWithHook(registry, context, jobs, consumerLane, lifecycle
|
|
|
176
176
|
onJobStart: jobs.onJobStart,
|
|
177
177
|
onJobComplete: jobs.onJobComplete,
|
|
178
178
|
onJobFailed: jobs.onJobFailed,
|
|
179
|
+
onJobQueued: jobs.onJobQueued,
|
|
180
|
+
onJobDropped: jobs.onJobDropped,
|
|
179
181
|
});
|
|
180
182
|
lifecycle.registerShutdownHook(hookName, async () => {
|
|
181
183
|
await jobRunner.stop();
|
package/dist/env/index.js
CHANGED
|
@@ -135,6 +135,42 @@ export function withOptionalEnvKeys(schema, keys) {
|
|
|
135
135
|
}
|
|
136
136
|
return Object.keys(relaxed).length === 0 ? schema : schema.safeExtend(relaxed);
|
|
137
137
|
}
|
|
138
|
+
// The very same field instance is a shared fragment (several features read one
|
|
139
|
+
// variable), not a conflict; the first declaring feature owns it.
|
|
140
|
+
function isSharedFeatureFragment(featureFields, key, field) {
|
|
141
|
+
return featureFields.get(key) === field;
|
|
142
|
+
}
|
|
143
|
+
// Required beats optional, independent of feature order: a feature outside
|
|
144
|
+
// optionalFeatures must not end up with a silently optional slot.
|
|
145
|
+
function keepStrictestOfSharedFragment(merged, key, field, declaringFeatureIsOptional) {
|
|
146
|
+
if (!declaringFeatureIsOptional)
|
|
147
|
+
merged[key] = field;
|
|
148
|
+
}
|
|
149
|
+
function envVarConflictError(key, firstOwner, secondOwner) {
|
|
150
|
+
return new KumikoBootError([
|
|
151
|
+
{
|
|
152
|
+
name: key,
|
|
153
|
+
kind: "invalid",
|
|
154
|
+
message: `env-var conflict: "${key}" declared by both ` +
|
|
155
|
+
`"${firstOwner}" and "${secondOwner}" — pick one owner.`,
|
|
156
|
+
},
|
|
157
|
+
]);
|
|
158
|
+
}
|
|
159
|
+
// The twin follows from the slot, so a versioned family (`…_V2`) needs no
|
|
160
|
+
// second declaration. An already declared twin (apps that predate this) stays.
|
|
161
|
+
function declareKmsCiphertextTwins(merged, sources, kmsFields) {
|
|
162
|
+
for (const { name, source } of kmsFields) {
|
|
163
|
+
const twin = `${name}_CIPHERTEXT`;
|
|
164
|
+
if (merged[twin] !== undefined)
|
|
165
|
+
continue;
|
|
166
|
+
merged[twin] = z
|
|
167
|
+
.string()
|
|
168
|
+
.min(1)
|
|
169
|
+
.optional()
|
|
170
|
+
.describe(`Key-Manager ciphertext of ${name}; used when ${name} is unset.`);
|
|
171
|
+
sources[twin] = source;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
138
174
|
export function composeEnvSchema(options) {
|
|
139
175
|
const optionalSet = new Set(options.optionalFeatures ?? []);
|
|
140
176
|
const merged = {};
|
|
@@ -157,22 +193,20 @@ export function composeEnvSchema(options) {
|
|
|
157
193
|
noteKms(key, field, "framework-core");
|
|
158
194
|
}
|
|
159
195
|
}
|
|
196
|
+
const featureFields = new Map();
|
|
160
197
|
for (const feature of options.features) {
|
|
161
198
|
if (!feature.envSchema)
|
|
162
199
|
continue;
|
|
163
|
-
const shape = zodShape(feature.envSchema);
|
|
164
200
|
const wrap = optionalSet.has(feature.name);
|
|
165
|
-
for (const [key, field] of Object.entries(
|
|
201
|
+
for (const [key, field] of Object.entries(zodShape(feature.envSchema))) {
|
|
166
202
|
if (merged[key] !== undefined) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
`"${sources[key]}" and "${feature.name}" — pick one owner.`,
|
|
173
|
-
},
|
|
174
|
-
]);
|
|
203
|
+
if (!isSharedFeatureFragment(featureFields, key, field)) {
|
|
204
|
+
throw envVarConflictError(key, sources[key], feature.name);
|
|
205
|
+
}
|
|
206
|
+
keepStrictestOfSharedFragment(merged, key, field, wrap);
|
|
207
|
+
continue;
|
|
175
208
|
}
|
|
209
|
+
featureFields.set(key, field);
|
|
176
210
|
merged[key] = wrap ? optionalKeepingKmsMeta(field) : field;
|
|
177
211
|
sources[key] = feature.name;
|
|
178
212
|
noteKms(key, field, feature.name);
|
|
@@ -195,19 +229,7 @@ export function composeEnvSchema(options) {
|
|
|
195
229
|
noteKms(key, field, "app");
|
|
196
230
|
}
|
|
197
231
|
}
|
|
198
|
-
|
|
199
|
-
// second declaration. An already declared twin (apps that predate this) stays.
|
|
200
|
-
for (const { name, source } of kmsFields) {
|
|
201
|
-
const twin = `${name}_CIPHERTEXT`;
|
|
202
|
-
if (merged[twin] !== undefined)
|
|
203
|
-
continue;
|
|
204
|
-
merged[twin] = z
|
|
205
|
-
.string()
|
|
206
|
-
.min(1)
|
|
207
|
-
.optional()
|
|
208
|
-
.describe(`Key-Manager ciphertext of ${name}; used when ${name} is unset.`);
|
|
209
|
-
sources[twin] = source;
|
|
210
|
-
}
|
|
232
|
+
declareKmsCiphertextTwins(merged, sources, kmsFields);
|
|
211
233
|
return {
|
|
212
234
|
schema: z.object(merged),
|
|
213
235
|
sources,
|
package/dist/errors/classes.d.ts
CHANGED
|
@@ -98,6 +98,14 @@ export declare class InternalError extends KumikoError {
|
|
|
98
98
|
readonly httpStatus = 500;
|
|
99
99
|
constructor(opts?: Pick<ErrorOpts, "message" | "i18nKey" | "i18nParams" | "details" | "cause">);
|
|
100
100
|
}
|
|
101
|
+
export declare class MemberReadTimeoutError extends KumikoError {
|
|
102
|
+
readonly code = "member_read_timeout";
|
|
103
|
+
readonly httpStatus = 504;
|
|
104
|
+
constructor(opts: {
|
|
105
|
+
readonly timeoutMs: number;
|
|
106
|
+
readonly cause?: Error;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
101
109
|
export type RateLimitDetails = {
|
|
102
110
|
readonly bucket: string;
|
|
103
111
|
readonly limit: number;
|
package/dist/errors/classes.js
CHANGED
|
@@ -189,6 +189,19 @@ export class InternalError extends KumikoError {
|
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
|
+
// A ctx.queryAsMember read ran into its statement timeout (SQLSTATE 57014). Reuses the generic
|
|
193
|
+
// errors.internal i18n key: the client can do nothing but retry, and the code is for log/alert routing.
|
|
194
|
+
export class MemberReadTimeoutError extends KumikoError {
|
|
195
|
+
code = "member_read_timeout";
|
|
196
|
+
httpStatus = 504;
|
|
197
|
+
constructor(opts) {
|
|
198
|
+
super({
|
|
199
|
+
message: `ctx.queryAsMember read exceeded its ${opts.timeoutMs}ms statement timeout`,
|
|
200
|
+
i18nKey: "errors.internal",
|
|
201
|
+
...(opts.cause && { cause: opts.cause }),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
192
205
|
export class RateLimitError extends KumikoError {
|
|
193
206
|
code = "rate_limited";
|
|
194
207
|
httpStatus = 429;
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { FeatureDisabledDetails, FieldIssue, NotFoundDetails, PreconditionFailedDetails, RateLimitDetails, UnconfiguredDetails, UniqueViolationDetails, UnprocessableOpts, ValidationDetails, VersionConflictDetails, } from "./classes.js";
|
|
2
|
-
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
2
|
+
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, MemberReadTimeoutError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
3
3
|
export type { ErrorDocsConfig } from "./docs-url.js";
|
|
4
4
|
export { resolveErrorDocsUrl } from "./docs-url.js";
|
|
5
5
|
export type { ErrorCtorInput, ErrorOpts } from "./kumiko-error.js";
|
|
@@ -9,6 +9,7 @@ export type { AgentReason, FrameworkReason } from "./reasons.js";
|
|
|
9
9
|
export { AgentReasons, FrameworkReasons } from "./reasons.js";
|
|
10
10
|
export type { ErrorLogEntry, ErrorResponseBody } from "./serialize.js";
|
|
11
11
|
export { buildErrorLog, serializeError } from "./serialize.js";
|
|
12
|
+
export { subjectErasedConflict } from "./subject-erased.js";
|
|
12
13
|
export { toKumikoError } from "./to-kumiko-error.js";
|
|
13
14
|
export type { InvalidTransitionDetails } from "./transition-details.js";
|
|
14
15
|
export { buildInvalidTransitionDetails } from "./transition-details.js";
|
package/dist/errors/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
1
|
+
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, MemberReadTimeoutError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
2
2
|
export { resolveErrorDocsUrl } from "./docs-url.js";
|
|
3
3
|
export { isKumikoError, KumikoError } from "./kumiko-error.js";
|
|
4
4
|
export { memberResolutionReadOnlyDenied } from "./member-resolution.js";
|
|
5
5
|
export { AgentReasons, FrameworkReasons } from "./reasons.js";
|
|
6
6
|
export { buildErrorLog, serializeError } from "./serialize.js";
|
|
7
|
+
export { subjectErasedConflict } from "./subject-erased.js";
|
|
7
8
|
export { toKumikoError } from "./to-kumiko-error.js";
|
|
8
9
|
export { buildInvalidTransitionDetails } from "./transition-details.js";
|
|
9
10
|
export { failNotFound, failTransition, failUnprocessable, reraiseAsKumikoError, toWriteErrorInfo, writeFailure, } from "./write-error-info.js";
|
package/dist/errors/reasons.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare const FrameworkReasons: {
|
|
|
12
12
|
readonly publicIntakeRequired: "public_intake_required";
|
|
13
13
|
readonly irreversibleOperationRequiresHighRisk: "irreversible_operation_requires_high_risk";
|
|
14
14
|
readonly instructionFieldWriteRequiresHighRisk: "instruction_field_write_requires_high_risk";
|
|
15
|
+
readonly subjectErased: "subject_erased";
|
|
15
16
|
};
|
|
16
17
|
export type FrameworkReason = (typeof FrameworkReasons)[keyof typeof FrameworkReasons];
|
|
17
18
|
export declare const AgentReasons: {
|
package/dist/errors/reasons.js
CHANGED
|
@@ -61,6 +61,9 @@ export const FrameworkReasons = {
|
|
|
61
61
|
// delegation via ctx.write/writeAs from a lower-risk handler does not
|
|
62
62
|
// inherit the gate.
|
|
63
63
|
instructionFieldWriteRequiresHighRisk: "instruction_field_write_requires_high_risk",
|
|
64
|
+
// ConflictError: a write touched a personal-data field of a data subject whose
|
|
65
|
+
// key was erased (crypto-shredding); the subject can no longer receive data.
|
|
66
|
+
subjectErased: "subject_erased",
|
|
64
67
|
};
|
|
65
68
|
// Reason codes the AI-agent runtime surfaces via `details.reason`. Kept as a
|
|
66
69
|
// sister catalog to FrameworkReasons instead of extending it — see the
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ConflictError } from "./classes.js";
|
|
2
|
+
import { FrameworkReasons } from "./reasons.js";
|
|
3
|
+
export function subjectErasedConflict(cause) {
|
|
4
|
+
return new ConflictError({
|
|
5
|
+
message: "the data subject was erased — its personal data can no longer be written",
|
|
6
|
+
details: { reason: FrameworkReasons.subjectErased },
|
|
7
|
+
...(cause instanceof Error && { cause }),
|
|
8
|
+
});
|
|
9
|
+
}
|