Sign In

@ultimat3/schema

Package Overview
Dependencies
Maintainers
1
Versions
20
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
3.0.0
to
4.0.0
+23
src/iso-date.ts
// Single responsibility: the one rule deciding whether a date-time STRING names an instant on its
// own. Its own file because both the validator and the HTTP coercion have to answer identically —
// two copies of this pair is how the wire path and the validation path came to disagree.
/**
* A clock time, and the zone it is stated in. `2026-08-19T10:00` carries the first and not the
* second, so `new Date` resolves it through the HOST process's zone: the same wire value is
* `14:00Z` on a `TZ=America/New_York` pod and `10:00Z` on a `TZ=UTC` one, from one request.
* That is the framework's "no date without an explicit zone, no ambient default anywhere" rule
* failing at the parse end rather than the format end.
*
* A date-only form carries no clock time and is UTC by specification, so it is not this.
*
* The same pair `@ultimat3/time`'s `fromIso` refuses on, character for character: one wire value,
* one rule, whichever door it comes through.
*/
const CLOCK_TIME = /[t ]\d{1,2}:\d{2}/i;
const UTC_OFFSET = /(?:z|[+-]\d{2}:?\d{2})$/i;
/** What a `t.date` string must not be: a clock time with no offset and no `Z`. */
export function isZonelessDateTime(value: string): boolean {
return CLOCK_TIME.test(value) && !UTC_OFFSET.test(value);
}
+11
-1

@@ -18,3 +18,5 @@ # @ultimat3/schema — agent notes

`describe-value → node → builder → money-value → validators → discriminated-union → provider → t`.
`standard.ts` and `errors.ts` depend on nothing but each other.
`standard.ts` and `errors.ts` depend on nothing but each other. `iso-date.ts` imports nothing and
is imported by `validators.ts` and `coerce.ts` — the two doors a `t.date` string comes through, so
the rule that a clock time must carry an offset or `Z` has one copy, not one per door.

@@ -79,2 +81,10 @@ **An issue message is a public surface.** `@ultimat3/http` folds it into `X_BODY_INVALID`'s `cause`,

- Unknown object keys are dropped by design; JSON Schema says `additionalProperties: false`.
- **An object parse reads every declared field with `Object.hasOwn` and answers a null-prototype
object** — the same two halves `recordSchema` has. A raw `value[key]` read `toString` off the
PROTOTYPE, so a field named after one was unsatisfiable for every input and its `.default()`
never fired; a `{}` output let a declared `__proto__` field write through the setter. Never
reintroduce either half, and never assume a parsed object has `Object.prototype` on it.
- `t.date` refuses a clock time with no offset and no `Z` (`iso-date.ts`): a zone-less string is a
different instant per host `TZ`, and `coerceQuery` puts it one query parameter from the wire.
A date-only string carries no clock time and is UTC by spec, so it still parses.
- Adding a `SchemaKind` means updating `json-schema.ts` and `coerce.ts` in the same commit.

@@ -81,0 +91,0 @@ - **Prefer a new `SchemaNode` FIELD to a new `SchemaKind`.** Every consumer that switches on `kind`

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

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

@@ -5,2 +5,3 @@ // Single responsibility: HTTP-boundary coercion. Kept out of validation on purpose — only the

import { isZonelessDateTime } from './iso-date';
import type { SchemaNode } from './node';

@@ -60,2 +61,6 @@ import { tryIntrospect } from './provider';

if (typeof raw !== 'string') return raw;
// Returned untouched rather than converted, this function's standing contract: converting
// it resolves a zone-less clock time through the CONTAINER's `TZ`, so `?at=2026-08-19T10:00`
// meant a different instant per pod. Validation states the refusal.
if (isZonelessDateTime(raw)) return raw;
const parsed = new Date(raw);

@@ -62,0 +67,0 @@ return Number.isNaN(parsed.getTime()) ? raw : parsed;

@@ -22,2 +22,3 @@ // Single responsibility: the builtin, dependency-free validators behind `t`. Small on purpose —

import { discriminatedUnionSchema } from './discriminated-union';
import { isZonelessDateTime } from './iso-date';
import { type MoneyValue, moneySchema } from './money-value';

@@ -176,5 +177,15 @@ import type { SchemaNode } from './node';

const issues: StandardIssue[] = [];
const out: Record<string, unknown> = {};
// `Object.create(null)`, for `recordSchema`'s reason one screen down: on a `{}` literal
// `out['__proto__'] = value` hits the `Object.prototype` SETTER, so a DECLARED `__proto__`
// field parsed to `{}` whose prototype was the caller's object — every key of it then read
// back off a value nobody sent.
const out: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
for (const [key, memberCheck] of checks) {
const result = memberCheck(value[key], [...path, key]);
// `Object.hasOwn`, never a raw index — the guard `coerce.ts` already applies to the same
// read. `value['toString']` answered the INHERITED function for an input that carried no
// such key, so a field named after a prototype member was unsatisfiable for every input,
// its `.default()` could never fire, and the HTTP path and this one disagreed about what
// the caller sent.
const raw = Object.hasOwn(value, key) ? value[key] : undefined;
const result = memberCheck(raw, [...path, key]);
if (result.ok) {

@@ -354,2 +365,9 @@ // Unknown keys are dropped, never forwarded — no mass assignment through an action.

if (typeof value === 'string' || typeof value === 'number') {
// Enforced, not documented: the node publishes `format: 'date-time'` and this refusal has
// always said ISO-8601, while `new Date` resolved a zone-less clock time through the host
// process's zone — reachable from the wire, since `coerceQuery` routes a `t.date` field
// here and a container's `TZ` then decided which instant a query parameter meant.
if (typeof value === 'string' && isZonelessDateTime(value)) {
return fail(path, expected('an ISO-8601 date-time with an offset or Z', value));
}
const parsed = new Date(value);

@@ -356,0 +374,0 @@ return Number.isNaN(parsed.getTime())