@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 +1 -1
- package/src/feature.ts +5 -4
- package/src/handlers.ts +32 -4
- package/src/kms-adapter-types.ts +11 -3
- package/src/screen.ts +46 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-types",
|
|
3
|
-
"version": "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 `
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
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
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
|
|
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
|
package/src/kms-adapter-types.ts
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
555
|
-
|
|
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`
|