@ultimat3/schema 15.0.0 → 17.0.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/CLAUDE.md CHANGED
@@ -118,6 +118,14 @@ Gotchas:
118
118
  PROTOTYPE, so a field named after one was unsatisfiable for every input and its `.default()`
119
119
  never fired; a `{}` output let a declared `__proto__` field write through the setter. Never
120
120
  reintroduce either half, and never assume a parsed object has `Object.prototype` on it.
121
+ - **Every map keyed by a DECLARED FIELD NAME is built on `Object.create(null)`** — `objectSchema`'s
122
+ `properties`, `json-schema.ts`'s converted `properties`, and `pick`/`omit`'s rebuilt shape. That
123
+ last pair is the one with teeth: `next['__proto__'] = member` on a `{}` literal hits the setter,
124
+ so `pick('__proto__')` answered a schema with **zero** properties and dropped the one field the
125
+ caller asked to keep from validation, from the IR and from `parse()`'s output. `extend` needs
126
+ nothing: object spread defines own properties and never invokes a setter. A field named
127
+ `__proto__` is declarable — `t.object({ ['__proto__']: t.string })`, a COMPUTED key, which is an
128
+ ordinary own property where the literal `{ __proto__: … }` form is the setter.
121
129
  - `t.date` refuses a clock time with no offset and no `Z` (`iso-date.ts`): a zone-less string is a
122
130
  different instant per host `TZ`, and `coerceQuery` puts it one query parameter from the wire.
123
131
  A date-only string carries no clock time and is UTC by spec, so it still parses.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/schema",
3
- "version": "15.0.0",
3
+ "version": "17.0.0",
4
4
  "description": "Ultimate's validation seam: Standard Schema interface, the t namespace, JSON Schema output",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/errors.ts CHANGED
@@ -16,17 +16,17 @@ import { formatIssues } from './standard';
16
16
  */
17
17
  // biome-ignore lint/suspicious/noControlCharactersInRegex: escaping them is the point.
18
18
  const CONTROL = /[\u0000-\u001f\u007f\u2028\u2029]/g;
19
- const ESCAPES: Readonly<Record<string, string>> = {
19
+ // Null-prototype, so `ESCAPES[char]` cannot answer an `Object.prototype` member no matter what
20
+ // `CONTROL` matched. The domain argument — a key is exactly one control character, and no
21
+ // prototype member has a single-character name — was true and is no longer load-bearing; a table
22
+ // that cannot reach the prototype needs no argument, and `bun run proto-index` stops reporting it.
23
+ const ESCAPES: Readonly<Record<string, string>> = Object.assign(Object.create(null), {
20
24
  '\n': String.raw`\n`,
21
25
  '\r': String.raw`\r`,
22
26
  '\t': String.raw`\t`,
23
27
  '\b': String.raw`\b`,
24
28
  '\f': String.raw`\f`,
25
- };
26
- // `ESCAPES[char]` is a computed read on a plain object and is safe by DOMAIN, not by luck: the
27
- // key is whatever `CONTROL` matched, so it is exactly one control character — and no
28
- // `Object.prototype` member has a single-character name. Every other computed read in this file
29
- // goes through `Object.hasOwn`.
29
+ });
30
30
  const singleLine = (text: string): string =>
31
31
  text.replace(
32
32
  CONTROL,
@@ -231,7 +231,13 @@ function convert(node: SchemaNode): JsonSchema {
231
231
  additionalProperties: false,
232
232
  });
233
233
  case 'object': {
234
- const properties: Record<string, JsonSchema> = {};
234
+ // `Object.create(null)`, the same rule `validators.ts` applies to the IR this reads: a
235
+ // `__proto__` property assigned into a `{}` literal reaches the setter and is published
236
+ // nowhere, so the document would omit a field the server enforces.
237
+ const properties: Record<string, JsonSchema> = Object.create(null) as Record<
238
+ string,
239
+ JsonSchema
240
+ >;
235
241
  for (const [key, child] of Object.entries(node.properties ?? {})) {
236
242
  properties[key] = convert(child);
237
243
  }
package/src/validators.ts CHANGED
@@ -155,7 +155,11 @@ function makeNumberSchema(node: SchemaNode): NumberSchema {
155
155
  }
156
156
 
157
157
  export function objectSchema<S extends Shape>(shape: S): ObjectSchema<S> {
158
- const properties: Record<string, SchemaNode> = {};
158
+ // `Object.create(null)` for the same reason the parse output below uses one, on the other side
159
+ // of the same rule: a DECLARED `__proto__` field assigned into a `{}` literal hits the prototype
160
+ // setter and never becomes a key, so the field vanished from the IR `json-schema.ts` publishes
161
+ // and `coerce.ts` walks while the `checks` list beside it went on validating it.
162
+ const properties: Record<string, SchemaNode> = Object.create(null) as Record<string, SchemaNode>;
159
163
  const checks: [string, Check<unknown>][] = [];
160
164
  for (const [key, member] of Object.entries(shape)) {
161
165
  properties[key] = member.node;
@@ -195,14 +199,20 @@ export function objectSchema<S extends Shape>(shape: S): ObjectSchema<S> {
195
199
  ...base,
196
200
  shape,
197
201
  extend: (extra) => objectSchema({ ...shape, ...extra } as Simplified<S & typeof extra>),
202
+ // `Object.create(null)` in both, for the reason the IR above states and with a worse outcome
203
+ // here: `next['__proto__'] = member` on a `{}` literal hits the prototype SETTER, so the field
204
+ // is not mis-published, it is GONE — `pick('__proto__')` answered a schema with no properties
205
+ // at all, dropping the one field the caller asked to keep from validation, from the IR and
206
+ // from `parse()`'s output. `extend` needs none of this: object spread defines own properties
207
+ // and never invokes a setter.
198
208
  pick: (...keys) => {
199
- const next: Record<string, AnySchema> = {};
209
+ const next: Record<string, AnySchema> = Object.create(null) as Record<string, AnySchema>;
200
210
  for (const key of keys) next[key] = shape[key] as AnySchema;
201
211
  return objectSchema(next) as ObjectSchema<Pick<S, (typeof keys)[number]>>;
202
212
  },
203
213
  omit: (...keys) => {
204
214
  const drop = new Set<string>(keys);
205
- const next: Record<string, AnySchema> = {};
215
+ const next: Record<string, AnySchema> = Object.create(null) as Record<string, AnySchema>;
206
216
  for (const [key, member] of Object.entries(shape)) {
207
217
  if (!drop.has(key)) next[key] = member;
208
218
  }