@cosmicdrift/kumiko-types 0.253.0 → 0.255.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.253.0",
3
+ "version": "0.255.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/feature.ts CHANGED
@@ -582,10 +582,11 @@ export type FeatureRegistrar<TFeature extends string = string> = {
582
582
  //
583
583
  // `options.piiFields` is a mandatory, explicit PII stance (fw#2558) —
584
584
  // registration fails without one. Pass a map of payload fields to their
585
- // owning `subjectField` for fields carrying personal data (encrypted
586
- // under that user's DEK, crypto-shredding #799), or `"none"` to declare
587
- // the payload holds no personal data. append() enforces the resulting
588
- // catalog on every write path.
585
+ // owning subject via `{ personal: { of: "<ownerField>" } }` (the older
586
+ // `{ subjectField: "<ownerField>" }` form still works) for fields carrying
587
+ // personal data (encrypted under that user's DEK, crypto-shredding #799),
588
+ // or `"none"` to declare the payload holds no personal data. append()
589
+ // enforces the resulting catalog on every write path.
589
590
  defineEvent<const TInner extends string, TPayload>(
590
591
  name: TInner,
591
592
  schema: ZodType<TPayload>,
package/src/handlers.ts CHANGED
@@ -732,10 +732,38 @@ export type QualifiedEventName<
732
732
  > = `${CamelToKebab<TFeature>}:event:${CamelToKebab<TInner>}`;
733
733
 
734
734
  // PII payload fields on a custom event (#799): `field` is encrypted under
735
- // the DEK of the user named by the payload's `subjectField` (crypto-
736
- // shredding). A null subject field leaves the value plaintext — there is
737
- // no user key to shred for system-triggered events.
738
- export type EventPiiFields = Readonly<Record<string, { readonly subjectField: string }>>;
735
+ // the DEK the subject spec names (crypto-shredding). A null owner-field
736
+ // value leaves a `personal: { of }` field plaintext — there is no user key
737
+ // to shred for system-triggered events.
738
+ //
739
+ // `{ personal: { of: "<ownerField>" } }` is the canonical user-subject form,
740
+ // matching the entity-field `personal` vocabulary (see fields.ts).
741
+ // `personal: "tenant"` and `personal: "self"` declare the event's tenant
742
+ // (envelope.tenantId) or its own aggregate stream (envelope.aggregateType +
743
+ // aggregateId) as the subject instead — resolved via `resolveEventSubject`
744
+ // in `packages/framework/src/crypto/subject-resolver.ts` (fw#2801 step 2).
745
+ export type EventPiiSubject =
746
+ | { readonly personal: { readonly of: string } }
747
+ | { readonly personal: "tenant" }
748
+ | { readonly personal: "self" }
749
+ | {
750
+ /** @deprecated use `{ personal: { of: "<ownerField>" } }` instead. */
751
+ readonly subjectField: string;
752
+ };
753
+
754
+ export type EventPiiFields = Readonly<Record<string, EventPiiSubject>>;
755
+
756
+ export type NormalizedEventPiiSubject =
757
+ | { readonly kind: "user"; readonly ownerField: string }
758
+ | { readonly kind: "tenant" }
759
+ | { readonly kind: "self" };
760
+
761
+ export function normalizeEventPiiSubject(spec: EventPiiSubject): NormalizedEventPiiSubject {
762
+ if ("subjectField" in spec) return { kind: "user", ownerField: spec.subjectField };
763
+ if (spec.personal === "tenant") return { kind: "tenant" };
764
+ if (spec.personal === "self") return { kind: "self" };
765
+ return { kind: "user", ownerField: spec.personal.of };
766
+ }
739
767
 
740
768
  // The full set of PII stances a defineEvent() call may declare. "none" is a
741
769
  // distinct value from `{}` — an empty object is indistinguishable from "the
@@ -20,10 +20,18 @@ export function subjectKeyForTenant(tenantId: TenantId): SubjectKey {
20
20
  return `tenant:${tenantId}`;
21
21
  }
22
22
 
23
+ // Registry entity names are identifier-shaped; a free-form aggregate_type
24
+ // (events-schema.ts is a plain text column) could otherwise mint a record
25
+ // key that subjectIdSchema (forget-subject.write.ts) later refuses to
26
+ // shred — enforced once here so minting and shredding can never disagree.
27
+ export const RECORD_ENTITY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
28
+
23
29
  export function subjectKeyForRecord(entity: string, id: string): SubjectKey {
24
- // The key is parsed back on exactly one ":" — an entity containing ":" would break the round-trip.
25
- if (entity === "" || entity.includes(":"))
26
- throw new Error(`Invalid record entity for subject key: ${entity}`);
30
+ if (!RECORD_ENTITY_PATTERN.test(entity)) {
31
+ throw new Error(
32
+ `Invalid record entity for subject key: "${entity}" — must match ${RECORD_ENTITY_PATTERN}`,
33
+ );
34
+ }
27
35
  if (id === "") throw new Error("Invalid record id for subject key: empty");
28
36
  return `record:${entity}:${id}`;
29
37
  }
package/src/screen.ts CHANGED
@@ -517,8 +517,36 @@ export type RecordHeaderSpec = {
517
517
  readonly title: string;
518
518
  readonly subtitle?: string;
519
519
  readonly status?: string;
520
+ /** Record field holding an absolute http(s) URL. When present and the
521
+ * field's value is such a URL, the subtitle renders as an external link
522
+ * (`target="_blank"`) instead of plain text. */
523
+ readonly subtitleHref?: string;
520
524
  };
521
525
 
526
+ // Same shape as `RowActionNavigate`'s screen/entity choice, minus the fields
527
+ // a metric click has no use for (id, confirm, style).
528
+ export type MetricNavigate = {
529
+ readonly screen?: string;
530
+ readonly entity?: string;
531
+ readonly entityId?: string;
532
+ readonly params?: RowFieldExtractor;
533
+ };
534
+
535
+ // A metric can navigate on click, so the plain string shorthand (field name,
536
+ // same as before) sits alongside an object form carrying an explicit i18n
537
+ // `label` and a `navigate` target.
538
+ export type MetricSpec =
539
+ | string
540
+ | {
541
+ readonly field: string;
542
+ readonly label?: string;
543
+ readonly navigate?: MetricNavigate;
544
+ };
545
+
546
+ export function metricField(metric: MetricSpec): string {
547
+ return typeof metric === "string" ? metric : metric.field;
548
+ }
549
+
522
550
  export type ProjectionDetailScreenDefinition = {
523
551
  readonly id: string;
524
552
  readonly type: "projectionDetail";
@@ -551,8 +579,9 @@ export type ProjectionDetailScreenDefinition = {
551
579
  * in the query row (not literals). Rendered only when set. */
552
580
  readonly header?: RecordHeaderSpec;
553
581
  /** Metric band above the layout — column names in the query row, labeled
554
- * via `fieldLabels`. Rendered only when set. */
555
- readonly metrics?: readonly string[];
582
+ * via `fieldLabels` (string shorthand) or the object form's own `label`.
583
+ * Rendered only when set. */
584
+ readonly metrics?: readonly MetricSpec[];
556
585
  /** Parent list screen (kurze id) für eine "Zurück"-Navigation. */
557
586
  readonly listScreenId?: string;
558
587
  readonly slots?: ScreenSlots;
@@ -790,6 +819,11 @@ export type EditFieldsSection = {
790
819
  * to EditFieldSpec.icon. No title → no icon, and no heuristic derives
791
820
  * one from the title (titles are free i18n strings). */
792
821
  readonly icon?: IconKey;
822
+ /** Record field rendered as a count badge in the tab label when the
823
+ * enclosing `EditLayout.mode` is "tabs" (e.g. an open-items counter).
824
+ * Ignored outside tabs mode or when the field's value is not a finite
825
+ * number. */
826
+ readonly countField?: string;
793
827
  };
794
828
 
795
829
  export type EditExtensionSection = {
@@ -811,6 +845,11 @@ export type EditExtensionSection = {
811
845
  * like NotesSection filters/writes against the right domain entity. Takes
812
846
  * precedence over the host value on every screen type. */
813
847
  readonly entityName?: string;
848
+ /** Record field rendered as a count badge in the tab label when the
849
+ * enclosing `EditLayout.mode` is "tabs" (e.g. an open-items counter).
850
+ * Ignored outside tabs mode or when the field's value is not a finite
851
+ * number. */
852
+ readonly countField?: string;
814
853
  };
815
854
 
816
855
  // Read-only list of related records, driven by its own query — for a
@@ -860,6 +899,11 @@ export type EditRelatedListSection = {
860
899
  * writeHandler action re-runs this section's own query, same as a
861
900
  * projectionList row action re-running its list query. */
862
901
  readonly rowActions?: readonly RowAction[];
902
+ /** Record field rendered as a count badge in the tab label when the
903
+ * enclosing `EditLayout.mode` is "tabs" (e.g. an open-items counter).
904
+ * Ignored outside tabs mode or when the field's value is not a finite
905
+ * number. */
906
+ readonly countField?: string;
863
907
  };
864
908
 
865
909
  // A declarative, self-persisting form section for `projectionDetail`