@blamejs/core 0.7.4 → 0.7.18
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/CHANGELOG.md +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -319
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/safe-schema.js
CHANGED
|
@@ -1,1266 +1,1266 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* safe-schema — declarative input validation, Zod-shaped surface.
|
|
4
|
-
*
|
|
5
|
-
* Built for: request-body validation, config validation, API payload
|
|
6
|
-
* validation, anywhere operators have an `unknown` shape they need
|
|
7
|
-
* to confirm before reading. Vendor-free; built on framework
|
|
8
|
-
* primitives. No JIT, no codegen, no chained-Promise weirdness.
|
|
9
|
-
*
|
|
10
|
-
* Public API:
|
|
11
|
-
* var s = b.safeSchema;
|
|
12
|
-
*
|
|
13
|
-
* var schema = s.object({
|
|
14
|
-
* email: s.string().email(),
|
|
15
|
-
* age: s.number().int().min(0).max(120),
|
|
16
|
-
* tags: s.array(s.string()).max(10),
|
|
17
|
-
* metadata: s.object({}).passthrough().optional(),
|
|
18
|
-
* });
|
|
19
|
-
*
|
|
20
|
-
* var result = schema.parse(input); // throws SafeSchemaError
|
|
21
|
-
* var safe = schema.safeParse(input); // → { ok, value?, errors? }
|
|
22
|
-
*
|
|
23
|
-
* // Errors carry per-field paths:
|
|
24
|
-
* // [{ path: ["age"], code: "number/too-large", message: "must be ≤ 120" }]
|
|
25
|
-
*
|
|
26
|
-
* Design choices:
|
|
27
|
-
* - Schemas are immutable. Chaining returns a new schema with one
|
|
28
|
-
* additional check; the original is untouched. Cheap because
|
|
29
|
-
* checks are concat'd into a small array, not deep-copied.
|
|
30
|
-
* - parse() throws SafeSchemaError carrying the full issues array;
|
|
31
|
-
* safeParse() never throws (operator-friendly for HTTP boundaries).
|
|
32
|
-
* - .optional() means "may be undefined"; .nullable() means "may be
|
|
33
|
-
* null"; .default(v) means "if undefined, substitute v";
|
|
34
|
-
* .catch(v) means "on ANY validation failure, substitute v".
|
|
35
|
-
* These compose: optional().default(0) → "may be undefined,
|
|
36
|
-
* in which case use 0".
|
|
37
|
-
* - Objects are STRICT by default: unknown keys produce an issue.
|
|
38
|
-
* Use .passthrough() to retain unknown keys, .strict() to flip
|
|
39
|
-
* back if a parent .passthrough() set the mode.
|
|
40
|
-
*
|
|
41
|
-
* Surface (every schema has these chained methods unless noted):
|
|
42
|
-
*
|
|
43
|
-
* Type constructors:
|
|
44
|
-
* string() .min, .max, .length, .regex, .email, .url, .uuid,
|
|
45
|
-
* .datetime (ISO-8601), .date (YYYY-MM-DD),
|
|
46
|
-
* .ip, .ipv4, .ipv6, .nonempty, .startsWith, .endsWith,
|
|
47
|
-
* .includes
|
|
48
|
-
* number() .int, .min, .max, .gt, .lt, .positive, .negative,
|
|
49
|
-
* .nonnegative, .nonpositive, .finite, .multipleOf
|
|
50
|
-
* boolean()
|
|
51
|
-
* literal(v)
|
|
52
|
-
* enum_([...]) | oneOf([...])
|
|
53
|
-
* null_(), undefined_(), any(), unknown()
|
|
54
|
-
*
|
|
55
|
-
* Composites:
|
|
56
|
-
* object({ ... }) .strict, .passthrough, .pick, .omit, .extend,
|
|
57
|
-
* .partial, .required (inverse of partial)
|
|
58
|
-
* array(item) .min, .max, .length, .nonempty
|
|
59
|
-
* tuple([...]) .rest(item) for variadic tails
|
|
60
|
-
* union([...]) first matching wins
|
|
61
|
-
* discriminatedUnion(key, [...]) faster + clearer-errors variant
|
|
62
|
-
* for tagged unions
|
|
63
|
-
* record(value) | record(key, value)
|
|
64
|
-
* lazy(() => schema) defer construction; for recursion
|
|
65
|
-
* preprocess(fn, schema) run fn before validation
|
|
66
|
-
*
|
|
67
|
-
* Modifiers (any schema):
|
|
68
|
-
* .optional() value may be undefined
|
|
69
|
-
* .nullable() value may be null
|
|
70
|
-
* .default(v|fn) undefined → v (implies optional). Function form
|
|
71
|
-
* is called per-parse for fresh values.
|
|
72
|
-
* .catch(v|fn) any failure → v (escape hatch for operator
|
|
73
|
-
* defaults; suppresses the error info, so use
|
|
74
|
-
* sparingly)
|
|
75
|
-
* .refine(fn, opts) custom predicate — returns false to fail
|
|
76
|
-
* .transform(fn) map the validated value to a new shape
|
|
77
|
-
* .pipe(next) feed validated output through `next` schema
|
|
78
|
-
* for a second round of validation
|
|
79
|
-
*
|
|
80
|
-
* Security guarantees:
|
|
81
|
-
* - Prototype-pollution defense: __proto__ / constructor / prototype
|
|
82
|
-
* keys are rejected at construction (object shape) and parse time
|
|
83
|
-
* (object + record input). Mirrors safe-json.js's POISONED_KEYS.
|
|
84
|
-
* - No code injection surface: regexes are static module-level
|
|
85
|
-
* constants; no string→regex parsing on the validation path; no
|
|
86
|
-
* eval/Function. Operator-supplied refine/transform fns are
|
|
87
|
-
* plain JS functions, not strings.
|
|
88
|
-
* - Predicate throws are caught: a refine() function throwing turns
|
|
89
|
-
* into a regular validation issue, not an unhandled exception.
|
|
90
|
-
* - Sync-only: no async refinements; operators await at the boundary.
|
|
91
|
-
*
|
|
92
|
-
* Deliberately not shipped (with structural reason):
|
|
93
|
-
* - z.bigint / z.date / z.map / z.set — no JSON representation; HTTP
|
|
94
|
-
* boundaries don't carry these. Use s.string().datetime() for
|
|
95
|
-
* ISO-8601 strings.
|
|
96
|
-
* - z.nativeEnum / z.never / z.void / z.function — TypeScript-specific.
|
|
97
|
-
* - z.coerce — loose-coercion is a security foot-gun (truthy/falsy
|
|
98
|
-
* ambiguity, "0" → 0 vs "0" → "0"). Operators do explicit
|
|
99
|
-
* s.preprocess(fn, schema) instead.
|
|
100
|
-
* - z.intersection — for object schemas use .extend(); intersections
|
|
101
|
-
* of unrelated schemas are structurally ambiguous.
|
|
102
|
-
* - z.brand — TypeScript compile-time tag with no runtime effect.
|
|
103
|
-
* - per-schema errorMap — operators chain .refine() with custom message.
|
|
104
|
-
*
|
|
105
|
-
* Relationship to forms.validate:
|
|
106
|
-
* forms.validate (HTML form spec validation) is a separate surface.
|
|
107
|
-
* Form specs carry HTML-specific concerns (checkbox coercion, select
|
|
108
|
-
* option allowlist) that don't belong on the general-purpose validator,
|
|
109
|
-
* so the two stay distinct rather than one wrapping the other.
|
|
110
|
-
*/
|
|
111
|
-
|
|
112
|
-
var C = require("./constants");
|
|
113
|
-
var safeJson = require("./safe-json");
|
|
114
|
-
var { defineClass } = require("./framework-error");
|
|
115
|
-
|
|
116
|
-
// Maximum URL length per RFC 7230 §3.1.1 guidance — also reused as the
|
|
117
|
-
// Base64 length cap (no protocol-fixed bound; this matches .url()).
|
|
118
|
-
var URL_MAX_LEN = C.BYTES.kib(8);
|
|
119
|
-
|
|
120
|
-
// Per-format defensive length caps. Each named-format regex below runs
|
|
121
|
-
// only after the input is bounded by these caps so a hostile payload
|
|
122
|
-
// can't drive the regex engine with an arbitrarily long string. Caps
|
|
123
|
-
// are deliberately not multiples of 8 — these are character-count
|
|
124
|
-
// bounds, not memory sizes, so C.BYTES.* helpers don't apply.
|
|
125
|
-
var EMAIL_MAX_LEN = 254; // RFC 5321 §4.5.3.1.3 forward-path bound
|
|
126
|
-
var UUID_MAX_LEN = 50; // RFC 4122 UUID is 36 chars; slack for whitespace edge cases
|
|
127
|
-
var DATE_MAX_LEN = 30; // YYYY-MM-DD is 10 chars
|
|
128
|
-
var DATETIME_MAX_LEN = 100; // ISO-8601 with offset + fractional seconds tops near 35
|
|
129
|
-
var CUID_MAX_LEN = 50; // CUID v1/v2 is 25 chars
|
|
130
|
-
var ULID_MAX_LEN = 50; // ULID is exactly 26 chars
|
|
131
|
-
|
|
132
|
-
var SafeSchemaError = defineClass("SafeSchemaError", { alwaysPermanent: true });
|
|
133
|
-
|
|
134
|
-
// Prototype-pollution defense — these key names are rejected in object
|
|
135
|
-
// and record schemas regardless of mode (strict, passthrough, or lazy
|
|
136
|
-
// match). Mirrors safe-json.js's POISONED_KEYS set so the framework
|
|
137
|
-
// presents one consistent guarantee: an attacker cannot pollute
|
|
138
|
-
// Object.prototype by submitting a JSON body with __proto__ /
|
|
139
|
-
// constructor / prototype keys, even if the operator schema is
|
|
140
|
-
// .passthrough().
|
|
141
|
-
var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
142
|
-
|
|
143
|
-
// Pragmatic regexes — RFC-correct is impractical without exploding the
|
|
144
|
-
// regex (especially email). Operators wanting deeper validation chain
|
|
145
|
-
// .refine() on top.
|
|
146
|
-
//
|
|
147
|
-
// All regexes are static module-level constants; nothing parses an input
|
|
148
|
-
// string into a regex on the validation path (no ReDoS-via-input vector,
|
|
149
|
-
// no dynamic regex compilation).
|
|
150
|
-
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
151
|
-
var URL_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s]+$/;
|
|
152
|
-
var UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
|
|
153
|
-
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
154
|
-
// ISO-8601 datetime with timezone (Z or ±HH:MM); fractional seconds optional.
|
|
155
|
-
var DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
156
|
-
var IPV4_RE = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
157
|
-
// CUID v1 / v2: 25-char base36, starts with 'c'. Common in TypeScript ecosystems.
|
|
158
|
-
var CUID_RE = /^c[a-z0-9]{24}$/;
|
|
159
|
-
// ULID: Crockford-base32, 26 chars, time-sortable.
|
|
160
|
-
var ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
161
|
-
// base64 (standard alphabet, with optional padding). Base64url variants
|
|
162
|
-
// rejected — operators chain .regex(...) for that.
|
|
163
|
-
var BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
164
|
-
// IPv6 structural pattern — full 8-hextet, every `::`-compressed shape,
|
|
165
|
-
// `::` and `::1` literals, IPv4-mapped (`::ffff:1.2.3.4`), and 6-prefix
|
|
166
|
-
// + IPv4 tail. Adapted from validator.js (Apache-2.0); zone IDs
|
|
167
|
-
// (`fe80::1%eth0`) are deliberately omitted — the framework rejects
|
|
168
|
-
// them as non-portable, matching `safe-json.formats.ipv6`. Bounded
|
|
169
|
-
// quantifiers, no nested-quantifier alternation, ReDoS-safe.
|
|
170
|
-
//
|
|
171
|
-
// `.ipv6()` schema method delegates to `safeJson.formats.ipv6` for
|
|
172
|
-
// stricter algorithmic validation; this regex is exported as a
|
|
173
|
-
// structural pattern for operators who want it directly.
|
|
174
|
-
var IPV6_RE = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
|
175
|
-
|
|
176
|
-
// ---- helpers ----
|
|
177
|
-
|
|
178
|
-
function _fail(path, code, message) {
|
|
179
|
-
return { ok: false, issues: [{ path: path.slice(), code: code, message: message }] };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function _formatIssues(issues) {
|
|
183
|
-
if (!issues || issues.length === 0) return "(no issues)";
|
|
184
|
-
return issues.map(function (i) {
|
|
185
|
-
var p = i.path && i.path.length > 0 ? i.path.join(".") + ": " : "";
|
|
186
|
-
return p + i.message;
|
|
187
|
-
}).join("; ");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Run a schema's modifier-aware pipeline. Used by both root parse() and
|
|
191
|
-
// child-key dispatch inside object/array/etc.
|
|
192
|
-
function _runWithModifiers(schema, value, path) {
|
|
193
|
-
// Apply default BEFORE catching, so a schema with both .default() and
|
|
194
|
-
// .catch() prefers the default for undefined input (predictable).
|
|
195
|
-
if (value === undefined) {
|
|
196
|
-
if (schema._hasDefault) value = typeof schema._default === "function" ? schema._default() : schema._default;
|
|
197
|
-
else if (schema._isOptional) return { ok: true, value: undefined };
|
|
198
|
-
else return _fail(path, "required", "is required");
|
|
199
|
-
}
|
|
200
|
-
if (value === null) {
|
|
201
|
-
if (schema._isNullable) return { ok: true, value: null };
|
|
202
|
-
if (schema._hasCatch) return { ok: true, value: schema._catch };
|
|
203
|
-
return _fail(path, "type", "must not be null");
|
|
204
|
-
}
|
|
205
|
-
var r = schema._run(value, path);
|
|
206
|
-
if (!r.ok && schema._hasCatch) {
|
|
207
|
-
return { ok: true, value: typeof schema._catch === "function" ? schema._catch() : schema._catch };
|
|
208
|
-
}
|
|
209
|
-
return r;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ---- core schema factory ----
|
|
213
|
-
//
|
|
214
|
-
// Every schema stores:
|
|
215
|
-
// _kind — for diagnostics
|
|
216
|
-
// _isOptional, _isNullable, _hasDefault, _default, _hasCatch, _catch
|
|
217
|
-
// _run(value, path) — type check + checks pipeline
|
|
218
|
-
//
|
|
219
|
-
// Modifier methods (optional/nullable/default/catch/refine/transform) live
|
|
220
|
-
// on the prototype; chained methods are added by each constructor's
|
|
221
|
-
// builder (string adds .min/.max etc., array adds .min/.length, etc.).
|
|
222
|
-
|
|
223
|
-
function _baseSchema(spec) {
|
|
224
|
-
var s = {
|
|
225
|
-
_kind: spec.kind,
|
|
226
|
-
_isOptional: spec.isOptional || false,
|
|
227
|
-
_isNullable: spec.isNullable || false,
|
|
228
|
-
_hasDefault: spec.hasDefault || false,
|
|
229
|
-
_default: spec.defaultValue,
|
|
230
|
-
_hasCatch: spec.hasCatch || false,
|
|
231
|
-
_catch: spec.catchValue,
|
|
232
|
-
_run: spec.run,
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
s.parse = function (input) {
|
|
236
|
-
var r = _runWithModifiers(s, input, []);
|
|
237
|
-
if (r.ok) return r.value;
|
|
238
|
-
var err = new SafeSchemaError(
|
|
239
|
-
"safe-schema/invalid",
|
|
240
|
-
"validation failed: " + _formatIssues(r.issues)
|
|
241
|
-
);
|
|
242
|
-
err.issues = r.issues;
|
|
243
|
-
throw err;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
s.safeParse = function (input) {
|
|
247
|
-
var r = _runWithModifiers(s, input, []);
|
|
248
|
-
if (r.ok) return { ok: true, value: r.value };
|
|
249
|
-
return { ok: false, errors: r.issues };
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
s.optional = function () {
|
|
253
|
-
return _baseSchema(_extendSpec(spec, { isOptional: true }));
|
|
254
|
-
};
|
|
255
|
-
s.nullable = function () {
|
|
256
|
-
return _baseSchema(_extendSpec(spec, { isNullable: true }));
|
|
257
|
-
};
|
|
258
|
-
s.default = function (v) {
|
|
259
|
-
return _baseSchema(_extendSpec(spec, { isOptional: true, hasDefault: true, defaultValue: v }));
|
|
260
|
-
};
|
|
261
|
-
s.catch = function (v) {
|
|
262
|
-
return _baseSchema(_extendSpec(spec, { hasCatch: true, catchValue: v }));
|
|
263
|
-
};
|
|
264
|
-
s.refine = function (predicate, opts) {
|
|
265
|
-
opts = opts || {};
|
|
266
|
-
var code = opts.code || "refine";
|
|
267
|
-
var message = opts.message || "failed custom validation";
|
|
268
|
-
var inner = s;
|
|
269
|
-
return _baseSchema(_extendSpec(spec, {
|
|
270
|
-
run: function (value, path) {
|
|
271
|
-
var r = inner._run(value, path);
|
|
272
|
-
if (!r.ok) return r;
|
|
273
|
-
try {
|
|
274
|
-
if (!predicate(r.value)) return _fail(path, code, message);
|
|
275
|
-
} catch (e) {
|
|
276
|
-
return _fail(path, code,
|
|
277
|
-
message + " (predicate threw: " + ((e && e.message) || String(e)) + ")");
|
|
278
|
-
}
|
|
279
|
-
return r;
|
|
280
|
-
},
|
|
281
|
-
}));
|
|
282
|
-
};
|
|
283
|
-
s.transform = function (fn) {
|
|
284
|
-
var inner = s;
|
|
285
|
-
return _baseSchema(_extendSpec(spec, {
|
|
286
|
-
run: function (value, path) {
|
|
287
|
-
var r = inner._run(value, path);
|
|
288
|
-
if (!r.ok) return r;
|
|
289
|
-
try {
|
|
290
|
-
return { ok: true, value: fn(r.value) };
|
|
291
|
-
} catch (e) {
|
|
292
|
-
return _fail(path, "transform",
|
|
293
|
-
"transform threw: " + ((e && e.message) || String(e)));
|
|
294
|
-
}
|
|
295
|
-
},
|
|
296
|
-
}));
|
|
297
|
-
};
|
|
298
|
-
// .pipe(next) — feed this schema's validated output into another
|
|
299
|
-
// schema for a second round of validation. Common idiom for
|
|
300
|
-
// "validate input shape, transform, re-validate output shape":
|
|
301
|
-
//
|
|
302
|
-
// var port = s.string().regex(/^\d+$/).transform(Number).pipe(
|
|
303
|
-
// s.number().int().min(1).max(65535)
|
|
304
|
-
// );
|
|
305
|
-
s.pipe = function (next) {
|
|
306
|
-
if (!next || typeof next._run !== "function") {
|
|
307
|
-
throw new SafeSchemaError("safe-schema/bad-pipe",
|
|
308
|
-
"pipe: argument must be a schema");
|
|
309
|
-
}
|
|
310
|
-
var inner = s;
|
|
311
|
-
return _baseSchema(_extendSpec(spec, {
|
|
312
|
-
run: function (value, path) {
|
|
313
|
-
var r = inner._run(value, path);
|
|
314
|
-
if (!r.ok) return r;
|
|
315
|
-
return _runWithModifiers(next, r.value, path);
|
|
316
|
-
},
|
|
317
|
-
}));
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
return s;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function _extendSpec(spec, overrides) {
|
|
324
|
-
return {
|
|
325
|
-
kind: overrides.kind != null ? overrides.kind : spec.kind,
|
|
326
|
-
isOptional: overrides.isOptional != null ? overrides.isOptional : spec.isOptional,
|
|
327
|
-
isNullable: overrides.isNullable != null ? overrides.isNullable : spec.isNullable,
|
|
328
|
-
hasDefault: overrides.hasDefault != null ? overrides.hasDefault : spec.hasDefault,
|
|
329
|
-
defaultValue: overrides.hasDefault != null ? overrides.defaultValue : spec.defaultValue,
|
|
330
|
-
hasCatch: overrides.hasCatch != null ? overrides.hasCatch : spec.hasCatch,
|
|
331
|
-
catchValue: overrides.hasCatch != null ? overrides.catchValue : spec.catchValue,
|
|
332
|
-
run: overrides.run != null ? overrides.run : spec.run,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Adds a check fn into the schema's _run pipeline by composing it on top
|
|
337
|
-
// of the existing _run. Used by string/number/array builders to chain
|
|
338
|
-
// .min/.max/etc. onto an existing schema instance.
|
|
339
|
-
//
|
|
340
|
-
// The check fn returns either:
|
|
341
|
-
// { ok: true } — pass; keep the inner value unchanged
|
|
342
|
-
// { ok: true, value: newVal } — pass and mutate the value (used by
|
|
343
|
-
// ergonomic transforms like .trim())
|
|
344
|
-
// { ok: false, issues } — fail with the given issue
|
|
345
|
-
function _withCheck(schema, spec, check) {
|
|
346
|
-
var inner = schema;
|
|
347
|
-
return _baseSchema(_extendSpec(spec, {
|
|
348
|
-
run: function (value, path) {
|
|
349
|
-
var r = inner._run(value, path);
|
|
350
|
-
if (!r.ok) return r;
|
|
351
|
-
var cr = check(r.value, path);
|
|
352
|
-
if (!cr.ok) return cr;
|
|
353
|
-
if (Object.prototype.hasOwnProperty.call(cr, "value")) {
|
|
354
|
-
return { ok: true, value: cr.value };
|
|
355
|
-
}
|
|
356
|
-
return r;
|
|
357
|
-
},
|
|
358
|
-
}));
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ---- string ----
|
|
362
|
-
|
|
363
|
-
function string() {
|
|
364
|
-
var spec = {
|
|
365
|
-
kind: "string",
|
|
366
|
-
run: function (value, path) {
|
|
367
|
-
if (typeof value !== "string") return _fail(path, "type", "must be a string");
|
|
368
|
-
return { ok: true, value: value };
|
|
369
|
-
},
|
|
370
|
-
};
|
|
371
|
-
return _stringMethods(_baseSchema(spec), spec);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function _stringMethods(schema, spec) {
|
|
375
|
-
function chain(check) {
|
|
376
|
-
var next = _withCheck(schema, spec, check);
|
|
377
|
-
return _stringMethods(next, _extendSpec(spec, { run: next._run }));
|
|
378
|
-
}
|
|
379
|
-
schema.min = function (n, msg) {
|
|
380
|
-
return chain(function (v, p) {
|
|
381
|
-
return v.length >= n ? { ok: true } :
|
|
382
|
-
_fail(p, "string/too-short", msg || ("must be at least " + n + " characters"));
|
|
383
|
-
});
|
|
384
|
-
};
|
|
385
|
-
schema.max = function (n, msg) {
|
|
386
|
-
return chain(function (v, p) {
|
|
387
|
-
return v.length <= n ? { ok: true } :
|
|
388
|
-
_fail(p, "string/too-long", msg || ("must be at most " + n + " characters"));
|
|
389
|
-
});
|
|
390
|
-
};
|
|
391
|
-
schema.length = function (n, msg) {
|
|
392
|
-
return chain(function (v, p) {
|
|
393
|
-
return v.length === n ? { ok: true } :
|
|
394
|
-
_fail(p, "string/wrong-length", msg || ("must be exactly " + n + " characters"));
|
|
395
|
-
});
|
|
396
|
-
};
|
|
397
|
-
schema.nonempty = function (msg) { return schema.min(1, msg || "must not be empty"); };
|
|
398
|
-
schema.regex = function (re, msg) {
|
|
399
|
-
return chain(function (v, p) {
|
|
400
|
-
return re.test(v) ? { ok: true } :
|
|
401
|
-
_fail(p, "string/regex", msg || "does not match required pattern");
|
|
402
|
-
});
|
|
403
|
-
};
|
|
404
|
-
schema.startsWith = function (prefix, msg) {
|
|
405
|
-
return chain(function (v, p) {
|
|
406
|
-
return v.indexOf(prefix) === 0 ? { ok: true } :
|
|
407
|
-
_fail(p, "string/starts-with", msg || "must start with '" + prefix + "'");
|
|
408
|
-
});
|
|
409
|
-
};
|
|
410
|
-
schema.endsWith = function (suffix, msg) {
|
|
411
|
-
return chain(function (v, p) {
|
|
412
|
-
return v.length >= suffix.length && v.slice(-suffix.length) === suffix ? { ok: true } :
|
|
413
|
-
_fail(p, "string/ends-with", msg || "must end with '" + suffix + "'");
|
|
414
|
-
});
|
|
415
|
-
};
|
|
416
|
-
schema.includes = function (needle, msg) {
|
|
417
|
-
return chain(function (v, p) {
|
|
418
|
-
return v.indexOf(needle) !== -1 ? { ok: true } :
|
|
419
|
-
_fail(p, "string/includes", msg || "must include '" + needle + "'");
|
|
420
|
-
});
|
|
421
|
-
};
|
|
422
|
-
schema.email = function () {
|
|
423
|
-
return chain(function (v, p) {
|
|
424
|
-
// RFC 5321 §4.5.3.1.3 — max forward-path is 256 octets including
|
|
425
|
-
// angle brackets, so the address itself is bounded at 254 chars.
|
|
426
|
-
// Without this cap an operator chaining .email() on a request body
|
|
427
|
-
// is open to a DoS shape (50 KB email -> downstream DB writes
|
|
428
|
-
// unbounded string columns, log lines, etc.). Operators with a
|
|
429
|
-
// legitimate non-RFC reason for longer emails skip .email() and
|
|
430
|
-
// chain .regex(custom) directly.
|
|
431
|
-
if (v.length > EMAIL_MAX_LEN) return _fail(p, "string/email-too-long",
|
|
432
|
-
"must be a valid email address (max " + EMAIL_MAX_LEN + " chars per RFC 5321)");
|
|
433
|
-
return EMAIL_RE.test(v) ? { ok: true } :
|
|
434
|
-
_fail(p, "string/email", "must be a valid email address");
|
|
435
|
-
});
|
|
436
|
-
};
|
|
437
|
-
schema.url = function () {
|
|
438
|
-
return chain(function (v, p) {
|
|
439
|
-
// RFC 9110 doesn't set a hard URL length, but RFC 7230 §3.1.1
|
|
440
|
-
// recommended 8000 octets and most HTTP origin servers + load
|
|
441
|
-
// balancers cap at 8 KB. Without this bound an operator chaining
|
|
442
|
-
// .url() on a request body was open to a 50 MB URL passing
|
|
443
|
-
// validation. Operators with a legitimate non-standard use
|
|
444
|
-
// (tunnels, proxies with embedded payloads) skip .url() and
|
|
445
|
-
// chain .regex(custom) directly.
|
|
446
|
-
if (v.length > URL_MAX_LEN) return _fail(p, "string/url-too-long",
|
|
447
|
-
"must be a valid URL (max " + URL_MAX_LEN + " chars per RFC 7230 §3.1.1 guidance)");
|
|
448
|
-
return URL_RE.test(v) ? { ok: true } :
|
|
449
|
-
_fail(p, "string/url", "must be a valid URL");
|
|
450
|
-
});
|
|
451
|
-
};
|
|
452
|
-
schema.uuid = function () {
|
|
453
|
-
return chain(function (v, p) {
|
|
454
|
-
// RFC 4122 UUID is 36 chars (8-4-4-4-12 + 4 dashes); cap defensively
|
|
455
|
-
// so a 50-MB string can't reach the regex engine.
|
|
456
|
-
if (typeof v !== "string" || v.length > UUID_MAX_LEN || !UUID_RE.test(v)) {
|
|
457
|
-
return _fail(p, "string/uuid", "must be a valid UUID");
|
|
458
|
-
}
|
|
459
|
-
return { ok: true };
|
|
460
|
-
});
|
|
461
|
-
};
|
|
462
|
-
schema.date = function () {
|
|
463
|
-
return chain(function (v, p) {
|
|
464
|
-
// YYYY-MM-DD is 10 chars; cap defensively before the regex test.
|
|
465
|
-
if (typeof v !== "string" || v.length > DATE_MAX_LEN || !DATE_RE.test(v)) {
|
|
466
|
-
return _fail(p, "string/date", "must be a YYYY-MM-DD date");
|
|
467
|
-
}
|
|
468
|
-
return { ok: true };
|
|
469
|
-
});
|
|
470
|
-
};
|
|
471
|
-
schema.datetime = function () {
|
|
472
|
-
return chain(function (v, p) {
|
|
473
|
-
// ISO-8601 with offset + fractional seconds tops out near 64 chars;
|
|
474
|
-
// cap defensively before the regex test.
|
|
475
|
-
if (typeof v !== "string" || v.length > DATETIME_MAX_LEN || !DATETIME_RE.test(v)) {
|
|
476
|
-
return _fail(p, "string/datetime", "must be an ISO-8601 datetime with timezone");
|
|
477
|
-
}
|
|
478
|
-
return { ok: true };
|
|
479
|
-
});
|
|
480
|
-
};
|
|
481
|
-
// IP-address validators delegate to safe-json's algorithmic format
|
|
482
|
-
// checks rather than re-running regex matches. The algorithmic path
|
|
483
|
-
// handles edge cases that pure regex misses (compressed `::` shapes,
|
|
484
|
-
// IPv4-mapped `::ffff:1.2.3.4`, multi-`::` rejection, group-count
|
|
485
|
-
// bounds) and keeps the framework's IP-validation behavior in one
|
|
486
|
-
// tested place. IPV4_RE / IPV6_RE remain exported for operators who
|
|
487
|
-
// want the structural pattern, but `.ipv4()` / `.ipv6()` / `.ip()`
|
|
488
|
-
// are the canonical validation surface.
|
|
489
|
-
schema.ipv4 = function () {
|
|
490
|
-
return chain(function (v, p) {
|
|
491
|
-
return (typeof v === "string" && safeJson.formats.ipv4(v))
|
|
492
|
-
? { ok: true }
|
|
493
|
-
: _fail(p, "string/ipv4", "must be a valid IPv4 address");
|
|
494
|
-
});
|
|
495
|
-
};
|
|
496
|
-
schema.ipv6 = function () {
|
|
497
|
-
return chain(function (v, p) {
|
|
498
|
-
return (typeof v === "string" && safeJson.formats.ipv6(v))
|
|
499
|
-
? { ok: true }
|
|
500
|
-
: _fail(p, "string/ipv6", "must be a valid IPv6 address");
|
|
501
|
-
});
|
|
502
|
-
};
|
|
503
|
-
schema.ip = function () {
|
|
504
|
-
return chain(function (v, p) {
|
|
505
|
-
return (typeof v === "string" && safeJson.formats.ip(v))
|
|
506
|
-
? { ok: true }
|
|
507
|
-
: _fail(p, "string/ip", "must be a valid IP address (v4 or v6)");
|
|
508
|
-
});
|
|
509
|
-
};
|
|
510
|
-
schema.cuid = function () {
|
|
511
|
-
return chain(function (v, p) {
|
|
512
|
-
// CUID v1/v2 is 25 chars; cap defensively before the regex test.
|
|
513
|
-
if (typeof v !== "string" || v.length > CUID_MAX_LEN || !CUID_RE.test(v)) {
|
|
514
|
-
return _fail(p, "string/cuid", "must be a valid CUID");
|
|
515
|
-
}
|
|
516
|
-
return { ok: true };
|
|
517
|
-
});
|
|
518
|
-
};
|
|
519
|
-
schema.ulid = function () {
|
|
520
|
-
return chain(function (v, p) {
|
|
521
|
-
// ULID is exactly 26 chars; cap defensively before the regex test.
|
|
522
|
-
if (typeof v !== "string" || v.length > ULID_MAX_LEN || !ULID_RE.test(v)) {
|
|
523
|
-
return _fail(p, "string/ulid", "must be a valid ULID");
|
|
524
|
-
}
|
|
525
|
-
return { ok: true };
|
|
526
|
-
});
|
|
527
|
-
};
|
|
528
|
-
schema.base64 = function () {
|
|
529
|
-
return chain(function (v, p) {
|
|
530
|
-
// Base64 has no protocol-fixed cap; bound at the same 8 KiB the
|
|
531
|
-
// .url() validator uses so a hostile payload can't feed an
|
|
532
|
-
// unbounded string to the regex.
|
|
533
|
-
if (typeof v !== "string" || v.length > URL_MAX_LEN) {
|
|
534
|
-
return _fail(p, "string/base64", "must be valid base64 (standard alphabet)");
|
|
535
|
-
}
|
|
536
|
-
return BASE64_RE.test(v) ? { ok: true } :
|
|
537
|
-
_fail(p, "string/base64", "must be valid base64 (standard alphabet)");
|
|
538
|
-
});
|
|
539
|
-
};
|
|
540
|
-
// Coercion-via-transform — these mutate the validated string before
|
|
541
|
-
// the next check runs. Apply EARLY in the chain (before .min, .email,
|
|
542
|
-
// etc.) so subsequent checks see the normalized form.
|
|
543
|
-
schema.trim = function () {
|
|
544
|
-
return chain(function (v) { return { ok: true, value: v.trim() }; });
|
|
545
|
-
};
|
|
546
|
-
schema.toLowerCase = function () {
|
|
547
|
-
return chain(function (v) { return { ok: true, value: v.toLowerCase() }; });
|
|
548
|
-
};
|
|
549
|
-
schema.toUpperCase = function () {
|
|
550
|
-
return chain(function (v) { return { ok: true, value: v.toUpperCase() }; });
|
|
551
|
-
};
|
|
552
|
-
return schema;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// ---- number ----
|
|
556
|
-
|
|
557
|
-
function number() {
|
|
558
|
-
var spec = {
|
|
559
|
-
kind: "number",
|
|
560
|
-
run: function (value, path) {
|
|
561
|
-
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
562
|
-
return _fail(path, "type", "must be a number");
|
|
563
|
-
}
|
|
564
|
-
return { ok: true, value: value };
|
|
565
|
-
},
|
|
566
|
-
};
|
|
567
|
-
return _numberMethods(_baseSchema(spec), spec);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function _numberMethods(schema, spec) {
|
|
571
|
-
function chain(check) {
|
|
572
|
-
var next = _withCheck(schema, spec, check);
|
|
573
|
-
return _numberMethods(next, _extendSpec(spec, { run: next._run }));
|
|
574
|
-
}
|
|
575
|
-
schema.int = function (msg) {
|
|
576
|
-
return chain(function (v, p) {
|
|
577
|
-
return Number.isInteger(v) ? { ok: true } :
|
|
578
|
-
_fail(p, "number/not-integer", msg || "must be an integer");
|
|
579
|
-
});
|
|
580
|
-
};
|
|
581
|
-
schema.min = function (n, msg) {
|
|
582
|
-
return chain(function (v, p) {
|
|
583
|
-
return v >= n ? { ok: true } : _fail(p, "number/too-small", msg || ("must be ≥ " + n));
|
|
584
|
-
});
|
|
585
|
-
};
|
|
586
|
-
schema.max = function (n, msg) {
|
|
587
|
-
return chain(function (v, p) {
|
|
588
|
-
return v <= n ? { ok: true } : _fail(p, "number/too-large", msg || ("must be ≤ " + n));
|
|
589
|
-
});
|
|
590
|
-
};
|
|
591
|
-
schema.gt = function (n, msg) {
|
|
592
|
-
return chain(function (v, p) {
|
|
593
|
-
return v > n ? { ok: true } : _fail(p, "number/not-gt", msg || ("must be > " + n));
|
|
594
|
-
});
|
|
595
|
-
};
|
|
596
|
-
schema.lt = function (n, msg) {
|
|
597
|
-
return chain(function (v, p) {
|
|
598
|
-
return v < n ? { ok: true } : _fail(p, "number/not-lt", msg || ("must be < " + n));
|
|
599
|
-
});
|
|
600
|
-
};
|
|
601
|
-
schema.positive = function (msg) { return schema.gt(0, msg || "must be positive"); };
|
|
602
|
-
schema.negative = function (msg) { return schema.lt(0, msg || "must be negative"); };
|
|
603
|
-
schema.nonnegative = function (msg) { return schema.min(0, msg || "must be non-negative"); };
|
|
604
|
-
schema.nonpositive = function (msg) { return schema.max(0, msg || "must be non-positive"); };
|
|
605
|
-
schema.finite = function (msg) {
|
|
606
|
-
return chain(function (v, p) {
|
|
607
|
-
return Number.isFinite(v) ? { ok: true } :
|
|
608
|
-
_fail(p, "number/not-finite", msg || "must be a finite number");
|
|
609
|
-
});
|
|
610
|
-
};
|
|
611
|
-
// safe() — Number.isSafeInteger range. Important for IDs that round-trip
|
|
612
|
-
// through JSON (which can't represent BigInts) and need to survive without
|
|
613
|
-
// precision loss.
|
|
614
|
-
schema.safe = function (msg) {
|
|
615
|
-
return chain(function (v, p) {
|
|
616
|
-
return Number.isSafeInteger(v) ? { ok: true } :
|
|
617
|
-
_fail(p, "number/not-safe", msg || "must be a safe integer (within ±2^53)");
|
|
618
|
-
});
|
|
619
|
-
};
|
|
620
|
-
schema.multipleOf = function (n, msg) {
|
|
621
|
-
return chain(function (v, p) {
|
|
622
|
-
// Use modulo with fp tolerance — exact mod on floats is fragile.
|
|
623
|
-
var quot = v / n;
|
|
624
|
-
return Math.abs(quot - Math.round(quot)) < 1e-9 ? { ok: true } :
|
|
625
|
-
_fail(p, "number/not-multiple-of", msg || ("must be a multiple of " + n));
|
|
626
|
-
});
|
|
627
|
-
};
|
|
628
|
-
return schema;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// ---- boolean ----
|
|
632
|
-
|
|
633
|
-
function boolean() {
|
|
634
|
-
return _baseSchema({
|
|
635
|
-
kind: "boolean",
|
|
636
|
-
run: function (value, path) {
|
|
637
|
-
if (typeof value !== "boolean") return _fail(path, "type", "must be a boolean");
|
|
638
|
-
return { ok: true, value: value };
|
|
639
|
-
},
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// ---- literal ----
|
|
644
|
-
|
|
645
|
-
function literal(expected) {
|
|
646
|
-
return _baseSchema({
|
|
647
|
-
kind: "literal",
|
|
648
|
-
run: function (value, path) {
|
|
649
|
-
if (value !== expected) {
|
|
650
|
-
return _fail(path, "literal", "must be exactly " + JSON.stringify(expected));
|
|
651
|
-
}
|
|
652
|
-
return { ok: true, value: value };
|
|
653
|
-
},
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// ---- enum / oneOf ----
|
|
658
|
-
|
|
659
|
-
function enum_(values) {
|
|
660
|
-
if (!Array.isArray(values) || values.length === 0) {
|
|
661
|
-
throw new SafeSchemaError("safe-schema/bad-enum",
|
|
662
|
-
"enum requires a non-empty array of allowed values");
|
|
663
|
-
}
|
|
664
|
-
var allowedSet = new Set(values);
|
|
665
|
-
return _baseSchema({
|
|
666
|
-
kind: "enum",
|
|
667
|
-
run: function (value, path) {
|
|
668
|
-
if (!allowedSet.has(value)) {
|
|
669
|
-
return _fail(path, "enum",
|
|
670
|
-
"must be one of: " + values.map(function (v) { return JSON.stringify(v); }).join(", "));
|
|
671
|
-
}
|
|
672
|
-
return { ok: true, value: value };
|
|
673
|
-
},
|
|
674
|
-
});
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// ---- null / undefined / any / unknown ----
|
|
678
|
-
|
|
679
|
-
function null_() {
|
|
680
|
-
return _baseSchema({
|
|
681
|
-
kind: "null",
|
|
682
|
-
isNullable: true,
|
|
683
|
-
run: function (value, path) {
|
|
684
|
-
// Modifier handler accepts null already; if we got here, value is non-null.
|
|
685
|
-
return _fail(path, "type", "must be null");
|
|
686
|
-
},
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function undefined_() {
|
|
691
|
-
return _baseSchema({
|
|
692
|
-
kind: "undefined",
|
|
693
|
-
isOptional: true,
|
|
694
|
-
run: function (_value, path) {
|
|
695
|
-
return _fail(path, "type", "must be undefined");
|
|
696
|
-
},
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function any() {
|
|
701
|
-
return _baseSchema({
|
|
702
|
-
kind: "any",
|
|
703
|
-
isOptional: true,
|
|
704
|
-
isNullable: true,
|
|
705
|
-
run: function (value) { return { ok: true, value: value }; },
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
function unknown() { return any(); }
|
|
710
|
-
|
|
711
|
-
// ---- object ----
|
|
712
|
-
|
|
713
|
-
function object(shape) {
|
|
714
|
-
if (shape == null || typeof shape !== "object") {
|
|
715
|
-
throw new SafeSchemaError("safe-schema/bad-shape",
|
|
716
|
-
"object() requires a shape object mapping field name to schema");
|
|
717
|
-
}
|
|
718
|
-
// Use getOwnPropertyNames so an operator who built the shape via
|
|
719
|
-
// Object.fromEntries / defineProperty (the only paths by which a
|
|
720
|
-
// POISONED_KEYS name can appear as an own property — object-literal
|
|
721
|
-
// {"__proto__": ...} syntax sets the prototype rather than creating
|
|
722
|
-
// such a key) gets a refusal at construction time.
|
|
723
|
-
var allOwnKeys = Object.getOwnPropertyNames(shape);
|
|
724
|
-
for (var ai = 0; ai < allOwnKeys.length; ai++) {
|
|
725
|
-
if (POISONED_KEYS.has(allOwnKeys[ai])) {
|
|
726
|
-
throw new SafeSchemaError("safe-schema/poisoned-shape-key",
|
|
727
|
-
"object shape: key '" + allOwnKeys[ai] + "' is forbidden (prototype-pollution defense)");
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
var keys = Object.keys(shape);
|
|
731
|
-
for (var k = 0; k < keys.length; k++) {
|
|
732
|
-
if (!shape[keys[k]] || typeof shape[keys[k]]._run !== "function") {
|
|
733
|
-
throw new SafeSchemaError("safe-schema/bad-shape",
|
|
734
|
-
"object shape: '" + keys[k] + "' is not a schema");
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
return _objectWithMode(shape, keys, "strict");
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function _objectWithMode(shape, keys, mode) {
|
|
741
|
-
var spec = {
|
|
742
|
-
kind: "object",
|
|
743
|
-
run: function (value, path) {
|
|
744
|
-
if (typeof value !== "object" || Array.isArray(value)) {
|
|
745
|
-
return _fail(path, "type", "must be an object");
|
|
746
|
-
}
|
|
747
|
-
var issues = [];
|
|
748
|
-
var out = {};
|
|
749
|
-
for (var i = 0; i < keys.length; i++) {
|
|
750
|
-
var key = keys[i];
|
|
751
|
-
var sub = shape[key];
|
|
752
|
-
var childPath = path.concat([key]);
|
|
753
|
-
var r = _runWithModifiers(sub, value[key], childPath);
|
|
754
|
-
if (!r.ok) {
|
|
755
|
-
for (var j = 0; j < r.issues.length; j++) issues.push(r.issues[j]);
|
|
756
|
-
continue;
|
|
757
|
-
}
|
|
758
|
-
if (r.value !== undefined) out[key] = r.value;
|
|
759
|
-
}
|
|
760
|
-
var inputKeys = Object.keys(value);
|
|
761
|
-
for (var ii = 0; ii < inputKeys.length; ii++) {
|
|
762
|
-
var ik = inputKeys[ii];
|
|
763
|
-
if (Object.prototype.hasOwnProperty.call(shape, ik)) continue;
|
|
764
|
-
// Prototype-pollution defense — refuse __proto__/constructor/
|
|
765
|
-
// prototype regardless of mode. .passthrough() never propagates
|
|
766
|
-
// these because they are always rejected as input.
|
|
767
|
-
if (POISONED_KEYS.has(ik)) {
|
|
768
|
-
issues.push({
|
|
769
|
-
path: path.concat([ik]),
|
|
770
|
-
code: "object/poisoned-key",
|
|
771
|
-
message: "key '" + ik + "' is forbidden (prototype-pollution defense)",
|
|
772
|
-
});
|
|
773
|
-
continue;
|
|
774
|
-
}
|
|
775
|
-
if (mode === "passthrough") {
|
|
776
|
-
out[ik] = value[ik];
|
|
777
|
-
} else {
|
|
778
|
-
issues.push({
|
|
779
|
-
path: path.concat([ik]),
|
|
780
|
-
code: "object/unknown-key",
|
|
781
|
-
message: "unknown key '" + ik + "' (use .passthrough() to allow extra keys)",
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
if (issues.length > 0) return { ok: false, issues: issues };
|
|
786
|
-
return { ok: true, value: out };
|
|
787
|
-
},
|
|
788
|
-
};
|
|
789
|
-
var schema = _baseSchema(spec);
|
|
790
|
-
schema.shape = shape;
|
|
791
|
-
schema.strict = function () { return _objectWithMode(shape, keys, "strict"); };
|
|
792
|
-
schema.passthrough = function () { return _objectWithMode(shape, keys, "passthrough"); };
|
|
793
|
-
|
|
794
|
-
// .pick(["a","b"]) → narrow to listed keys
|
|
795
|
-
schema.pick = function (pickKeys) {
|
|
796
|
-
var newShape = {};
|
|
797
|
-
for (var i = 0; i < pickKeys.length; i++) {
|
|
798
|
-
var k = pickKeys[i];
|
|
799
|
-
if (Object.prototype.hasOwnProperty.call(shape, k)) newShape[k] = shape[k];
|
|
800
|
-
}
|
|
801
|
-
return _objectWithMode(newShape, Object.keys(newShape), mode);
|
|
802
|
-
};
|
|
803
|
-
// .omit(["a","b"]) → drop listed keys
|
|
804
|
-
schema.omit = function (omitKeys) {
|
|
805
|
-
var omitSet = new Set(omitKeys);
|
|
806
|
-
var newShape = {};
|
|
807
|
-
var newKeys = [];
|
|
808
|
-
for (var i = 0; i < keys.length; i++) {
|
|
809
|
-
if (!omitSet.has(keys[i])) {
|
|
810
|
-
newShape[keys[i]] = shape[keys[i]];
|
|
811
|
-
newKeys.push(keys[i]);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
return _objectWithMode(newShape, newKeys, mode);
|
|
815
|
-
};
|
|
816
|
-
// .extend({ ... }) → merge additional shape; new keys override
|
|
817
|
-
schema.extend = function (additional) {
|
|
818
|
-
if (!additional || typeof additional !== "object") {
|
|
819
|
-
throw new SafeSchemaError("safe-schema/bad-extend",
|
|
820
|
-
"extend() requires a shape object");
|
|
821
|
-
}
|
|
822
|
-
var merged = Object.assign({}, shape, additional);
|
|
823
|
-
return _objectWithMode(merged, Object.keys(merged), mode);
|
|
824
|
-
};
|
|
825
|
-
// .partial() → mark every key optional
|
|
826
|
-
schema.partial = function () {
|
|
827
|
-
var newShape = {};
|
|
828
|
-
var newKeys = [];
|
|
829
|
-
for (var i = 0; i < keys.length; i++) {
|
|
830
|
-
newShape[keys[i]] = shape[keys[i]].optional();
|
|
831
|
-
newKeys.push(keys[i]);
|
|
832
|
-
}
|
|
833
|
-
return _objectWithMode(newShape, newKeys, mode);
|
|
834
|
-
};
|
|
835
|
-
// .required() — inverse of partial(). Strips optional + default + nullable
|
|
836
|
-
// off every key (operators sometimes call .partial() and then peel back
|
|
837
|
-
// a subset; this is the cleanest path).
|
|
838
|
-
schema.required = function () {
|
|
839
|
-
var newShape = {};
|
|
840
|
-
var newKeys = [];
|
|
841
|
-
for (var i = 0; i < keys.length; i++) {
|
|
842
|
-
var inner = shape[keys[i]];
|
|
843
|
-
// Strip modifiers by rebuilding without them. We don't have the
|
|
844
|
-
// pre-modifier schema cached, but we can reach it: the underlying
|
|
845
|
-
// _run is preserved; clone with all modifier flags off.
|
|
846
|
-
newShape[keys[i]] = _baseSchema({
|
|
847
|
-
kind: inner._kind,
|
|
848
|
-
isOptional: false,
|
|
849
|
-
isNullable: false,
|
|
850
|
-
hasDefault: false,
|
|
851
|
-
hasCatch: inner._hasCatch,
|
|
852
|
-
catchValue: inner._catch,
|
|
853
|
-
run: inner._run,
|
|
854
|
-
});
|
|
855
|
-
newKeys.push(keys[i]);
|
|
856
|
-
}
|
|
857
|
-
return _objectWithMode(newShape, newKeys, mode);
|
|
858
|
-
};
|
|
859
|
-
return schema;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// ---- array ----
|
|
863
|
-
|
|
864
|
-
function array(itemSchema) {
|
|
865
|
-
if (!itemSchema || typeof itemSchema._run !== "function") {
|
|
866
|
-
throw new SafeSchemaError("safe-schema/bad-item",
|
|
867
|
-
"array() requires an item schema");
|
|
868
|
-
}
|
|
869
|
-
var spec = {
|
|
870
|
-
kind: "array",
|
|
871
|
-
run: function (value, path) {
|
|
872
|
-
if (!Array.isArray(value)) return _fail(path, "type", "must be an array");
|
|
873
|
-
var issues = [];
|
|
874
|
-
var out = [];
|
|
875
|
-
for (var i = 0; i < value.length; i++) {
|
|
876
|
-
var childPath = path.concat([i]);
|
|
877
|
-
var r = _runWithModifiers(itemSchema, value[i], childPath);
|
|
878
|
-
if (!r.ok) {
|
|
879
|
-
for (var j = 0; j < r.issues.length; j++) issues.push(r.issues[j]);
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
out.push(r.value);
|
|
883
|
-
}
|
|
884
|
-
if (issues.length > 0) return { ok: false, issues: issues };
|
|
885
|
-
return { ok: true, value: out };
|
|
886
|
-
},
|
|
887
|
-
};
|
|
888
|
-
return _arrayMethods(_baseSchema(spec), spec);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function _arrayMethods(schema, spec) {
|
|
892
|
-
function chain(check) {
|
|
893
|
-
var next = _withCheck(schema, spec, check);
|
|
894
|
-
return _arrayMethods(next, _extendSpec(spec, { run: next._run }));
|
|
895
|
-
}
|
|
896
|
-
schema.min = function (n, msg) {
|
|
897
|
-
return chain(function (v, p) {
|
|
898
|
-
return v.length >= n ? { ok: true } :
|
|
899
|
-
_fail(p, "array/too-short", msg || ("must contain at least " + n + " items"));
|
|
900
|
-
});
|
|
901
|
-
};
|
|
902
|
-
schema.max = function (n, msg) {
|
|
903
|
-
return chain(function (v, p) {
|
|
904
|
-
return v.length <= n ? { ok: true } :
|
|
905
|
-
_fail(p, "array/too-long", msg || ("must contain at most " + n + " items"));
|
|
906
|
-
});
|
|
907
|
-
};
|
|
908
|
-
schema.length = function (n, msg) {
|
|
909
|
-
return chain(function (v, p) {
|
|
910
|
-
return v.length === n ? { ok: true } :
|
|
911
|
-
_fail(p, "array/wrong-length", msg || ("must contain exactly " + n + " items"));
|
|
912
|
-
});
|
|
913
|
-
};
|
|
914
|
-
schema.nonempty = function (msg) { return schema.min(1, msg || "must not be empty"); };
|
|
915
|
-
return schema;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
// ---- tuple ----
|
|
919
|
-
|
|
920
|
-
function tuple(items) {
|
|
921
|
-
if (!Array.isArray(items) || items.length === 0) {
|
|
922
|
-
throw new SafeSchemaError("safe-schema/bad-tuple",
|
|
923
|
-
"tuple() requires a non-empty array of item schemas");
|
|
924
|
-
}
|
|
925
|
-
for (var i = 0; i < items.length; i++) {
|
|
926
|
-
if (!items[i] || typeof items[i]._run !== "function") {
|
|
927
|
-
throw new SafeSchemaError("safe-schema/bad-tuple",
|
|
928
|
-
"tuple item " + i + " is not a schema");
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
return _tupleWithRest(items, null);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function _tupleWithRest(items, restSchema) {
|
|
935
|
-
var schema = _baseSchema({
|
|
936
|
-
kind: "tuple",
|
|
937
|
-
run: function (value, path) {
|
|
938
|
-
if (!Array.isArray(value)) return _fail(path, "type", "must be an array (tuple)");
|
|
939
|
-
if (restSchema === null && value.length !== items.length) {
|
|
940
|
-
return _fail(path, "tuple/wrong-length",
|
|
941
|
-
"tuple must contain exactly " + items.length + " items (got " + value.length + ")");
|
|
942
|
-
}
|
|
943
|
-
if (restSchema !== null && value.length < items.length) {
|
|
944
|
-
return _fail(path, "tuple/wrong-length",
|
|
945
|
-
"tuple must contain at least " + items.length + " items (got " + value.length + ")");
|
|
946
|
-
}
|
|
947
|
-
var issues = [];
|
|
948
|
-
var out = [];
|
|
949
|
-
for (var i = 0; i < items.length; i++) {
|
|
950
|
-
var r = _runWithModifiers(items[i], value[i], path.concat([i]));
|
|
951
|
-
if (!r.ok) {
|
|
952
|
-
for (var j = 0; j < r.issues.length; j++) issues.push(r.issues[j]);
|
|
953
|
-
continue;
|
|
954
|
-
}
|
|
955
|
-
out.push(r.value);
|
|
956
|
-
}
|
|
957
|
-
// Variadic tail — every extra item is checked against restSchema.
|
|
958
|
-
if (restSchema !== null) {
|
|
959
|
-
for (var k = items.length; k < value.length; k++) {
|
|
960
|
-
var rr = _runWithModifiers(restSchema, value[k], path.concat([k]));
|
|
961
|
-
if (!rr.ok) {
|
|
962
|
-
for (var jj = 0; jj < rr.issues.length; jj++) issues.push(rr.issues[jj]);
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
out.push(rr.value);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
if (issues.length > 0) return { ok: false, issues: issues };
|
|
969
|
-
return { ok: true, value: out };
|
|
970
|
-
},
|
|
971
|
-
});
|
|
972
|
-
// .rest(itemSchema) — append a variadic tail to the tuple. Common for
|
|
973
|
-
// protocol shapes like [verb, ...args] or [event, payload, ...metadata].
|
|
974
|
-
schema.rest = function (item) {
|
|
975
|
-
if (!item || typeof item._run !== "function") {
|
|
976
|
-
throw new SafeSchemaError("safe-schema/bad-tuple-rest",
|
|
977
|
-
"tuple.rest(): argument must be a schema");
|
|
978
|
-
}
|
|
979
|
-
return _tupleWithRest(items, item);
|
|
980
|
-
};
|
|
981
|
-
return schema;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// ---- union ----
|
|
985
|
-
|
|
986
|
-
function union(options) {
|
|
987
|
-
if (!Array.isArray(options) || options.length === 0) {
|
|
988
|
-
throw new SafeSchemaError("safe-schema/bad-union",
|
|
989
|
-
"union() requires a non-empty array of option schemas");
|
|
990
|
-
}
|
|
991
|
-
for (var i = 0; i < options.length; i++) {
|
|
992
|
-
if (!options[i] || typeof options[i]._run !== "function") {
|
|
993
|
-
throw new SafeSchemaError("safe-schema/bad-union",
|
|
994
|
-
"union option " + i + " is not a schema");
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
return _baseSchema({
|
|
998
|
-
kind: "union",
|
|
999
|
-
run: function (value, path) {
|
|
1000
|
-
var collected = [];
|
|
1001
|
-
for (var i = 0; i < options.length; i++) {
|
|
1002
|
-
var r = _runWithModifiers(options[i], value, path);
|
|
1003
|
-
if (r.ok) return r;
|
|
1004
|
-
// Collect each option's issues so the operator sees the full
|
|
1005
|
-
// failure surface; helpful for debugging which option matched
|
|
1006
|
-
// most closely.
|
|
1007
|
-
for (var j = 0; j < r.issues.length; j++) collected.push(r.issues[j]);
|
|
1008
|
-
}
|
|
1009
|
-
// No option matched — emit a union-level issue at the parent path
|
|
1010
|
-
// plus the collected per-option issues for deep diagnostics.
|
|
1011
|
-
var summary = _fail(path, "union",
|
|
1012
|
-
"did not match any of the " + options.length + " allowed shapes");
|
|
1013
|
-
summary.issues = summary.issues.concat(collected);
|
|
1014
|
-
return summary;
|
|
1015
|
-
},
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
// ---- record ----
|
|
1020
|
-
// record(value) — string keys, schema-typed values
|
|
1021
|
-
// record(keySchema, value) — both keys and values are schema-validated
|
|
1022
|
-
|
|
1023
|
-
function record(a, b) {
|
|
1024
|
-
var keySchema, valueSchema;
|
|
1025
|
-
if (b === undefined) {
|
|
1026
|
-
keySchema = null; // any string key
|
|
1027
|
-
valueSchema = a;
|
|
1028
|
-
} else {
|
|
1029
|
-
keySchema = a;
|
|
1030
|
-
valueSchema = b;
|
|
1031
|
-
}
|
|
1032
|
-
if (!valueSchema || typeof valueSchema._run !== "function") {
|
|
1033
|
-
throw new SafeSchemaError("safe-schema/bad-value-schema",
|
|
1034
|
-
"record() requires a value schema");
|
|
1035
|
-
}
|
|
1036
|
-
if (keySchema && typeof keySchema._run !== "function") {
|
|
1037
|
-
throw new SafeSchemaError("safe-schema/bad-key-schema",
|
|
1038
|
-
"record(keySchema, valueSchema): keySchema must be a schema");
|
|
1039
|
-
}
|
|
1040
|
-
return _baseSchema({
|
|
1041
|
-
kind: "record",
|
|
1042
|
-
run: function (value, path) {
|
|
1043
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1044
|
-
return _fail(path, "type", "must be a plain object (record)");
|
|
1045
|
-
}
|
|
1046
|
-
var issues = [];
|
|
1047
|
-
var out = {};
|
|
1048
|
-
var keys = Object.keys(value);
|
|
1049
|
-
for (var i = 0; i < keys.length; i++) {
|
|
1050
|
-
var k = keys[i];
|
|
1051
|
-
// Prototype-pollution defense — same shape as object() schema.
|
|
1052
|
-
if (POISONED_KEYS.has(k)) {
|
|
1053
|
-
issues.push({
|
|
1054
|
-
path: path.concat([k]),
|
|
1055
|
-
code: "record/poisoned-key",
|
|
1056
|
-
message: "key '" + k + "' is forbidden (prototype-pollution defense)",
|
|
1057
|
-
});
|
|
1058
|
-
continue;
|
|
1059
|
-
}
|
|
1060
|
-
if (keySchema) {
|
|
1061
|
-
var kr = _runWithModifiers(keySchema, k, path.concat([k]));
|
|
1062
|
-
if (!kr.ok) {
|
|
1063
|
-
for (var jj = 0; jj < kr.issues.length; jj++) issues.push(kr.issues[jj]);
|
|
1064
|
-
continue;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
var r = _runWithModifiers(valueSchema, value[k], path.concat([k]));
|
|
1068
|
-
if (!r.ok) {
|
|
1069
|
-
for (var j = 0; j < r.issues.length; j++) issues.push(r.issues[j]);
|
|
1070
|
-
continue;
|
|
1071
|
-
}
|
|
1072
|
-
if (r.value !== undefined) out[k] = r.value;
|
|
1073
|
-
}
|
|
1074
|
-
if (issues.length > 0) return { ok: false, issues: issues };
|
|
1075
|
-
return { ok: true, value: out };
|
|
1076
|
-
},
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// ---- discriminatedUnion ----
|
|
1081
|
-
//
|
|
1082
|
-
// Performance + ergonomics improvement over union for the common case
|
|
1083
|
-
// of "tagged variants": a literal field on each option distinguishes
|
|
1084
|
-
// the branches, so we dispatch on that field's value directly rather
|
|
1085
|
-
// than trying every option in turn.
|
|
1086
|
-
//
|
|
1087
|
-
// var event = s.discriminatedUnion("kind", [
|
|
1088
|
-
// s.object({ kind: s.literal("created"), at: s.string().datetime() }),
|
|
1089
|
-
// s.object({ kind: s.literal("deleted"), reason: s.string() }),
|
|
1090
|
-
// ]);
|
|
1091
|
-
//
|
|
1092
|
-
// Each option must be an object schema whose `discriminator` key is a
|
|
1093
|
-
// literal schema. Mismatched discriminator fails fast with a clear
|
|
1094
|
-
// "expected one of [...]" message rather than burying the operator in
|
|
1095
|
-
// per-branch issues.
|
|
1096
|
-
function discriminatedUnion(discriminator, options) {
|
|
1097
|
-
if (typeof discriminator !== "string" || discriminator.length === 0) {
|
|
1098
|
-
throw new SafeSchemaError("safe-schema/bad-discriminator",
|
|
1099
|
-
"discriminatedUnion: discriminator must be a non-empty string key name");
|
|
1100
|
-
}
|
|
1101
|
-
if (POISONED_KEYS.has(discriminator)) {
|
|
1102
|
-
throw new SafeSchemaError("safe-schema/poisoned-discriminator",
|
|
1103
|
-
"discriminatedUnion: discriminator key '" + discriminator + "' is forbidden");
|
|
1104
|
-
}
|
|
1105
|
-
if (!Array.isArray(options) || options.length === 0) {
|
|
1106
|
-
throw new SafeSchemaError("safe-schema/bad-union",
|
|
1107
|
-
"discriminatedUnion: options must be a non-empty array");
|
|
1108
|
-
}
|
|
1109
|
-
for (var i = 0; i < options.length; i++) {
|
|
1110
|
-
var opt = options[i];
|
|
1111
|
-
if (!opt || opt._kind !== "object" || !opt.shape) {
|
|
1112
|
-
throw new SafeSchemaError("safe-schema/bad-discriminated-option",
|
|
1113
|
-
"discriminatedUnion option " + i + " must be an object schema");
|
|
1114
|
-
}
|
|
1115
|
-
var disc = opt.shape[discriminator];
|
|
1116
|
-
if (!disc || disc._kind !== "literal") {
|
|
1117
|
-
throw new SafeSchemaError("safe-schema/bad-discriminated-option",
|
|
1118
|
-
"discriminatedUnion option " + i + ": discriminator '" + discriminator +
|
|
1119
|
-
"' must be a literal schema");
|
|
1120
|
-
}
|
|
1121
|
-
// The literal's expected value lives in its closure; we extract via
|
|
1122
|
-
// a probe rather than introspection. literal(v)._run(v) succeeds.
|
|
1123
|
-
// We sample by trying every option's discriminator separately at
|
|
1124
|
-
// dispatch time (cheap; literal._run is just a triple-equals).
|
|
1125
|
-
}
|
|
1126
|
-
return _baseSchema({
|
|
1127
|
-
kind: "discriminatedUnion",
|
|
1128
|
-
run: function (value, path) {
|
|
1129
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1130
|
-
return _fail(path, "type", "must be an object (discriminated union)");
|
|
1131
|
-
}
|
|
1132
|
-
var disc = value[discriminator];
|
|
1133
|
-
// Find the option whose discriminator schema accepts disc.
|
|
1134
|
-
for (var i = 0; i < options.length; i++) {
|
|
1135
|
-
var opt = options[i];
|
|
1136
|
-
var d = opt.shape[discriminator];
|
|
1137
|
-
var dr = d._run(disc, path.concat([discriminator]));
|
|
1138
|
-
if (dr.ok) {
|
|
1139
|
-
return _runWithModifiers(opt, value, path);
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
return _fail(path.concat([discriminator]), "discriminated-union/no-match",
|
|
1143
|
-
"discriminator '" + discriminator + "' did not match any option");
|
|
1144
|
-
},
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// ---- preprocess ----
|
|
1149
|
-
//
|
|
1150
|
-
// Run a transform BEFORE validation. Common at HTTP boundaries where
|
|
1151
|
-
// query strings arrive as strings but the operator wants a number /
|
|
1152
|
-
// boolean schema downstream.
|
|
1153
|
-
//
|
|
1154
|
-
// var port = s.preprocess(function (v) { return Number(v); }, s.number().int().min(1).max(65535));
|
|
1155
|
-
//
|
|
1156
|
-
// fn errors propagate as a 'preprocess' issue at the parent path; they
|
|
1157
|
-
// don't crash the validate call.
|
|
1158
|
-
function preprocess(fn, inner) {
|
|
1159
|
-
if (typeof fn !== "function") {
|
|
1160
|
-
throw new SafeSchemaError("safe-schema/bad-preprocess",
|
|
1161
|
-
"preprocess: first arg must be a function");
|
|
1162
|
-
}
|
|
1163
|
-
if (!inner || typeof inner._run !== "function") {
|
|
1164
|
-
throw new SafeSchemaError("safe-schema/bad-preprocess",
|
|
1165
|
-
"preprocess: second arg must be a schema");
|
|
1166
|
-
}
|
|
1167
|
-
return _baseSchema({
|
|
1168
|
-
kind: "preprocess",
|
|
1169
|
-
run: function (value, path) {
|
|
1170
|
-
var preprocessed;
|
|
1171
|
-
try {
|
|
1172
|
-
preprocessed = fn(value);
|
|
1173
|
-
} catch (e) {
|
|
1174
|
-
return _fail(path, "preprocess",
|
|
1175
|
-
"preprocess fn threw: " + ((e && e.message) || String(e)));
|
|
1176
|
-
}
|
|
1177
|
-
return _runWithModifiers(inner, preprocessed, path);
|
|
1178
|
-
},
|
|
1179
|
-
});
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// ---- lazy (recursive schemas) ----
|
|
1183
|
-
//
|
|
1184
|
-
// Defers schema construction until first parse. Operators wanting a
|
|
1185
|
-
// recursive shape — comment threads, file-tree nodes, etc. — wrap the
|
|
1186
|
-
// recursive reference in a function that returns the schema:
|
|
1187
|
-
//
|
|
1188
|
-
// var commentSchema = s.object({
|
|
1189
|
-
// id: s.string(),
|
|
1190
|
-
// replies: s.array(s.lazy(function () { return commentSchema; })),
|
|
1191
|
-
// });
|
|
1192
|
-
//
|
|
1193
|
-
// The function is called lazily and cached per-call site; cycles in the
|
|
1194
|
-
// returned schema are fine.
|
|
1195
|
-
function lazy(getter) {
|
|
1196
|
-
if (typeof getter !== "function") {
|
|
1197
|
-
throw new SafeSchemaError("safe-schema/bad-lazy",
|
|
1198
|
-
"lazy: argument must be a function returning a schema");
|
|
1199
|
-
}
|
|
1200
|
-
var cached = null;
|
|
1201
|
-
return _baseSchema({
|
|
1202
|
-
kind: "lazy",
|
|
1203
|
-
run: function (value, path) {
|
|
1204
|
-
if (!cached) {
|
|
1205
|
-
cached = getter();
|
|
1206
|
-
if (!cached || typeof cached._run !== "function") {
|
|
1207
|
-
return _fail(path, "lazy",
|
|
1208
|
-
"lazy() function did not return a schema");
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
return _runWithModifiers(cached, value, path);
|
|
1212
|
-
},
|
|
1213
|
-
});
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
// ---- top-level modifier helpers ----
|
|
1217
|
-
|
|
1218
|
-
function optional(inner) { return inner.optional(); }
|
|
1219
|
-
function nullable(inner) { return inner.nullable(); }
|
|
1220
|
-
|
|
1221
|
-
module.exports = {
|
|
1222
|
-
// Primitives
|
|
1223
|
-
string: string,
|
|
1224
|
-
number: number,
|
|
1225
|
-
boolean: boolean,
|
|
1226
|
-
literal: literal,
|
|
1227
|
-
null_: null_,
|
|
1228
|
-
undefined_: undefined_,
|
|
1229
|
-
any: any,
|
|
1230
|
-
unknown: unknown,
|
|
1231
|
-
|
|
1232
|
-
// Composites
|
|
1233
|
-
object: object,
|
|
1234
|
-
array: array,
|
|
1235
|
-
tuple: tuple,
|
|
1236
|
-
union: union,
|
|
1237
|
-
discriminatedUnion: discriminatedUnion,
|
|
1238
|
-
record: record,
|
|
1239
|
-
lazy: lazy,
|
|
1240
|
-
preprocess: preprocess,
|
|
1241
|
-
|
|
1242
|
-
// enum is a reserved word in some tooling — ship both names
|
|
1243
|
-
enum_: enum_,
|
|
1244
|
-
oneOf: enum_,
|
|
1245
|
-
|
|
1246
|
-
// Modifier helpers (chained methods exist on every schema; these are
|
|
1247
|
-
// the equivalents for operators who prefer composition over chaining)
|
|
1248
|
-
optional: optional,
|
|
1249
|
-
nullable: nullable,
|
|
1250
|
-
|
|
1251
|
-
// Errors
|
|
1252
|
-
SafeSchemaError: SafeSchemaError,
|
|
1253
|
-
|
|
1254
|
-
// Validation regexes — exported so other modules don't re-declare
|
|
1255
|
-
// their own copies. Pragmatic patterns; operators wanting RFC-strict
|
|
1256
|
-
// behavior chain `.refine()` on top of the schema instead.
|
|
1257
|
-
EMAIL_RE: EMAIL_RE,
|
|
1258
|
-
URL_RE: URL_RE,
|
|
1259
|
-
UUID_RE: UUID_RE,
|
|
1260
|
-
DATE_RE: DATE_RE,
|
|
1261
|
-
DATETIME_RE: DATETIME_RE,
|
|
1262
|
-
IPV4_RE: IPV4_RE,
|
|
1263
|
-
IPV6_RE: IPV6_RE,
|
|
1264
|
-
CUID_RE: CUID_RE,
|
|
1265
|
-
ULID_RE: ULID_RE,
|
|
1266
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* safe-schema — declarative input validation, Zod-shaped surface.
|
|
4
|
+
*
|
|
5
|
+
* Built for: request-body validation, config validation, API payload
|
|
6
|
+
* validation, anywhere operators have an `unknown` shape they need
|
|
7
|
+
* to confirm before reading. Vendor-free; built on framework
|
|
8
|
+
* primitives. No JIT, no codegen, no chained-Promise weirdness.
|
|
9
|
+
*
|
|
10
|
+
* Public API:
|
|
11
|
+
* var s = b.safeSchema;
|
|
12
|
+
*
|
|
13
|
+
* var schema = s.object({
|
|
14
|
+
* email: s.string().email(),
|
|
15
|
+
* age: s.number().int().min(0).max(120),
|
|
16
|
+
* tags: s.array(s.string()).max(10),
|
|
17
|
+
* metadata: s.object({}).passthrough().optional(),
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* var result = schema.parse(input); // throws SafeSchemaError
|
|
21
|
+
* var safe = schema.safeParse(input); // → { ok, value?, errors? }
|
|
22
|
+
*
|
|
23
|
+
* // Errors carry per-field paths:
|
|
24
|
+
* // [{ path: ["age"], code: "number/too-large", message: "must be ≤ 120" }]
|
|
25
|
+
*
|
|
26
|
+
* Design choices:
|
|
27
|
+
* - Schemas are immutable. Chaining returns a new schema with one
|
|
28
|
+
* additional check; the original is untouched. Cheap because
|
|
29
|
+
* checks are concat'd into a small array, not deep-copied.
|
|
30
|
+
* - parse() throws SafeSchemaError carrying the full issues array;
|
|
31
|
+
* safeParse() never throws (operator-friendly for HTTP boundaries).
|
|
32
|
+
* - .optional() means "may be undefined"; .nullable() means "may be
|
|
33
|
+
* null"; .default(v) means "if undefined, substitute v";
|
|
34
|
+
* .catch(v) means "on ANY validation failure, substitute v".
|
|
35
|
+
* These compose: optional().default(0) → "may be undefined,
|
|
36
|
+
* in which case use 0".
|
|
37
|
+
* - Objects are STRICT by default: unknown keys produce an issue.
|
|
38
|
+
* Use .passthrough() to retain unknown keys, .strict() to flip
|
|
39
|
+
* back if a parent .passthrough() set the mode.
|
|
40
|
+
*
|
|
41
|
+
* Surface (every schema has these chained methods unless noted):
|
|
42
|
+
*
|
|
43
|
+
* Type constructors:
|
|
44
|
+
* string() .min, .max, .length, .regex, .email, .url, .uuid,
|
|
45
|
+
* .datetime (ISO-8601), .date (YYYY-MM-DD),
|
|
46
|
+
* .ip, .ipv4, .ipv6, .nonempty, .startsWith, .endsWith,
|
|
47
|
+
* .includes
|
|
48
|
+
* number() .int, .min, .max, .gt, .lt, .positive, .negative,
|
|
49
|
+
* .nonnegative, .nonpositive, .finite, .multipleOf
|
|
50
|
+
* boolean()
|
|
51
|
+
* literal(v)
|
|
52
|
+
* enum_([...]) | oneOf([...])
|
|
53
|
+
* null_(), undefined_(), any(), unknown()
|
|
54
|
+
*
|
|
55
|
+
* Composites:
|
|
56
|
+
* object({ ... }) .strict, .passthrough, .pick, .omit, .extend,
|
|
57
|
+
* .partial, .required (inverse of partial)
|
|
58
|
+
* array(item) .min, .max, .length, .nonempty
|
|
59
|
+
* tuple([...]) .rest(item) for variadic tails
|
|
60
|
+
* union([...]) first matching wins
|
|
61
|
+
* discriminatedUnion(key, [...]) faster + clearer-errors variant
|
|
62
|
+
* for tagged unions
|
|
63
|
+
* record(value) | record(key, value)
|
|
64
|
+
* lazy(() => schema) defer construction; for recursion
|
|
65
|
+
* preprocess(fn, schema) run fn before validation
|
|
66
|
+
*
|
|
67
|
+
* Modifiers (any schema):
|
|
68
|
+
* .optional() value may be undefined
|
|
69
|
+
* .nullable() value may be null
|
|
70
|
+
* .default(v|fn) undefined → v (implies optional). Function form
|
|
71
|
+
* is called per-parse for fresh values.
|
|
72
|
+
* .catch(v|fn) any failure → v (escape hatch for operator
|
|
73
|
+
* defaults; suppresses the error info, so use
|
|
74
|
+
* sparingly)
|
|
75
|
+
* .refine(fn, opts) custom predicate — returns false to fail
|
|
76
|
+
* .transform(fn) map the validated value to a new shape
|
|
77
|
+
* .pipe(next) feed validated output through `next` schema
|
|
78
|
+
* for a second round of validation
|
|
79
|
+
*
|
|
80
|
+
* Security guarantees:
|
|
81
|
+
* - Prototype-pollution defense: __proto__ / constructor / prototype
|
|
82
|
+
* keys are rejected at construction (object shape) and parse time
|
|
83
|
+
* (object + record input). Mirrors safe-json.js's POISONED_KEYS.
|
|
84
|
+
* - No code injection surface: regexes are static module-level
|
|
85
|
+
* constants; no string→regex parsing on the validation path; no
|
|
86
|
+
* eval/Function. Operator-supplied refine/transform fns are
|
|
87
|
+
* plain JS functions, not strings.
|
|
88
|
+
* - Predicate throws are caught: a refine() function throwing turns
|
|
89
|
+
* into a regular validation issue, not an unhandled exception.
|
|
90
|
+
* - Sync-only: no async refinements; operators await at the boundary.
|
|
91
|
+
*
|
|
92
|
+
* Deliberately not shipped (with structural reason):
|
|
93
|
+
* - z.bigint / z.date / z.map / z.set — no JSON representation; HTTP
|
|
94
|
+
* boundaries don't carry these. Use s.string().datetime() for
|
|
95
|
+
* ISO-8601 strings.
|
|
96
|
+
* - z.nativeEnum / z.never / z.void / z.function — TypeScript-specific.
|
|
97
|
+
* - z.coerce — loose-coercion is a security foot-gun (truthy/falsy
|
|
98
|
+
* ambiguity, "0" → 0 vs "0" → "0"). Operators do explicit
|
|
99
|
+
* s.preprocess(fn, schema) instead.
|
|
100
|
+
* - z.intersection — for object schemas use .extend(); intersections
|
|
101
|
+
* of unrelated schemas are structurally ambiguous.
|
|
102
|
+
* - z.brand — TypeScript compile-time tag with no runtime effect.
|
|
103
|
+
* - per-schema errorMap — operators chain .refine() with custom message.
|
|
104
|
+
*
|
|
105
|
+
* Relationship to forms.validate:
|
|
106
|
+
* forms.validate (HTML form spec validation) is a separate surface.
|
|
107
|
+
* Form specs carry HTML-specific concerns (checkbox coercion, select
|
|
108
|
+
* option allowlist) that don't belong on the general-purpose validator,
|
|
109
|
+
* so the two stay distinct rather than one wrapping the other.
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
var C = require("./constants");
|
|
113
|
+
var safeJson = require("./safe-json");
|
|
114
|
+
var { defineClass } = require("./framework-error");
|
|
115
|
+
|
|
116
|
+
// Maximum URL length per RFC 7230 §3.1.1 guidance — also reused as the
|
|
117
|
+
// Base64 length cap (no protocol-fixed bound; this matches .url()).
|
|
118
|
+
var URL_MAX_LEN = C.BYTES.kib(8);
|
|
119
|
+
|
|
120
|
+
// Per-format defensive length caps. Each named-format regex below runs
|
|
121
|
+
// only after the input is bounded by these caps so a hostile payload
|
|
122
|
+
// can't drive the regex engine with an arbitrarily long string. Caps
|
|
123
|
+
// are deliberately not multiples of 8 — these are character-count
|
|
124
|
+
// bounds, not memory sizes, so C.BYTES.* helpers don't apply.
|
|
125
|
+
var EMAIL_MAX_LEN = 254; // RFC 5321 §4.5.3.1.3 forward-path bound
|
|
126
|
+
var UUID_MAX_LEN = 50; // RFC 4122 UUID is 36 chars; slack for whitespace edge cases
|
|
127
|
+
var DATE_MAX_LEN = 30; // YYYY-MM-DD is 10 chars
|
|
128
|
+
var DATETIME_MAX_LEN = 100; // ISO-8601 with offset + fractional seconds tops near 35
|
|
129
|
+
var CUID_MAX_LEN = 50; // CUID v1/v2 is 25 chars
|
|
130
|
+
var ULID_MAX_LEN = 50; // ULID is exactly 26 chars
|
|
131
|
+
|
|
132
|
+
var SafeSchemaError = defineClass("SafeSchemaError", { alwaysPermanent: true });
|
|
133
|
+
|
|
134
|
+
// Prototype-pollution defense — these key names are rejected in object
|
|
135
|
+
// and record schemas regardless of mode (strict, passthrough, or lazy
|
|
136
|
+
// match). Mirrors safe-json.js's POISONED_KEYS set so the framework
|
|
137
|
+
// presents one consistent guarantee: an attacker cannot pollute
|
|
138
|
+
// Object.prototype by submitting a JSON body with __proto__ /
|
|
139
|
+
// constructor / prototype keys, even if the operator schema is
|
|
140
|
+
// .passthrough().
|
|
141
|
+
var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
142
|
+
|
|
143
|
+
// Pragmatic regexes — RFC-correct is impractical without exploding the
|
|
144
|
+
// regex (especially email). Operators wanting deeper validation chain
|
|
145
|
+
// .refine() on top.
|
|
146
|
+
//
|
|
147
|
+
// All regexes are static module-level constants; nothing parses an input
|
|
148
|
+
// string into a regex on the validation path (no ReDoS-via-input vector,
|
|
149
|
+
// no dynamic regex compilation).
|
|
150
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
151
|
+
var URL_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s]+$/;
|
|
152
|
+
var UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
|
|
153
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
154
|
+
// ISO-8601 datetime with timezone (Z or ±HH:MM); fractional seconds optional.
|
|
155
|
+
var DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
156
|
+
var IPV4_RE = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
157
|
+
// CUID v1 / v2: 25-char base36, starts with 'c'. Common in TypeScript ecosystems.
|
|
158
|
+
var CUID_RE = /^c[a-z0-9]{24}$/;
|
|
159
|
+
// ULID: Crockford-base32, 26 chars, time-sortable.
|
|
160
|
+
var ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
161
|
+
// base64 (standard alphabet, with optional padding). Base64url variants
|
|
162
|
+
// rejected — operators chain .regex(...) for that.
|
|
163
|
+
var BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
164
|
+
// IPv6 structural pattern — full 8-hextet, every `::`-compressed shape,
|
|
165
|
+
// `::` and `::1` literals, IPv4-mapped (`::ffff:1.2.3.4`), and 6-prefix
|
|
166
|
+
// + IPv4 tail. Adapted from validator.js (Apache-2.0); zone IDs
|
|
167
|
+
// (`fe80::1%eth0`) are deliberately omitted — the framework rejects
|
|
168
|
+
// them as non-portable, matching `safe-json.formats.ipv6`. Bounded
|
|
169
|
+
// quantifiers, no nested-quantifier alternation, ReDoS-safe.
|
|
170
|
+
//
|
|
171
|
+
// `.ipv6()` schema method delegates to `safeJson.formats.ipv6` for
|
|
172
|
+
// stricter algorithmic validation; this regex is exported as a
|
|
173
|
+
// structural pattern for operators who want it directly.
|
|
174
|
+
var IPV6_RE = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
|
175
|
+
|
|
176
|
+
// ---- helpers ----
|
|
177
|
+
|
|
178
|
+
function _fail(path, code, message) {
|
|
179
|
+
return { ok: false, issues: [{ path: path.slice(), code: code, message: message }] };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _formatIssues(issues) {
|
|
183
|
+
if (!issues || issues.length === 0) return "(no issues)";
|
|
184
|
+
return issues.map(function (i) {
|
|
185
|
+
var p = i.path && i.path.length > 0 ? i.path.join(".") + ": " : "";
|
|
186
|
+
return p + i.message;
|
|
187
|
+
}).join("; ");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Run a schema's modifier-aware pipeline. Used by both root parse() and
|
|
191
|
+
// child-key dispatch inside object/array/etc.
|
|
192
|
+
function _runWithModifiers(schema, value, path) {
|
|
193
|
+
// Apply default BEFORE catching, so a schema with both .default() and
|
|
194
|
+
// .catch() prefers the default for undefined input (predictable).
|
|
195
|
+
if (value === undefined) {
|
|
196
|
+
if (schema._hasDefault) value = typeof schema._default === "function" ? schema._default() : schema._default;
|
|
197
|
+
else if (schema._isOptional) return { ok: true, value: undefined };
|
|
198
|
+
else return _fail(path, "required", "is required");
|
|
199
|
+
}
|
|
200
|
+
if (value === null) {
|
|
201
|
+
if (schema._isNullable) return { ok: true, value: null };
|
|
202
|
+
if (schema._hasCatch) return { ok: true, value: schema._catch };
|
|
203
|
+
return _fail(path, "type", "must not be null");
|
|
204
|
+
}
|
|
205
|
+
var r = schema._run(value, path);
|
|
206
|
+
if (!r.ok && schema._hasCatch) {
|
|
207
|
+
return { ok: true, value: typeof schema._catch === "function" ? schema._catch() : schema._catch };
|
|
208
|
+
}
|
|
209
|
+
return r;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---- core schema factory ----
|
|
213
|
+
//
|
|
214
|
+
// Every schema stores:
|
|
215
|
+
// _kind — for diagnostics
|
|
216
|
+
// _isOptional, _isNullable, _hasDefault, _default, _hasCatch, _catch
|
|
217
|
+
// _run(value, path) — type check + checks pipeline
|
|
218
|
+
//
|
|
219
|
+
// Modifier methods (optional/nullable/default/catch/refine/transform) live
|
|
220
|
+
// on the prototype; chained methods are added by each constructor's
|
|
221
|
+
// builder (string adds .min/.max etc., array adds .min/.length, etc.).
|
|
222
|
+
|
|
223
|
+
function _baseSchema(spec) {
|
|
224
|
+
var s = {
|
|
225
|
+
_kind: spec.kind,
|
|
226
|
+
_isOptional: spec.isOptional || false,
|
|
227
|
+
_isNullable: spec.isNullable || false,
|
|
228
|
+
_hasDefault: spec.hasDefault || false,
|
|
229
|
+
_default: spec.defaultValue,
|
|
230
|
+
_hasCatch: spec.hasCatch || false,
|
|
231
|
+
_catch: spec.catchValue,
|
|
232
|
+
_run: spec.run,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
s.parse = function (input) {
|
|
236
|
+
var r = _runWithModifiers(s, input, []);
|
|
237
|
+
if (r.ok) return r.value;
|
|
238
|
+
var err = new SafeSchemaError(
|
|
239
|
+
"safe-schema/invalid",
|
|
240
|
+
"validation failed: " + _formatIssues(r.issues)
|
|
241
|
+
);
|
|
242
|
+
err.issues = r.issues;
|
|
243
|
+
throw err;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
s.safeParse = function (input) {
|
|
247
|
+
var r = _runWithModifiers(s, input, []);
|
|
248
|
+
if (r.ok) return { ok: true, value: r.value };
|
|
249
|
+
return { ok: false, errors: r.issues };
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
s.optional = function () {
|
|
253
|
+
return _baseSchema(_extendSpec(spec, { isOptional: true }));
|
|
254
|
+
};
|
|
255
|
+
s.nullable = function () {
|
|
256
|
+
return _baseSchema(_extendSpec(spec, { isNullable: true }));
|
|
257
|
+
};
|
|
258
|
+
s.default = function (v) {
|
|
259
|
+
return _baseSchema(_extendSpec(spec, { isOptional: true, hasDefault: true, defaultValue: v }));
|
|
260
|
+
};
|
|
261
|
+
s.catch = function (v) {
|
|
262
|
+
return _baseSchema(_extendSpec(spec, { hasCatch: true, catchValue: v }));
|
|
263
|
+
};
|
|
264
|
+
s.refine = function (predicate, opts) {
|
|
265
|
+
opts = opts || {};
|
|
266
|
+
var code = opts.code || "refine";
|
|
267
|
+
var message = opts.message || "failed custom validation";
|
|
268
|
+
var inner = s;
|
|
269
|
+
return _baseSchema(_extendSpec(spec, {
|
|
270
|
+
run: function (value, path) {
|
|
271
|
+
var r = inner._run(value, path);
|
|
272
|
+
if (!r.ok) return r;
|
|
273
|
+
try {
|
|
274
|
+
if (!predicate(r.value)) return _fail(path, code, message);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
return _fail(path, code,
|
|
277
|
+
message + " (predicate threw: " + ((e && e.message) || String(e)) + ")");
|
|
278
|
+
}
|
|
279
|
+
return r;
|
|
280
|
+
},
|
|
281
|
+
}));
|
|
282
|
+
};
|
|
283
|
+
s.transform = function (fn) {
|
|
284
|
+
var inner = s;
|
|
285
|
+
return _baseSchema(_extendSpec(spec, {
|
|
286
|
+
run: function (value, path) {
|
|
287
|
+
var r = inner._run(value, path);
|
|
288
|
+
if (!r.ok) return r;
|
|
289
|
+
try {
|
|
290
|
+
return { ok: true, value: fn(r.value) };
|
|
291
|
+
} catch (e) {
|
|
292
|
+
return _fail(path, "transform",
|
|
293
|
+
"transform threw: " + ((e && e.message) || String(e)));
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
}));
|
|
297
|
+
};
|
|
298
|
+
// .pipe(next) — feed this schema's validated output into another
|
|
299
|
+
// schema for a second round of validation. Common idiom for
|
|
300
|
+
// "validate input shape, transform, re-validate output shape":
|
|
301
|
+
//
|
|
302
|
+
// var port = s.string().regex(/^\d+$/).transform(Number).pipe(
|
|
303
|
+
// s.number().int().min(1).max(65535)
|
|
304
|
+
// );
|
|
305
|
+
s.pipe = function (next) {
|
|
306
|
+
if (!next || typeof next._run !== "function") {
|
|
307
|
+
throw new SafeSchemaError("safe-schema/bad-pipe",
|
|
308
|
+
"pipe: argument must be a schema");
|
|
309
|
+
}
|
|
310
|
+
var inner = s;
|
|
311
|
+
return _baseSchema(_extendSpec(spec, {
|
|
312
|
+
run: function (value, path) {
|
|
313
|
+
var r = inner._run(value, path);
|
|
314
|
+
if (!r.ok) return r;
|
|
315
|
+
return _runWithModifiers(next, r.value, path);
|
|
316
|
+
},
|
|
317
|
+
}));
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return s;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function _extendSpec(spec, overrides) {
|
|
324
|
+
return {
|
|
325
|
+
kind: overrides.kind != null ? overrides.kind : spec.kind,
|
|
326
|
+
isOptional: overrides.isOptional != null ? overrides.isOptional : spec.isOptional,
|
|
327
|
+
isNullable: overrides.isNullable != null ? overrides.isNullable : spec.isNullable,
|
|
328
|
+
hasDefault: overrides.hasDefault != null ? overrides.hasDefault : spec.hasDefault,
|
|
329
|
+
defaultValue: overrides.hasDefault != null ? overrides.defaultValue : spec.defaultValue,
|
|
330
|
+
hasCatch: overrides.hasCatch != null ? overrides.hasCatch : spec.hasCatch,
|
|
331
|
+
catchValue: overrides.hasCatch != null ? overrides.catchValue : spec.catchValue,
|
|
332
|
+
run: overrides.run != null ? overrides.run : spec.run,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Adds a check fn into the schema's _run pipeline by composing it on top
|
|
337
|
+
// of the existing _run. Used by string/number/array builders to chain
|
|
338
|
+
// .min/.max/etc. onto an existing schema instance.
|
|
339
|
+
//
|
|
340
|
+
// The check fn returns either:
|
|
341
|
+
// { ok: true } — pass; keep the inner value unchanged
|
|
342
|
+
// { ok: true, value: newVal } — pass and mutate the value (used by
|
|
343
|
+
// ergonomic transforms like .trim())
|
|
344
|
+
// { ok: false, issues } — fail with the given issue
|
|
345
|
+
function _withCheck(schema, spec, check) {
|
|
346
|
+
var inner = schema;
|
|
347
|
+
return _baseSchema(_extendSpec(spec, {
|
|
348
|
+
run: function (value, path) {
|
|
349
|
+
var r = inner._run(value, path);
|
|
350
|
+
if (!r.ok) return r;
|
|
351
|
+
var cr = check(r.value, path);
|
|
352
|
+
if (!cr.ok) return cr;
|
|
353
|
+
if (Object.prototype.hasOwnProperty.call(cr, "value")) {
|
|
354
|
+
return { ok: true, value: cr.value };
|
|
355
|
+
}
|
|
356
|
+
return r;
|
|
357
|
+
},
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---- string ----
|
|
362
|
+
|
|
363
|
+
function string() {
|
|
364
|
+
var spec = {
|
|
365
|
+
kind: "string",
|
|
366
|
+
run: function (value, path) {
|
|
367
|
+
if (typeof value !== "string") return _fail(path, "type", "must be a string");
|
|
368
|
+
return { ok: true, value: value };
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
return _stringMethods(_baseSchema(spec), spec);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function _stringMethods(schema, spec) {
|
|
375
|
+
function chain(check) {
|
|
376
|
+
var next = _withCheck(schema, spec, check);
|
|
377
|
+
return _stringMethods(next, _extendSpec(spec, { run: next._run }));
|
|
378
|
+
}
|
|
379
|
+
schema.min = function (n, msg) {
|
|
380
|
+
return chain(function (v, p) {
|
|
381
|
+
return v.length >= n ? { ok: true } :
|
|
382
|
+
_fail(p, "string/too-short", msg || ("must be at least " + n + " characters"));
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
schema.max = function (n, msg) {
|
|
386
|
+
return chain(function (v, p) {
|
|
387
|
+
return v.length <= n ? { ok: true } :
|
|
388
|
+
_fail(p, "string/too-long", msg || ("must be at most " + n + " characters"));
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
schema.length = function (n, msg) {
|
|
392
|
+
return chain(function (v, p) {
|
|
393
|
+
return v.length === n ? { ok: true } :
|
|
394
|
+
_fail(p, "string/wrong-length", msg || ("must be exactly " + n + " characters"));
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
schema.nonempty = function (msg) { return schema.min(1, msg || "must not be empty"); };
|
|
398
|
+
schema.regex = function (re, msg) {
|
|
399
|
+
return chain(function (v, p) {
|
|
400
|
+
return re.test(v) ? { ok: true } :
|
|
401
|
+
_fail(p, "string/regex", msg || "does not match required pattern");
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
schema.startsWith = function (prefix, msg) {
|
|
405
|
+
return chain(function (v, p) {
|
|
406
|
+
return v.indexOf(prefix) === 0 ? { ok: true } :
|
|
407
|
+
_fail(p, "string/starts-with", msg || "must start with '" + prefix + "'");
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
schema.endsWith = function (suffix, msg) {
|
|
411
|
+
return chain(function (v, p) {
|
|
412
|
+
return v.length >= suffix.length && v.slice(-suffix.length) === suffix ? { ok: true } :
|
|
413
|
+
_fail(p, "string/ends-with", msg || "must end with '" + suffix + "'");
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
schema.includes = function (needle, msg) {
|
|
417
|
+
return chain(function (v, p) {
|
|
418
|
+
return v.indexOf(needle) !== -1 ? { ok: true } :
|
|
419
|
+
_fail(p, "string/includes", msg || "must include '" + needle + "'");
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
schema.email = function () {
|
|
423
|
+
return chain(function (v, p) {
|
|
424
|
+
// RFC 5321 §4.5.3.1.3 — max forward-path is 256 octets including
|
|
425
|
+
// angle brackets, so the address itself is bounded at 254 chars.
|
|
426
|
+
// Without this cap an operator chaining .email() on a request body
|
|
427
|
+
// is open to a DoS shape (50 KB email -> downstream DB writes
|
|
428
|
+
// unbounded string columns, log lines, etc.). Operators with a
|
|
429
|
+
// legitimate non-RFC reason for longer emails skip .email() and
|
|
430
|
+
// chain .regex(custom) directly.
|
|
431
|
+
if (v.length > EMAIL_MAX_LEN) return _fail(p, "string/email-too-long",
|
|
432
|
+
"must be a valid email address (max " + EMAIL_MAX_LEN + " chars per RFC 5321)");
|
|
433
|
+
return EMAIL_RE.test(v) ? { ok: true } :
|
|
434
|
+
_fail(p, "string/email", "must be a valid email address");
|
|
435
|
+
});
|
|
436
|
+
};
|
|
437
|
+
schema.url = function () {
|
|
438
|
+
return chain(function (v, p) {
|
|
439
|
+
// RFC 9110 doesn't set a hard URL length, but RFC 7230 §3.1.1
|
|
440
|
+
// recommended 8000 octets and most HTTP origin servers + load
|
|
441
|
+
// balancers cap at 8 KB. Without this bound an operator chaining
|
|
442
|
+
// .url() on a request body was open to a 50 MB URL passing
|
|
443
|
+
// validation. Operators with a legitimate non-standard use
|
|
444
|
+
// (tunnels, proxies with embedded payloads) skip .url() and
|
|
445
|
+
// chain .regex(custom) directly.
|
|
446
|
+
if (v.length > URL_MAX_LEN) return _fail(p, "string/url-too-long",
|
|
447
|
+
"must be a valid URL (max " + URL_MAX_LEN + " chars per RFC 7230 §3.1.1 guidance)");
|
|
448
|
+
return URL_RE.test(v) ? { ok: true } :
|
|
449
|
+
_fail(p, "string/url", "must be a valid URL");
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
schema.uuid = function () {
|
|
453
|
+
return chain(function (v, p) {
|
|
454
|
+
// RFC 4122 UUID is 36 chars (8-4-4-4-12 + 4 dashes); cap defensively
|
|
455
|
+
// so a 50-MB string can't reach the regex engine.
|
|
456
|
+
if (typeof v !== "string" || v.length > UUID_MAX_LEN || !UUID_RE.test(v)) {
|
|
457
|
+
return _fail(p, "string/uuid", "must be a valid UUID");
|
|
458
|
+
}
|
|
459
|
+
return { ok: true };
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
schema.date = function () {
|
|
463
|
+
return chain(function (v, p) {
|
|
464
|
+
// YYYY-MM-DD is 10 chars; cap defensively before the regex test.
|
|
465
|
+
if (typeof v !== "string" || v.length > DATE_MAX_LEN || !DATE_RE.test(v)) {
|
|
466
|
+
return _fail(p, "string/date", "must be a YYYY-MM-DD date");
|
|
467
|
+
}
|
|
468
|
+
return { ok: true };
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
schema.datetime = function () {
|
|
472
|
+
return chain(function (v, p) {
|
|
473
|
+
// ISO-8601 with offset + fractional seconds tops out near 64 chars;
|
|
474
|
+
// cap defensively before the regex test.
|
|
475
|
+
if (typeof v !== "string" || v.length > DATETIME_MAX_LEN || !DATETIME_RE.test(v)) {
|
|
476
|
+
return _fail(p, "string/datetime", "must be an ISO-8601 datetime with timezone");
|
|
477
|
+
}
|
|
478
|
+
return { ok: true };
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
// IP-address validators delegate to safe-json's algorithmic format
|
|
482
|
+
// checks rather than re-running regex matches. The algorithmic path
|
|
483
|
+
// handles edge cases that pure regex misses (compressed `::` shapes,
|
|
484
|
+
// IPv4-mapped `::ffff:1.2.3.4`, multi-`::` rejection, group-count
|
|
485
|
+
// bounds) and keeps the framework's IP-validation behavior in one
|
|
486
|
+
// tested place. IPV4_RE / IPV6_RE remain exported for operators who
|
|
487
|
+
// want the structural pattern, but `.ipv4()` / `.ipv6()` / `.ip()`
|
|
488
|
+
// are the canonical validation surface.
|
|
489
|
+
schema.ipv4 = function () {
|
|
490
|
+
return chain(function (v, p) {
|
|
491
|
+
return (typeof v === "string" && safeJson.formats.ipv4(v))
|
|
492
|
+
? { ok: true }
|
|
493
|
+
: _fail(p, "string/ipv4", "must be a valid IPv4 address");
|
|
494
|
+
});
|
|
495
|
+
};
|
|
496
|
+
schema.ipv6 = function () {
|
|
497
|
+
return chain(function (v, p) {
|
|
498
|
+
return (typeof v === "string" && safeJson.formats.ipv6(v))
|
|
499
|
+
? { ok: true }
|
|
500
|
+
: _fail(p, "string/ipv6", "must be a valid IPv6 address");
|
|
501
|
+
});
|
|
502
|
+
};
|
|
503
|
+
schema.ip = function () {
|
|
504
|
+
return chain(function (v, p) {
|
|
505
|
+
return (typeof v === "string" && safeJson.formats.ip(v))
|
|
506
|
+
? { ok: true }
|
|
507
|
+
: _fail(p, "string/ip", "must be a valid IP address (v4 or v6)");
|
|
508
|
+
});
|
|
509
|
+
};
|
|
510
|
+
schema.cuid = function () {
|
|
511
|
+
return chain(function (v, p) {
|
|
512
|
+
// CUID v1/v2 is 25 chars; cap defensively before the regex test.
|
|
513
|
+
if (typeof v !== "string" || v.length > CUID_MAX_LEN || !CUID_RE.test(v)) {
|
|
514
|
+
return _fail(p, "string/cuid", "must be a valid CUID");
|
|
515
|
+
}
|
|
516
|
+
return { ok: true };
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
schema.ulid = function () {
|
|
520
|
+
return chain(function (v, p) {
|
|
521
|
+
// ULID is exactly 26 chars; cap defensively before the regex test.
|
|
522
|
+
if (typeof v !== "string" || v.length > ULID_MAX_LEN || !ULID_RE.test(v)) {
|
|
523
|
+
return _fail(p, "string/ulid", "must be a valid ULID");
|
|
524
|
+
}
|
|
525
|
+
return { ok: true };
|
|
526
|
+
});
|
|
527
|
+
};
|
|
528
|
+
schema.base64 = function () {
|
|
529
|
+
return chain(function (v, p) {
|
|
530
|
+
// Base64 has no protocol-fixed cap; bound at the same 8 KiB the
|
|
531
|
+
// .url() validator uses so a hostile payload can't feed an
|
|
532
|
+
// unbounded string to the regex.
|
|
533
|
+
if (typeof v !== "string" || v.length > URL_MAX_LEN) {
|
|
534
|
+
return _fail(p, "string/base64", "must be valid base64 (standard alphabet)");
|
|
535
|
+
}
|
|
536
|
+
return BASE64_RE.test(v) ? { ok: true } :
|
|
537
|
+
_fail(p, "string/base64", "must be valid base64 (standard alphabet)");
|
|
538
|
+
});
|
|
539
|
+
};
|
|
540
|
+
// Coercion-via-transform — these mutate the validated string before
|
|
541
|
+
// the next check runs. Apply EARLY in the chain (before .min, .email,
|
|
542
|
+
// etc.) so subsequent checks see the normalized form.
|
|
543
|
+
schema.trim = function () {
|
|
544
|
+
return chain(function (v) { return { ok: true, value: v.trim() }; });
|
|
545
|
+
};
|
|
546
|
+
schema.toLowerCase = function () {
|
|
547
|
+
return chain(function (v) { return { ok: true, value: v.toLowerCase() }; });
|
|
548
|
+
};
|
|
549
|
+
schema.toUpperCase = function () {
|
|
550
|
+
return chain(function (v) { return { ok: true, value: v.toUpperCase() }; });
|
|
551
|
+
};
|
|
552
|
+
return schema;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ---- number ----
|
|
556
|
+
|
|
557
|
+
function number() {
|
|
558
|
+
var spec = {
|
|
559
|
+
kind: "number",
|
|
560
|
+
run: function (value, path) {
|
|
561
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
562
|
+
return _fail(path, "type", "must be a number");
|
|
563
|
+
}
|
|
564
|
+
return { ok: true, value: value };
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
return _numberMethods(_baseSchema(spec), spec);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function _numberMethods(schema, spec) {
|
|
571
|
+
function chain(check) {
|
|
572
|
+
var next = _withCheck(schema, spec, check);
|
|
573
|
+
return _numberMethods(next, _extendSpec(spec, { run: next._run }));
|
|
574
|
+
}
|
|
575
|
+
schema.int = function (msg) {
|
|
576
|
+
return chain(function (v, p) {
|
|
577
|
+
return Number.isInteger(v) ? { ok: true } :
|
|
578
|
+
_fail(p, "number/not-integer", msg || "must be an integer");
|
|
579
|
+
});
|
|
580
|
+
};
|
|
581
|
+
schema.min = function (n, msg) {
|
|
582
|
+
return chain(function (v, p) {
|
|
583
|
+
return v >= n ? { ok: true } : _fail(p, "number/too-small", msg || ("must be ≥ " + n));
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
schema.max = function (n, msg) {
|
|
587
|
+
return chain(function (v, p) {
|
|
588
|
+
return v <= n ? { ok: true } : _fail(p, "number/too-large", msg || ("must be ≤ " + n));
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
schema.gt = function (n, msg) {
|
|
592
|
+
return chain(function (v, p) {
|
|
593
|
+
return v > n ? { ok: true } : _fail(p, "number/not-gt", msg || ("must be > " + n));
|
|
594
|
+
});
|
|
595
|
+
};
|
|
596
|
+
schema.lt = function (n, msg) {
|
|
597
|
+
return chain(function (v, p) {
|
|
598
|
+
return v < n ? { ok: true } : _fail(p, "number/not-lt", msg || ("must be < " + n));
|
|
599
|
+
});
|
|
600
|
+
};
|
|
601
|
+
schema.positive = function (msg) { return schema.gt(0, msg || "must be positive"); };
|
|
602
|
+
schema.negative = function (msg) { return schema.lt(0, msg || "must be negative"); };
|
|
603
|
+
schema.nonnegative = function (msg) { return schema.min(0, msg || "must be non-negative"); };
|
|
604
|
+
schema.nonpositive = function (msg) { return schema.max(0, msg || "must be non-positive"); };
|
|
605
|
+
schema.finite = function (msg) {
|
|
606
|
+
return chain(function (v, p) {
|
|
607
|
+
return Number.isFinite(v) ? { ok: true } :
|
|
608
|
+
_fail(p, "number/not-finite", msg || "must be a finite number");
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
// safe() — Number.isSafeInteger range. Important for IDs that round-trip
|
|
612
|
+
// through JSON (which can't represent BigInts) and need to survive without
|
|
613
|
+
// precision loss.
|
|
614
|
+
schema.safe = function (msg) {
|
|
615
|
+
return chain(function (v, p) {
|
|
616
|
+
return Number.isSafeInteger(v) ? { ok: true } :
|
|
617
|
+
_fail(p, "number/not-safe", msg || "must be a safe integer (within ±2^53)");
|
|
618
|
+
});
|
|
619
|
+
};
|
|
620
|
+
schema.multipleOf = function (n, msg) {
|
|
621
|
+
return chain(function (v, p) {
|
|
622
|
+
// Use modulo with fp tolerance — exact mod on floats is fragile.
|
|
623
|
+
var quot = v / n;
|
|
624
|
+
return Math.abs(quot - Math.round(quot)) < 1e-9 ? { ok: true } :
|
|
625
|
+
_fail(p, "number/not-multiple-of", msg || ("must be a multiple of " + n));
|
|
626
|
+
});
|
|
627
|
+
};
|
|
628
|
+
return schema;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// ---- boolean ----
|
|
632
|
+
|
|
633
|
+
function boolean() {
|
|
634
|
+
return _baseSchema({
|
|
635
|
+
kind: "boolean",
|
|
636
|
+
run: function (value, path) {
|
|
637
|
+
if (typeof value !== "boolean") return _fail(path, "type", "must be a boolean");
|
|
638
|
+
return { ok: true, value: value };
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ---- literal ----
|
|
644
|
+
|
|
645
|
+
function literal(expected) {
|
|
646
|
+
return _baseSchema({
|
|
647
|
+
kind: "literal",
|
|
648
|
+
run: function (value, path) {
|
|
649
|
+
if (value !== expected) {
|
|
650
|
+
return _fail(path, "literal", "must be exactly " + JSON.stringify(expected));
|
|
651
|
+
}
|
|
652
|
+
return { ok: true, value: value };
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ---- enum / oneOf ----
|
|
658
|
+
|
|
659
|
+
function enum_(values) {
|
|
660
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
661
|
+
throw new SafeSchemaError("safe-schema/bad-enum",
|
|
662
|
+
"enum requires a non-empty array of allowed values");
|
|
663
|
+
}
|
|
664
|
+
var allowedSet = new Set(values);
|
|
665
|
+
return _baseSchema({
|
|
666
|
+
kind: "enum",
|
|
667
|
+
run: function (value, path) {
|
|
668
|
+
if (!allowedSet.has(value)) {
|
|
669
|
+
return _fail(path, "enum",
|
|
670
|
+
"must be one of: " + values.map(function (v) { return JSON.stringify(v); }).join(", "));
|
|
671
|
+
}
|
|
672
|
+
return { ok: true, value: value };
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ---- null / undefined / any / unknown ----
|
|
678
|
+
|
|
679
|
+
function null_() {
|
|
680
|
+
return _baseSchema({
|
|
681
|
+
kind: "null",
|
|
682
|
+
isNullable: true,
|
|
683
|
+
run: function (value, path) {
|
|
684
|
+
// Modifier handler accepts null already; if we got here, value is non-null.
|
|
685
|
+
return _fail(path, "type", "must be null");
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function undefined_() {
|
|
691
|
+
return _baseSchema({
|
|
692
|
+
kind: "undefined",
|
|
693
|
+
isOptional: true,
|
|
694
|
+
run: function (_value, path) {
|
|
695
|
+
return _fail(path, "type", "must be undefined");
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function any() {
|
|
701
|
+
return _baseSchema({
|
|
702
|
+
kind: "any",
|
|
703
|
+
isOptional: true,
|
|
704
|
+
isNullable: true,
|
|
705
|
+
run: function (value) { return { ok: true, value: value }; },
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function unknown() { return any(); }
|
|
710
|
+
|
|
711
|
+
// ---- object ----
|
|
712
|
+
|
|
713
|
+
function object(shape) {
|
|
714
|
+
if (shape == null || typeof shape !== "object") {
|
|
715
|
+
throw new SafeSchemaError("safe-schema/bad-shape",
|
|
716
|
+
"object() requires a shape object mapping field name to schema");
|
|
717
|
+
}
|
|
718
|
+
// Use getOwnPropertyNames so an operator who built the shape via
|
|
719
|
+
// Object.fromEntries / defineProperty (the only paths by which a
|
|
720
|
+
// POISONED_KEYS name can appear as an own property — object-literal
|
|
721
|
+
// {"__proto__": ...} syntax sets the prototype rather than creating
|
|
722
|
+
// such a key) gets a refusal at construction time.
|
|
723
|
+
var allOwnKeys = Object.getOwnPropertyNames(shape);
|
|
724
|
+
for (var ai = 0; ai < allOwnKeys.length; ai++) {
|
|
725
|
+
if (POISONED_KEYS.has(allOwnKeys[ai])) {
|
|
726
|
+
throw new SafeSchemaError("safe-schema/poisoned-shape-key",
|
|
727
|
+
"object shape: key '" + allOwnKeys[ai] + "' is forbidden (prototype-pollution defense)");
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
var keys = Object.keys(shape);
|
|
731
|
+
for (var k = 0; k < keys.length; k++) {
|
|
732
|
+
if (!shape[keys[k]] || typeof shape[keys[k]]._run !== "function") {
|
|
733
|
+
throw new SafeSchemaError("safe-schema/bad-shape",
|
|
734
|
+
"object shape: '" + keys[k] + "' is not a schema");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return _objectWithMode(shape, keys, "strict");
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function _objectWithMode(shape, keys, mode) {
|
|
741
|
+
var spec = {
|
|
742
|
+
kind: "object",
|
|
743
|
+
run: function (value, path) {
|
|
744
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
745
|
+
return _fail(path, "type", "must be an object");
|
|
746
|
+
}
|
|
747
|
+
var issues = [];
|
|
748
|
+
var out = {};
|
|
749
|
+
for (var i = 0; i < keys.length; i++) {
|
|
750
|
+
var key = keys[i];
|
|
751
|
+
var sub = shape[key];
|
|
752
|
+
var childPath = path.concat([key]);
|
|
753
|
+
var r = _runWithModifiers(sub, value[key], childPath);
|
|
754
|
+
if (!r.ok) {
|
|
755
|
+
for (var j = 0; j < r.issues.length; j++) issues.push(r.issues[j]);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (r.value !== undefined) out[key] = r.value;
|
|
759
|
+
}
|
|
760
|
+
var inputKeys = Object.keys(value);
|
|
761
|
+
for (var ii = 0; ii < inputKeys.length; ii++) {
|
|
762
|
+
var ik = inputKeys[ii];
|
|
763
|
+
if (Object.prototype.hasOwnProperty.call(shape, ik)) continue;
|
|
764
|
+
// Prototype-pollution defense — refuse __proto__/constructor/
|
|
765
|
+
// prototype regardless of mode. .passthrough() never propagates
|
|
766
|
+
// these because they are always rejected as input.
|
|
767
|
+
if (POISONED_KEYS.has(ik)) {
|
|
768
|
+
issues.push({
|
|
769
|
+
path: path.concat([ik]),
|
|
770
|
+
code: "object/poisoned-key",
|
|
771
|
+
message: "key '" + ik + "' is forbidden (prototype-pollution defense)",
|
|
772
|
+
});
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (mode === "passthrough") {
|
|
776
|
+
out[ik] = value[ik];
|
|
777
|
+
} else {
|
|
778
|
+
issues.push({
|
|
779
|
+
path: path.concat([ik]),
|
|
780
|
+
code: "object/unknown-key",
|
|
781
|
+
message: "unknown key '" + ik + "' (use .passthrough() to allow extra keys)",
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (issues.length > 0) return { ok: false, issues: issues };
|
|
786
|
+
return { ok: true, value: out };
|
|
787
|
+
},
|
|
788
|
+
};
|
|
789
|
+
var schema = _baseSchema(spec);
|
|
790
|
+
schema.shape = shape;
|
|
791
|
+
schema.strict = function () { return _objectWithMode(shape, keys, "strict"); };
|
|
792
|
+
schema.passthrough = function () { return _objectWithMode(shape, keys, "passthrough"); };
|
|
793
|
+
|
|
794
|
+
// .pick(["a","b"]) → narrow to listed keys
|
|
795
|
+
schema.pick = function (pickKeys) {
|
|
796
|
+
var newShape = {};
|
|
797
|
+
for (var i = 0; i < pickKeys.length; i++) {
|
|
798
|
+
var k = pickKeys[i];
|
|
799
|
+
if (Object.prototype.hasOwnProperty.call(shape, k)) newShape[k] = shape[k];
|
|
800
|
+
}
|
|
801
|
+
return _objectWithMode(newShape, Object.keys(newShape), mode);
|
|
802
|
+
};
|
|
803
|
+
// .omit(["a","b"]) → drop listed keys
|
|
804
|
+
schema.omit = function (omitKeys) {
|
|
805
|
+
var omitSet = new Set(omitKeys);
|
|
806
|
+
var newShape = {};
|
|
807
|
+
var newKeys = [];
|
|
808
|
+
for (var i = 0; i < keys.length; i++) {
|
|
809
|
+
if (!omitSet.has(keys[i])) {
|
|
810
|
+
newShape[keys[i]] = shape[keys[i]];
|
|
811
|
+
newKeys.push(keys[i]);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return _objectWithMode(newShape, newKeys, mode);
|
|
815
|
+
};
|
|
816
|
+
// .extend({ ... }) → merge additional shape; new keys override
|
|
817
|
+
schema.extend = function (additional) {
|
|
818
|
+
if (!additional || typeof additional !== "object") {
|
|
819
|
+
throw new SafeSchemaError("safe-schema/bad-extend",
|
|
820
|
+
"extend() requires a shape object");
|
|
821
|
+
}
|
|
822
|
+
var merged = Object.assign({}, shape, additional);
|
|
823
|
+
return _objectWithMode(merged, Object.keys(merged), mode);
|
|
824
|
+
};
|
|
825
|
+
// .partial() → mark every key optional
|
|
826
|
+
schema.partial = function () {
|
|
827
|
+
var newShape = {};
|
|
828
|
+
var newKeys = [];
|
|
829
|
+
for (var i = 0; i < keys.length; i++) {
|
|
830
|
+
newShape[keys[i]] = shape[keys[i]].optional();
|
|
831
|
+
newKeys.push(keys[i]);
|
|
832
|
+
}
|
|
833
|
+
return _objectWithMode(newShape, newKeys, mode);
|
|
834
|
+
};
|
|
835
|
+
// .required() — inverse of partial(). Strips optional + default + nullable
|
|
836
|
+
// off every key (operators sometimes call .partial() and then peel back
|
|
837
|
+
// a subset; this is the cleanest path).
|
|
838
|
+
schema.required = function () {
|
|
839
|
+
var newShape = {};
|
|
840
|
+
var newKeys = [];
|
|
841
|
+
for (var i = 0; i < keys.length; i++) {
|
|
842
|
+
var inner = shape[keys[i]];
|
|
843
|
+
// Strip modifiers by rebuilding without them. We don't have the
|
|
844
|
+
// pre-modifier schema cached, but we can reach it: the underlying
|
|
845
|
+
// _run is preserved; clone with all modifier flags off.
|
|
846
|
+
newShape[keys[i]] = _baseSchema({
|
|
847
|
+
kind: inner._kind,
|
|
848
|
+
isOptional: false,
|
|
849
|
+
isNullable: false,
|
|
850
|
+
hasDefault: false,
|
|
851
|
+
hasCatch: inner._hasCatch,
|
|
852
|
+
catchValue: inner._catch,
|
|
853
|
+
run: inner._run,
|
|
854
|
+
});
|
|
855
|
+
newKeys.push(keys[i]);
|
|
856
|
+
}
|
|
857
|
+
return _objectWithMode(newShape, newKeys, mode);
|
|
858
|
+
};
|
|
859
|
+
return schema;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// ---- array ----
|
|
863
|
+
|
|
864
|
+
function array(itemSchema) {
|
|
865
|
+
if (!itemSchema || typeof itemSchema._run !== "function") {
|
|
866
|
+
throw new SafeSchemaError("safe-schema/bad-item",
|
|
867
|
+
"array() requires an item schema");
|
|
868
|
+
}
|
|
869
|
+
var spec = {
|
|
870
|
+
kind: "array",
|
|
871
|
+
run: function (value, path) {
|
|
872
|
+
if (!Array.isArray(value)) return _fail(path, "type", "must be an array");
|
|
873
|
+
var issues = [];
|
|
874
|
+
var out = [];
|
|
875
|
+
for (var i = 0; i < value.length; i++) {
|
|
876
|
+
var childPath = path.concat([i]);
|
|
877
|
+
var r = _runWithModifiers(itemSchema, value[i], childPath);
|
|
878
|
+
if (!r.ok) {
|
|
879
|
+
for (var j = 0; j < r.issues.length; j++) issues.push(r.issues[j]);
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
out.push(r.value);
|
|
883
|
+
}
|
|
884
|
+
if (issues.length > 0) return { ok: false, issues: issues };
|
|
885
|
+
return { ok: true, value: out };
|
|
886
|
+
},
|
|
887
|
+
};
|
|
888
|
+
return _arrayMethods(_baseSchema(spec), spec);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function _arrayMethods(schema, spec) {
|
|
892
|
+
function chain(check) {
|
|
893
|
+
var next = _withCheck(schema, spec, check);
|
|
894
|
+
return _arrayMethods(next, _extendSpec(spec, { run: next._run }));
|
|
895
|
+
}
|
|
896
|
+
schema.min = function (n, msg) {
|
|
897
|
+
return chain(function (v, p) {
|
|
898
|
+
return v.length >= n ? { ok: true } :
|
|
899
|
+
_fail(p, "array/too-short", msg || ("must contain at least " + n + " items"));
|
|
900
|
+
});
|
|
901
|
+
};
|
|
902
|
+
schema.max = function (n, msg) {
|
|
903
|
+
return chain(function (v, p) {
|
|
904
|
+
return v.length <= n ? { ok: true } :
|
|
905
|
+
_fail(p, "array/too-long", msg || ("must contain at most " + n + " items"));
|
|
906
|
+
});
|
|
907
|
+
};
|
|
908
|
+
schema.length = function (n, msg) {
|
|
909
|
+
return chain(function (v, p) {
|
|
910
|
+
return v.length === n ? { ok: true } :
|
|
911
|
+
_fail(p, "array/wrong-length", msg || ("must contain exactly " + n + " items"));
|
|
912
|
+
});
|
|
913
|
+
};
|
|
914
|
+
schema.nonempty = function (msg) { return schema.min(1, msg || "must not be empty"); };
|
|
915
|
+
return schema;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ---- tuple ----
|
|
919
|
+
|
|
920
|
+
function tuple(items) {
|
|
921
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
922
|
+
throw new SafeSchemaError("safe-schema/bad-tuple",
|
|
923
|
+
"tuple() requires a non-empty array of item schemas");
|
|
924
|
+
}
|
|
925
|
+
for (var i = 0; i < items.length; i++) {
|
|
926
|
+
if (!items[i] || typeof items[i]._run !== "function") {
|
|
927
|
+
throw new SafeSchemaError("safe-schema/bad-tuple",
|
|
928
|
+
"tuple item " + i + " is not a schema");
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return _tupleWithRest(items, null);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function _tupleWithRest(items, restSchema) {
|
|
935
|
+
var schema = _baseSchema({
|
|
936
|
+
kind: "tuple",
|
|
937
|
+
run: function (value, path) {
|
|
938
|
+
if (!Array.isArray(value)) return _fail(path, "type", "must be an array (tuple)");
|
|
939
|
+
if (restSchema === null && value.length !== items.length) {
|
|
940
|
+
return _fail(path, "tuple/wrong-length",
|
|
941
|
+
"tuple must contain exactly " + items.length + " items (got " + value.length + ")");
|
|
942
|
+
}
|
|
943
|
+
if (restSchema !== null && value.length < items.length) {
|
|
944
|
+
return _fail(path, "tuple/wrong-length",
|
|
945
|
+
"tuple must contain at least " + items.length + " items (got " + value.length + ")");
|
|
946
|
+
}
|
|
947
|
+
var issues = [];
|
|
948
|
+
var out = [];
|
|
949
|
+
for (var i = 0; i < items.length; i++) {
|
|
950
|
+
var r = _runWithModifiers(items[i], value[i], path.concat([i]));
|
|
951
|
+
if (!r.ok) {
|
|
952
|
+
for (var j = 0; j < r.issues.length; j++) issues.push(r.issues[j]);
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
out.push(r.value);
|
|
956
|
+
}
|
|
957
|
+
// Variadic tail — every extra item is checked against restSchema.
|
|
958
|
+
if (restSchema !== null) {
|
|
959
|
+
for (var k = items.length; k < value.length; k++) {
|
|
960
|
+
var rr = _runWithModifiers(restSchema, value[k], path.concat([k]));
|
|
961
|
+
if (!rr.ok) {
|
|
962
|
+
for (var jj = 0; jj < rr.issues.length; jj++) issues.push(rr.issues[jj]);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
out.push(rr.value);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (issues.length > 0) return { ok: false, issues: issues };
|
|
969
|
+
return { ok: true, value: out };
|
|
970
|
+
},
|
|
971
|
+
});
|
|
972
|
+
// .rest(itemSchema) — append a variadic tail to the tuple. Common for
|
|
973
|
+
// protocol shapes like [verb, ...args] or [event, payload, ...metadata].
|
|
974
|
+
schema.rest = function (item) {
|
|
975
|
+
if (!item || typeof item._run !== "function") {
|
|
976
|
+
throw new SafeSchemaError("safe-schema/bad-tuple-rest",
|
|
977
|
+
"tuple.rest(): argument must be a schema");
|
|
978
|
+
}
|
|
979
|
+
return _tupleWithRest(items, item);
|
|
980
|
+
};
|
|
981
|
+
return schema;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// ---- union ----
|
|
985
|
+
|
|
986
|
+
function union(options) {
|
|
987
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
988
|
+
throw new SafeSchemaError("safe-schema/bad-union",
|
|
989
|
+
"union() requires a non-empty array of option schemas");
|
|
990
|
+
}
|
|
991
|
+
for (var i = 0; i < options.length; i++) {
|
|
992
|
+
if (!options[i] || typeof options[i]._run !== "function") {
|
|
993
|
+
throw new SafeSchemaError("safe-schema/bad-union",
|
|
994
|
+
"union option " + i + " is not a schema");
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return _baseSchema({
|
|
998
|
+
kind: "union",
|
|
999
|
+
run: function (value, path) {
|
|
1000
|
+
var collected = [];
|
|
1001
|
+
for (var i = 0; i < options.length; i++) {
|
|
1002
|
+
var r = _runWithModifiers(options[i], value, path);
|
|
1003
|
+
if (r.ok) return r;
|
|
1004
|
+
// Collect each option's issues so the operator sees the full
|
|
1005
|
+
// failure surface; helpful for debugging which option matched
|
|
1006
|
+
// most closely.
|
|
1007
|
+
for (var j = 0; j < r.issues.length; j++) collected.push(r.issues[j]);
|
|
1008
|
+
}
|
|
1009
|
+
// No option matched — emit a union-level issue at the parent path
|
|
1010
|
+
// plus the collected per-option issues for deep diagnostics.
|
|
1011
|
+
var summary = _fail(path, "union",
|
|
1012
|
+
"did not match any of the " + options.length + " allowed shapes");
|
|
1013
|
+
summary.issues = summary.issues.concat(collected);
|
|
1014
|
+
return summary;
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// ---- record ----
|
|
1020
|
+
// record(value) — string keys, schema-typed values
|
|
1021
|
+
// record(keySchema, value) — both keys and values are schema-validated
|
|
1022
|
+
|
|
1023
|
+
function record(a, b) {
|
|
1024
|
+
var keySchema, valueSchema;
|
|
1025
|
+
if (b === undefined) {
|
|
1026
|
+
keySchema = null; // any string key
|
|
1027
|
+
valueSchema = a;
|
|
1028
|
+
} else {
|
|
1029
|
+
keySchema = a;
|
|
1030
|
+
valueSchema = b;
|
|
1031
|
+
}
|
|
1032
|
+
if (!valueSchema || typeof valueSchema._run !== "function") {
|
|
1033
|
+
throw new SafeSchemaError("safe-schema/bad-value-schema",
|
|
1034
|
+
"record() requires a value schema");
|
|
1035
|
+
}
|
|
1036
|
+
if (keySchema && typeof keySchema._run !== "function") {
|
|
1037
|
+
throw new SafeSchemaError("safe-schema/bad-key-schema",
|
|
1038
|
+
"record(keySchema, valueSchema): keySchema must be a schema");
|
|
1039
|
+
}
|
|
1040
|
+
return _baseSchema({
|
|
1041
|
+
kind: "record",
|
|
1042
|
+
run: function (value, path) {
|
|
1043
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1044
|
+
return _fail(path, "type", "must be a plain object (record)");
|
|
1045
|
+
}
|
|
1046
|
+
var issues = [];
|
|
1047
|
+
var out = {};
|
|
1048
|
+
var keys = Object.keys(value);
|
|
1049
|
+
for (var i = 0; i < keys.length; i++) {
|
|
1050
|
+
var k = keys[i];
|
|
1051
|
+
// Prototype-pollution defense — same shape as object() schema.
|
|
1052
|
+
if (POISONED_KEYS.has(k)) {
|
|
1053
|
+
issues.push({
|
|
1054
|
+
path: path.concat([k]),
|
|
1055
|
+
code: "record/poisoned-key",
|
|
1056
|
+
message: "key '" + k + "' is forbidden (prototype-pollution defense)",
|
|
1057
|
+
});
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
if (keySchema) {
|
|
1061
|
+
var kr = _runWithModifiers(keySchema, k, path.concat([k]));
|
|
1062
|
+
if (!kr.ok) {
|
|
1063
|
+
for (var jj = 0; jj < kr.issues.length; jj++) issues.push(kr.issues[jj]);
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
var r = _runWithModifiers(valueSchema, value[k], path.concat([k]));
|
|
1068
|
+
if (!r.ok) {
|
|
1069
|
+
for (var j = 0; j < r.issues.length; j++) issues.push(r.issues[j]);
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
if (r.value !== undefined) out[k] = r.value;
|
|
1073
|
+
}
|
|
1074
|
+
if (issues.length > 0) return { ok: false, issues: issues };
|
|
1075
|
+
return { ok: true, value: out };
|
|
1076
|
+
},
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// ---- discriminatedUnion ----
|
|
1081
|
+
//
|
|
1082
|
+
// Performance + ergonomics improvement over union for the common case
|
|
1083
|
+
// of "tagged variants": a literal field on each option distinguishes
|
|
1084
|
+
// the branches, so we dispatch on that field's value directly rather
|
|
1085
|
+
// than trying every option in turn.
|
|
1086
|
+
//
|
|
1087
|
+
// var event = s.discriminatedUnion("kind", [
|
|
1088
|
+
// s.object({ kind: s.literal("created"), at: s.string().datetime() }),
|
|
1089
|
+
// s.object({ kind: s.literal("deleted"), reason: s.string() }),
|
|
1090
|
+
// ]);
|
|
1091
|
+
//
|
|
1092
|
+
// Each option must be an object schema whose `discriminator` key is a
|
|
1093
|
+
// literal schema. Mismatched discriminator fails fast with a clear
|
|
1094
|
+
// "expected one of [...]" message rather than burying the operator in
|
|
1095
|
+
// per-branch issues.
|
|
1096
|
+
function discriminatedUnion(discriminator, options) {
|
|
1097
|
+
if (typeof discriminator !== "string" || discriminator.length === 0) {
|
|
1098
|
+
throw new SafeSchemaError("safe-schema/bad-discriminator",
|
|
1099
|
+
"discriminatedUnion: discriminator must be a non-empty string key name");
|
|
1100
|
+
}
|
|
1101
|
+
if (POISONED_KEYS.has(discriminator)) {
|
|
1102
|
+
throw new SafeSchemaError("safe-schema/poisoned-discriminator",
|
|
1103
|
+
"discriminatedUnion: discriminator key '" + discriminator + "' is forbidden");
|
|
1104
|
+
}
|
|
1105
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
1106
|
+
throw new SafeSchemaError("safe-schema/bad-union",
|
|
1107
|
+
"discriminatedUnion: options must be a non-empty array");
|
|
1108
|
+
}
|
|
1109
|
+
for (var i = 0; i < options.length; i++) {
|
|
1110
|
+
var opt = options[i];
|
|
1111
|
+
if (!opt || opt._kind !== "object" || !opt.shape) {
|
|
1112
|
+
throw new SafeSchemaError("safe-schema/bad-discriminated-option",
|
|
1113
|
+
"discriminatedUnion option " + i + " must be an object schema");
|
|
1114
|
+
}
|
|
1115
|
+
var disc = opt.shape[discriminator];
|
|
1116
|
+
if (!disc || disc._kind !== "literal") {
|
|
1117
|
+
throw new SafeSchemaError("safe-schema/bad-discriminated-option",
|
|
1118
|
+
"discriminatedUnion option " + i + ": discriminator '" + discriminator +
|
|
1119
|
+
"' must be a literal schema");
|
|
1120
|
+
}
|
|
1121
|
+
// The literal's expected value lives in its closure; we extract via
|
|
1122
|
+
// a probe rather than introspection. literal(v)._run(v) succeeds.
|
|
1123
|
+
// We sample by trying every option's discriminator separately at
|
|
1124
|
+
// dispatch time (cheap; literal._run is just a triple-equals).
|
|
1125
|
+
}
|
|
1126
|
+
return _baseSchema({
|
|
1127
|
+
kind: "discriminatedUnion",
|
|
1128
|
+
run: function (value, path) {
|
|
1129
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1130
|
+
return _fail(path, "type", "must be an object (discriminated union)");
|
|
1131
|
+
}
|
|
1132
|
+
var disc = value[discriminator];
|
|
1133
|
+
// Find the option whose discriminator schema accepts disc.
|
|
1134
|
+
for (var i = 0; i < options.length; i++) {
|
|
1135
|
+
var opt = options[i];
|
|
1136
|
+
var d = opt.shape[discriminator];
|
|
1137
|
+
var dr = d._run(disc, path.concat([discriminator]));
|
|
1138
|
+
if (dr.ok) {
|
|
1139
|
+
return _runWithModifiers(opt, value, path);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return _fail(path.concat([discriminator]), "discriminated-union/no-match",
|
|
1143
|
+
"discriminator '" + discriminator + "' did not match any option");
|
|
1144
|
+
},
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// ---- preprocess ----
|
|
1149
|
+
//
|
|
1150
|
+
// Run a transform BEFORE validation. Common at HTTP boundaries where
|
|
1151
|
+
// query strings arrive as strings but the operator wants a number /
|
|
1152
|
+
// boolean schema downstream.
|
|
1153
|
+
//
|
|
1154
|
+
// var port = s.preprocess(function (v) { return Number(v); }, s.number().int().min(1).max(65535));
|
|
1155
|
+
//
|
|
1156
|
+
// fn errors propagate as a 'preprocess' issue at the parent path; they
|
|
1157
|
+
// don't crash the validate call.
|
|
1158
|
+
function preprocess(fn, inner) {
|
|
1159
|
+
if (typeof fn !== "function") {
|
|
1160
|
+
throw new SafeSchemaError("safe-schema/bad-preprocess",
|
|
1161
|
+
"preprocess: first arg must be a function");
|
|
1162
|
+
}
|
|
1163
|
+
if (!inner || typeof inner._run !== "function") {
|
|
1164
|
+
throw new SafeSchemaError("safe-schema/bad-preprocess",
|
|
1165
|
+
"preprocess: second arg must be a schema");
|
|
1166
|
+
}
|
|
1167
|
+
return _baseSchema({
|
|
1168
|
+
kind: "preprocess",
|
|
1169
|
+
run: function (value, path) {
|
|
1170
|
+
var preprocessed;
|
|
1171
|
+
try {
|
|
1172
|
+
preprocessed = fn(value);
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
return _fail(path, "preprocess",
|
|
1175
|
+
"preprocess fn threw: " + ((e && e.message) || String(e)));
|
|
1176
|
+
}
|
|
1177
|
+
return _runWithModifiers(inner, preprocessed, path);
|
|
1178
|
+
},
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// ---- lazy (recursive schemas) ----
|
|
1183
|
+
//
|
|
1184
|
+
// Defers schema construction until first parse. Operators wanting a
|
|
1185
|
+
// recursive shape — comment threads, file-tree nodes, etc. — wrap the
|
|
1186
|
+
// recursive reference in a function that returns the schema:
|
|
1187
|
+
//
|
|
1188
|
+
// var commentSchema = s.object({
|
|
1189
|
+
// id: s.string(),
|
|
1190
|
+
// replies: s.array(s.lazy(function () { return commentSchema; })),
|
|
1191
|
+
// });
|
|
1192
|
+
//
|
|
1193
|
+
// The function is called lazily and cached per-call site; cycles in the
|
|
1194
|
+
// returned schema are fine.
|
|
1195
|
+
function lazy(getter) {
|
|
1196
|
+
if (typeof getter !== "function") {
|
|
1197
|
+
throw new SafeSchemaError("safe-schema/bad-lazy",
|
|
1198
|
+
"lazy: argument must be a function returning a schema");
|
|
1199
|
+
}
|
|
1200
|
+
var cached = null;
|
|
1201
|
+
return _baseSchema({
|
|
1202
|
+
kind: "lazy",
|
|
1203
|
+
run: function (value, path) {
|
|
1204
|
+
if (!cached) {
|
|
1205
|
+
cached = getter();
|
|
1206
|
+
if (!cached || typeof cached._run !== "function") {
|
|
1207
|
+
return _fail(path, "lazy",
|
|
1208
|
+
"lazy() function did not return a schema");
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return _runWithModifiers(cached, value, path);
|
|
1212
|
+
},
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// ---- top-level modifier helpers ----
|
|
1217
|
+
|
|
1218
|
+
function optional(inner) { return inner.optional(); }
|
|
1219
|
+
function nullable(inner) { return inner.nullable(); }
|
|
1220
|
+
|
|
1221
|
+
module.exports = {
|
|
1222
|
+
// Primitives
|
|
1223
|
+
string: string,
|
|
1224
|
+
number: number,
|
|
1225
|
+
boolean: boolean,
|
|
1226
|
+
literal: literal,
|
|
1227
|
+
null_: null_,
|
|
1228
|
+
undefined_: undefined_,
|
|
1229
|
+
any: any,
|
|
1230
|
+
unknown: unknown,
|
|
1231
|
+
|
|
1232
|
+
// Composites
|
|
1233
|
+
object: object,
|
|
1234
|
+
array: array,
|
|
1235
|
+
tuple: tuple,
|
|
1236
|
+
union: union,
|
|
1237
|
+
discriminatedUnion: discriminatedUnion,
|
|
1238
|
+
record: record,
|
|
1239
|
+
lazy: lazy,
|
|
1240
|
+
preprocess: preprocess,
|
|
1241
|
+
|
|
1242
|
+
// enum is a reserved word in some tooling — ship both names
|
|
1243
|
+
enum_: enum_,
|
|
1244
|
+
oneOf: enum_,
|
|
1245
|
+
|
|
1246
|
+
// Modifier helpers (chained methods exist on every schema; these are
|
|
1247
|
+
// the equivalents for operators who prefer composition over chaining)
|
|
1248
|
+
optional: optional,
|
|
1249
|
+
nullable: nullable,
|
|
1250
|
+
|
|
1251
|
+
// Errors
|
|
1252
|
+
SafeSchemaError: SafeSchemaError,
|
|
1253
|
+
|
|
1254
|
+
// Validation regexes — exported so other modules don't re-declare
|
|
1255
|
+
// their own copies. Pragmatic patterns; operators wanting RFC-strict
|
|
1256
|
+
// behavior chain `.refine()` on top of the schema instead.
|
|
1257
|
+
EMAIL_RE: EMAIL_RE,
|
|
1258
|
+
URL_RE: URL_RE,
|
|
1259
|
+
UUID_RE: UUID_RE,
|
|
1260
|
+
DATE_RE: DATE_RE,
|
|
1261
|
+
DATETIME_RE: DATETIME_RE,
|
|
1262
|
+
IPV4_RE: IPV4_RE,
|
|
1263
|
+
IPV6_RE: IPV6_RE,
|
|
1264
|
+
CUID_RE: CUID_RE,
|
|
1265
|
+
ULID_RE: ULID_RE,
|
|
1266
|
+
};
|