@plasius/schema 1.2.17 → 1.3.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/README.md CHANGED
@@ -30,6 +30,25 @@ uses `dist-cjs/*.js` with `type: module`, it must generate
30
30
 
31
31
  ---
32
32
 
33
+ ## Player-system event contracts
34
+
35
+ The package exports versioned contracts for a blob-backed, privacy-safe player
36
+ event pipeline:
37
+
38
+ ```ts
39
+ import {
40
+ PlayerSystemCuratedSnapshotSchema,
41
+ PlayerSystemNormalizedEventBatchSchema,
42
+ PlayerSystemObservedEventBatchSchema,
43
+ } from "@plasius/schema";
44
+ ```
45
+
46
+ The contracts cover raw observed-event ingress, normalized batches with
47
+ deterministic keys, and curated per-player Event Log/Achievement snapshots.
48
+ Only the stable `playerId` is treated as a personal identifier. It is retained
49
+ for storage partitioning and pseudonymized by `sanitizeForLog`; unknown fields
50
+ and hidden server truth are excluded by schema serialization.
51
+
33
52
  ## Demo
34
53
 
35
54
  ```bash
@@ -88,6 +107,21 @@ const UserFields = {
88
107
  ```
89
108
 
90
109
  Common methods (non‑exhaustive): `.required()`, `.optional()`, `.default(v|fn)`, `.description(text)`, and type‑specific helpers like `.email()`, `.uuid()`, `.min()`, `.max()`, `.enum([...])`.
110
+
111
+ Validation standards notes:
112
+
113
+ - `languageCode()` checks a documented RFC 5646/BCP 47 subset
114
+ case-insensitively, including grandfathered and private-use tags;
115
+ two-letter primaries are checked against the package's ISO 639-1 registry.
116
+ Registered 4-8 letter primaries remain outside this dependency-free subset.
117
+ - `uuid()` accepts the RFC 9562 versions 1-8, Nil, and Max text forms. Legacy
118
+ RFC 4122-named module paths remain compatibility aliases.
119
+ - `email()` is a pragmatic dot-domain mailbox subset, not a claim of complete
120
+ RFC 5322 grammar support.
121
+
122
+ Runtime rollout inherits `governance.rfc-compliance-remediation.enabled`;
123
+ disabled consumers may retain the prior validators only during a documented
124
+ migration window.
91
125
  Defaults are applied during validation when inputs are missing/`undefined`.
92
126
  Fields are required by default; call `.optional()` (or provide `.default()`) to allow omission.
93
127
  Use `.internal()` or `.exposure("internal")` on fields that the server may validate/store but must not include in client-facing payloads.
package/dist/index.cjs CHANGED
@@ -22,6 +22,12 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  FieldBuilder: () => FieldBuilder,
24
24
  IsoLanguageCode: () => IsoLanguageCode,
25
+ PLAYER_SYSTEM_ACHIEVEMENT_TIERS: () => PLAYER_SYSTEM_ACHIEVEMENT_TIERS,
26
+ PLAYER_SYSTEM_ACHIEVEMENT_TRACK_STATUSES: () => PLAYER_SYSTEM_ACHIEVEMENT_TRACK_STATUSES,
27
+ PLAYER_SYSTEM_EVENT_KINDS: () => PLAYER_SYSTEM_EVENT_KINDS,
28
+ PlayerSystemCuratedSnapshotSchema: () => PlayerSystemCuratedSnapshotSchema,
29
+ PlayerSystemNormalizedEventBatchSchema: () => PlayerSystemNormalizedEventBatchSchema,
30
+ PlayerSystemObservedEventBatchSchema: () => PlayerSystemObservedEventBatchSchema,
25
31
  createComponentSchema: () => createComponentSchema,
26
32
  createSchema: () => createSchema,
27
33
  field: () => field,
@@ -284,8 +290,8 @@ var validateUrl = (value) => {
284
290
  // src/validation/uuid.RFC4122.ts
285
291
  var validateUUID = (value) => {
286
292
  if (typeof value !== "string") return false;
287
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
288
- return uuidRegex.test(value);
293
+ const standard = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
294
+ return standard.test(value) || value.toLowerCase() === "00000000-0000-0000-0000-000000000000" || value.toLowerCase() === "ffffffff-ffff-ffff-ffff-ffffffffffff";
289
295
  };
290
296
 
291
297
  // src/validation/dateTime.ISO8601.ts
@@ -1032,10 +1038,10 @@ function isIsoLanguageCode(value) {
1032
1038
  return typeof value === "string" && ISO_LANGUAGE_SET.has(value.toLowerCase());
1033
1039
  }
1034
1040
  function isRegionSubtag(value) {
1035
- return /^[A-Z]{2}$/.test(value) || /^\d{3}$/.test(value);
1041
+ return /^[A-Za-z]{2}$/.test(value) || /^\d{3}$/.test(value);
1036
1042
  }
1037
1043
  function isScriptSubtag(value) {
1038
- return /^[A-Z][a-z]{3}$/.test(value);
1044
+ return /^[A-Za-z]{4}$/.test(value);
1039
1045
  }
1040
1046
  function isVariantSubtag(value) {
1041
1047
  return /^([0-9][A-Za-z0-9]{3}|[A-Za-z0-9]{5,8})$/.test(value);
@@ -1055,20 +1061,42 @@ function isPrivateUseSubtag(value) {
1055
1061
  function validateLanguage(value) {
1056
1062
  if (typeof value !== "string" || value.length === 0) return false;
1057
1063
  const parts = value.split("-");
1064
+ if (parts.some((part) => part.length === 0)) return false;
1065
+ const lowerTag = value.toLowerCase();
1066
+ if (GRANDFATHERED_TAGS.has(lowerTag)) return true;
1067
+ if (parts[0]?.toLowerCase() === "x") {
1068
+ return parts.length > 1 && parts.slice(1).every(isPrivateUseSubtag);
1069
+ }
1058
1070
  let i = 0;
1059
1071
  const lang = parts[i];
1060
- if (!lang || !isIsoLanguageCode(lang)) return false;
1072
+ if (!lang || !/^[A-Za-z]{2,3}$/.test(lang)) return false;
1073
+ if (lang.length === 2 && !isIsoLanguageCode(lang)) return false;
1061
1074
  i += 1;
1075
+ if (lang.length <= 3) {
1076
+ let extlangCount = 0;
1077
+ while (i < parts.length && extlangCount < 3 && /^[A-Za-z]{3}$/.test(parts[i])) {
1078
+ i += 1;
1079
+ extlangCount += 1;
1080
+ }
1081
+ }
1062
1082
  if (i < parts.length && isScriptSubtag(parts[i])) {
1063
1083
  i += 1;
1064
1084
  }
1065
1085
  if (i < parts.length && isRegionSubtag(parts[i].toUpperCase())) {
1066
1086
  i += 1;
1067
1087
  }
1088
+ const variants = /* @__PURE__ */ new Set();
1068
1089
  while (i < parts.length && isVariantSubtag(parts[i])) {
1090
+ const variant = parts[i].toLowerCase();
1091
+ if (variants.has(variant)) return false;
1092
+ variants.add(variant);
1069
1093
  i += 1;
1070
1094
  }
1095
+ const extensionSingletons = /* @__PURE__ */ new Set();
1071
1096
  while (i < parts.length && isExtensionSingleton(parts[i])) {
1097
+ const singleton = parts[i].toLowerCase();
1098
+ if (extensionSingletons.has(singleton)) return false;
1099
+ extensionSingletons.add(singleton);
1072
1100
  i += 1;
1073
1101
  if (!(i < parts.length && isExtensionSubtag(parts[i]))) return false;
1074
1102
  while (i < parts.length && isExtensionSubtag(parts[i])) {
@@ -1084,6 +1112,34 @@ function validateLanguage(value) {
1084
1112
  }
1085
1113
  return i === parts.length;
1086
1114
  }
1115
+ var GRANDFATHERED_TAGS = /* @__PURE__ */ new Set([
1116
+ "art-lojban",
1117
+ "cel-gaulish",
1118
+ "en-gb-oed",
1119
+ "i-ami",
1120
+ "i-bnn",
1121
+ "i-default",
1122
+ "i-enochian",
1123
+ "i-hak",
1124
+ "i-klingon",
1125
+ "i-lux",
1126
+ "i-mingo",
1127
+ "i-navajo",
1128
+ "i-pwn",
1129
+ "i-tao",
1130
+ "i-tay",
1131
+ "i-tsu",
1132
+ "no-bok",
1133
+ "no-nyn",
1134
+ "sgn-be-fr",
1135
+ "sgn-be-nl",
1136
+ "sgn-ch-de",
1137
+ "zh-guoyu",
1138
+ "zh-hakka",
1139
+ "zh-min",
1140
+ "zh-min-nan",
1141
+ "zh-xiang"
1142
+ ]);
1087
1143
 
1088
1144
  // src/field.ts
1089
1145
  var field = {
@@ -2509,10 +2565,129 @@ function getComponentSchema(type) {
2509
2565
  function getAllComponentSchemas() {
2510
2566
  return Array.from(componentSchemaRegistry.entries());
2511
2567
  }
2568
+
2569
+ // src/player-system-events.ts
2570
+ var PLAYER_SYSTEM_EVENT_KINDS = [
2571
+ "combat",
2572
+ "social",
2573
+ "mission",
2574
+ "crafting",
2575
+ "world"
2576
+ ];
2577
+ var PLAYER_SYSTEM_ACHIEVEMENT_TIERS = [
2578
+ "bronze",
2579
+ "silver",
2580
+ "gold"
2581
+ ];
2582
+ var PLAYER_SYSTEM_ACHIEVEMENT_TRACK_STATUSES = [
2583
+ "tracking",
2584
+ "earned"
2585
+ ];
2586
+ var playerIdField = () => field.string().min(1).max(128).PID({
2587
+ classification: "low",
2588
+ action: "none",
2589
+ logHandling: "pseudonym",
2590
+ purpose: "a stable player-scoped storage identifier"
2591
+ }).description(
2592
+ "Stable player identifier; the only personal identifier retained in event projections"
2593
+ );
2594
+ var eventIdField = () => field.string().min(1).max(128);
2595
+ var eventKindField = () => field.string().enum(PLAYER_SYSTEM_EVENT_KINDS);
2596
+ var eventTitleField = () => field.generalText().min(1).max(120).description("Player-visible event title");
2597
+ var eventSummaryField = () => field.generalText().min(1).max(320).description("Player-visible event summary");
2598
+ var observedEventFields = (defaultHighlighted = true) => ({
2599
+ id: eventIdField(),
2600
+ playerId: playerIdField(),
2601
+ occurredAt: field.dateTimeISO(),
2602
+ kind: eventKindField(),
2603
+ title: eventTitleField(),
2604
+ summary: eventSummaryField(),
2605
+ highlighted: defaultHighlighted ? field.boolean().default(false) : field.boolean()
2606
+ });
2607
+ var eventHighlightFields = () => ({
2608
+ id: eventIdField(),
2609
+ title: eventTitleField(),
2610
+ summary: eventSummaryField(),
2611
+ occurredAt: field.dateTimeISO(),
2612
+ kind: eventKindField()
2613
+ });
2614
+ var eventEntryFields = () => ({
2615
+ id: eventIdField(),
2616
+ title: eventTitleField(),
2617
+ summary: eventSummaryField(),
2618
+ occurredAt: field.dateTimeISO(),
2619
+ kind: eventKindField(),
2620
+ highlighted: field.boolean()
2621
+ });
2622
+ var achievementFields = () => ({
2623
+ id: eventIdField(),
2624
+ title: eventTitleField(),
2625
+ tier: field.string().enum(PLAYER_SYSTEM_ACHIEVEMENT_TIERS),
2626
+ progressLabel: eventSummaryField(),
2627
+ earnedAt: field.dateTimeISO().optional()
2628
+ });
2629
+ var achievementTrackFields = () => ({
2630
+ id: eventIdField(),
2631
+ title: eventTitleField(),
2632
+ current: field.number().min(0).max(1e3),
2633
+ target: field.number().min(1).max(1e3),
2634
+ status: field.string().enum(PLAYER_SYSTEM_ACHIEVEMENT_TRACK_STATUSES)
2635
+ });
2636
+ var PlayerSystemObservedEventBatchSchema = createSchema(
2637
+ {
2638
+ source: field.string().min(1).max(120),
2639
+ emittedAt: field.dateTimeISO(),
2640
+ events: field.array(field.object(observedEventFields()))
2641
+ },
2642
+ "player-system-observed-event-batch",
2643
+ {
2644
+ version: "1.0.0",
2645
+ piiEnforcement: "strict"
2646
+ }
2647
+ );
2648
+ var PlayerSystemNormalizedEventBatchSchema = createSchema(
2649
+ {
2650
+ sourceBlobName: field.string().min(1).max(512),
2651
+ processedAt: field.dateTimeISO(),
2652
+ events: field.array(
2653
+ field.object({
2654
+ ...observedEventFields(false),
2655
+ deterministicKey: field.string().min(1).max(256)
2656
+ })
2657
+ )
2658
+ },
2659
+ "player-system-normalized-event-batch",
2660
+ {
2661
+ version: "1.0.0",
2662
+ piiEnforcement: "strict"
2663
+ }
2664
+ );
2665
+ var PlayerSystemCuratedSnapshotSchema = createSchema(
2666
+ {
2667
+ playerId: playerIdField(),
2668
+ projectedAt: field.dateTimeISO(),
2669
+ lastObservedAt: field.dateTimeISO(),
2670
+ highlights: field.array(field.object(eventHighlightFields())),
2671
+ events: field.array(field.object(eventEntryFields())),
2672
+ achievements: field.array(field.object(achievementFields())),
2673
+ tracks: field.array(field.object(achievementTrackFields()))
2674
+ },
2675
+ "player-system-curated-snapshot",
2676
+ {
2677
+ version: "1.0.0",
2678
+ piiEnforcement: "strict"
2679
+ }
2680
+ );
2512
2681
  // Annotate the CommonJS export names for ESM import in node:
2513
2682
  0 && (module.exports = {
2514
2683
  FieldBuilder,
2515
2684
  IsoLanguageCode,
2685
+ PLAYER_SYSTEM_ACHIEVEMENT_TIERS,
2686
+ PLAYER_SYSTEM_ACHIEVEMENT_TRACK_STATUSES,
2687
+ PLAYER_SYSTEM_EVENT_KINDS,
2688
+ PlayerSystemCuratedSnapshotSchema,
2689
+ PlayerSystemNormalizedEventBatchSchema,
2690
+ PlayerSystemObservedEventBatchSchema,
2516
2691
  createComponentSchema,
2517
2692
  createSchema,
2518
2693
  field,