@cosmicdrift/kumiko-framework 0.338.0 → 0.340.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.
Files changed (46) hide show
  1. package/dist/api/auth-middleware.d.ts +1 -0
  2. package/dist/api/auth-middleware.js +54 -10
  3. package/dist/api/routes.d.ts +2 -1
  4. package/dist/api/routes.js +29 -6
  5. package/dist/api/server.js +9 -1
  6. package/dist/bun-db/index.d.ts +2 -2
  7. package/dist/bun-db/index.js +1 -1
  8. package/dist/bun-db/query.d.ts +25 -1
  9. package/dist/bun-db/query.js +144 -17
  10. package/dist/crypto/event-pii.js +13 -4
  11. package/dist/crypto/index.d.ts +1 -1
  12. package/dist/crypto/index.js +1 -1
  13. package/dist/crypto/pii-ciphertext-format.d.ts +4 -0
  14. package/dist/crypto/pii-ciphertext-format.js +11 -0
  15. package/dist/crypto/pii-field-encryption.d.ts +4 -2
  16. package/dist/crypto/pii-field-encryption.js +50 -21
  17. package/dist/crypto/system-event-pii.js +2 -2
  18. package/dist/db/queries/backfill-pii.js +4 -0
  19. package/dist/db/query-api.d.ts +1 -1
  20. package/dist/db/query.d.ts +1 -1
  21. package/dist/db/tenant-db.js +16 -12
  22. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  23. package/dist/engine/boot-validator/entity-handler.js +19 -0
  24. package/dist/engine/boot-validator/pii-retention.js +30 -0
  25. package/dist/engine/define-handler.js +1 -0
  26. package/dist/engine/extensions/user-data.d.ts +1 -1
  27. package/dist/engine/feature-ast/extractors/handlers.js +1 -0
  28. package/dist/engine/feature-entity-handlers.js +1 -0
  29. package/dist/engine/feature-manifest.d.ts +7 -0
  30. package/dist/engine/feature-manifest.js +8 -0
  31. package/dist/engine/index.d.ts +2 -1
  32. package/dist/engine/index.js +1 -0
  33. package/dist/engine/registry-ingest.d.ts +1 -0
  34. package/dist/engine/registry-ingest.js +9 -0
  35. package/dist/engine/registry.js +2 -1
  36. package/dist/engine/steps/retry.js +0 -1
  37. package/dist/engine/steps/unsafe-projection-upsert.js +1 -1
  38. package/dist/engine/steps/wait-for-event.js +0 -1
  39. package/dist/engine/steps/wait.js +0 -4
  40. package/dist/engine/steps/workflow-system-events.d.ts +2 -0
  41. package/dist/engine/steps/workflow-system-events.js +89 -0
  42. package/dist/pipeline/append-event-core.d.ts +3 -1
  43. package/dist/pipeline/append-event-core.js +8 -4
  44. package/dist/pipeline/dispatch-shared.js +5 -2
  45. package/package.json +4 -4
  46. package/src/changes.json +48 -0
@@ -6,6 +6,6 @@ export { type KekSourceEnv, type KekSourceOptions, resolvePlatformKeks } from ".
6
6
  export { isLocalKeyKmsAdapter, KeyAlreadyExistsError, KeyErasedError, KeyNotFoundError, type KmsAdapter, type KmsContext, type KmsHealth, type LocalKeyKmsAdapter, RECORD_ENTITY_PATTERN, type RemoteCryptoKmsAdapter, type SubjectDek, type SubjectId, type SubjectKey, subjectIdFromKey, subjectIdSchema, subjectIdToKey, subjectKeyForRecord, subjectKeyForTenant, subjectKeyForUser, } from "./kms-adapter.js";
7
7
  export { type ActiveKmsWiring, buildPgKmsOptions, type KmsWiring, type KmsWiringEnv, type KmsWiringOptions, type KmsWiringRelease, type PgKmsRotationEnv, type PlaintextPiiWiring, requireKmsWiring, requireKmsWiringAsync, resolveKmsWiring, resolveKmsWiringAsync, } from "./kms-wiring.js";
8
8
  export { createPgKmsAdapter, PgKmsAdapter, type PgKmsAdapterOptions, type RewrapOptions, type RewrapResult, rewrapSubjectKeys, } from "./pg-kms-adapter.js";
9
- export { configuredPiiSubjectKms, configurePiiSubjectKms, decryptPiiFieldValues, decryptPiiValueForSubject, type EncryptPiiOptions, encryptPiiFieldValues, encryptPiiValueForSubject, isPiiCiphertext, PII_CIPHERTEXT_PREFIX, PII_ERASED_SENTINEL, } from "./pii-field-encryption.js";
9
+ export { configuredPiiSubjectKms, configurePiiSubjectKms, decryptPiiFieldValues, decryptPiiValueForSubject, type EncryptPiiOptions, encryptPiiFieldValues, encryptPiiJsonValueForSubject, encryptPiiValueForSubject, isPiiCiphertext, PII_CIPHERTEXT_PREFIX, PII_CIPHERTEXT_PREFIX_JSON, PII_ERASED_SENTINEL, } from "./pii-field-encryption.js";
10
10
  export { createRequestKmsCache, type RequestKmsCache, } from "./request-kms-cache.js";
11
11
  export { collectPiiSubjectFields, collectSearchableSubjectFields, type ResolveSubjectOptions, resolveSubjectForField, SubjectResolutionError, } from "./subject-resolver.js";
@@ -6,6 +6,6 @@ export { resolvePlatformKeks } from "./kek-source.js";
6
6
  export { isLocalKeyKmsAdapter, KeyAlreadyExistsError, KeyErasedError, KeyNotFoundError, RECORD_ENTITY_PATTERN, subjectIdFromKey, subjectIdSchema, subjectIdToKey, subjectKeyForRecord, subjectKeyForTenant, subjectKeyForUser, } from "./kms-adapter.js";
7
7
  export { buildPgKmsOptions, requireKmsWiring, requireKmsWiringAsync, resolveKmsWiring, resolveKmsWiringAsync, } from "./kms-wiring.js";
8
8
  export { createPgKmsAdapter, PgKmsAdapter, rewrapSubjectKeys, } from "./pg-kms-adapter.js";
9
- export { configuredPiiSubjectKms, configurePiiSubjectKms, decryptPiiFieldValues, decryptPiiValueForSubject, encryptPiiFieldValues, encryptPiiValueForSubject, isPiiCiphertext, PII_CIPHERTEXT_PREFIX, PII_ERASED_SENTINEL, } from "./pii-field-encryption.js";
9
+ export { configuredPiiSubjectKms, configurePiiSubjectKms, decryptPiiFieldValues, decryptPiiValueForSubject, encryptPiiFieldValues, encryptPiiJsonValueForSubject, encryptPiiValueForSubject, isPiiCiphertext, PII_CIPHERTEXT_PREFIX, PII_CIPHERTEXT_PREFIX_JSON, PII_ERASED_SENTINEL, } from "./pii-field-encryption.js";
10
10
  export { createRequestKmsCache, } from "./request-kms-cache.js";
11
11
  export { collectPiiSubjectFields, collectSearchableSubjectFields, resolveSubjectForField, SubjectResolutionError, } from "./subject-resolver.js";
@@ -0,0 +1,4 @@
1
+ export declare const PII_CIPHERTEXT_PREFIX_V1 = "kumiko-pii:v1:";
2
+ export declare const PII_CIPHERTEXT_PREFIX = "kumiko-pii:v2:";
3
+ export declare const PII_CIPHERTEXT_PREFIX_JSON = "kumiko-pii:v3:";
4
+ export declare function isPiiCiphertext(value: unknown): value is string;
@@ -0,0 +1,11 @@
1
+ // Leaf module (no engine imports) so bun-db and the executors can sniff PII
2
+ // ciphertext without pulling the KMS/pipeline graph into an import cycle.
3
+ export const PII_CIPHERTEXT_PREFIX_V1 = "kumiko-pii:v1:";
4
+ export const PII_CIPHERTEXT_PREFIX = "kumiko-pii:v2:";
5
+ export const PII_CIPHERTEXT_PREFIX_JSON = "kumiko-pii:v3:";
6
+ export function isPiiCiphertext(value) {
7
+ return (typeof value === "string" &&
8
+ (value.startsWith(PII_CIPHERTEXT_PREFIX) ||
9
+ value.startsWith(PII_CIPHERTEXT_PREFIX_JSON) ||
10
+ value.startsWith(PII_CIPHERTEXT_PREFIX_V1)));
11
+ }
@@ -1,10 +1,11 @@
1
1
  import type { EntityDefinition } from "../engine/types/fields.js";
2
2
  import type { TenantId } from "../engine/types/identifiers.js";
3
3
  import { type KmsAdapter, type KmsContext, type LocalKeyKmsAdapter, type SubjectId } from "./kms-adapter.js";
4
+ import { isPiiCiphertext, PII_CIPHERTEXT_PREFIX, PII_CIPHERTEXT_PREFIX_JSON } from "./pii-ciphertext-format.js";
5
+ export { isPiiCiphertext, PII_CIPHERTEXT_PREFIX, PII_CIPHERTEXT_PREFIX_JSON };
4
6
  export declare const PII_ERASED_SENTINEL = "[[erased]]";
5
- export declare const PII_CIPHERTEXT_PREFIX = "kumiko-pii:v2:";
6
- export declare function isPiiCiphertext(value: unknown): value is string;
7
7
  export declare function encryptPiiValueForSubject(kms: LocalKeyKmsAdapter, subject: SubjectId, value: string, kmsCtx: KmsContext, field: string): Promise<string>;
8
+ export declare function encryptPiiJsonValueForSubject(kms: LocalKeyKmsAdapter, subject: SubjectId, value: unknown, kmsCtx: KmsContext, field: string): Promise<string>;
8
9
  export declare function decryptPiiValueForSubject(kms: LocalKeyKmsAdapter, value: string, kmsCtx: KmsContext, field: string): Promise<string>;
9
10
  export interface EncryptPiiOptions {
10
11
  readonly onlyKeys?: Iterable<string>;
@@ -13,6 +14,7 @@ export interface EncryptPiiOptions {
13
14
  readonly entityName: string;
14
15
  }
15
16
  export declare function encryptPiiFieldValues(row: Record<string, unknown>, entity: EntityDefinition, piiFields: readonly string[], kms: LocalKeyKmsAdapter, kmsCtx: KmsContext, opts: EncryptPiiOptions): Promise<Record<string, unknown>>;
17
+ export declare function serializeJsonbValue(field: string, value: unknown): string;
16
18
  export declare function decryptPiiFieldValues(row: Record<string, unknown>, piiFields: readonly string[], kms: LocalKeyKmsAdapter, kmsCtx: KmsContext): Promise<Record<string, unknown>>;
17
19
  export declare function configurePiiSubjectKms(adapter: KmsAdapter | undefined): void;
18
20
  export declare function configuredPiiSubjectKms(): LocalKeyKmsAdapter | undefined;
@@ -9,34 +9,41 @@
9
9
  // SAME subject (same DEK, so key selection alone can't catch that) from
10
10
  // decrypting silently. v1 (no AAD) stays decrypt-only for pre-#1263 rows;
11
11
  // every new write emits v2.
12
+ // v3 = v2 for jsonb fields: the plaintext is JSON.stringify(value) and decrypt
13
+ // JSON.parses it back, so a jsonb string/number/object round-trips by type.
14
+ // The version digit (not a suffix) keeps `kumiko-pii:v\d+:` leak guards and
15
+ // the `kumiko-pii:v%:` LIKE sweeps matching. The AAD carries a "json" tag so
16
+ // a v2<->v3 prefix swap fails GCM instead of silently changing the type.
12
17
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
13
18
  import { assertIrreversibleOperationAllowed } from "../pipeline/irreversible-operation-gate.js";
14
19
  import { isLocalKeyKmsAdapter, KeyAlreadyExistsError, KeyErasedError, KeyNotFoundError, subjectIdFromKey, subjectIdToKey, } from "./kms-adapter.js";
20
+ import { isPiiCiphertext, PII_CIPHERTEXT_PREFIX, PII_CIPHERTEXT_PREFIX_JSON, PII_CIPHERTEXT_PREFIX_V1, } from "./pii-ciphertext-format.js";
15
21
  import { resolveSubjectForField } from "./subject-resolver.js";
16
22
  // Spec value (crypto-shredding.md) — renderers show it verbatim.
23
+ export { isPiiCiphertext, PII_CIPHERTEXT_PREFIX, PII_CIPHERTEXT_PREFIX_JSON };
17
24
  export const PII_ERASED_SENTINEL = "[[erased]]";
18
- const PII_CIPHERTEXT_PREFIX_V1 = "kumiko-pii:v1:";
19
- export const PII_CIPHERTEXT_PREFIX = "kumiko-pii:v2:";
20
25
  const IV_LENGTH = 12;
21
26
  const AUTH_TAG_LENGTH = 16;
22
- export function isPiiCiphertext(value) {
23
- return (typeof value === "string" &&
24
- (value.startsWith(PII_CIPHERTEXT_PREFIX) || value.startsWith(PII_CIPHERTEXT_PREFIX_V1)));
27
+ function buildAad(subject, field, isJson) {
28
+ return Buffer.from(`${subjectIdToKey(subject)}|${field}${isJson ? "|json" : ""}`, "utf8");
25
29
  }
26
- function buildAad(subject, field) {
27
- return Buffer.from(`${subjectIdToKey(subject)}|${field}`, "utf8");
28
- }
29
- function encryptValue(subject, dek, plaintext, field) {
30
+ function encryptValue(subject, dek, plaintext, field, isJson = false) {
30
31
  const iv = randomBytes(IV_LENGTH);
31
32
  const cipher = createCipheriv("aes-256-gcm", dek, iv);
32
- cipher.setAAD(buildAad(subject, field));
33
+ cipher.setAAD(buildAad(subject, field, isJson));
33
34
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
34
35
  const blob = Buffer.concat([iv, cipher.getAuthTag(), ciphertext]);
35
- return `${PII_CIPHERTEXT_PREFIX}${subjectIdToKey(subject)}:${blob.toString("base64")}`;
36
+ const prefix = isJson ? PII_CIPHERTEXT_PREFIX_JSON : PII_CIPHERTEXT_PREFIX;
37
+ return `${prefix}${subjectIdToKey(subject)}:${blob.toString("base64")}`;
36
38
  }
37
39
  function parseCiphertext(value) {
38
- const hasAad = value.startsWith(PII_CIPHERTEXT_PREFIX);
39
- const prefix = hasAad ? PII_CIPHERTEXT_PREFIX : PII_CIPHERTEXT_PREFIX_V1;
40
+ const isJson = value.startsWith(PII_CIPHERTEXT_PREFIX_JSON);
41
+ const hasAad = isJson || value.startsWith(PII_CIPHERTEXT_PREFIX);
42
+ const prefix = isJson
43
+ ? PII_CIPHERTEXT_PREFIX_JSON
44
+ : hasAad
45
+ ? PII_CIPHERTEXT_PREFIX
46
+ : PII_CIPHERTEXT_PREFIX_V1;
40
47
  const rest = value.slice(prefix.length);
41
48
  // subjectKey itself contains ":" ("user:<id>") — base64 never does, so the
42
49
  // last ":" is always the key/blob separator.
@@ -47,15 +54,16 @@ function parseCiphertext(value) {
47
54
  subject: subjectIdFromKey(rest.slice(0, sep)),
48
55
  blob: Buffer.from(rest.slice(sep + 1), "base64"),
49
56
  hasAad,
57
+ isJson,
50
58
  };
51
59
  }
52
- function decryptValue(dek, blob, subject, field, hasAad) {
60
+ function decryptValue(dek, blob, subject, field, hasAad, isJson) {
53
61
  const iv = blob.subarray(0, IV_LENGTH);
54
62
  const tag = blob.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
55
63
  const ciphertext = blob.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
56
64
  const decipher = createDecipheriv("aes-256-gcm", dek, iv);
57
65
  if (hasAad)
58
- decipher.setAAD(buildAad(subject, field));
66
+ decipher.setAAD(buildAad(subject, field, isJson));
59
67
  decipher.setAuthTag(tag);
60
68
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
61
69
  }
@@ -91,6 +99,12 @@ export async function encryptPiiValueForSubject(kms, subject, value, kmsCtx, fie
91
99
  const dek = await getOrCreateDek(kms, subject, kmsCtx);
92
100
  return encryptValue(subject, dek, value, field);
93
101
  }
102
+ // Event-payload variant for non-string values (jsonb-typed payload fields):
103
+ // same DEK path, but the self-describing v3 format so decrypt restores the type.
104
+ export async function encryptPiiJsonValueForSubject(kms, subject, value, kmsCtx, field) {
105
+ const dek = await getOrCreateDek(kms, subject, kmsCtx);
106
+ return encryptValue(subject, dek, serializeJsonbValue(field, value), field, true);
107
+ }
94
108
  // Single-value decrypt for callers that don't have an entity/field-map to
95
109
  // pass through decryptPiiFieldValues (config values). The subject lives
96
110
  // inside the ciphertext itself, so no subject/scope resolution is needed
@@ -99,9 +113,12 @@ export async function encryptPiiValueForSubject(kms, subject, value, kmsCtx, fie
99
113
  export async function decryptPiiValueForSubject(kms, value, kmsCtx, field) {
100
114
  if (!isPiiCiphertext(value))
101
115
  return value;
102
- const { subject, blob, hasAad } = parseCiphertext(value);
116
+ const { subject, blob, hasAad, isJson } = parseCiphertext(value);
117
+ if (isJson) {
118
+ throw new Error(`PII field "${field}" holds a jsonb ciphertext — decrypt it via decryptPiiFieldValues`);
119
+ }
103
120
  try {
104
- return decryptValue(await kms.getKey(subject, kmsCtx), blob, subject, field, hasAad);
121
+ return decryptValue(await kms.getKey(subject, kmsCtx), blob, subject, field, hasAad, false);
105
122
  }
106
123
  catch (e) {
107
124
  if (!(e instanceof KeyErasedError))
@@ -127,9 +144,11 @@ export async function encryptPiiFieldValues(row, entity, piiFields, kms, kmsCtx,
127
144
  // are already ciphertext or the erased sentinel — both stay as-is.
128
145
  if (isPiiCiphertext(value) || value === PII_ERASED_SENTINEL)
129
146
  continue;
130
- if (typeof value !== "string") {
147
+ const isJsonbField = entity.fields[name]?.type === "jsonb";
148
+ if (!isJsonbField && typeof value !== "string") {
131
149
  throw new Error(`PII field "${name}" must be a string, got ${typeof value}`);
132
150
  }
151
+ const plaintext = isJsonbField ? serializeJsonbValue(name, value) : String(value);
133
152
  const subject = resolveSubjectForField(entity, name, subjectSource, {
134
153
  entityName: opts.entityName,
135
154
  ...(opts.tenantId !== undefined && { tenantId: opts.tenantId }),
@@ -138,10 +157,17 @@ export async function encryptPiiFieldValues(row, entity, piiFields, kms, kmsCtx,
138
157
  if (subject === null)
139
158
  continue;
140
159
  const dek = await getOrCreateDek(kms, subject, kmsCtx);
141
- out[name] = encryptValue(subject, dek, value, name);
160
+ out[name] = encryptValue(subject, dek, plaintext, name, isJsonbField);
142
161
  }
143
162
  return out;
144
163
  }
164
+ export function serializeJsonbValue(field, value) {
165
+ const json = JSON.stringify(value);
166
+ // undefined/function/symbol serialize to undefined — never storable as jsonb
167
+ if (json === undefined)
168
+ throw new Error(`PII jsonb field "${field}" is not JSON-serializable`);
169
+ return json;
170
+ }
145
171
  export async function decryptPiiFieldValues(row, piiFields, kms, kmsCtx) {
146
172
  if (piiFields.length === 0)
147
173
  return row;
@@ -152,9 +178,12 @@ export async function decryptPiiFieldValues(row, piiFields, kms, kmsCtx) {
152
178
  // work during rollout; backfill is tracked in kumiko-framework#799).
153
179
  if (!isPiiCiphertext(value))
154
180
  continue;
155
- const { subject, blob, hasAad } = parseCiphertext(value);
181
+ const { subject, blob, hasAad, isJson } = parseCiphertext(value);
156
182
  try {
157
- out[name] = decryptValue(await kms.getKey(subject, kmsCtx), blob, subject, name, hasAad);
183
+ const key = await kms.getKey(subject, kmsCtx);
184
+ const plain = decryptValue(key, blob, subject, name, hasAad, isJson);
185
+ // @cast-boundary crypto-decrypt — AEAD-authenticated plaintext that this module serialized
186
+ out[name] = isJson ? JSON.parse(plain) : plain;
158
187
  }
159
188
  catch (e) {
160
189
  // KeyNotFound deliberately propagates: ciphertext without a key row
@@ -9,8 +9,8 @@ export const SYSTEM_EVENT_PREFIX = "kumiko:system:";
9
9
  // an event-store import cycle; transfer.ts re-exports it.
10
10
  export const AGGREGATE_TRANSFERRED_EVENT_TYPE = `${SYSTEM_EVENT_PREFIX}aggregate.transferred`;
11
11
  const SELF = { personal: "self" };
12
- // Workflow payloads carry `triggerPayload`, the stored payload of the trigger
13
- // event: catalogued fields there are already ciphertext, so "none" leaks nothing.
12
+ // Workflow run-stream payloads carry references and step bookkeeping only,
13
+ // never a copy of a foreign event payload, so "none" leaves nothing to shred.
14
14
  export const SYSTEM_EVENT_PII_STANCES = new Map([
15
15
  [
16
16
  STEP_DISPATCH_REQUESTED_TYPE,
@@ -300,6 +300,10 @@ export async function backfillEventPiiEncryptionBatch(db, registry, options = {}
300
300
  const value = section[field];
301
301
  if (value === null || value === undefined)
302
302
  continue;
303
+ // jsonb PII is not retro-encrypted (consumer wave); a legacy string
304
+ // value here would otherwise get the v2 text format into a jsonb column.
305
+ if (target.entity.fields[field]?.type === "jsonb")
306
+ continue;
303
307
  if (typeof value !== "string")
304
308
  continue;
305
309
  // Already-ciphertext/sentinel fields must never enter owner
@@ -1,2 +1,2 @@
1
- export type { SelectOptions, WhereObject, WhereOperator, WhereValue, } from "../db/query.js";
1
+ export type { JsonTextMatch, SelectOptions, WhereObject, WhereOperator, WhereValue, } from "../db/query.js";
2
2
  export { aggregateWhere, asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "../db/query.js";
@@ -1 +1 @@
1
- export { type AnyDb, aggregateWhere, asEntityTableMeta, asRawClient, coerceRow, countWhere, type DeleteManyBatchedOptions, type DeleteManyBatchedResult, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, type IncrementCounterOptions, incrementCounter, insertMany, insertOne, type OrderByClause, runInNewTransaction, runInSavepoint, runInSavepointIfSupported, type SelectOptions, selectMany, type TableInfo, transaction, type UpsertOnConflictOptions, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, type WhereObject, type WhereOperator, type WhereValue, } from "../bun-db/query.js";
1
+ export { type AnyDb, aggregateWhere, asEntityTableMeta, asRawClient, coerceRow, countWhere, type DeleteManyBatchedOptions, type DeleteManyBatchedResult, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, type IncrementCounterOptions, incrementCounter, insertMany, insertOne, type JsonTextMatch, type OrderByClause, runInNewTransaction, runInSavepoint, runInSavepointIfSupported, type SelectOptions, selectMany, type TableInfo, transaction, type UpsertOnConflictOptions, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, type WhereObject, type WhereOperator, type WhereValue, } from "../bun-db/query.js";
@@ -11,7 +11,7 @@ const declaredUnsafeRawRunners = new WeakMap();
11
11
  // TenantDb it was handed for its gate. Bound inside createTenantDb so rebound instances
12
12
  // (withUnsafeRawGrant, acknowledgeConventionCrossTenant) carry it too.
13
13
  const personalDataGates = new WeakMap();
14
- // Lets createTenantDb(ctx.db.unsafeRaw(reason), ...) inherit the gate. Keyed by a
14
+ // Lets createTenantDb(ctx.db.unsafeRaw(), ...) inherit the gate. Keyed by a
15
15
  // per-grant proxy, never the shared pool/tx: tagging that would gate every sibling TenantDb.
16
16
  const runnerPersonalDataGates = new WeakMap();
17
17
  function gatedRunner(runner, gate) {
@@ -61,7 +61,7 @@ export function unsafeRawForDeclaredStep(holder, reason) {
61
61
  "createUncheckedSystemDb, or createSystemDbView — no declared unsafeRaw runner bound.",
62
62
  });
63
63
  }
64
- return runner(reason, true);
64
+ return runner(reason);
65
65
  }
66
66
  const systemDbRebinders = new WeakMap();
67
67
  // Rebinds a hook's own escapeHatch onto ctx.systemDb, mirroring withUnsafeRawGrant: always
@@ -104,7 +104,7 @@ function buildUncheckedSystemDb(db, dbOutsideTransaction, report, gate) {
104
104
  message: "createSystemDbView received a TenantDb with no declared unsafeRaw runner bound.",
105
105
  });
106
106
  }
107
- return sourceRunner(reason, forwardedDeclaredStep);
107
+ return sourceRunner(forwardedDeclaredStep ? reason : undefined);
108
108
  }
109
109
  if (reason.trim().length === 0) {
110
110
  throw new Error("unsafeRaw requires a non-empty reason");
@@ -170,7 +170,14 @@ function buildUncheckedSystemDb(db, dbOutsideTransaction, report, gate) {
170
170
  },
171
171
  },
172
172
  };
173
- declaredUnsafeRawRunners.set(uncheckedSystemDb, grantedUnsafeRawRunner);
173
+ declaredUnsafeRawRunners.set(uncheckedSystemDb, (declaredStepReason) => {
174
+ if (declaredStepReason === undefined) {
175
+ throw new InternalError({
176
+ message: "A declared unsafeRaw runner on a system db requires the step reason.",
177
+ });
178
+ }
179
+ return grantedUnsafeRawRunner(declaredStepReason, true);
180
+ });
174
181
  if (gate?.kind !== "source-tenant-db") {
175
182
  systemDbRebinders.set(uncheckedSystemDb, (grant, deniedCallerLabel) => buildUncheckedSystemDb(withUnsafeRawGrant(db, grant), dbOutsideTransaction && withUnsafeRawGrant(dbOutsideTransaction, grant), report, { kind: "hook-grant", grant, deniedCallerLabel }));
176
183
  }
@@ -426,29 +433,26 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
426
433
  // @cast-boundary type-brand — GlobalWrites is only present in the type when TTable is not ExecutorOnly; the object above always carries the methods.
427
434
  };
428
435
  }
429
- function grantedUnsafeRawRunner(reason, forwardedDeclaredStep) {
430
- if (reason.trim().length === 0) {
431
- throw new Error("unsafeRaw requires a non-empty reason");
432
- }
436
+ function grantedUnsafeRawRunner(declaredStepReason) {
433
437
  // Ahead of the grant check: a declared escapeHatch must not buy a raw runner here either.
434
438
  if (grants?.memberReadOnly) {
435
439
  throw memberResolutionReadOnlyDenied();
436
440
  }
437
441
  if (!hasGrant(grants?.unsafeRaw)) {
438
442
  throw new AccessDeniedError({
439
- message: 'ctx.db.unsafeRaw(reason): rejected — declare `escapeHatch: { reason: "..." }` on ' +
443
+ message: 'ctx.db.unsafeRaw(): rejected — declare `escapeHatch: { reason: "..." }` on ' +
440
444
  "the handler or hook to allow unsafeRaw.",
441
445
  });
442
446
  }
443
- // Engine-forwarded steps carry their own declared reason; a caller-supplied one never reaches the audit trail.
444
- report("unsafe-raw", forwardedDeclaredStep ? reason : (grants?.unsafeRaw?.reason ?? reason));
447
+ // Engine-forwarded steps carry their own declared reason; otherwise the grant's reason is audited.
448
+ report("unsafe-raw", declaredStepReason ?? grants?.unsafeRaw?.reason ?? "");
445
449
  return personalDataGate ? gatedRunner(db, personalDataGate) : db;
446
450
  }
447
451
  const tenantDb = {
448
452
  tenantId,
449
453
  mode,
450
454
  global: globalTable,
451
- unsafeRaw: (reason) => grantedUnsafeRawRunner(reason, false),
455
+ unsafeRaw: () => grantedUnsafeRawRunner(),
452
456
  selectMany(table, where, options) {
453
457
  const filter = readWhere(table, where);
454
458
  return withDbSpan("select", table, async () => bunSelectMany(db, table, filter, options));
@@ -6,6 +6,7 @@ export declare const PII_USER_REFERENCE_NAME_HINTS: ReadonlySet<string>;
6
6
  /** Extensions with preSave must target an entity that has mapped write handlers. */
7
7
  export declare function validateExtensionPreSaveWiring(features: readonly FeatureDefinition[]): void;
8
8
  export declare function validateHandlerAccess(feature: FeatureDefinition): void;
9
+ export declare function validateTenantlessAnonymous(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
9
10
  export declare function validateRateLimitDisabledReason(featureName: string, kind: "write" | "query" | "stream", handlerName: string, rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"]): void;
10
11
  export declare function validateNoAllRoleInHandlerAccess(featureName: string, kind: "write" | "query" | "stream", handlerName: string, access: NonNullable<FeatureDefinition["writeHandlers"][string]["access"]>): void;
11
12
  export declare function validateAnonymousRateLimit(featureName: string, kind: "write" | "query" | "stream", handlerName: string, access: NonNullable<FeatureDefinition["writeHandlers"][string]["access"]>, rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"]): void;
@@ -134,6 +134,25 @@ export function validateHandlerAccess(feature) {
134
134
  validateRateLimitDisabledReason(feature.name, kind, name, handler.rateLimit);
135
135
  }
136
136
  }
137
+ for (const [name, handler] of Object.entries(feature.writeHandlers)) {
138
+ validateTenantlessAnonymous(feature.name, name, handler);
139
+ }
140
+ }
141
+ export function validateTenantlessAnonymous(featureName, handlerName, handler) {
142
+ // skip: handler does not opt into tenantless anonymous calls
143
+ if (!handler.tenantlessAnonymous)
144
+ return;
145
+ const where = `write handler "${featureName}:write:${handlerName}" declares tenantlessAnonymous`;
146
+ const { access } = handler;
147
+ const isAnonymousOnly = "roles" in access && access.roles.length === 1 && access.roles[0] === "anonymous";
148
+ if (!isAnonymousOnly) {
149
+ throw new Error(`${where} but its access is not exactly { roles: ["anonymous"] } — a tenantless request ` +
150
+ `carries no principal beyond the anonymous visitor.`);
151
+ }
152
+ if (!handler.rateLimit || isRateLimitDisabled(handler.rateLimit)) {
153
+ throw new Error(`${where} without a rateLimit — a handler reachable on hosts that resolve no tenant ` +
154
+ `needs a real upper bound (rateLimit: { per: "ip", ... }); { disabled: true } does not count.`);
155
+ }
137
156
  }
138
157
  export function validateRateLimitDisabledReason(featureName, kind, handlerName, rateLimit) {
139
158
  // skip: rate limit isn't disabled, so no reason is required
@@ -71,6 +71,33 @@ function validateWriteOnlyField(featureName, entityName, fieldName, field) {
71
71
  }
72
72
  }
73
73
  }
74
+ // A personal jsonb field is stored as a ciphertext JSON string scalar, so
75
+ // anything that reads inside the column (index, filter, sort, search, jsonb
76
+ // operators/merges such as the custom-fields column) sees only ciphertext.
77
+ const JSONB_PERSONAL_FORBIDDEN_FLAGS = [
78
+ "sortable",
79
+ "filterable",
80
+ "searchable",
81
+ "lookupable",
82
+ ];
83
+ const CUSTOM_FIELDS_COLUMN = "customFields";
84
+ function validatePersonalJsonbField(featureName, entityName, entity, fieldName, field) {
85
+ const where = `[Feature ${featureName}] Field "${fieldName}" on entity "${entityName}"`;
86
+ const reason = "a personal jsonb field is stored as a ciphertext string, so nothing inside it can be indexed, filtered, sorted, searched or merged";
87
+ const flags = field; // @cast-boundary schema-walk
88
+ for (const flag of JSONB_PERSONAL_FORBIDDEN_FLAGS) {
89
+ if (flags[flag] !== undefined && flags[flag] !== false) {
90
+ throw new Error(`${where} combines a personal annotation on a jsonb field with { ${flag} } — ${reason}.`);
91
+ }
92
+ }
93
+ const index = entity.indexes?.find((def) => def.columns.includes(fieldName));
94
+ if (index) {
95
+ throw new Error(`${where} is a personal jsonb field used in an entity index — ${reason}. Drop the index or the personal annotation.`);
96
+ }
97
+ if (fieldName === CUSTOM_FIELDS_COLUMN) {
98
+ throw new Error(`${where} is the custom-fields jsonb column — its values are merged and searched per key, which ciphertext cannot support. Drop the personal annotation.`);
99
+ }
100
+ }
74
101
  // --- PII / Subject-Key Annotations + Retention validation ---
75
102
  //
76
103
  // Drei Klassen von Checks:
@@ -153,6 +180,9 @@ export function validatePiiAndRetention(feature) {
153
180
  throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" combines { sensitive: true } with { searchable: true } — sensitive means ciphertext at rest, never searchable, stripped from event echoes — authorized readers can still read it back (passwords, tokens, tax IDs). Subject-annotated identity fields may be searchable (#1610); sensitive fields may not. To never return the value at all, add { writeOnly: true }.`);
154
181
  }
155
182
  }
183
+ if (field.type === "jsonb" && annotCount > 0) {
184
+ validatePersonalJsonbField(feature.name, entityName, entity, fieldName, field);
185
+ }
156
186
  if (annot.userOwned) {
157
187
  const ownerName = annot.userOwned.ownerField;
158
188
  if (!ownerName || typeof ownerName !== "string") {
@@ -51,6 +51,7 @@ export function defineWriteHandler(def,
51
51
  }),
52
52
  ...(def.rateLimit && { rateLimit: def.rateLimit }),
53
53
  ...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
54
+ ...(def.tenantlessAnonymous && { tenantlessAnonymous: true }),
54
55
  };
55
56
  if ("perform" in def && def.perform !== undefined) {
56
57
  const performDef = def.perform;
@@ -30,7 +30,7 @@ export type TenantUserModel = "single-user" | "multi-user";
30
30
  * fw#2914 — `db` is a tenant-filtered `TenantDb`, bound to `tenantId`
31
31
  * (or the per-user sub-tx in the forget path). Unfiltered access needs
32
32
  * `escapeHatch: { reason }` on the `r.useExtension(...)` registration,
33
- * then `ctx.db.unsafeRaw(reason)`.
33
+ * then `ctx.db.unsafeRaw()`.
34
34
  */
35
35
  /**
36
36
  * Minimal storage surface a file-aware forget hook needs to erase binaries.
@@ -32,6 +32,7 @@ const WRITE_HANDLER_KEY_KINDS = {
32
32
  unsafeSkipTransitionGuard: "modeled",
33
33
  rateLimit: "modeled",
34
34
  escapeHatch: "modeled",
35
+ tenantlessAnonymous: "opaque",
35
36
  perform: "opaque",
36
37
  };
37
38
  const QUERY_HANDLER_KEY_KINDS = {
@@ -106,6 +106,7 @@ export function buildEntityHandlerMethods(state, name) {
106
106
  ...(def.unsafeSkipTransitionGuard && { unsafeSkipTransitionGuard: true }),
107
107
  ...(def.rateLimit && { rateLimit: def.rateLimit }),
108
108
  ...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
109
+ ...(def.tenantlessAnonymous && { tenantlessAnonymous: true }),
109
110
  // Forward the pipeline-build closure so boot-validators and
110
111
  // Designer/AI tooling can inspect the step list. Absent on
111
112
  // free-form handlers — defineWriteHandler only sets `perform`
@@ -30,6 +30,12 @@ export type ManifestExtension = {
30
30
  readonly extensionName: string;
31
31
  readonly entityName: string;
32
32
  };
33
+ /** Framework-owned registered event (`kumiko:system:*`), e.g. the workflow
34
+ * run-stream events a projection can apply. Not owned by any feature. */
35
+ export type ManifestSystemEvent = {
36
+ readonly name: string;
37
+ readonly version: number;
38
+ };
33
39
  export type ManifestFeature = {
34
40
  readonly name: string;
35
41
  readonly description: string | null;
@@ -58,6 +64,7 @@ export type FeatureManifest = {
58
64
  readonly source: string;
59
65
  readonly featureCount: number;
60
66
  readonly features: readonly ManifestFeature[];
67
+ readonly systemEvents?: readonly ManifestSystemEvent[];
61
68
  readonly tier?: string;
62
69
  };
63
70
  export type BuildManifestOptions = {
@@ -5,6 +5,7 @@
5
5
  // die Kopie still. Quelle der Wahrheit ist die GEBOOTETE Registry — der
6
6
  // AST-Parser kann die imperativen Factory-Helper der bundled features nicht
7
7
  // lesen.
8
+ import { SYSTEM_EVENT_PREFIX } from "../crypto/system-event-pii.js";
8
9
  import { compareByCodepoint } from "../utils/index.js";
9
10
  import { isEncryptedAtRest } from "./config-helpers.js";
10
11
  import { qualifyEntityName } from "./qualified-name.js";
@@ -76,10 +77,17 @@ export function buildManifestFromRegistry(registry, options) {
76
77
  });
77
78
  }
78
79
  manifestFeatures.sort((a, b) => compareByCodepoint(a.name, b.name));
80
+ const systemEvents = [];
81
+ for (const [name, info] of registry.getEventUpcasters()) {
82
+ if (name.startsWith(SYSTEM_EVENT_PREFIX))
83
+ systemEvents.push({ name, version: info.currentVersion });
84
+ }
85
+ systemEvents.sort((a, b) => compareByCodepoint(a.name, b.name));
79
86
  return {
80
87
  source: options.source,
81
88
  featureCount: manifestFeatures.length,
82
89
  features: manifestFeatures,
90
+ systemEvents,
83
91
  ...(options.tier !== undefined && { tier: options.tier }),
84
92
  };
85
93
  }
@@ -40,7 +40,7 @@ export { createBigIntField, createBooleanField, createDateField, createDecimalFi
40
40
  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";
41
41
  export { addPattern, applyChanges, createFeaturePatcher, getPatternSchema, groupByCategory, PATTERN_LIBRARY, parseFeatureFile, parsePatternChanges, parseSourceFile, removePattern, renderFeatureFile, renderPattern, replacePattern, updatePattern, VERSION_HEADER, } from "./feature-ast/index.js";
42
42
  export { type ChangelogEntry, type ChangelogType, compareVersions, type FeatureChangelog, filterEntriesAfter, parseFeatureChangelog, sortEntries, validateChangelog, } from "./feature-changelog.js";
43
- export { type BuildManifestOptions, buildManifestFromRegistry, type FeatureManifest, type ManifestConfigKey, type ManifestExtension, type ManifestFeature, type ManifestSecret, serializeManifest, } from "./feature-manifest.js";
43
+ export { type BuildManifestOptions, buildManifestFromRegistry, type FeatureManifest, type ManifestConfigKey, type ManifestExtension, type ManifestFeature, type ManifestSecret, type ManifestSystemEvent, serializeManifest, } from "./feature-manifest.js";
44
44
  export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, maskWriteOnlyFields, } from "./field-access.js";
45
45
  export { resolveName, withResponseData } from "./handler-helpers.js";
46
46
  export { i18nKey } from "./i18n-key.js";
@@ -70,6 +70,7 @@ export { defineTransitions, guardTransition } from "./state-machine.js";
70
70
  export { evaluateEventMatch } from "./steps/_event-match.js";
71
71
  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
72
  export { describeWorkflowStepError } from "./steps/describe-workflow-step-error.js";
73
+ export { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
73
74
  export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
74
75
  export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
75
76
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
@@ -64,6 +64,7 @@ export { defineTransitions, guardTransition } from "./state-machine.js";
64
64
  export { evaluateEventMatch } from "./steps/_event-match.js";
65
65
  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
66
  export { describeWorkflowStepError } from "./steps/describe-workflow-step-error.js";
67
+ export { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
67
68
  export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
68
69
  export { findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, } from "./tier-resolver-extension.js";
69
70
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
@@ -5,6 +5,7 @@ export declare function populateHandlers(state: RegistryState, feature: FeatureD
5
5
  export declare function populateConfigKeys(state: RegistryState, feature: FeatureDefinition): void;
6
6
  export declare function populateJobsAndNotifications(state: RegistryState, feature: FeatureDefinition): void;
7
7
  export declare function populateEvents(state: RegistryState, feature: FeatureDefinition): void;
8
+ export declare function populateSystemEvents(state: RegistryState): void;
8
9
  export declare function populateTranslations(state: RegistryState, feature: FeatureDefinition): void;
9
10
  export declare function populateHooks(state: RegistryState, feature: FeatureDefinition): void;
10
11
  export declare function populateExtensionsAndSeeds(state: RegistryState, feature: FeatureDefinition): void;
@@ -1,6 +1,7 @@
1
1
  import { resolveTableName } from "../db/entity-table-meta.js";
2
2
  import { buildMetricName, validateMetricName } from "../observability/index.js";
3
3
  import { mergeHookList, mergeHookListQualified, qualify } from "./registry-state.js";
4
+ import { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
4
5
  // Feature registration + entities (globally-unique, physical-table-checked) + relations
5
6
  // (additive per entity, duplicate-per-name guarded).
6
7
  export function populateFeatureCore(state, feature) {
@@ -132,6 +133,14 @@ export function populateEvents(state, feature) {
132
133
  state.eventMap.set(qualified, { ...eventDef, name: qualified });
133
134
  }
134
135
  }
136
+ // Framework-owned events outside any feature namespace (kumiko:system:workflow.*).
137
+ // Seeded into the same eventMap as r.defineEvent so apply-key validation, the
138
+ // PII catalog and the upcaster chain see them through one path.
139
+ export function populateSystemEvents(state) {
140
+ for (const [name, eventDef] of WORKFLOW_SYSTEM_EVENT_DEFS) {
141
+ state.eventMap.set(name, eventDef);
142
+ }
143
+ }
135
144
  export function populateTranslations(state, feature) {
136
145
  // Translations prefixed with featureName: (i18next namespace convention).
137
146
  // Keys that already carry the feature's own namespace prefix (e.g. a nav
@@ -1,7 +1,7 @@
1
1
  import { validateExtensionPreSaveWiring } from "./boot-validator/entity-handler.js";
2
2
  import { dedupeFeatures } from "./dedupe-features.js";
3
3
  import { buildRegistryFacade } from "./registry-facade.js";
4
- import { populateClaimsAndAuth, populateConfigKeys, populateEvents, populateExtensionsAndSeeds, populateFeatureCore, populateHandlers, populateHooks, populateJobsAndNotifications, populateMetricsAndSecrets, populateProjectionsAndTables, populateScreensNavWorkspaces, populateTranslations, } from "./registry-ingest.js";
4
+ import { populateClaimsAndAuth, populateConfigKeys, populateEvents, populateExtensionsAndSeeds, populateFeatureCore, populateHandlers, populateHooks, populateJobsAndNotifications, populateMetricsAndSecrets, populateProjectionsAndTables, populateScreensNavWorkspaces, populateSystemEvents, populateTranslations, } from "./registry-ingest.js";
5
5
  import { createInitialState } from "./registry-state.js";
6
6
  import { applyExtensionUsages, autoWireSoftDeleteJobs, buildEventUpcasterChains, buildImplicitProjections, buildIncomingRelations, buildSearchableSortableCaches, computeHasRateLimitedHandler, finalizeWorkspaceNavMembership, populateHandlerEntityMappings, publishEventPiiCatalog, resolveJobTriggers, resolveNotificationTriggersAndRegisterHooks, validateBootGates, validateEntityHookTargets, validateEventMigrationVersions, validateExtensionSelectors, validateExtensionUsageTargets, validateFieldAccessHandlersAreEntityMapped, validateJobBackoff, validateLifecycleHookTargets, validateLiveEntities, validateProjectionApplyKeys, validateRelationTargetsExist, validateRequiredFeatures, } from "./registry-validate.js";
7
7
  // This is where the magic happens. By "magic" I mean: precomputed maps.
@@ -24,6 +24,7 @@ export function createRegistry(rawFeatures) {
24
24
  populateClaimsAndAuth(state, feature);
25
25
  populateScreensNavWorkspaces(state, feature);
26
26
  }
27
+ populateSystemEvents(state);
27
28
  finalizeWorkspaceNavMembership(state);
28
29
  populateHandlerEntityMappings(state, features);
29
30
  validateExtensionSelectors(state);
@@ -54,7 +54,6 @@ defineStep({
54
54
  workflowName: ctx.workflow.workflowName,
55
55
  error: describeWorkflowStepError(error),
56
56
  triggerEventType: ctx.event.type,
57
- triggerPayload: ctx.event.payload,
58
57
  ...(ctx.workflow.definitionFingerprint && {
59
58
  definitionFingerprint: ctx.workflow.definitionFingerprint,
60
59
  }),
@@ -4,7 +4,7 @@
4
4
  // natural key + tenantId). Skips lifecycle hooks, field-access,
5
5
  // crypto-shredding, schema-versioning, audit-trail, read-access-log.
6
6
  // See "Was unsafeProjection.* überspringt" in
7
- // docs/plans/architecture/intern/step-vocabulary.md.
7
+ // kumiko-platform/docs/archive/plans/architecture/intern/step-vocabulary.md.
8
8
  //
9
9
  // Use only on tables explicitly declared via r.requires.projection in
10
10
  // the owning feature. Aggregate-tables (registered via r.entity) are
@@ -49,7 +49,6 @@ defineStep({
49
49
  stepIndex: ctx.workflow.stepIndex,
50
50
  workflowName: ctx.workflow.workflowName,
51
51
  triggerEventType: ctx.event.type,
52
- triggerPayload: ctx.event.payload,
53
52
  ...(ctx.workflow.definitionFingerprint && {
54
53
  definitionFingerprint: ctx.workflow.definitionFingerprint,
55
54
  }),
@@ -32,11 +32,7 @@ defineStep({
32
32
  wakeAt,
33
33
  stepIndex: ctx.workflow.stepIndex,
34
34
  workflowName: ctx.workflow.workflowName,
35
- // Trigger snapshot: pinned so the resume-loop re-feeds the
36
- // pipeline with what the original run saw. event-sourcing across
37
- // suspensions hinges on this being stable.
38
35
  triggerEventType: ctx.event.type,
39
- triggerPayload: ctx.event.payload,
40
36
  ...(ctx.workflow.definitionFingerprint && {
41
37
  definitionFingerprint: ctx.workflow.definitionFingerprint,
42
38
  }),
@@ -0,0 +1,2 @@
1
+ import type { EventDef } from "@cosmicdrift/kumiko-types/handlers";
2
+ export declare const WORKFLOW_SYSTEM_EVENT_DEFS: ReadonlyMap<string, EventDef>;