@plasius/schema 1.4.1 → 1.4.3

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,6 +113,7 @@ reporting:
113
113
 
114
114
  ```ts
115
115
  import {
116
+ FeedbackBugHealthMetricsProjectionSchema,
116
117
  FeedbackBugPacketSchema,
117
118
  FeedbackCommittedAcceptanceEvidenceSchema,
118
119
  FeedbackPublicSummarySchema,
@@ -243,13 +244,34 @@ rather than by enumerating packet storage, preventing an immutable Blob written
243
244
  before a failed commit from entering aggregates. See
244
245
  [ADR-0011](./docs/adrs/adr-0011-identifier-free-feedback-acceptance-evidence.md).
245
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
+
259
+ Materialization manifests may identify a revision as a `metrics-source`
260
+ correction when an independently finalized identifier-free metric changed but
261
+ no packet arrived late. That reason requires `lateArrivalCount: 0`; ordinary
262
+ late-arrival corrections continue to require a positive late-arrival count.
263
+
246
264
  All feedback contracts opt in to recursive unknown-field rejection, including
247
265
  direct references and arrays of references. Unshaped references admit only
248
266
  `type` and `id`; shaped references admit only those keys plus their declared
249
267
  fields. Closed feedback vocabularies and definition objects are runtime frozen
250
268
  before schemas capture them. Before recursive cloning, strict validation also
251
- performs an iterative whole-value node/depth budget so malformed containers
252
- hidden under scalar fields cannot exhaust the call stack. The
269
+ performs an iterative whole-value node/depth budget and inspects own data
270
+ property descriptors. It rejects enumerable accessors, symbol keys, exotic
271
+ prototypes, sparse or oversized arrays, and malformed proxy traps without
272
+ evaluating accessor values or reflecting thrown messages, so malformed
273
+ containers hidden under scalar fields cannot exhaust the call stack or expand
274
+ into an attacker-selected logical array length. The
253
275
  general schema default remains backwards-compatible stripping:
254
276
 
255
277
  ```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