@ultimat3/schema 2.0.0 → 3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/schema",
3
- "version": "2.0.0",
3
+ "version": "3.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/coerce.ts CHANGED
@@ -81,7 +81,10 @@ export function coerceNode(node: SchemaNode, raw: unknown): unknown {
81
81
  const source = raw as Record<string, unknown>;
82
82
  const out: Record<string, unknown> = { ...source };
83
83
  for (const [key, child] of Object.entries(node.properties)) {
84
- if (key in source) out[key] = coerceNode(child, source[key]);
84
+ // `Object.hasOwn`, never `key in source`: `{ ...source }` above already dropped what a
85
+ // prototype carries, so an `in` check put it back — a schema field named `toString` or
86
+ // `valueOf` was coerced from the INHERITED member and handed validation a function.
87
+ if (Object.hasOwn(source, key)) out[key] = coerceNode(child, source[key]);
85
88
  }
86
89
  return out;
87
90
  }
@@ -111,11 +114,17 @@ export function coerceNode(node: SchemaNode, raw: unknown): unknown {
111
114
  }
112
115
  }
113
116
 
117
+ /**
118
+ * `Object.create(null)`, for the reason `@ultimat3/http`'s `parseQuery` already uses one: this
119
+ * record is built from caller-controlled keys. On a `{}` literal `out['__proto__'] = …` hits the
120
+ * prototype accessor instead of declaring a key, and every member of `Object.prototype` reads
121
+ * back as present when the client sent nothing.
122
+ */
114
123
  function toRecord(source: QuerySource): Record<string, string | readonly string[] | undefined> {
124
+ const out = Object.create(null) as Record<string, string | readonly string[] | undefined>;
115
125
  if (!(source instanceof URLSearchParams)) {
116
- return { ...source };
126
+ return Object.assign(out, source);
117
127
  }
118
- const out: Record<string, string | readonly string[]> = {};
119
128
  for (const key of new Set(source.keys())) {
120
129
  const all = source.getAll(key);
121
130
  out[key] = all.length > 1 ? all : (all[0] as string);
@@ -134,7 +143,8 @@ export function coerceQuery(schema: unknown, source: QuerySource): Record<string
134
143
 
135
144
  const out: Record<string, unknown> = { ...record };
136
145
  for (const [key, child] of Object.entries(node.properties)) {
137
- if (!(key in record)) continue;
146
+ // See `toRecord`: a declared property is coerced only when the caller actually sent it.
147
+ if (!Object.hasOwn(record, key)) continue;
138
148
  const raw = record[key];
139
149
  out[key] = coerceNode(child, child.kind === 'array' ? (raw ?? []) : normaliseSingle(raw));
140
150
  }
package/src/provider.ts CHANGED
@@ -11,8 +11,10 @@ export interface SchemaProvider {
11
11
  readonly vendor: string;
12
12
  readonly t: TNamespace;
13
13
  /**
14
- * Return the IR for one of this provider's schemas. Required for OpenAPI and MCP tool
15
- * schemas; omit it only if the provider also supplies `toJsonSchema`.
14
+ * Return the IR for one of this provider's schemas. Required for OpenAPI, MCP tool schemas
15
+ * and the admin form generator: `toJsonSchema()` calls it unconditionally and throws
16
+ * `X_SCHEMA_UNSUPPORTED` without it. There is no second way to describe a schema — the IR is
17
+ * the one projection surface, and every generator reads it.
16
18
  */
17
19
  introspect?(schema: unknown): SchemaNode | undefined;
18
20
  }