@cosmicdrift/kumiko-framework 0.352.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/pii-leak-guard.js +3 -0
- 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/route-registrars.d.ts +2 -1
- package/dist/api/route-registrars.js +8 -0
- package/dist/api/routes.js +4 -2
- package/dist/api/server.js +39 -6
- package/dist/api/sse-broker.d.ts +7 -3
- package/dist/api/sse-route.js +7 -1
- 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 +36 -4
- package/dist/compliance/sub-processors.js +5 -0
- package/dist/crypto/event-pii.js +4 -2
- package/dist/crypto/kms-wiring.js +31 -5
- package/dist/crypto/pii-field-encryption.d.ts +2 -3
- package/dist/crypto/pii-field-encryption.js +2 -3
- package/dist/db/eagerload.d.ts +8 -3
- package/dist/db/eagerload.js +85 -81
- package/dist/db/entity-table-meta.js +11 -0
- package/dist/db/event-store-executor-context.d.ts +12 -0
- package/dist/db/event-store-executor-context.js +78 -42
- 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 +10 -2
- package/dist/db/queries/backfill-pii.d.ts +2 -0
- package/dist/db/queries/backfill-pii.js +24 -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 +37 -5
- 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 +21 -1
- 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 +53 -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 +136 -15
- package/dist/engine/boot-validator/select-option-tone.d.ts +2 -0
- package/dist/engine/boot-validator/select-option-tone.js +4 -0
- 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 +33 -2
- package/dist/engine/build-config-feature-schema.js +6 -0
- package/dist/engine/changeset-changes.d.ts +1 -0
- package/dist/engine/changeset-changes.js +49 -33
- package/dist/engine/dedupe-features.js +17 -1
- package/dist/engine/define-feature.js +11 -0
- package/dist/engine/entity-handlers.d.ts +3 -3
- package/dist/engine/entity-handlers.js +70 -40
- 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-builder-state.d.ts +2 -7
- 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 +44 -4
- package/dist/engine/feature-entity-handlers.js +1 -1
- 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 +13 -3
- package/dist/engine/index.d.ts +9 -3
- package/dist/engine/index.js +7 -2
- package/dist/engine/ownership.js +3 -4
- package/dist/engine/pipeline.js +18 -1
- package/dist/engine/project-app-schema-for-roles.js +11 -3
- package/dist/engine/registry-ingest.d.ts +1 -0
- package/dist/engine/registry-ingest.js +8 -3
- package/dist/engine/role-assignment.d.ts +2 -0
- package/dist/engine/role-assignment.js +2 -0
- package/dist/engine/run-pipeline.js +12 -1
- package/dist/engine/schema-builder.js +8 -0
- 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/engine/where-rule-lint.js +6 -3
- package/dist/entrypoint/index.d.ts +2 -0
- package/dist/entrypoint/index.js +2 -0
- package/dist/env/index.d.ts +4 -0
- package/dist/env/index.js +49 -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/es-ops/context.js +19 -1
- 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-ref-entity.d.ts +6 -0
- package/dist/files/file-ref-table.d.ts +6 -0
- package/dist/files/file-routes.js +13 -7
- package/dist/files/resolve-content-type.d.ts +2 -0
- package/dist/files/resolve-content-type.js +8 -0
- package/dist/files/types.js +21 -1
- 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/metrics-wiring.d.ts +1 -1
- package/dist/observability/metrics-wiring.js +2 -2
- 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/member-reader.js +1 -0
- 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/testing/shared-entities.d.ts +10 -0
- 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 +228 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +2 -1
|
@@ -13,6 +13,39 @@ function isAbsentable(field) {
|
|
|
13
13
|
return parsed.success && (parsed.data === null || parsed.data === undefined);
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
+
const NON_STRING_SCHEMA_TYPES = new Set([
|
|
17
|
+
"number",
|
|
18
|
+
"int",
|
|
19
|
+
"boolean",
|
|
20
|
+
"bigint",
|
|
21
|
+
"date",
|
|
22
|
+
"object",
|
|
23
|
+
"array",
|
|
24
|
+
]);
|
|
25
|
+
// An owner id is resolved through "non-empty string or absent" at append time,
|
|
26
|
+
// so a schema that can never yield a string would make every append throw in
|
|
27
|
+
// prod. Wrappers (optional/nullable/default/...) are looked through.
|
|
28
|
+
function neverYieldsString(field) {
|
|
29
|
+
let current = field;
|
|
30
|
+
for (;;) {
|
|
31
|
+
const def = current._zod.def;
|
|
32
|
+
if (def.innerType === undefined)
|
|
33
|
+
return NON_STRING_SCHEMA_TYPES.has(def.type);
|
|
34
|
+
current = def.innerType;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function assertOwnerFieldYieldsPresentString(args) {
|
|
38
|
+
const { featureName, eventName, field, ownerField, owner } = args;
|
|
39
|
+
if (neverYieldsString(owner)) {
|
|
40
|
+
throw new Error(`[Feature ${featureName}] defineEvent("${eventName}"): piiFields."${field}" is owned by "${ownerField}", whose payload schema is not a string — the subject id must be a non-empty string, so every append would fail. ` +
|
|
41
|
+
`Use a string owner field, or declare whenAbsent ("tenant" | "plaintext").`);
|
|
42
|
+
}
|
|
43
|
+
if (isAbsentable(owner)) {
|
|
44
|
+
throw new Error(`[Feature ${featureName}] defineEvent("${eventName}"): piiFields."${field}" is owned by "${ownerField}", which the payload schema allows to be null/undefined. ` +
|
|
45
|
+
`Declare what happens then: { personal: { of: "${ownerField}", whenAbsent: "tenant" } } encrypts under the envelope tenant key, ` +
|
|
46
|
+
`whenAbsent: "plaintext" acknowledges that the value ships unencrypted and cannot be crypto-shredded (fw#2776).`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
16
49
|
// Builds config/secrets/claims/events/jobs/notifications registrar methods.
|
|
17
50
|
export function buildConfigEventsJobsMethods(state, name) {
|
|
18
51
|
function config(arg1, arg2) {
|
|
@@ -73,16 +106,23 @@ export function buildConfigEventsJobsMethods(state, name) {
|
|
|
73
106
|
if (field === normalized.ownerField) {
|
|
74
107
|
throw new Error(`[Feature ${name}] defineEvent("${eventName}"): piiFields."${field}" cannot use itself as the owner field — the subject id is a plaintext pseudonymous fk, the pii field is the value it owns.`);
|
|
75
108
|
}
|
|
109
|
+
if (normalized.ownerField === "id") {
|
|
110
|
+
throw new Error(`[Feature ${name}] defineEvent("${eventName}"): piiFields."${field}" uses owner field "id" — on entities \`of: "id"\` means record-owned, but on events it would encrypt under a phantom user key that a forget never reaches. Use \`personal: "self"\` for a record-owned event field.`);
|
|
111
|
+
}
|
|
76
112
|
for (const required of [field, normalized.ownerField]) {
|
|
77
113
|
if (shape && !(required in shape)) {
|
|
78
114
|
throw new Error(`[Feature ${name}] defineEvent("${eventName}"): piiFields references "${required}" which is not a field of the payload schema.`);
|
|
79
115
|
}
|
|
80
116
|
}
|
|
81
117
|
const owner = shape?.[normalized.ownerField];
|
|
82
|
-
if (normalized.whenAbsent === undefined && owner !== undefined
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
118
|
+
if (normalized.whenAbsent === undefined && owner !== undefined) {
|
|
119
|
+
assertOwnerFieldYieldsPresentString({
|
|
120
|
+
featureName: name,
|
|
121
|
+
eventName,
|
|
122
|
+
field,
|
|
123
|
+
ownerField: normalized.ownerField,
|
|
124
|
+
owner,
|
|
125
|
+
});
|
|
86
126
|
}
|
|
87
127
|
}
|
|
88
128
|
}
|
|
@@ -55,7 +55,7 @@ function agentSlots(source) {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
function requireInlineHandlerArgs(kind, schema, handler, options) {
|
|
58
|
-
if (!schema || !handler || !options) {
|
|
58
|
+
if (!schema || !handler || !options?.access) {
|
|
59
59
|
throw new Error(`${kind} inline form requires schema + handler + options.access`);
|
|
60
60
|
}
|
|
61
61
|
return { schema, handler, options };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ZodObject } from "zod";
|
|
2
|
+
import { zodDef, zodMeta, zodShapeField } from "../env/_zod-introspect.js";
|
|
3
|
+
export const HANDLER_FIELD_REFERENCES_META_KEY = "references";
|
|
4
|
+
const MAX_WRAPPER_DEPTH = 8;
|
|
5
|
+
function referencesFromMeta(field) {
|
|
6
|
+
const meta = zodMeta(field);
|
|
7
|
+
if (typeof meta !== "object" || meta === null)
|
|
8
|
+
return undefined;
|
|
9
|
+
const value = Reflect.get(meta, HANDLER_FIELD_REFERENCES_META_KEY);
|
|
10
|
+
return typeof value === "string" ? value : undefined;
|
|
11
|
+
}
|
|
12
|
+
// `.optional()` / `.nullable()` / `.default()` after `.meta()` hide the meta
|
|
13
|
+
// from the outer type, so every wrapper layer is probed on the way in.
|
|
14
|
+
export function handlerFieldReference(schema, field) {
|
|
15
|
+
let current = schema;
|
|
16
|
+
for (let depth = 0; depth < MAX_WRAPPER_DEPTH && current !== undefined; depth++) {
|
|
17
|
+
if (current instanceof ZodObject)
|
|
18
|
+
break;
|
|
19
|
+
current = zodDef(current)?.innerType;
|
|
20
|
+
}
|
|
21
|
+
if (!(current instanceof ZodObject))
|
|
22
|
+
return undefined;
|
|
23
|
+
let fieldSchema = zodShapeField(current, field);
|
|
24
|
+
for (let depth = 0; depth < MAX_WRAPPER_DEPTH && fieldSchema !== undefined; depth++) {
|
|
25
|
+
const reference = referencesFromMeta(fieldSchema);
|
|
26
|
+
if (reference !== undefined)
|
|
27
|
+
return reference;
|
|
28
|
+
fieldSchema = zodDef(fieldSchema)?.innerType;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
package/dist/engine/i18n-key.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
// Dot-form labels are indistinguishable from literal display text ("actions.open"),
|
|
2
2
|
// so authors mark the i18n ones explicitly; the registry is what boot validation reads.
|
|
3
|
-
|
|
3
|
+
// A mark applies process-wide, by string value, across all features and framework copies.
|
|
4
|
+
// The registry sits on globalThis so a second installed copy reads the other's marks.
|
|
5
|
+
const REGISTRY_KEY = Symbol.for("kumiko.i18n.explicitDotFormKeys");
|
|
6
|
+
function explicitDotFormKeys() {
|
|
7
|
+
const existing = Reflect.get(globalThis, REGISTRY_KEY);
|
|
8
|
+
if (existing instanceof Set)
|
|
9
|
+
return existing;
|
|
10
|
+
const created = new Set();
|
|
11
|
+
Reflect.set(globalThis, REGISTRY_KEY, created);
|
|
12
|
+
return created;
|
|
13
|
+
}
|
|
4
14
|
export function i18nKey(value) {
|
|
5
|
-
explicitDotFormKeys.add(value);
|
|
15
|
+
explicitDotFormKeys().add(value);
|
|
6
16
|
return value;
|
|
7
17
|
}
|
|
8
18
|
export function isExplicitDotFormKey(value) {
|
|
9
|
-
return explicitDotFormKeys.has(value);
|
|
19
|
+
return explicitDotFormKeys().has(value);
|
|
10
20
|
}
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { handlerFieldLabelKey, handlerTitleKey } from "../i18n/required-surface-keys.js";
|
|
1
2
|
export { hasAccess } from "./access.js";
|
|
2
3
|
export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, type PrincipalProfile, type PrincipalStatus, type PrincipalStatusPlugin, TENANT_TEARDOWN_STATUSES, type TenantLifecycleStatusPlugin, } from "./active-membership.js";
|
|
3
4
|
export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
|
|
@@ -34,7 +35,7 @@ export { EXTENSION_SELECTOR_HINT_KEY, extensionSelectorTargets, SELECTED_EXTENSI
|
|
|
34
35
|
export { extensionUsageEscapeHatchReason } from "./extensions/escape-hatch-usage.js";
|
|
35
36
|
export type { StorageProviderDestroyTenantHook, StorageProviderExtensionHooks, StorageProviderHookCtx, } from "./extensions/storage-provider.js";
|
|
36
37
|
export { isTenantDataExtensionHooks, type TenantDataDestroyHook, type TenantDataExtensionHooks, type TenantDataHookCtx, } from "./extensions/tenant-data.js";
|
|
37
|
-
export { isTenantResourceExtensionHooks, type TenantResourceDestroyHook, type TenantResourceExtensionHooks, type TenantResourceHookCtx, } from "./extensions/tenant-resource.js";
|
|
38
|
+
export { isTenantResourceExtensionHooks, type TenantDestroyHookResult, type TenantResourceDestroyHook, type TenantResourceExtensionHooks, type TenantResourceHookCtx, } from "./extensions/tenant-resource.js";
|
|
38
39
|
export type { TenantUserModel, UserDataDeleteHook, UserDataDeleteStrategy, UserDataExportHook, UserDataExportSnippet, UserDataExtensionHooks, UserDataExtensionOptions, UserDataHookCtx, UserDataStorageProvider, } from "./extensions/user-data.js";
|
|
39
40
|
export { createBigIntField, createBooleanField, createDateField, createDecimalField, createDerivedField, createEmbeddedField, createEmbeddedListField, createEntity, createFileField, createFilesField, createImageField, createImagesField, createJsonbField, createLocatedTimestampField, createLongTextField, createMoneyField, createMultiSelectField, createNumberField, createSelectField, createTextField, createTimestampField, createTzField, } from "./factories.js";
|
|
40
41
|
export type { AddEntityArgs, AddHookArgs, AddRelationArgs, AddWriteHandlerArgs, FeaturePatcher, FeaturePattern, FeaturePatternKind, FormFieldLabel, FormFieldSpec, FormInputType, HandlerHeaderUpdate, ParseError, ParseResult, PatternCategory, PatternChange, PatternChangeIssue, PatternChangesParseResult, PatternFormSchema, PatternId, RenderFeatureFileInput, SourceLocation, } from "./feature-ast/index.js";
|
|
@@ -42,6 +43,7 @@ export { addPattern, applyChanges, createFeaturePatcher, getPatternSchema, group
|
|
|
42
43
|
export { type ChangelogEntry, type ChangelogType, compareVersions, type FeatureChangelog, filterEntriesAfter, parseFeatureChangelog, sortEntries, validateChangelog, } from "./feature-changelog.js";
|
|
43
44
|
export { type BuildManifestOptions, buildManifestFromRegistry, type FeatureManifest, type ManifestConfigKey, type ManifestExtension, type ManifestFeature, type ManifestSecret, type ManifestSystemEvent, serializeManifest, } from "./feature-manifest.js";
|
|
44
45
|
export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, maskWriteOnlyFields, } from "./field-access.js";
|
|
46
|
+
export { HANDLER_FIELD_REFERENCES_META_KEY, handlerFieldReference, } from "./handler-field-references.js";
|
|
45
47
|
export { resolveName, withResponseData } from "./handler-helpers.js";
|
|
46
48
|
export { i18nKey } from "./i18n-key.js";
|
|
47
49
|
export { buildSessionRoles, findForbiddenMembershipRole, isForbiddenMembershipRole, stripForbiddenMembershipRoles, } from "./membership-roles.js";
|
|
@@ -56,23 +58,27 @@ export type { BuiltinQnType, ParsedQn, QnType } from "./qualified-name.js";
|
|
|
56
58
|
export { isValidQn, parseQn, QnTypes, qn, toKebab } from "./qualified-name.js";
|
|
57
59
|
export { readClaim } from "./read-claim.js";
|
|
58
60
|
export { createRegistry } from "./registry.js";
|
|
61
|
+
export { qualifyTranslationKey } from "./registry-ingest.js";
|
|
59
62
|
export type { ClampInfo, ResolveOptions } from "./resolve-config-or-param.js";
|
|
60
63
|
export { resolveConfigOrParam } from "./resolve-config-or-param.js";
|
|
61
64
|
export type { AssignableAppRoles, AssignableFromRole } from "./role-assignment.js";
|
|
62
|
-
export { assignableAppRolesFromUsages, assignableAppRolesOf, canActorAssignRole, findForbiddenRoleAssignment, isAssignableByRole, mergeAssignedRoles, } from "./role-assignment.js";
|
|
65
|
+
export { assignableAppRolesFromUsages, assignableAppRolesOf, BUILT_IN_MEMBERSHIP_ROLES, canActorAssignRole, findForbiddenRoleAssignment, isAssignableByRole, mergeAssignedRoles, } from "./role-assignment.js";
|
|
63
66
|
export { runsInLane } from "./run-in.js";
|
|
64
67
|
export type { StepListOutcome } from "./run-pipeline.js";
|
|
65
68
|
export { runPipeline, runStepList } from "./run-pipeline.js";
|
|
66
69
|
export { buildInsertSchema, buildUpdateSchema, fieldToZod, SYSTEM_ONLY_JSON_SCHEMA_KEY, } from "./schema-builder.js";
|
|
67
|
-
export {
|
|
70
|
+
export type { UnavailableSelectOption } from "./screen-helpers.js";
|
|
71
|
+
export { availableSelectOptions, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, normalizeListColumn, sectionFieldSpecs, } from "./screen-helpers.js";
|
|
68
72
|
export type { TransitionGraph } from "./state-machine.js";
|
|
69
73
|
export { defineTransitions, guardTransition } from "./state-machine.js";
|
|
70
74
|
export { evaluateEventMatch } from "./steps/_event-match.js";
|
|
71
75
|
export { STEP_DISPATCH_AGGREGATE_TYPE, STEP_DISPATCH_FAILED_TYPE, STEP_DISPATCH_REQUESTED_TYPE, STEP_DISPATCHED_TYPE, SUSPEND_SENTINEL, WORKFLOW_AGGREGATE_TYPE, WORKFLOW_RESUMED_TYPE, WORKFLOW_RETRY_SCHEDULED_TYPE, WORKFLOW_RUN_COMPLETED_TYPE, WORKFLOW_RUN_FAILED_TYPE, WORKFLOW_RUN_STARTED_TYPE, WORKFLOW_WAITING_FOR_EVENT_TYPE, WORKFLOW_WAITING_TYPE, } from "./steps/_step-dispatch-constants.js";
|
|
72
76
|
export { describeWorkflowStepError } from "./steps/describe-workflow-step-error.js";
|
|
77
|
+
export { unwrapWorkflowStepError, WorkflowStepError } from "./steps/workflow-step-error.js";
|
|
73
78
|
export { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
|
|
74
79
|
export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
|
|
75
80
|
export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
|
|
81
|
+
export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTransferAdjacency, type TransferAdjacency, type TransferEdge, type TransferEdgeLink, } from "./transfer-adjacency.js";
|
|
76
82
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
|
|
77
83
|
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
|
|
78
84
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
package/dist/engine/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Public API
|
|
2
|
+
export { handlerFieldLabelKey, handlerTitleKey } from "../i18n/required-surface-keys.js";
|
|
2
3
|
export { hasAccess } from "./access.js";
|
|
3
4
|
export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, TENANT_TEARDOWN_STATUSES, } from "./active-membership.js";
|
|
4
5
|
export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
|
|
@@ -37,6 +38,7 @@ export { addPattern, applyChanges, createFeaturePatcher, getPatternSchema, group
|
|
|
37
38
|
export { compareVersions, filterEntriesAfter, parseFeatureChangelog, sortEntries, validateChangelog, } from "./feature-changelog.js";
|
|
38
39
|
export { buildManifestFromRegistry, serializeManifest, } from "./feature-manifest.js";
|
|
39
40
|
export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, maskWriteOnlyFields, } from "./field-access.js";
|
|
41
|
+
export { HANDLER_FIELD_REFERENCES_META_KEY, handlerFieldReference, } from "./handler-field-references.js";
|
|
40
42
|
export { resolveName, withResponseData } from "./handler-helpers.js";
|
|
41
43
|
export { i18nKey } from "./i18n-key.js";
|
|
42
44
|
// findForbiddenMembershipRole/isForbiddenMembershipRole/
|
|
@@ -54,19 +56,22 @@ export { defineApply, defineMspApply, setFields } from "./projection-helpers.js"
|
|
|
54
56
|
export { isValidQn, parseQn, QnTypes, qn, toKebab } from "./qualified-name.js";
|
|
55
57
|
export { readClaim } from "./read-claim.js";
|
|
56
58
|
export { createRegistry } from "./registry.js";
|
|
59
|
+
export { qualifyTranslationKey } from "./registry-ingest.js";
|
|
57
60
|
export { resolveConfigOrParam } from "./resolve-config-or-param.js";
|
|
58
|
-
export { assignableAppRolesFromUsages, assignableAppRolesOf, canActorAssignRole, findForbiddenRoleAssignment, isAssignableByRole, mergeAssignedRoles, } from "./role-assignment.js";
|
|
61
|
+
export { assignableAppRolesFromUsages, assignableAppRolesOf, BUILT_IN_MEMBERSHIP_ROLES, canActorAssignRole, findForbiddenRoleAssignment, isAssignableByRole, mergeAssignedRoles, } from "./role-assignment.js";
|
|
59
62
|
export { runsInLane } from "./run-in.js";
|
|
60
63
|
export { runPipeline, runStepList } from "./run-pipeline.js";
|
|
61
64
|
export { buildInsertSchema, buildUpdateSchema, fieldToZod, SYSTEM_ONLY_JSON_SCHEMA_KEY, } from "./schema-builder.js";
|
|
62
|
-
export { isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, normalizeListColumn, sectionFieldSpecs, } from "./screen-helpers.js";
|
|
65
|
+
export { availableSelectOptions, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, normalizeListColumn, sectionFieldSpecs, } from "./screen-helpers.js";
|
|
63
66
|
export { defineTransitions, guardTransition } from "./state-machine.js";
|
|
64
67
|
export { evaluateEventMatch } from "./steps/_event-match.js";
|
|
65
68
|
export { STEP_DISPATCH_AGGREGATE_TYPE, STEP_DISPATCH_FAILED_TYPE, STEP_DISPATCH_REQUESTED_TYPE, STEP_DISPATCHED_TYPE, SUSPEND_SENTINEL, WORKFLOW_AGGREGATE_TYPE, WORKFLOW_RESUMED_TYPE, WORKFLOW_RETRY_SCHEDULED_TYPE, WORKFLOW_RUN_COMPLETED_TYPE, WORKFLOW_RUN_FAILED_TYPE, WORKFLOW_RUN_STARTED_TYPE, WORKFLOW_WAITING_FOR_EVENT_TYPE, WORKFLOW_WAITING_TYPE, } from "./steps/_step-dispatch-constants.js";
|
|
66
69
|
export { describeWorkflowStepError } from "./steps/describe-workflow-step-error.js";
|
|
70
|
+
export { unwrapWorkflowStepError, WorkflowStepError } from "./steps/workflow-step-error.js";
|
|
67
71
|
export { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
|
|
68
72
|
export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
|
|
69
73
|
export { findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, } from "./tier-resolver-extension.js";
|
|
74
|
+
export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTransferAdjacency, } from "./transfer-adjacency.js";
|
|
70
75
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
|
|
71
76
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
|
72
77
|
export { runValidation } from "./validation.js";
|
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;
|
package/dist/engine/pipeline.js
CHANGED
|
@@ -79,9 +79,26 @@ export function stepsPipeline(closure) {
|
|
|
79
79
|
// carried in (D4, workflow-resume-loop.md), never an unchecked external
|
|
80
80
|
// value.
|
|
81
81
|
export function buildPipelineSteps(pipelineDef, event) {
|
|
82
|
-
|
|
82
|
+
const steps = pipelineDef.build({
|
|
83
83
|
event,
|
|
84
84
|
r: stepBuilder,
|
|
85
85
|
awaits: (pipelineDef.awaits ?? {}),
|
|
86
86
|
});
|
|
87
|
+
assertUniqueWaitForEventTypes(steps);
|
|
88
|
+
return steps;
|
|
89
|
+
}
|
|
90
|
+
// waitForEvent's result key is the awaited event type, so a second wait on the
|
|
91
|
+
// same type would overwrite the first one's resumed payload.
|
|
92
|
+
function assertUniqueWaitForEventTypes(steps) {
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
for (const step of steps) {
|
|
95
|
+
if (step.kind !== "workflow.waitForEvent")
|
|
96
|
+
continue;
|
|
97
|
+
const { event } = step.args; // @cast-boundary engine-payload
|
|
98
|
+
if (seen.has(event)) {
|
|
99
|
+
throw new Error(`r.step.waitForEvent: event "${String(event)}" is awaited more than once in one workflow — ` +
|
|
100
|
+
"each awaits entry can back only one waitForEvent step.");
|
|
101
|
+
}
|
|
102
|
+
seen.add(event);
|
|
103
|
+
}
|
|
87
104
|
}
|
|
@@ -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);
|
|
@@ -530,20 +532,26 @@ function projectActionFormScreen(screen, feature, indices, keptScreenQns) {
|
|
|
530
532
|
};
|
|
531
533
|
}
|
|
532
534
|
function projectSecretMintScreen(screen, feature, indices, keptScreenQns) {
|
|
533
|
-
const { redirect, cancelTarget, layout, listScreenId, ...rest } = screen;
|
|
535
|
+
const { redirect, cancelTarget, layout, listScreenId, confirm, ...rest } = screen;
|
|
534
536
|
const nextRedirect = projectRedirect(feature.featureName, redirect, keptScreenQns);
|
|
535
537
|
const nextCancelTarget = projectCancelTarget(feature.featureName, cancelTarget, keptScreenQns);
|
|
536
538
|
const nextLayout = projectEditLayout(layout, feature, indices, keptScreenQns);
|
|
537
539
|
const nextListScreenId = projectListScreenId(listScreenId, indices, keptScreenQns);
|
|
540
|
+
const nextConfirmLayout = confirm === undefined
|
|
541
|
+
? undefined
|
|
542
|
+
: projectEditLayout(confirm.layout, feature, indices, keptScreenQns);
|
|
538
543
|
const unchanged = nextRedirect === redirect &&
|
|
539
544
|
nextCancelTarget === cancelTarget &&
|
|
540
545
|
nextLayout === layout &&
|
|
541
|
-
nextListScreenId === listScreenId
|
|
546
|
+
nextListScreenId === listScreenId &&
|
|
547
|
+
nextConfirmLayout === confirm?.layout;
|
|
542
548
|
if (unchanged)
|
|
543
549
|
return screen;
|
|
544
550
|
return {
|
|
545
551
|
...rest,
|
|
546
552
|
layout: nextLayout,
|
|
553
|
+
...(confirm !== undefined &&
|
|
554
|
+
nextConfirmLayout !== undefined && { confirm: { ...confirm, layout: nextConfirmLayout } }),
|
|
547
555
|
...(nextRedirect !== undefined && { redirect: nextRedirect }),
|
|
548
556
|
...(nextCancelTarget !== undefined && { cancelTarget: nextCancelTarget }),
|
|
549
557
|
...(nextListScreenId !== undefined && { listScreenId: nextListScreenId }),
|
|
@@ -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) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Registry } from "./types/feature.js";
|
|
2
|
+
/** Every membership role the engine ranks, excluding the internal `system` actor. */
|
|
3
|
+
export declare const BUILT_IN_MEMBERSHIP_ROLES: readonly string[];
|
|
2
4
|
declare const ASSIGNABLE_FROM_ROLE_LIST: readonly ["User", "Member", "Editor", "Admin", "TenantAdmin", "SystemAdmin"];
|
|
3
5
|
export type AssignableFromRole = (typeof ASSIGNABLE_FROM_ROLE_LIST)[number];
|
|
4
6
|
/** App role → lowest built-in role allowed to assign it. */
|
|
@@ -16,6 +16,8 @@ const ROLE_RANKS = new Map([
|
|
|
16
16
|
["SystemAdmin", 4],
|
|
17
17
|
["system", 5],
|
|
18
18
|
]);
|
|
19
|
+
/** Every membership role the engine ranks, excluding the internal `system` actor. */
|
|
20
|
+
export const BUILT_IN_MEMBERSHIP_ROLES = [...ROLE_RANKS.keys()].filter((role) => role !== "system");
|
|
19
21
|
const ASSIGNABLE_FROM_ROLE_LIST = [
|
|
20
22
|
"User",
|
|
21
23
|
"Member",
|
|
@@ -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.
|
|
@@ -87,6 +87,14 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
87
87
|
let schema = z.string();
|
|
88
88
|
if (field.maxLength)
|
|
89
89
|
schema = schema.max(field.maxLength);
|
|
90
|
+
if (field.minLength) {
|
|
91
|
+
// "" must stay valid for optional fields and writeOnly updates (""
|
|
92
|
+
// = unchanged); `required` rejects it separately via min(1) below.
|
|
93
|
+
const minLength = field.minLength;
|
|
94
|
+
schema = schema.refine((value) => value.length === 0 || value.length >= minLength, {
|
|
95
|
+
message: `at least ${minLength} characters`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
90
98
|
if (field.writeOnly === true) {
|
|
91
99
|
// Update: "" = unchanged (dropped before the executor), null = clear,
|
|
92
100
|
// unless required. Create: "" would leave a required field unset.
|
|
@@ -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
|
+
}
|