@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 +8 -0
- package/package.json +1 -1
- package/src/errors.ts +6 -6
- package/src/json-schema.ts +7 -1
- package/src/validators.ts +13 -3
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
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
|
-
|
|
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,
|
package/src/json-schema.ts
CHANGED
|
@@ -231,7 +231,13 @@ function convert(node: SchemaNode): JsonSchema {
|
|
|
231
231
|
additionalProperties: false,
|
|
232
232
|
});
|
|
233
233
|
case 'object': {
|
|
234
|
-
|
|
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
|
-
|
|
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
|
}
|