@plasius/schema 1.4.0 → 1.4.2

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
@@ -113,7 +113,9 @@ reporting:
113
113
 
114
114
  ```ts
115
115
  import {
116
+ FeedbackBugHealthMetricsProjectionSchema,
116
117
  FeedbackBugPacketSchema,
118
+ FeedbackCommittedAcceptanceEvidenceSchema,
117
119
  FeedbackPublicSummarySchema,
118
120
  FeedbackRichTextAstSchema,
119
121
  FeedbackReviewPacketSchema,
@@ -227,13 +229,44 @@ hours and review suppression at 30 days; acceptance receipts distinguish bugs
227
229
  from reviews and accept only the five-step bug ladder or exact 30-day review
228
230
  period.
229
231
 
232
+ `FeedbackCommittedAcceptanceEvidenceSchema` is a separate, durable proof that
233
+ the control-plane commit for one immutable packet completed. It contains only
234
+ the canonical packet UUID, `bug`/`review` kind, and the packet's canonical
235
+ server-owned UTC acceptance time. Canonical timestamp validation rejects
236
+ impossible calendar or clock values as ordinary closed validation failures; it
237
+ does not expose the rejected value or a native date exception. It cannot
238
+ express a control-state key, reservation, idempotency value, reporter
239
+ pseudonym, request metadata, Blob locator, content hash, narrative, ciphertext,
240
+ or pixels, and its PII audit is empty. Only a trusted delivery worker may
241
+ construct this evidence after an atomic control-plane commit; it is not an
242
+ intake request. Report processors must select packets through this evidence
243
+ rather than by enumerating packet storage, preventing an immutable Blob written
244
+ before a failed commit from entering aggregates. See
245
+ [ADR-0011](./docs/adrs/adr-0011-identifier-free-feedback-acceptance-evidence.md).
246
+
247
+ `FeedbackBugHealthMetricsProjectionSchema` is the separate trusted input for
248
+ facts that accepted packets cannot supply: application rejection counts,
249
+ privacy-safe traffic denominators, and allowlisted abuse-control block bands.
250
+ It covers one exact UTC hour, is finalized no earlier than the window end, and
251
+ contains no raw edge event, request, reporter, client, route, narrative, or
252
+ pixel data. Abuse bands are unique, non-zero, and ordered by the frozen
253
+ `FEEDBACK_ABUSE_BLOCK_BANDS` vocabulary. A producer must derive the
254
+ `projectionId` deterministically from the hour and consumers must verify that
255
+ binding before materialization; the schema deliberately performs no
256
+ environment-dependent hashing. See
257
+ [ADR-0012](./docs/adrs/adr-0012-identifier-free-bug-health-metrics-projection.md).
258
+
230
259
  All feedback contracts opt in to recursive unknown-field rejection, including
231
260
  direct references and arrays of references. Unshaped references admit only
232
261
  `type` and `id`; shaped references admit only those keys plus their declared
233
262
  fields. Closed feedback vocabularies and definition objects are runtime frozen
234
263
  before schemas capture them. Before recursive cloning, strict validation also
235
- performs an iterative whole-value node/depth budget so malformed containers
236
- hidden under scalar fields cannot exhaust the call stack. The
264
+ performs an iterative whole-value node/depth budget and inspects own data
265
+ property descriptors. It rejects enumerable accessors, symbol keys, exotic
266
+ prototypes, sparse or oversized arrays, and malformed proxy traps without
267
+ evaluating accessor values or reflecting thrown messages, so malformed
268
+ containers hidden under scalar fields cannot exhaust the call stack or expand
269
+ into an attacker-selected logical array length. The
237
270
  general schema default remains backwards-compatible stripping:
238
271
 
239
272
  ```ts
@@ -1446,45 +1446,113 @@ function getShape(def) {
1446
1446
  var MAX_STRICT_VALIDATION_NODES = 1e4;
1447
1447
  var MAX_STRICT_VALIDATION_DEPTH = 256;
1448
1448
  var MAX_STRICT_VALIDATION_OBJECTS = MAX_STRICT_VALIDATION_NODES;
1449
- function strictInputIsWithinComplexityLimit(input) {
1450
- const pending = [
1451
- { value: input, depth: 0 }
1452
- ];
1449
+ function snapshotStrictInput(input) {
1450
+ const snapshot = Array.isArray(input) ? [] : {};
1451
+ const pending = [{ source: input, target: snapshot, depth: 0 }];
1453
1452
  const seen = /* @__PURE__ */ new WeakSet();
1454
- let visitedNodes = 0;
1453
+ let visitedNodes = 1;
1455
1454
  try {
1456
1455
  while (pending.length > 0) {
1457
1456
  const current = pending.pop();
1458
1457
  if (!current) {
1459
1458
  continue;
1460
1459
  }
1461
- visitedNodes += 1;
1462
1460
  if (visitedNodes > MAX_STRICT_VALIDATION_NODES || current.depth > MAX_STRICT_VALIDATION_DEPTH) {
1463
- return false;
1461
+ return { valid: false };
1464
1462
  }
1465
- if (typeof current.value !== "object" || current.value === null) {
1466
- continue;
1463
+ if (seen.has(current.source)) {
1464
+ return { valid: false };
1465
+ }
1466
+ seen.add(current.source);
1467
+ const isArray = Array.isArray(current.source);
1468
+ const prototype = Object.getPrototypeOf(current.source);
1469
+ if (isArray && prototype !== Array.prototype || !isArray && prototype !== Object.prototype && prototype !== null) {
1470
+ return { valid: false };
1471
+ }
1472
+ const ownKeys = Reflect.ownKeys(current.source);
1473
+ if (visitedNodes + ownKeys.length > MAX_STRICT_VALIDATION_NODES) {
1474
+ return { valid: false };
1467
1475
  }
1468
- if (seen.has(current.value)) {
1469
- return false;
1476
+ const descriptors = /* @__PURE__ */ new Map();
1477
+ for (const key of ownKeys) {
1478
+ const descriptor = Object.getOwnPropertyDescriptor(
1479
+ current.source,
1480
+ key
1481
+ );
1482
+ if (descriptor === void 0) {
1483
+ return { valid: false };
1484
+ }
1485
+ descriptors.set(key, descriptor);
1470
1486
  }
1471
- seen.add(current.value);
1472
- const keys = Object.keys(current.value);
1473
- if (visitedNodes + pending.length + keys.length > MAX_STRICT_VALIDATION_NODES) {
1474
- return false;
1487
+ if (isArray) {
1488
+ const lengthDescriptor = descriptors.get("length");
1489
+ if (lengthDescriptor === void 0 || !("value" in lengthDescriptor) || !Number.isSafeInteger(lengthDescriptor.value) || lengthDescriptor.value < 0 || lengthDescriptor.value > MAX_STRICT_VALIDATION_NODES) {
1490
+ return { valid: false };
1491
+ }
1492
+ let elementCount = 0;
1493
+ for (const key of ownKeys) {
1494
+ if (key === "length") {
1495
+ continue;
1496
+ }
1497
+ if (typeof key !== "string" || !/^(?:0|[1-9]\d*)$/.test(key)) {
1498
+ return { valid: false };
1499
+ }
1500
+ const descriptor = descriptors.get(key);
1501
+ if (descriptor === void 0 || descriptor.enumerable !== true || Number(key) >= lengthDescriptor.value) {
1502
+ return { valid: false };
1503
+ }
1504
+ elementCount += 1;
1505
+ }
1506
+ if (elementCount !== lengthDescriptor.value) {
1507
+ return { valid: false };
1508
+ }
1475
1509
  }
1476
- const record = current.value;
1510
+ visitedNodes += ownKeys.length;
1511
+ const keys = ownKeys.filter(
1512
+ (key) => descriptors.get(key)?.enumerable
1513
+ );
1477
1514
  for (const key of keys) {
1478
- pending.push({
1479
- value: record[key],
1480
- depth: current.depth + 1
1515
+ if (typeof key !== "string") {
1516
+ return { valid: false };
1517
+ }
1518
+ if (isArray && !/^(?:0|[1-9]\d*)$/.test(key)) {
1519
+ return { valid: false };
1520
+ }
1521
+ const descriptor = descriptors.get(key);
1522
+ if (descriptor === void 0 || !("value" in descriptor) || descriptor.get !== void 0 || descriptor.set !== void 0) {
1523
+ return { valid: false };
1524
+ }
1525
+ const child = descriptor.value;
1526
+ if (typeof child === "function") {
1527
+ return { valid: false };
1528
+ }
1529
+ if (typeof child === "object" && child !== null) {
1530
+ const childSnapshot = Array.isArray(child) ? [] : {};
1531
+ Object.defineProperty(current.target, key, {
1532
+ value: childSnapshot,
1533
+ enumerable: true,
1534
+ configurable: true,
1535
+ writable: true
1536
+ });
1537
+ pending.push({
1538
+ source: child,
1539
+ target: childSnapshot,
1540
+ depth: current.depth + 1
1541
+ });
1542
+ continue;
1543
+ }
1544
+ Object.defineProperty(current.target, key, {
1545
+ value: child,
1546
+ enumerable: true,
1547
+ configurable: true,
1548
+ writable: true
1481
1549
  });
1482
1550
  }
1483
1551
  }
1484
1552
  } catch {
1485
- return false;
1553
+ return { valid: false };
1486
1554
  }
1487
- return true;
1555
+ return { valid: true, value: snapshot };
1488
1556
  }
1489
1557
  function strictReferenceShape(def) {
1490
1558
  return {
@@ -2051,8 +2119,10 @@ function createSchema(_shape, entityType, options = {
2051
2119
  if (typeof input !== "object" || input === null) {
2052
2120
  return { valid: false, errors: ["Input must be an object"] };
2053
2121
  }
2122
+ let strictInputSnapshot;
2054
2123
  if (options.unknownFields === "reject") {
2055
- if (!strictInputIsWithinComplexityLimit(input)) {
2124
+ const snapshot = snapshotStrictInput(input);
2125
+ if (!snapshot.valid) {
2056
2126
  return {
2057
2127
  valid: false,
2058
2128
  errors: ["Strict validation complexity limit exceeded."],
@@ -2065,16 +2135,32 @@ function createSchema(_shape, entityType, options = {
2065
2135
  ]
2066
2136
  };
2067
2137
  }
2138
+ strictInputSnapshot = snapshot.value;
2068
2139
  const scanState = {
2069
2140
  remainingObjects: MAX_STRICT_VALIDATION_OBJECTS,
2070
2141
  limitExceeded: false
2071
2142
  };
2072
- const locations = unknownFieldLocations(
2073
- input,
2074
- schema._shape,
2075
- "root",
2076
- scanState
2077
- );
2143
+ let locations;
2144
+ try {
2145
+ locations = unknownFieldLocations(
2146
+ strictInputSnapshot,
2147
+ schema._shape,
2148
+ "root",
2149
+ scanState
2150
+ );
2151
+ } catch {
2152
+ return {
2153
+ valid: false,
2154
+ errors: ["Strict validation complexity limit exceeded."],
2155
+ issues: [
2156
+ {
2157
+ path: "root",
2158
+ code: "validation_complexity_limit",
2159
+ message: "Strict validation complexity limit exceeded."
2160
+ }
2161
+ ]
2162
+ };
2163
+ }
2078
2164
  if (scanState.limitExceeded) {
2079
2165
  return {
2080
2166
  valid: false,
@@ -2102,7 +2188,23 @@ function createSchema(_shape, entityType, options = {
2102
2188
  };
2103
2189
  }
2104
2190
  }
2105
- const working = typeof input === "object" && input !== null ? deepClone(input) : { ...input };
2191
+ let working;
2192
+ try {
2193
+ working = strictInputSnapshot ?? (typeof input === "object" && input !== null ? deepClone(input) : { ...input });
2194
+ } catch {
2195
+ return {
2196
+ valid: false,
2197
+ errors: ["Input could not be safely cloned."],
2198
+ issues: [
2199
+ {
2200
+ path: "root",
2201
+ code: "unsafe_input",
2202
+ message: "Input could not be safely cloned."
2203
+ }
2204
+ ]
2205
+ };
2206
+ }
2207
+ const providedFieldNames = new Set(Object.keys(working));
2106
2208
  if (working.type == null) working.type = entityType;
2107
2209
  if (working.version == null) working.version = version;
2108
2210
  const fromVersion = String(working.version ?? "0.0.0");
@@ -2219,9 +2321,8 @@ function createSchema(_shape, entityType, options = {
2219
2321
  }
2220
2322
  if (errors.length === 0 && options.schemaValidator) {
2221
2323
  const castValue = result;
2222
- const source = input;
2223
2324
  const validationContext = Object.freeze({
2224
- wasProvided: (fieldName) => Object.prototype.hasOwnProperty.call(source, fieldName)
2325
+ wasProvided: (fieldName) => providedFieldNames.has(fieldName)
2225
2326
  });
2226
2327
  if (!options.schemaValidator(castValue, validationContext)) {
2227
2328
  errors.push("Schema-level validation failed.");
@@ -2603,4 +2704,4 @@ export {
2603
2704
  FeedbackGameDiagnosticsSchema,
2604
2705
  validateFeedbackGameDiagnostics
2605
2706
  };
2606
- //# sourceMappingURL=chunk-UQHPDQZX.js.map
2707
+ //# sourceMappingURL=chunk-GNL2RBO5.js.map