@cosmicdrift/kumiko-types 0.289.0 → 0.291.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-types",
3
- "version": "0.289.0",
3
+ "version": "0.291.0",
4
4
  "description": "Framework-Type-Definitions für Kumiko — FeatureDefinition, BootCheck-Types und die reinen Engine-Types. Erlaubt Downstream-Konsumenten, gegen die Type-Contracts zu bauen, ohne das ganze Framework-Package zu importieren. Enthaelt keine identitaets-sensitiven Runtime-Werte mehr (Error-Klassen leben seit #1629 in kumiko-framework, Brand-Symbole nutzen Symbol.for) und ist deshalb eine plain dependency, keine peerDependency.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.290.0",
4
+ "type": "improvement",
5
+ "title": "EventMetadata: feature + handler + UNATTRIBUTED_ORIGIN",
6
+ "detail": "EventMetadata traegt zwei neue optionale Felder (feature, handler) und exportiert die Sentinel-Konstante UNATTRIBUTED_ORIGIN fuer Appends ausserhalb eines attribuierten Ausfuehrungsscopes. Additiv, bestehende Zeilen bleiben ohne Backfill lesbar."
7
+ },
2
8
  {
3
9
  "version": "0.289.0",
4
10
  "type": "improvement",
package/src/config.ts CHANGED
@@ -394,6 +394,22 @@ export type JobDefinition = {
394
394
  readonly runIn?: JobRunIn | undefined;
395
395
  // Grants `ctx.db.unsafeRaw(reason)` for this job, audited as `unsafe-raw`.
396
396
  readonly escapeHatch?: EscapeHatchDeclaration | undefined;
397
+ // Opt in to a tenant-visible failure record (`jobs:query:failures`). Only a
398
+ // translation key ever reaches the tenant: a thrown KumikoError's `i18nKey`
399
+ // when it carries one, otherwise `messageKey`. The provider's own message
400
+ // stays in the run log. Only the last attempt records, and a later
401
+ // successful run of the same job and subject clears the record.
402
+ // `subjectFields` names payload fields that scope the record (e.g.
403
+ // `["campaignId", "language"]`); each must hold a primitive or the run
404
+ // throws. Omit it for one record per job and tenant. Their values are
405
+ // stored in clear, unlike the encrypted run payload — never name a PII
406
+ // field here.
407
+ readonly tenantVisibleFailure?:
408
+ | {
409
+ readonly messageKey: string;
410
+ readonly subjectFields?: readonly string[] | undefined;
411
+ }
412
+ | undefined;
397
413
  };
398
414
 
399
415
  // --- Notifications ---
@@ -37,8 +37,21 @@ export type EventMetadata = {
37
37
  // app reads them when filtering/auditing. Keep values JSON-primitive
38
38
  // (string|number|boolean) so JSON serialization stays bulletproof.
39
39
  readonly headers?: Readonly<Record<string, string | number | boolean>>;
40
+ // Feature that owned the execution scope which wrote this event, and the
41
+ // qualified name of the handler/consumer/job inside it. Derived in
42
+ // event-store.append() from the ambient request scope, never passed in by
43
+ // the writer — attribution handed over can lie, attribution derived cannot.
44
+ // Optional so rows written before #3043 stay readable without a backfill;
45
+ // every new row carries both, falling back to UNATTRIBUTED_ORIGIN.
46
+ readonly feature?: string;
47
+ readonly handler?: string;
40
48
  };
41
49
 
50
+ // Stamped when an append runs outside any attributed scope (seed scripts,
51
+ // tests, direct event-store use). A distinct value, not an omission —
52
+ // omitted would be indistinguishable from a pre-#3043 row.
53
+ export const UNATTRIBUTED_ORIGIN = "unknown";
54
+
42
55
  // Generic over the payload shape. Default = Record<string, unknown> keeps
43
56
  // all existing consumers backwards-compatible; annotate `StoredEvent<MyEventPayload>`
44
57
  // where a typed payload read is needed.
package/src/feature.ts CHANGED
@@ -617,6 +617,12 @@ export type FeatureRegistrar<TFeature extends string = string> = {
617
617
  // personal data (encrypted under that user's DEK, crypto-shredding #799),
618
618
  // or `"none"` to declare the payload holds no personal data. append()
619
619
  // enforces the resulting catalog on every write path.
620
+ //
621
+ // When the schema lets the owner field be null/undefined, the stance must
622
+ // also say what happens on those writes — `whenAbsent: "tenant"` encrypts
623
+ // under the envelope tenant key, `whenAbsent: "plaintext"` acknowledges an
624
+ // unshreddable value. Registration fails without one, and an owner that is
625
+ // empty at append time with no declared fallback aborts the write (fw#2776).
620
626
  defineEvent<const TInner extends string, TPayload>(
621
627
  name: TInner,
622
628
  schema: ZodType<TPayload>,
package/src/fields.ts CHANGED
@@ -445,12 +445,16 @@ export type DecimalFieldDef = {
445
445
  readonly access?: FieldAccess;
446
446
  } & ResolvedPiiFlags;
447
447
 
448
- /** Where a money field's currency comes from, for the fields that opt in.
449
- * Discriminated union so #2839 can add `{ kind: "literal", code }` and
450
- * `{ kind: "field", of }` variants without a breaking change. `"tenant"` is
451
- * the tenant-settings bundle's per-tenant currency (`tenant-settings:config:
452
- * currency`) — see kumiko-framework#2933. */
453
- export type MoneyCurrencySource = { readonly kind: "tenant" };
448
+ /** Where a money field's currency comes from. `"tenant"` is the
449
+ * tenant-settings bundle's per-tenant currency (`tenant-settings:config:
450
+ * currency`, kumiko-framework#2933); `"literal"` names a fixed ISO code and
451
+ * is checked against the app's `currencies` list at boot
452
+ * (kumiko-framework#2839). Discriminated union so a `{ kind: "field", of }`
453
+ * variant (amount and currency-picker side by side) can still be added
454
+ * without a breaking change, once a multi-currency form actually exists. */
455
+ export type MoneyCurrencySource =
456
+ | { readonly kind: "tenant" }
457
+ | { readonly kind: "literal"; readonly code: string };
454
458
 
455
459
  /** Qualified config key a `{ kind: "tenant" }` MoneyCurrencySource resolves
456
460
  * against — the tenant-settings bundle's per-tenant currency. Lives here
@@ -513,6 +517,25 @@ export type ReferenceFieldDef = {
513
517
  * boot validator rejects it otherwise, since ordering by a UUID produces a
514
518
  * sequence that looks meaningful to the user but is arbitrary. */
515
519
  readonly sortable?: true;
520
+ /** Fill the picker from this query handler instead of the referenced
521
+ * entity's own list handler (fw#2780) — for targets whose identity is
522
+ * composed from joined rows, where no single column of the referenced
523
+ * entity is a readable label. Fully-qualified QN
524
+ * ("<feature>:query:<short>"), boot-validated against the registered
525
+ * handlers; result contract `{ rows: { id, label }[] }`, called with
526
+ * `{ limit, search? }` like the default list handler. `id` rather than the
527
+ * `{ value, label }` of `DashboardFilterDefinition.optionsQuery`, because
528
+ * the stored value here IS the referenced row's id.
529
+ *
530
+ * Additive to `labelField`, which keeps serving the paths a query handler
531
+ * cannot back: list cells, `searchable` and `sortable` all resolve to an
532
+ * SQL column on the referenced table.
533
+ *
534
+ * The handler must return the row for an id that is already stored, even
535
+ * when it otherwise curates its result (say, to active records only) —
536
+ * the read-only display resolves a stored value against these rows and
537
+ * falls back to the raw id for one it does not find. */
538
+ readonly optionsQuery?: string;
516
539
  } & ResolvedPiiFlags;
517
540
 
518
541
  // --- Currency ---
@@ -568,6 +591,8 @@ export type EmbeddedSubFieldDef =
568
591
  readonly type: "reference";
569
592
  readonly entity: string;
570
593
  readonly labelField?: string;
594
+ /** Same contract as `ReferenceFieldDef.optionsQuery` (fw#2780). */
595
+ readonly optionsQuery?: string;
571
596
  });
572
597
 
573
598
  /** A cell computed from other cells of the same row. `from` names the
@@ -811,6 +836,16 @@ export type FieldDefinition =
811
836
  | FilesFieldDef
812
837
  | ImagesFieldDef;
813
838
 
839
+ /** FieldDefinition as an entity-less inline form screen (actionForm,
840
+ * secretMint) may declare it: a `money` field there has no entity to borrow
841
+ * `defaultCurrency` from, so it must name its own currency source, or the
842
+ * form seeds a bare `0` that the handler's zod schema then rejects
843
+ * (kumiko-framework#2839). Entity fields keep the optional `currency` —
844
+ * `entity.defaultCurrency` is already boot-enforced for them. */
845
+ export type FormFieldDefinition =
846
+ | Exclude<FieldDefinition, MoneyFieldDef>
847
+ | (MoneyFieldDef & { readonly currency: MoneyCurrencySource });
848
+
814
849
  // Union of all field variants that represent uploaded files. They share
815
850
  // `maxSize` and `accept`, which is what upload validation cares about.
816
851
  export type AnyFileFieldDef = FileFieldDef | ImageFieldDef | FilesFieldDef | ImagesFieldDef;
package/src/handlers.ts CHANGED
@@ -832,9 +832,7 @@ export type QualifiedEventName<
832
832
  > = `${CamelToKebab<TFeature>}:event:${CamelToKebab<TInner>}`;
833
833
 
834
834
  // PII payload fields on a custom event (#799): `field` is encrypted under
835
- // the DEK the subject spec names (crypto-shredding). A null owner-field
836
- // value leaves a `personal: { of }` field plaintext — there is no user key
837
- // to shred for system-triggered events.
835
+ // the DEK the subject spec names (crypto-shredding).
838
836
  //
839
837
  // `{ personal: { of: "<ownerField>" } }` is the canonical user-subject form,
840
838
  // matching the entity-field `personal` vocabulary (see fields.ts).
@@ -842,8 +840,21 @@ export type QualifiedEventName<
842
840
  // (envelope.tenantId) or its own aggregate stream (envelope.aggregateType +
843
841
  // aggregateId) as the subject instead — resolved via `resolveEventSubject`
844
842
  // in `packages/framework/src/crypto/subject-resolver.ts` (fw#2801).
843
+ //
844
+ // `whenAbsent` decides what happens when the named owner field carries no
845
+ // id (system- or cron-triggered writes): "tenant" re-targets the envelope
846
+ // tenant key, "plaintext" is an explicit acknowledgement that the value
847
+ // ships unencrypted and cannot be crypto-shredded. Undeclared is not a
848
+ // third option — the append fails closed instead of leaking (fw#2776).
849
+ export type EventPiiAbsentSubject = "plaintext" | "tenant";
850
+
845
851
  export type EventPiiSubject =
846
- | { readonly personal: { readonly of: string } }
852
+ | {
853
+ readonly personal: {
854
+ readonly of: string;
855
+ readonly whenAbsent?: EventPiiAbsentSubject;
856
+ };
857
+ }
847
858
  | { readonly personal: "tenant" }
848
859
  | { readonly personal: "self" }
849
860
  | {
@@ -854,15 +865,21 @@ export type EventPiiSubject =
854
865
  export type EventPiiFields = Readonly<Record<string, EventPiiSubject>>;
855
866
 
856
867
  export type NormalizedEventPiiSubject =
857
- | { readonly kind: "user"; readonly ownerField: string }
868
+ | {
869
+ readonly kind: "user";
870
+ readonly ownerField: string;
871
+ readonly whenAbsent: EventPiiAbsentSubject | undefined;
872
+ }
858
873
  | { readonly kind: "tenant" }
859
874
  | { readonly kind: "self" };
860
875
 
861
876
  export function normalizeEventPiiSubject(spec: EventPiiSubject): NormalizedEventPiiSubject {
862
- if ("subjectField" in spec) return { kind: "user", ownerField: spec.subjectField };
877
+ if ("subjectField" in spec) {
878
+ return { kind: "user", ownerField: spec.subjectField, whenAbsent: undefined };
879
+ }
863
880
  if (spec.personal === "tenant") return { kind: "tenant" };
864
881
  if (spec.personal === "self") return { kind: "self" };
865
- return { kind: "user", ownerField: spec.personal.of };
882
+ return { kind: "user", ownerField: spec.personal.of, whenAbsent: spec.personal.whenAbsent };
866
883
  }
867
884
 
868
885
  // The full set of PII stances a defineEvent() call may declare. "none" is a
package/src/screen.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { FieldIconKey } from "./field-icon";
2
- import type { FieldDefinition } from "./fields";
2
+ import type { FieldDefinition, FormFieldDefinition } from "./fields";
3
3
  import type { AccessRule, AgentHandlerHints } from "./handlers";
4
4
  import type { IconKey, NavIconKey } from "./nav-icon";
5
5
 
@@ -1180,8 +1180,11 @@ export type ActionFormScreenDefinition = {
1180
1180
  readonly handler: string;
1181
1181
  /** Form-Shape: Field-Map pro Name. Nutzt dieselben FieldDefinitions
1182
1182
  * wie Entity-Felder. Mindestens ein Feld erforderlich (Boot-
1183
- * Validator). */
1184
- readonly fields: Readonly<Record<string, FieldDefinition>>;
1183
+ * Validator).
1184
+ *
1185
+ * A `money` field here must name its own currency source: this screen
1186
+ * has no entity whose `defaultCurrency` it could inherit (fw#2839). */
1187
+ readonly fields: Readonly<Record<string, FormFieldDefinition>>;
1185
1188
  /** Layout analog zu EntityEditScreen: sections mit fields aus dem
1186
1189
  * fields-Map oben. */
1187
1190
  readonly layout: EditLayout;
@@ -1272,7 +1275,7 @@ export type SecretRevealField = {
1272
1275
  export type SecretMintConfirmStep = {
1273
1276
  /** Write-handler QN dispatched when the confirm form is submitted. */
1274
1277
  readonly handler: string;
1275
- readonly fields: Readonly<Record<string, FieldDefinition>>;
1278
+ readonly fields: Readonly<Record<string, FormFieldDefinition>>;
1276
1279
  readonly layout: EditLayout;
1277
1280
  /** Mint success-payload fields merged into the confirm payload (e.g. a
1278
1281
  * short-lived setup token). Held in component state only — never rendered,
@@ -1314,7 +1317,7 @@ export type SecretMintScreenDefinition = {
1314
1317
  readonly urlPrefillFields?: readonly string[];
1315
1318
  /** Write-handler QN dispatched on submit. */
1316
1319
  readonly handler: string;
1317
- readonly fields: Readonly<Record<string, FieldDefinition>>;
1320
+ readonly fields: Readonly<Record<string, FormFieldDefinition>>;
1318
1321
  readonly layout: EditLayout;
1319
1322
  /** Navigate here after the reveal is confirmed. Short id (same-feature) or
1320
1323
  * a fully-qualified cross-feature screen QN — no result-id navigation