@cosmicdrift/kumiko-types 0.290.0 → 0.292.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.290.0",
3
+ "version": "0.292.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/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 ---
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
@@ -49,6 +49,12 @@ export function isSystemTenant(tenantId: TenantId | null | undefined): boolean {
49
49
  return !tenantId || tenantId === SYSTEM_TENANT_ID;
50
50
  }
51
51
 
52
+ // "No human caller" marker on event-store `createdBy`. Stringified so it
53
+ // round-trips through SessionUser.id; not a real UUID, and createdBy is text.
54
+ // Lives here rather than next to createSystemUser() so client-side code (the
55
+ // reference-label map) can read it without importing a runtime module.
56
+ export const SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000";
57
+
52
58
  // Primary-key identifier for any entity row. Two shapes coexist because of
53
59
  // the entity-def `idType` switch: classic CRUD entities keep `serial` (number),
54
60
  // while tenant + ES aggregates run on `uuid` (string). Call sites that pass
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
 
@@ -473,6 +473,8 @@ export type EntityListScreenDefinition = {
473
473
  // `falseLabel`) are i18n keys resolved via `translate()` with passthrough
474
474
  // for unknown keys (fw#2373) — not raw display strings. Sent to the server
475
475
  // as `{field, op:"in", value}` in `payload.filters`, ANDed with `filter`.
476
+ // Exception: "dateRange" sends its bounds as the two top-level payload keys
477
+ // named in `params`, not as a `filters` entry.
476
478
  export type ListFacetSpec =
477
479
  | {
478
480
  readonly field: string;
@@ -497,6 +499,20 @@ export type ListFacetSpec =
497
499
  /** Row field on the referenced entity shown as the option label
498
500
  * (default "id"). */
499
501
  readonly labelField?: string;
502
+ }
503
+ | {
504
+ /** Declarative only — the date field the range narrows. It is NOT sent
505
+ * to the server (the bounds go under `params`); it namespaces the
506
+ * facet's URL keys and tells a reading agent which column this list
507
+ * is time-rangeable on. */
508
+ readonly field: string;
509
+ readonly type: "dateRange";
510
+ readonly label: string;
511
+ /** Query-payload keys the chosen bounds are sent under as ISO instants.
512
+ * Explicit rather than a `from`/`to` convention — a query is free to
513
+ * name its time bounds anything, and the boot-validator checks these
514
+ * keys against the handler's Zod schema. */
515
+ readonly params: { readonly from: string; readonly to: string };
500
516
  };
501
517
 
502
518
  export type ProjectionListScreenDefinition = {
@@ -535,7 +551,9 @@ export type ProjectionListScreenDefinition = {
535
551
  readonly filter?: ScreenFilter;
536
552
  /** User-toggleable facet dropdowns — see `ListFacetSpec` doc. `field`
537
553
  * must be a declared column; the bound query handler must accept
538
- * `filters` in its Zod schema — the boot-validator checks both. */
554
+ * `filters` in its Zod schema — the boot-validator checks both. A
555
+ * "dateRange" facet instead needs its two `params` keys in that schema;
556
+ * it sends no `filters` entry. */
539
557
  readonly facets?: readonly ListFacetSpec[];
540
558
  readonly slots?: ScreenSlots;
541
559
  readonly access?: AccessRule;
@@ -1180,8 +1198,11 @@ export type ActionFormScreenDefinition = {
1180
1198
  readonly handler: string;
1181
1199
  /** Form-Shape: Field-Map pro Name. Nutzt dieselben FieldDefinitions
1182
1200
  * wie Entity-Felder. Mindestens ein Feld erforderlich (Boot-
1183
- * Validator). */
1184
- readonly fields: Readonly<Record<string, FieldDefinition>>;
1201
+ * Validator).
1202
+ *
1203
+ * A `money` field here must name its own currency source: this screen
1204
+ * has no entity whose `defaultCurrency` it could inherit (fw#2839). */
1205
+ readonly fields: Readonly<Record<string, FormFieldDefinition>>;
1185
1206
  /** Layout analog zu EntityEditScreen: sections mit fields aus dem
1186
1207
  * fields-Map oben. */
1187
1208
  readonly layout: EditLayout;
@@ -1272,7 +1293,7 @@ export type SecretRevealField = {
1272
1293
  export type SecretMintConfirmStep = {
1273
1294
  /** Write-handler QN dispatched when the confirm form is submitted. */
1274
1295
  readonly handler: string;
1275
- readonly fields: Readonly<Record<string, FieldDefinition>>;
1296
+ readonly fields: Readonly<Record<string, FormFieldDefinition>>;
1276
1297
  readonly layout: EditLayout;
1277
1298
  /** Mint success-payload fields merged into the confirm payload (e.g. a
1278
1299
  * short-lived setup token). Held in component state only — never rendered,
@@ -1314,7 +1335,7 @@ export type SecretMintScreenDefinition = {
1314
1335
  readonly urlPrefillFields?: readonly string[];
1315
1336
  /** Write-handler QN dispatched on submit. */
1316
1337
  readonly handler: string;
1317
- readonly fields: Readonly<Record<string, FieldDefinition>>;
1338
+ readonly fields: Readonly<Record<string, FormFieldDefinition>>;
1318
1339
  readonly layout: EditLayout;
1319
1340
  /** Navigate here after the reveal is confirmed. Short id (same-feature) or
1320
1341
  * a fully-qualified cross-feature screen QN — no result-id navigation