New:Socket for Asana Is Now Available.Learn more
Sign In

@ultimat3/schema

Package Overview
Dependencies
Maintainers
1
Versions
23
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/schema - npm Package Compare versions

Comparing version
13.0.0
to
14.0.0
+23
-0
CLAUDE.md

@@ -12,2 +12,3 @@ # @ultimat3/schema — agent notes

| **Issue messages** | the shape of the rejected value, **never its content** — see `describe-value.ts` |
| **Issue paths** | framework-chosen segments only. A `t.object` segment is a DECLARED field name; a `t.record` KEY is the caller's, so `recordSchema` names a failing entry by POSITION (`meta[3]`) |
| Coercion | HTTP boundary only — never call it from actions, jobs or MCP |

@@ -27,2 +28,13 @@ | Exports | explicit in `src/index.ts`; no `export *`; a namespace member and its free function ship together (`t.nullable`/`nullableSchema`) |

**An issue PATH is the same public surface as its message, `As of 2026-08-25`.** It travels
`formatIssue` -> `@ultimat3/http`'s `bodyInvalid` -> `X_BODY_INVALID`'s `cause` -> the problem
document **and** the log line, and `bodyInvalid`'s own doc block promises the `issues` it renders
"name only facts the framework itself chose". A `t.record` key is not one: a record keyed by an
email address, a phone number or a pasted credential wrote every one of them into the central log
index, in the shape the password bug did. `recordSchema` emits the entry's INDEX — the segment
`arraySchema` already uses, so `formatPath` renders `meta[3]` with no new spelling — because
dropping the segment entirely makes three failing entries render three identical lines. It is
`Object.entries` order, so it names an entry that exists rather than a byte offset in the body.
A `t.object` segment stays as written: the schema author chose it, not the caller.
**An issue message is a public surface.** `@ultimat3/http` folds it into `X_BODY_INVALID`'s `cause`,

@@ -90,2 +102,13 @@ which is returned to the caller AND interpolated into the log line — and core's logger redacts by

`t.number.int()` demands `Number.isSafeInteger`, not `Number.isInteger` (`As of 2026-08-25`) — the
defect `money-value.ts` carries the write-up for having fixed one file over, and `@ultimat3/entity`'s
`columns.ts` had it right too. `t.number.int()` was the one that did not get the fix: it accepted
`2 ** 53` at the boundary as a 200, the policy gate and the handler ran, and the ROW WRITE refused
it as a 500 — the same value refused twice, once with a field path and once without. `json-schema.ts`
publishes the same bound (`minimum`/`maximum` at `±Number.MAX_SAFE_INTEGER`, a caller's own bound
when it is narrower and clamped when it is not), because a contract promising what the parser
refuses is that disagreement one layer out. The IR node is untouched — `@ultimat3/action`'s
`sampleNumber` reads `node.minimum`, and a default there would make every generated contract sample
`-9007199254740991`.
Gotchas:

@@ -92,0 +115,0 @@ - `Schema<In, Out>` splits input from output: `.default()` makes the key optional on input and

+1
-1
{
"name": "@ultimat3/schema",
"version": "13.0.0",
"version": "14.0.0",
"description": "Ultimate's validation seam: Standard Schema interface, the t namespace, JSON Schema output",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -114,2 +114,14 @@ # ✅ @ultimat3/schema

**The issue's PATH is the same public surface**, so it too names only what the framework chose. A
`t.object` segment is a declared field name and stays as written; a `t.record` KEY is the caller's
data, so a failing record entry is named by POSITION — `meta[3]`, the segment `t.array` already
uses. `@ultimat3/http`'s `bodyInvalid` states this contract in its own doc block: the `issues` it
renders "name only facts the framework itself chose". A record keyed by an email address, a phone
number or a pasted credential wrote every one of them into the log index otherwise.
`t.number.int()` demands a **safe** integer. `2 ** 53` is a whole number, and accepting it here
meant the boundary answered 200 and the row write answered 500 for the same value. The published
JSON Schema carries the same bound: `minimum`/`maximum` at `±Number.MAX_SAFE_INTEGER`, or your own
bound where it is narrower.
`error.issues` is `{ path, expected, received, message }[]` with paths like `items[0].price`;

@@ -116,0 +128,0 @@ `formatIssues()` renders one line per issue for the dev overlay. `validate()` never throws and

@@ -24,2 +24,10 @@ // Single responsibility: render a REJECTED value as its shape, and the `expected X, received Y`

* infinities name themselves because "received a number" for a `NaN` reads as a lie.
*
* **The absolute is about caller data, not only about the rejected VALUE.** An issue's `path` is
* the other half of the same string, and a `t.record` KEY is the caller's — so `recordSchema`
* names a failing entry by its POSITION and never by its key, and `validators.test.ts` is that
* half's enforcement as `describe-value.test.ts` is this half's. A `t.object` path segment is a
* DECLARED field name and stays as written: the schema author chose it, not the caller.
* `@ultimat3/http`'s `bodyInvalid` is the seam that makes this binding rather than tasteful — its
* own doc block promises the `issues` it renders "name only facts the framework itself chose".
*/

@@ -26,0 +34,0 @@ export function describeValue(value: unknown): string {

@@ -98,2 +98,35 @@ // Single responsibility: SchemaNode -> JSON Schema. Load-bearing: OpenAPI request/response

/**
* The range an `integer` node's validator actually enforces — `Number.isSafeInteger`, the rule
* `validators.ts` applies and `money-value.ts` already published. Spelled `-MAX_SAFE_INTEGER`
* rather than `MIN_SAFE_INTEGER` (they are the same number) so the two projections of one rule
* read identically.
*
* A caller's own bound is the published one when it is NARROWER, and is clamped when it is not:
* `t.number.int().max(2 ** 60)` refuses `2 ** 60` at the boundary whatever the node says, so
* publishing it would be a promise the parser breaks — which is the disagreement this whole
* function exists to prevent, one layer out.
*/
const SAFE_INTEGER_MIN = -Number.MAX_SAFE_INTEGER;
const SAFE_INTEGER_MAX = Number.MAX_SAFE_INTEGER;
/**
* A non-integer `t.number` publishes only what the caller declared: it accepts every finite
* double, so a safe-integer range on it would tell a generated client to refuse `0.5`.
*/
function numberNode(node: SchemaNode): JsonSchema {
const integer = node.integer === true;
const minimum = integer
? Math.max(node.minimum ?? SAFE_INTEGER_MIN, SAFE_INTEGER_MIN)
: node.minimum;
const maximum = integer
? Math.min(node.maximum ?? SAFE_INTEGER_MAX, SAFE_INTEGER_MAX)
: node.maximum;
return {
type: integer ? 'integer' : 'number',
...(minimum === undefined ? {} : { minimum }),
...(maximum === undefined ? {} : { maximum }),
};
}
/**
* JSON Schema's `pattern` is an ECMA-262 source with no flag syntax, so a flagged pattern is

@@ -145,7 +178,3 @@ * stated in prose instead of silently narrowed: a consumer applying `pattern` alone would refuse

case 'number':
return annotate({
type: node.integer === true ? 'integer' : 'number',
...(node.minimum === undefined ? {} : { minimum: node.minimum }),
...(node.maximum === undefined ? {} : { maximum: node.maximum }),
});
return annotate(numberNode(node));
case 'boolean':

@@ -152,0 +181,0 @@ return annotate({ type: 'boolean' });

@@ -131,4 +131,10 @@ // Single responsibility: the builtin, dependency-free validators behind `t`. Small on purpose —

}
if (node.integer === true && !Number.isInteger(value)) {
return fail(path, expected('an integer', value));
// Safe, not merely whole — the same rule `money-value.ts` states one file over, for the same
// reason: `Number.isInteger` let 2^53 through the boundary as a 200, so the policy gate and
// the handler ran and the ROW WRITE refused it as a 500 (`entity`'s `columns.ts` demands
// `Number.isSafeInteger`). The same value refused twice, once with a field path and once
// without. Above 2^53 a double cannot name its own successor, so a "whole" number there is
// already a rounded one.
if (node.integer === true && !Number.isSafeInteger(value)) {
return fail(path, expected('a safe integer', value));
}

@@ -283,11 +289,28 @@ if (node.minimum !== undefined && value < node.minimum) {

const out: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
for (const [key, entry] of Object.entries(value)) {
for (const [index, [key, entry]] of Object.entries(value).entries()) {
// The entry's POSITION, never its key. `describe-value.ts`'s absolute is stated about the
// rejected value, and a record's KEY is the caller's data just as much: the path travels
// `formatIssue` -> `bodyInvalid`'s `cause` -> the problem document AND the log line, and
// core's logger redacts by key, so a key baked into a string has no key left to redact.
// `@ultimat3/http`'s `bodyInvalid` states the contract this was breaking in its own doc
// block — *"`issues` … must name only facts the framework itself chose"*. A record keyed
// by an email address, a phone number or a pasted credential wrote every one of them into
// the central log index, in the same shape the password bug did.
//
// The index rather than nothing at all: the segment's whole job is to say WHICH entry
// failed, and dropping it makes three failing entries render three identical lines. It is
// the same segment `arraySchema` already uses, so `formatPath` renders `meta[3]` with no
// new spelling to learn. It is `Object.entries` order — integer-like keys sort ahead of
// string ones — so it names an entry that exists rather than a byte offset in the body.
const at = [...path, index];
if (PROTOTYPE_KEYS.has(key)) {
issues.push({
// The refused NAMES are a closed set declared right here, so the message may state
// them: that is the one part of this line the caller did not choose.
message: expected(`a record key that is not ${[...PROTOTYPE_KEYS].join(' | ')}`, key),
path: [...path, key],
path: at,
});
continue;
}
const result = valueCheck(entry, [...path, key]);
const result = valueCheck(entry, at);
if (result.ok) out[key] = result.value;

@@ -294,0 +317,0 @@ else issues.push(...result.issues);