@cosmicdrift/kumiko-framework 0.249.0 → 0.250.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 (53) hide show
  1. package/package.json +3 -3
  2. package/src/__tests__/full-stack.integration.test.ts +3 -1
  3. package/src/crypto/__tests__/event-pii.test.ts +25 -3
  4. package/src/crypto/__tests__/kms-wiring.test.ts +15 -2
  5. package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +18 -0
  6. package/src/crypto/event-pii.ts +7 -1
  7. package/src/crypto/index.ts +1 -0
  8. package/src/crypto/kms-wiring.ts +23 -10
  9. package/src/db/__tests__/event-store-executor-reference-sort.integration.test.ts +258 -0
  10. package/src/db/__tests__/table-builder-meta-lockstep.test.ts +33 -0
  11. package/src/db/entity-table-meta.ts +9 -2
  12. package/src/db/event-store-executor-read.ts +124 -7
  13. package/src/db/table-builder.ts +10 -0
  14. package/src/engine/__tests__/engine.test.ts +132 -2
  15. package/src/engine/__tests__/event-migration-declarative.test.ts +2 -0
  16. package/src/engine/__tests__/extend-entity-projection.test.ts +15 -5
  17. package/src/engine/__tests__/pipeline-handler.integration.test.ts +1 -1
  18. package/src/engine/entity-handlers.ts +4 -0
  19. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -0
  20. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
  21. package/src/engine/feature-ast/__tests__/parse.test.ts +27 -1
  22. package/src/engine/feature-ast/__tests__/patch.test.ts +1 -0
  23. package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -0
  24. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +56 -3
  25. package/src/engine/feature-ast/extractors/events.ts +24 -0
  26. package/src/engine/feature-ast/patcher.ts +6 -1
  27. package/src/engine/feature-ast/patterns.ts +6 -0
  28. package/src/engine/feature-ast/render.ts +3 -4
  29. package/src/engine/feature-config-events-jobs.ts +33 -9
  30. package/src/engine/pattern-library/__tests__/library.test.ts +1 -0
  31. package/src/engine/registry-facade.ts +10 -1
  32. package/src/engine/registry-state.ts +8 -0
  33. package/src/engine/registry-validate.ts +54 -3
  34. package/src/engine/types/index.ts +1 -0
  35. package/src/entrypoint/__tests__/entrypoint-attach-dispatcher.integration.test.ts +4 -1
  36. package/src/entrypoint/__tests__/split-deploy.integration.test.ts +10 -3
  37. package/src/event-store/__tests__/upcaster.integration.test.ts +8 -1
  38. package/src/jobs/__tests__/job-write-as.integration.test.ts +206 -0
  39. package/src/jobs/job-runner.ts +12 -4
  40. package/src/pipeline/__tests__/archive-stream.integration.test.ts +3 -1
  41. package/src/pipeline/__tests__/causation-chain.integration.test.ts +3 -1
  42. package/src/pipeline/__tests__/domain-events-projections.integration.test.ts +3 -1
  43. package/src/pipeline/__tests__/event-define-event-strict.integration.test.ts +6 -2
  44. package/src/pipeline/__tests__/fetch-for-writing.integration.test.ts +6 -2
  45. package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +3 -1
  46. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +1 -0
  47. package/src/pipeline/__tests__/msp-multi-hop.integration.test.ts +5 -3
  48. package/src/pipeline/__tests__/msp-rebuild.integration.test.ts +3 -0
  49. package/src/pipeline/__tests__/multi-stream-projection.integration.test.ts +2 -0
  50. package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +3 -1
  51. package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +260 -0
  52. package/src/pipeline/__tests__/try-append-event.integration.test.ts +3 -1
  53. package/src/pipeline/system-hooks.ts +94 -25
@@ -131,6 +131,17 @@ export function extractDefineEvent(
131
131
  const v = readDataLiteralNode(versionInit);
132
132
  if (typeof v === "number") version = v;
133
133
  }
134
+ const piiFieldsInit = obj
135
+ .getProperty("piiFields")
136
+ ?.asKind(SyntaxKind.PropertyAssignment)
137
+ ?.getInitializer();
138
+ if (!piiFieldsInit) {
139
+ return fail(
140
+ "defineEvent",
141
+ sourceLocationFromNode(call, sourceFile),
142
+ 'requires an explicit `piiFields` stance (an object literal or "none")',
143
+ );
144
+ }
134
145
  const migrationsInit = obj
135
146
  .getProperty("migrations")
136
147
  ?.asKind(SyntaxKind.PropertyAssignment)
@@ -143,6 +154,7 @@ export function extractDefineEvent(
143
154
  source: sourceLocationFromNode(call, sourceFile),
144
155
  eventName: nameInit.getLiteralValue(),
145
156
  schemaSource: sourceLocationFromNode(schemaInit, sourceFile),
157
+ piiFields: sourceLocationFromNode(piiFieldsInit, sourceFile),
146
158
  ...(version !== undefined && { version }),
147
159
  ...(migrations !== undefined && { migrations }),
148
160
  });
@@ -168,6 +180,17 @@ export function extractDefineEvent(
168
180
  let migrations: Readonly<Record<string, SourceLocation>> | undefined;
169
181
  const optionsArg = args[2];
170
182
  const optionsObj = optionsArg?.asKind(SyntaxKind.ObjectLiteralExpression);
183
+ const piiFieldsInit = optionsObj
184
+ ?.getProperty("piiFields")
185
+ ?.asKind(SyntaxKind.PropertyAssignment)
186
+ ?.getInitializer();
187
+ if (!piiFieldsInit) {
188
+ return fail(
189
+ "defineEvent",
190
+ sourceLocationFromNode(call, sourceFile),
191
+ 'requires an explicit `piiFields` stance (an object literal or "none")',
192
+ );
193
+ }
171
194
  if (optionsObj) {
172
195
  const versionInit = optionsObj
173
196
  .getProperty("version")
@@ -191,6 +214,7 @@ export function extractDefineEvent(
191
214
  eventName: eventNameRef.value,
192
215
  ...(eventNameRef.raw !== undefined && { eventNameRaw: eventNameRef.raw }),
193
216
  schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
217
+ piiFields: sourceLocationFromNode(piiFieldsInit, sourceFile),
194
218
  ...(version !== undefined && { version }),
195
219
  ...(migrations !== undefined && { migrations }),
196
220
  });
@@ -147,6 +147,10 @@ export type AddHttpRouteArgs = {
147
147
  export type AddDefineEventArgs = {
148
148
  readonly name: string;
149
149
  readonly schemaSource: string;
150
+ // Raw source of the `piiFields` option value (e.g. `'"none"'` or an object
151
+ // literal) — mandatory, mirroring r.defineEvent's own required stance
152
+ // (fw#2558).
153
+ readonly piiFieldsSource: string;
150
154
  readonly version?: number;
151
155
  // Keyed by fromVersion (as a string) → the transform source text for the
152
156
  // fromVersion -> fromVersion+1 step. Folded in from the former
@@ -496,7 +500,7 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
496
500
  });
497
501
  },
498
502
 
499
- addDefineEvent({ name, schemaSource, version, migrations }) {
503
+ addDefineEvent({ name, schemaSource, piiFieldsSource, version, migrations }) {
500
504
  const migrationLocs = migrations
501
505
  ? Object.fromEntries(
502
506
  Object.entries(migrations).map(([fromVersion, source]) => [
@@ -510,6 +514,7 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
510
514
  source: SYNTHETIC_LOC,
511
515
  eventName: name,
512
516
  schemaSource: rawLoc(schemaSource),
517
+ piiFields: rawLoc(piiFieldsSource),
513
518
  ...(version !== undefined && { version }),
514
519
  ...(migrationLocs !== undefined && { migrations: migrationLocs }),
515
520
  });
@@ -561,6 +561,12 @@ export type DefineEventPattern = {
561
561
  readonly eventNameRaw?: string;
562
562
  readonly schemaSource: SourceLocation;
563
563
  readonly version?: number;
564
+ // Raw source of the `piiFields` option value ("none", an object literal,
565
+ // or a constant reference) — mandatory because `r.defineEvent` itself
566
+ // requires an explicit PII stance (fw#2558); round-tripping it verbatim
567
+ // avoids the extractor ever having to invent a stance the author didn't
568
+ // author.
569
+ readonly piiFields: SourceLocation;
564
570
  // Map fromVersion (as string, e.g. "1") → SourceLocation of the transform
565
571
  // closure for the fromVersion → fromVersion+1 step.
566
572
  readonly migrations?: Readonly<Record<string, SourceLocation>>;
@@ -596,11 +596,10 @@ function renderMultiStreamProjection(p: MultiStreamProjectionPattern): string {
596
596
  function renderDefineEvent(p: DefineEventPattern): string {
597
597
  const nameLiteral = p.eventNameRaw ?? JSON.stringify(p.eventName);
598
598
  const migrationEntries = p.migrations !== undefined ? Object.entries(p.migrations) : [];
599
- const hasOptions = p.version !== undefined || migrationEntries.length > 0;
600
- if (!hasOptions) {
601
- return `r.defineEvent(${nameLiteral}, ${p.schemaSource.raw});`;
602
- }
599
+ // piiFields is a mandatory option (fw#2558) — the options object is
600
+ // always rendered, unlike the old version/migrations-only shortcut.
603
601
  const lines: string[] = [`r.defineEvent(${nameLiteral}, ${p.schemaSource.raw}, {`];
602
+ lines.push(` piiFields: ${p.piiFields.raw},`);
604
603
  if (p.version !== undefined) lines.push(` version: ${p.version},`);
605
604
  if (migrationEntries.length > 0) {
606
605
  lines.push(" migrations: [");
@@ -14,6 +14,7 @@ import type {
14
14
  DeclarativeEventMigration,
15
15
  EventDef,
16
16
  EventPiiFields,
17
+ EventPiiStance,
17
18
  EventUpcastFn,
18
19
  JobDefinition,
19
20
  JobHandlerFn,
@@ -217,9 +218,9 @@ export function buildConfigEventsJobsMethods<TName extends string>(
217
218
  defineEvent: <const TInner extends string, TPayload>(
218
219
  eventName: TInner,
219
220
  schema: ZodType<TPayload>,
220
- options?: {
221
+ options: {
221
222
  readonly version?: number;
222
- readonly piiFields?: EventPiiFields;
223
+ readonly piiFields: EventPiiStance;
223
224
  // Step-wise upcast chain for this event, folded in from the former
224
225
  // standalone r.eventMigration() call (#1082 step 8) — an event and
225
226
  // its schema evolution are one lifecycle, not two registrar
@@ -232,6 +233,33 @@ export function buildConfigEventsJobsMethods<TName extends string>(
232
233
  }[];
233
234
  },
234
235
  ): EventDef<TPayload, QualifiedEventName<TName, TInner>> => {
236
+ // Fail-closed PII stance gate (fw#2558) — runs before anything else so
237
+ // an untyped JS consumer (no compiler to stop it) still can't register
238
+ // an event without declaring whether its payload carries personal data.
239
+ const missingStanceError = () =>
240
+ new Error(
241
+ `[Feature ${name}] defineEvent("${eventName}") must declare an explicit PII stance. Pass { piiFields: { <payloadField>: { subjectField: "<userIdField>" } } } for payload fields holding personal data, or { piiFields: "none" } when the payload holds none.`,
242
+ );
243
+ if (options === undefined || options.piiFields === undefined) {
244
+ throw missingStanceError();
245
+ }
246
+ const piiFields = options.piiFields;
247
+ // @cast-boundary runtime-guard — an untyped JS caller can hand
248
+ // anything through `piiFields` (including null), so re-check the
249
+ // shape before trusting it as EventPiiStance: `typeof null === "object"`
250
+ // would otherwise pass through to Object.keys(null) below and throw a
251
+ // raw TypeError instead of this guidance message.
252
+ if (piiFields !== "none" && (typeof piiFields !== "object" || piiFields === null)) {
253
+ throw missingStanceError();
254
+ }
255
+ if (piiFields !== "none" && Object.keys(piiFields).length === 0) {
256
+ throw new Error(
257
+ `[Feature ${name}] defineEvent("${eventName}"): piiFields: {} is not a stance — use piiFields: "none" to declare the payload holds no personal data.`,
258
+ );
259
+ }
260
+ if (piiFields !== "none") {
261
+ validateEventPiiFields(eventName, schema, piiFields);
262
+ }
235
263
  // Return the fully-qualified event name so callers can pass it
236
264
  // straight to ctx.appendEvent without hand-building the
237
265
  // "<feature>:event:<name>" shape. Registry keeps events keyed by
@@ -243,16 +271,12 @@ export function buildConfigEventsJobsMethods<TName extends string>(
243
271
  // returned `name` carries the literal qualified shape that the
244
272
  // augmented `KumikoEventTypeMap` keys against.
245
273
  const qualified = qn(toKebab(name), "event", toKebab(eventName));
246
- const version = options?.version ?? 1;
274
+ const version = options.version ?? 1;
247
275
  if (!Number.isInteger(version) || version < 1) {
248
276
  throw new Error(
249
277
  `[Feature ${name}] defineEvent("${eventName}"): version must be a positive integer, got ${String(version)}`,
250
278
  );
251
279
  }
252
- const piiFields = options?.piiFields;
253
- if (piiFields) {
254
- validateEventPiiFields(eventName, schema, piiFields);
255
- }
256
280
  // @cast-boundary engine-bridge — runtime-string mirrors the
257
281
  // template-literal-type via QualifiedEventName + toKebab. Both
258
282
  // sides are tested (CamelToKebab type-tests + scan-events kebab
@@ -261,10 +285,10 @@ export function buildConfigEventsJobsMethods<TName extends string>(
261
285
  name: qualified as QualifiedEventName<TName, TInner>,
262
286
  schema,
263
287
  version,
264
- ...(piiFields !== undefined && { piiFields }),
288
+ piiFields,
265
289
  };
266
290
  state.events[eventName] = def;
267
- for (const m of options?.migrations ?? []) {
291
+ for (const m of options.migrations ?? []) {
268
292
  registerEventMigration(eventName, m.fromVersion, m.toVersion, m.transform);
269
293
  }
270
294
  return def;
@@ -333,6 +333,7 @@ function makePlaceholderPattern(kind: FeaturePatternKind): FeaturePattern {
333
333
  source: PLACEHOLDER_LOC,
334
334
  eventName: "x",
335
335
  schemaSource: PLACEHOLDER_BODY_LOC,
336
+ piiFields: PLACEHOLDER_BODY_LOC,
336
337
  };
337
338
  case "extendsRegistrar":
338
339
  return {
@@ -1,4 +1,9 @@
1
- import type { IncomingRelation, RegistryState, SearchableReferenceField } from "./registry-state";
1
+ import type {
2
+ IncomingRelation,
3
+ RegistryState,
4
+ SearchableReferenceField,
5
+ SortableReferenceField,
6
+ } from "./registry-state";
2
7
  import { filterByPhase, filterOwned } from "./registry-state";
3
8
  import type {
4
9
  AuthClaimsHookDef,
@@ -101,6 +106,10 @@ export function buildRegistryFacade(state: RegistryState): Registry {
101
106
  return state.searchableReferencesCache.get(entityName) ?? [];
102
107
  },
103
108
 
109
+ getSortableReferences(entityName: string): readonly SortableReferenceField[] {
110
+ return state.sortableReferencesCache.get(entityName) ?? [];
111
+ },
112
+
104
113
  getIncomingRelations(entityName: string): readonly IncomingRelation[] {
105
114
  return state.incomingRelationsCache.get(entityName) ?? [];
106
115
  },
@@ -60,6 +60,12 @@ export type SearchableReferenceField = {
60
60
  readonly labelField: string;
61
61
  };
62
62
 
63
+ // One entry per `ReferenceFieldDef` with `sortable: true` (fw#2741). Same
64
+ // resolved shape as the searchable variant and the same boot guarantee on
65
+ // `labelField` — ORDER BY on the UUID column would look meaningful and be
66
+ // arbitrary.
67
+ export type SortableReferenceField = SearchableReferenceField;
68
+
63
69
  const IMPLICIT_PROJECTION_SUFFIX = "-entity" as const;
64
70
 
65
71
  // Pro r.entity-Registration eine ImplicitProjection mit auto-generierten
@@ -235,6 +241,7 @@ export type RegistryState = {
235
241
  searchableFieldsCache: Map<string, readonly string[]>;
236
242
  sortableFieldsCache: Map<string, readonly string[]>;
237
243
  searchableReferencesCache: Map<string, readonly SearchableReferenceField[]>;
244
+ sortableReferencesCache: Map<string, readonly SortableReferenceField[]>;
238
245
  incomingRelationsCache: Map<string, IncomingRelation[]>;
239
246
  hasRateLimitedHandlerCached: boolean;
240
247
  };
@@ -298,6 +305,7 @@ export function createInitialState(): RegistryState {
298
305
  searchableFieldsCache: new Map(),
299
306
  sortableFieldsCache: new Map(),
300
307
  searchableReferencesCache: new Map(),
308
+ sortableReferencesCache: new Map(),
301
309
  incomingRelationsCache: new Map(),
302
310
  hasRateLimitedHandlerCached: false,
303
311
  };
@@ -1,6 +1,10 @@
1
1
  import { configureEventPiiCatalog } from "../crypto/event-pii";
2
2
  import { resolveName } from "./handler-helpers";
3
- import type { RegistryState, SearchableReferenceField } from "./registry-state";
3
+ import type {
4
+ RegistryState,
5
+ SearchableReferenceField,
6
+ SortableReferenceField,
7
+ } from "./registry-state";
4
8
  import { buildImplicitProjection, hasFieldAccessRules, qualify } from "./registry-state";
5
9
  import {
6
10
  buildSoftDeleteCleanupJob,
@@ -192,12 +196,43 @@ function buildSearchableReferenceField(
192
196
  };
193
197
  }
194
198
 
195
- // Precompute: searchable/sortable fields, searchable reference fields.
199
+ // fw#2741: ORDER BY runs against the referenced row's label column, so the
200
+ // same explicit-labelField requirement as searchable applies — ordering by the
201
+ // "id" default would be a UUID order that looks deliberate to the user.
202
+ function buildSortableReferenceField(
203
+ entityName: string,
204
+ fieldName: string,
205
+ field: ReferenceFieldDef,
206
+ ): SortableReferenceField {
207
+ if (field.labelField === undefined || field.labelField === "id") {
208
+ throw new Error(
209
+ `[Entity ${entityName}] field "${fieldName}": sortable reference fields require an ` +
210
+ `explicit, non-"id" labelField. Ordering by "id" is a UUID order — arbitrary to the ` +
211
+ `user while looking deliberate. Set labelField to a human-readable field on the ` +
212
+ `referenced entity.`,
213
+ );
214
+ }
215
+ if (field.multiple === true) {
216
+ throw new Error(
217
+ `[Entity ${entityName}] field "${fieldName}": sortable is not supported on multiple ` +
218
+ `(array) reference fields — there is no single label to order by; remove "multiple" ` +
219
+ `or "sortable".`,
220
+ );
221
+ }
222
+ return {
223
+ fieldName,
224
+ targetEntityName: parseReferenceTargetEntityName(field.entity),
225
+ labelField: field.labelField,
226
+ };
227
+ }
228
+
229
+ // Precompute: searchable/sortable fields, searchable + sortable reference fields.
196
230
  export function buildSearchableSortableCaches(state: RegistryState): void {
197
231
  for (const [name, entity] of state.entityMap) {
198
232
  const searchable: string[] = [];
199
233
  const sortable: string[] = [];
200
234
  const searchableReferences: SearchableReferenceField[] = [];
235
+ const sortableReferences: SortableReferenceField[] = [];
201
236
  for (const [fieldName, field] of Object.entries(entity.fields)) {
202
237
  if (field.type === "text" && field.searchable === true) searchable.push(fieldName);
203
238
  if (field.type === "text" && field.sortable === true) sortable.push(fieldName);
@@ -209,10 +244,14 @@ export function buildSearchableSortableCaches(state: RegistryState): void {
209
244
  if (field.type === "reference" && field.searchable === true) {
210
245
  searchableReferences.push(buildSearchableReferenceField(name, fieldName, field));
211
246
  }
247
+ if (field.type === "reference" && field.sortable === true) {
248
+ sortableReferences.push(buildSortableReferenceField(name, fieldName, field));
249
+ }
212
250
  }
213
251
  state.searchableFieldsCache.set(name, searchable);
214
252
  state.sortableFieldsCache.set(name, sortable);
215
253
  state.searchableReferencesCache.set(name, searchableReferences);
254
+ state.sortableReferencesCache.set(name, sortableReferences);
216
255
  }
217
256
  }
218
257
 
@@ -664,9 +703,21 @@ export function publishEventPiiCatalog(state: RegistryState): void {
664
703
  // into kumiko_events — encrypts catalogued payload fields regardless of
665
704
  // which path produced the event (ctx.appendEvent, MSP-apply, low-level
666
705
  // append in delivery/jobs loggers).
706
+ //
707
+ // Defense-in-depth (fw#2558): defineEvent is the only producer of
708
+ // EventDef.piiFields and already fails closed without a stance, but this
709
+ // boot gate re-checks every registered event — a future producer that
710
+ // forgets to run through defineEvent would otherwise leave a hole where
711
+ // append() silently ships plaintext instead of refusing to boot.
667
712
  const eventPiiCatalog = new Map<string, EventPiiFields>();
668
713
  for (const [qualified, def] of state.eventMap) {
669
- if (def.piiFields) eventPiiCatalog.set(qualified, def.piiFields);
714
+ if (def.piiFields === undefined) {
715
+ throw new Error(
716
+ `Registered event "${qualified}" has no PII stance. Every r.defineEvent(...) call must pass { piiFields: { ... } } or { piiFields: "none" }.`,
717
+ );
718
+ }
719
+ if (def.piiFields === "none") continue;
720
+ eventPiiCatalog.set(qualified, def.piiFields);
670
721
  }
671
722
  configureEventPiiCatalog(eventPiiCatalog);
672
723
  }
@@ -158,6 +158,7 @@ export type {
158
158
  EventDef,
159
159
  EventMigrationDef,
160
160
  EventPiiFields,
161
+ EventPiiStance,
161
162
  EventUpcastCtx,
162
163
  EventUpcastFn,
163
164
  FetchForWritingArgs,
@@ -25,7 +25,10 @@ const writeProbeResults: Array<{ isSuccess: boolean }> = [];
25
25
  const writeProbeFailures: string[] = [];
26
26
 
27
27
  const writeProbeFeature = defineFeature("writeProbe", (r) => {
28
- const noted = r.defineEvent("noted", z.object({ note: z.string() }), { version: 1 });
28
+ const noted = r.defineEvent("noted", z.object({ note: z.string() }), {
29
+ piiFields: "none",
30
+ version: 1,
31
+ });
29
32
  r.writeHandler(
30
33
  "note",
31
34
  z.object({ note: z.string() }),
@@ -23,7 +23,10 @@ import { createTestRedis, type TestRedis, TestUsers } from "../../stack";
23
23
  import { createAllInOneEntrypoint, createApiEntrypoint, createWorkerEntrypoint } from "../index";
24
24
 
25
25
  const splitFeature = defineFeature("split", (r) => {
26
- const tick = r.defineEvent("tick", z.object({ note: z.string() }), { version: 1 });
26
+ const tick = r.defineEvent("tick", z.object({ note: z.string() }), {
27
+ piiFields: "none",
28
+ version: 1,
29
+ });
27
30
  r.multiStreamProjection({
28
31
  name: "spy",
29
32
  apply: {
@@ -38,7 +41,10 @@ const splitFeature = defineFeature("split", (r) => {
38
41
  const consumedNotes: string[] = [];
39
42
 
40
43
  const workerWriteFeature = defineFeature("workerWrite", (r) => {
41
- const noted = r.defineEvent("noted", z.object({ note: z.string() }), { version: 1 });
44
+ const noted = r.defineEvent("noted", z.object({ note: z.string() }), {
45
+ piiFields: "none",
46
+ version: 1,
47
+ });
42
48
  r.writeHandler(
43
49
  "note",
44
50
  z.object({ note: z.string() }),
@@ -76,6 +82,7 @@ const jobSawFilesRef: string[] = [];
76
82
 
77
83
  const fileJobFeature = defineFeature("fileJob", (r) => {
78
84
  const requested = r.defineEvent("bytes-requested", z.object({ storageKey: z.string() }), {
85
+ piiFields: "none",
79
86
  version: 1,
80
87
  });
81
88
  // Stands in for file-foundation, which this package cannot import.
@@ -310,7 +317,7 @@ describe("entrypoint factories", () => {
310
317
  // process's lane are skipped during buildServer()".
311
318
 
312
319
  const laneFeature = defineFeature("lane", (r) => {
313
- const ping = r.defineEvent("ping", z.object({}), { version: 1 });
320
+ const ping = r.defineEvent("ping", z.object({}), { piiFields: "none", version: 1 });
314
321
  // Three MSPs: one pinned to api, one to worker (explicit), one to both.
315
322
  // A fourth would be a default-undefined-runIn MSP which resolves to
316
323
  // "worker" — covered implicitly by the worker test below.
@@ -57,6 +57,7 @@ const orderFeature = defineFeature("upcastshop", (r) => {
57
57
  "priced",
58
58
  z.object({ totalCents: z.number().int(), currency: z.string() }),
59
59
  {
60
+ piiFields: "none",
60
61
  version: 3,
61
62
  migrations: [
62
63
  // v1 → v2: renamed totalEuros → total (kept as string for this step)
@@ -321,6 +322,7 @@ describe("upcaster: async (Marten AsyncOnlyEventUpcaster — DB-Lookups)", () =>
321
322
  "placed",
322
323
  z.object({ customerId: z.string(), segment: z.string() }),
323
324
  {
325
+ piiFields: "none",
324
326
  version: 2,
325
327
  migrations: [
326
328
  {
@@ -407,6 +409,7 @@ describe("upcaster: boot-time validation", () => {
407
409
  r.entity("hole-order", orderEntity);
408
410
  // Only 1→2 registered — the 2→3 gap must be rejected.
409
411
  r.defineEvent("bad", z.object({ v3: z.string() }), {
412
+ piiFields: "none",
410
413
  version: 3,
411
414
  migrations: [{ fromVersion: 1, toVersion: 2, transform: (p) => p }],
412
415
  });
@@ -424,6 +427,7 @@ describe("upcaster: boot-time validation", () => {
424
427
  const future = defineFeature("future", (r) => {
425
428
  r.entity("future-order", orderEntity);
426
429
  r.defineEvent("early", z.object({ x: z.number() }), {
430
+ piiFields: "none",
427
431
  version: 1,
428
432
  migrations: [{ fromVersion: 1, toVersion: 2, transform: (p) => p }],
429
433
  });
@@ -435,6 +439,7 @@ describe("upcaster: boot-time validation", () => {
435
439
  const gaps = defineFeature("gaps", (r) => {
436
440
  r.entity("gap-order", orderEntity);
437
441
  r.defineEvent("jumpy", z.object({ v: z.number() }), {
442
+ piiFields: "none",
438
443
  version: 4,
439
444
  migrations: [
440
445
  { fromVersion: 1, toVersion: 2, transform: (p) => p },
@@ -452,6 +457,7 @@ describe("upcaster: registrar input validation", () => {
452
457
  defineFeature("bigstep", (r) => {
453
458
  r.entity("bigstep-order", orderEntity);
454
459
  r.defineEvent("biz", z.object({ x: z.number() }), {
460
+ piiFields: "none",
455
461
  version: 3,
456
462
  migrations: [{ fromVersion: 1, toVersion: 3, transform: (p) => p }],
457
463
  });
@@ -464,6 +470,7 @@ describe("upcaster: registrar input validation", () => {
464
470
  defineFeature("dupestep", (r) => {
465
471
  r.entity("dup-order", orderEntity);
466
472
  r.defineEvent("dup", z.object({ x: z.number() }), {
473
+ piiFields: "none",
467
474
  version: 2,
468
475
  migrations: [
469
476
  { fromVersion: 1, toVersion: 2, transform: (p) => p },
@@ -478,7 +485,7 @@ describe("upcaster: registrar input validation", () => {
478
485
  expect(() =>
479
486
  defineFeature("badver", (r) => {
480
487
  r.entity("badver-order", orderEntity);
481
- r.defineEvent("neg", z.object({ x: z.number() }), { version: 0 });
488
+ r.defineEvent("neg", z.object({ x: z.number() }), { piiFields: "none", version: 0 });
482
489
  }),
483
490
  ).toThrow(/positive integer/);
484
491
  });