@blamejs/core 0.8.43 → 0.8.50
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 +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/safe-schema.js
CHANGED
|
@@ -1,34 +1,87 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.safeSchema
|
|
4
|
+
* @featured true
|
|
5
|
+
* @nav Validation
|
|
6
|
+
* @title Safe Schema
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
* validation
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Declarative input validation with a Zod-shaped chained-method
|
|
10
|
+
* surface. Built for request-body validation, config validation,
|
|
11
|
+
* API payload validation, anywhere operators have an `unknown`
|
|
12
|
+
* shape they need to confirm before reading. Vendor-free; built
|
|
13
|
+
* on framework primitives. No JIT, no codegen, no chained-Promise
|
|
14
|
+
* weirdness.
|
|
9
15
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* Schemas are immutable — every chained check returns a new
|
|
17
|
+
* schema; the original is untouched. `parse(input)` throws
|
|
18
|
+
* `SafeSchemaError` carrying a full per-field issues array;
|
|
19
|
+
* `safeParse(input)` never throws and returns `{ ok, value?,
|
|
20
|
+
* errors? }` (operator-friendly at HTTP boundaries).
|
|
12
21
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
22
|
+
* Security guarantees: prototype-pollution defense — `__proto__`
|
|
23
|
+
* / `constructor` / `prototype` keys are rejected at object-shape
|
|
24
|
+
* construction AND at parse-time on object + record inputs (mirrors
|
|
25
|
+
* `b.safeJson`'s POISONED_KEYS). Per-format defensive length caps
|
|
26
|
+
* (email 254, url 8 KiB, uuid 50, datetime 100 chars …) bound input
|
|
27
|
+
* BEFORE the regex engine runs, so a hostile payload can't ReDoS
|
|
28
|
+
* `.email()` / `.url()` / `.datetime()`. All format regexes are
|
|
29
|
+
* static module-level constants — no string→regex parsing on the
|
|
30
|
+
* validation path. `.refine()` predicates that throw turn into a
|
|
31
|
+
* regular validation issue rather than crashing the request.
|
|
32
|
+
*
|
|
33
|
+
* Coercion is deliberately not shipped (`.coerce` is a footgun:
|
|
34
|
+
* "0" → 0 vs "0" → "0" ambiguity, truthy/falsy edge cases).
|
|
35
|
+
* Operators do explicit `s.preprocess(fn, schema)` instead.
|
|
36
|
+
*
|
|
37
|
+
* Relationship to `b.forms.validate`: forms.validate carries
|
|
38
|
+
* HTML-spec concerns (checkbox coercion, select option allowlist)
|
|
39
|
+
* that don't belong on the general-purpose validator, so the two
|
|
40
|
+
* surfaces stay distinct rather than one wrapping the other.
|
|
19
41
|
*
|
|
20
|
-
*
|
|
21
|
-
* var safe = schema.safeParse(input); // → { ok, value?, errors? }
|
|
42
|
+
* Surface (every schema has these chained methods unless noted):
|
|
22
43
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
44
|
+
* Type constructors:
|
|
45
|
+
* string() .min, .max, .length, .regex, .email, .url, .uuid,
|
|
46
|
+
* .datetime (ISO-8601), .date (YYYY-MM-DD),
|
|
47
|
+
* .ip, .ipv4, .ipv6, .nonempty, .startsWith,
|
|
48
|
+
* .endsWith, .includes, .cuid, .ulid, .base64,
|
|
49
|
+
* .trim, .toLowerCase, .toUpperCase
|
|
50
|
+
* number() .int, .min, .max, .gt, .lt, .positive, .negative,
|
|
51
|
+
* .nonnegative, .nonpositive, .finite, .safe,
|
|
52
|
+
* .multipleOf
|
|
53
|
+
* boolean()
|
|
54
|
+
* literal(v)
|
|
55
|
+
* enum_([...]) | oneOf([...])
|
|
56
|
+
* null_(), undefined_(), any(), unknown()
|
|
57
|
+
*
|
|
58
|
+
* Composites:
|
|
59
|
+
* object({...}) .strict, .passthrough, .pick, .omit, .extend,
|
|
60
|
+
* .partial, .required
|
|
61
|
+
* array(item) .min, .max, .length, .nonempty
|
|
62
|
+
* tuple([...]) .rest(item)
|
|
63
|
+
* union([...]) first matching option wins
|
|
64
|
+
* discriminatedUnion(key, [...]) tagged-union dispatch
|
|
65
|
+
* record(value) | record(key, value)
|
|
66
|
+
* lazy(() => schema) deferred (recursion)
|
|
67
|
+
* preprocess(fn, schema) pre-validation transform
|
|
68
|
+
*
|
|
69
|
+
* Modifiers (any schema):
|
|
70
|
+
* .optional(), .nullable(), .default(v|fn), .catch(v|fn),
|
|
71
|
+
* .refine(fn, opts), .transform(fn), .pipe(next)
|
|
72
|
+
*
|
|
73
|
+
* Deliberately not shipped (with structural reason): z.bigint /
|
|
74
|
+
* z.date / z.map / z.set (no JSON representation), z.nativeEnum
|
|
75
|
+
* / z.never / z.void / z.function (TypeScript-specific), z.coerce
|
|
76
|
+
* (security foot-gun — use s.preprocess), z.intersection
|
|
77
|
+
* (use .extend() for object schemas), z.brand (compile-time tag,
|
|
78
|
+
* no runtime effect), per-schema errorMap (chain .refine() with
|
|
79
|
+
* custom message instead).
|
|
25
80
|
*
|
|
26
81
|
* Design choices:
|
|
27
82
|
* - Schemas are immutable. Chaining returns a new schema with one
|
|
28
83
|
* additional check; the original is untouched. Cheap because
|
|
29
84
|
* 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
85
|
* - .optional() means "may be undefined"; .nullable() means "may be
|
|
33
86
|
* null"; .default(v) means "if undefined, substitute v";
|
|
34
87
|
* .catch(v) means "on ANY validation failure, substitute v".
|
|
@@ -37,76 +90,10 @@
|
|
|
37
90
|
* - Objects are STRICT by default: unknown keys produce an issue.
|
|
38
91
|
* Use .passthrough() to retain unknown keys, .strict() to flip
|
|
39
92
|
* 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
93
|
* - Sync-only: no async refinements; operators await at the boundary.
|
|
91
94
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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.
|
|
95
|
+
* @card
|
|
96
|
+
* Declarative input validation with a Zod-shaped chained-method surface.
|
|
110
97
|
*/
|
|
111
98
|
|
|
112
99
|
var C = require("./constants");
|
|
@@ -129,6 +116,35 @@ var DATETIME_MAX_LEN = 100; // ISO-8601 with offset + fractional seconds tops
|
|
|
129
116
|
var CUID_MAX_LEN = 50; // CUID v1/v2 is 25 chars
|
|
130
117
|
var ULID_MAX_LEN = 50; // ULID is exactly 26 chars
|
|
131
118
|
|
|
119
|
+
/**
|
|
120
|
+
* @primitive b.safeSchema.SafeSchemaError
|
|
121
|
+
* @signature b.safeSchema.SafeSchemaError
|
|
122
|
+
* @since 0.1.0
|
|
123
|
+
* @status stable
|
|
124
|
+
* @related b.safeSchema.string, b.safeSchema.object
|
|
125
|
+
*
|
|
126
|
+
* Error class thrown by every `b.safeSchema` primitive on
|
|
127
|
+
* construction-time misuse (bad shape / bad enum / bad union /
|
|
128
|
+
* poisoned key) and by `schema.parse(...)` on validation failure.
|
|
129
|
+
* Built via `b.framework.defineClass` and marked `alwaysPermanent`
|
|
130
|
+
* so it never round-trips through retry or transient-error logic.
|
|
131
|
+
* The thrown instance carries `.issues` — the full per-field
|
|
132
|
+
* issues array (`{ path, code, message }[]`) — so HTTP middleware
|
|
133
|
+
* can surface every failure in one 400 response.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* var b = require("blamejs");
|
|
137
|
+
* var s = b.safeSchema;
|
|
138
|
+
*
|
|
139
|
+
* try {
|
|
140
|
+
* s.string().min(3).parse("ab");
|
|
141
|
+
* } catch (e) {
|
|
142
|
+
* e instanceof s.SafeSchemaError;
|
|
143
|
+
* // → true
|
|
144
|
+
* e.issues[0].code;
|
|
145
|
+
* // → "string/too-short"
|
|
146
|
+
* }
|
|
147
|
+
*/
|
|
132
148
|
var SafeSchemaError = defineClass("SafeSchemaError", { alwaysPermanent: true });
|
|
133
149
|
|
|
134
150
|
// Prototype-pollution defense — these key names are rejected in object
|
|
@@ -360,6 +376,43 @@ function _withCheck(schema, spec, check) {
|
|
|
360
376
|
|
|
361
377
|
// ---- string ----
|
|
362
378
|
|
|
379
|
+
/**
|
|
380
|
+
* @primitive b.safeSchema.string
|
|
381
|
+
* @signature b.safeSchema.string()
|
|
382
|
+
* @since 0.1.0
|
|
383
|
+
* @status stable
|
|
384
|
+
* @related b.safeSchema.number, b.safeSchema.object
|
|
385
|
+
*
|
|
386
|
+
* Construct a string-typed schema. Chain `.min`, `.max`, `.length`,
|
|
387
|
+
* `.regex`, `.email`, `.url`, `.uuid`, `.datetime`, `.date`, `.ipv4`,
|
|
388
|
+
* `.ipv6`, `.ip`, `.cuid`, `.ulid`, `.base64`, `.startsWith`,
|
|
389
|
+
* `.endsWith`, `.includes`, `.nonempty`, `.trim`, `.toLowerCase`,
|
|
390
|
+
* `.toUpperCase` to add checks and coercions. Each chained call
|
|
391
|
+
* returns a new immutable schema.
|
|
392
|
+
*
|
|
393
|
+
* The named-format methods (`.email` / `.url` / `.uuid` / etc.)
|
|
394
|
+
* apply a defensive length cap BEFORE running the regex so a
|
|
395
|
+
* hostile payload cannot drive the regex engine with an arbitrarily
|
|
396
|
+
* long string.
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* var b = require("blamejs");
|
|
400
|
+
* var s = b.safeSchema;
|
|
401
|
+
*
|
|
402
|
+
* var name = s.string().min(1).max(80);
|
|
403
|
+
* name.parse("alice");
|
|
404
|
+
* // → "alice"
|
|
405
|
+
*
|
|
406
|
+
* var email = s.string().trim().toLowerCase().email();
|
|
407
|
+
* email.parse(" Alice@Example.COM ");
|
|
408
|
+
* // → "alice@example.com"
|
|
409
|
+
*
|
|
410
|
+
* var safe = s.string().min(3).safeParse("ab");
|
|
411
|
+
* safe.ok;
|
|
412
|
+
* // → false
|
|
413
|
+
* safe.errors[0].code;
|
|
414
|
+
* // → "string/too-short"
|
|
415
|
+
*/
|
|
363
416
|
function string() {
|
|
364
417
|
var spec = {
|
|
365
418
|
kind: "string",
|
|
@@ -554,6 +607,37 @@ function _stringMethods(schema, spec) {
|
|
|
554
607
|
|
|
555
608
|
// ---- number ----
|
|
556
609
|
|
|
610
|
+
/**
|
|
611
|
+
* @primitive b.safeSchema.number
|
|
612
|
+
* @signature b.safeSchema.number()
|
|
613
|
+
* @since 0.1.0
|
|
614
|
+
* @status stable
|
|
615
|
+
* @related b.safeSchema.string, b.safeSchema.literal
|
|
616
|
+
*
|
|
617
|
+
* Construct a number-typed schema. Rejects `NaN` at the type check.
|
|
618
|
+
* Chain `.int`, `.min`, `.max`, `.gt`, `.lt`, `.positive`,
|
|
619
|
+
* `.negative`, `.nonnegative`, `.nonpositive`, `.finite`, `.safe`,
|
|
620
|
+
* `.multipleOf` to bound the value. `.safe()` enforces the
|
|
621
|
+
* `Number.isSafeInteger` range — important for IDs that round-trip
|
|
622
|
+
* through JSON (no BigInt support) and need to survive without
|
|
623
|
+
* precision loss.
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* var b = require("blamejs");
|
|
627
|
+
* var s = b.safeSchema;
|
|
628
|
+
*
|
|
629
|
+
* var age = s.number().int().min(0).max(150);
|
|
630
|
+
* age.parse(30);
|
|
631
|
+
* // → 30
|
|
632
|
+
*
|
|
633
|
+
* try { age.parse(200); }
|
|
634
|
+
* catch (e) { e.issues[0].code; }
|
|
635
|
+
* // → "number/too-large"
|
|
636
|
+
*
|
|
637
|
+
* var price = s.number().finite().multipleOf(0.01);
|
|
638
|
+
* price.parse(19.99);
|
|
639
|
+
* // → 19.99
|
|
640
|
+
*/
|
|
557
641
|
function number() {
|
|
558
642
|
var spec = {
|
|
559
643
|
kind: "number",
|
|
@@ -630,6 +714,29 @@ function _numberMethods(schema, spec) {
|
|
|
630
714
|
|
|
631
715
|
// ---- boolean ----
|
|
632
716
|
|
|
717
|
+
/**
|
|
718
|
+
* @primitive b.safeSchema.boolean
|
|
719
|
+
* @signature b.safeSchema.boolean()
|
|
720
|
+
* @since 0.1.0
|
|
721
|
+
* @status stable
|
|
722
|
+
* @related b.safeSchema.literal, b.safeSchema.preprocess
|
|
723
|
+
*
|
|
724
|
+
* Construct a boolean-typed schema. Strict `typeof === "boolean"`
|
|
725
|
+
* check — does not coerce truthy/falsy values. To accept the strings
|
|
726
|
+
* `"true"` / `"false"` from query parameters, wrap in
|
|
727
|
+
* `s.preprocess(fn, s.boolean())`.
|
|
728
|
+
*
|
|
729
|
+
* @example
|
|
730
|
+
* var b = require("blamejs");
|
|
731
|
+
* var s = b.safeSchema;
|
|
732
|
+
*
|
|
733
|
+
* s.boolean().parse(true);
|
|
734
|
+
* // → true
|
|
735
|
+
*
|
|
736
|
+
* try { s.boolean().parse("true"); }
|
|
737
|
+
* catch (e) { e.issues[0].code; }
|
|
738
|
+
* // → "type"
|
|
739
|
+
*/
|
|
633
740
|
function boolean() {
|
|
634
741
|
return _baseSchema({
|
|
635
742
|
kind: "boolean",
|
|
@@ -642,6 +749,29 @@ function boolean() {
|
|
|
642
749
|
|
|
643
750
|
// ---- literal ----
|
|
644
751
|
|
|
752
|
+
/**
|
|
753
|
+
* @primitive b.safeSchema.literal
|
|
754
|
+
* @signature b.safeSchema.literal(expected)
|
|
755
|
+
* @since 0.1.0
|
|
756
|
+
* @status stable
|
|
757
|
+
* @related b.safeSchema.enum_, b.safeSchema.discriminatedUnion
|
|
758
|
+
*
|
|
759
|
+
* Construct a schema that accepts exactly one specific value
|
|
760
|
+
* (compared via `===`). Useful as the discriminator on tagged
|
|
761
|
+
* unions — see `s.discriminatedUnion`.
|
|
762
|
+
*
|
|
763
|
+
* @example
|
|
764
|
+
* var b = require("blamejs");
|
|
765
|
+
* var s = b.safeSchema;
|
|
766
|
+
*
|
|
767
|
+
* var version = s.literal("v1");
|
|
768
|
+
* version.parse("v1");
|
|
769
|
+
* // → "v1"
|
|
770
|
+
*
|
|
771
|
+
* try { version.parse("v2"); }
|
|
772
|
+
* catch (e) { e.issues[0].code; }
|
|
773
|
+
* // → "literal"
|
|
774
|
+
*/
|
|
645
775
|
function literal(expected) {
|
|
646
776
|
return _baseSchema({
|
|
647
777
|
kind: "literal",
|
|
@@ -656,6 +786,36 @@ function literal(expected) {
|
|
|
656
786
|
|
|
657
787
|
// ---- enum / oneOf ----
|
|
658
788
|
|
|
789
|
+
/**
|
|
790
|
+
* @primitive b.safeSchema.enum_
|
|
791
|
+
* @signature b.safeSchema.enum_(values)
|
|
792
|
+
* @since 0.1.0
|
|
793
|
+
* @status stable
|
|
794
|
+
* @related b.safeSchema.literal, b.safeSchema.union
|
|
795
|
+
*
|
|
796
|
+
* Construct a schema that accepts any value from the given
|
|
797
|
+
* non-empty array (compared via `Set.has`). Also exported as
|
|
798
|
+
* `b.safeSchema.oneOf` because `enum` is a reserved word in some
|
|
799
|
+
* tooling. Throws `SafeSchemaError` (`safe-schema/bad-enum`) when
|
|
800
|
+
* `values` is not a non-empty array.
|
|
801
|
+
*
|
|
802
|
+
* @example
|
|
803
|
+
* var b = require("blamejs");
|
|
804
|
+
* var s = b.safeSchema;
|
|
805
|
+
*
|
|
806
|
+
* var role = s.enum_(["admin", "editor", "viewer"]);
|
|
807
|
+
* role.parse("editor");
|
|
808
|
+
* // → "editor"
|
|
809
|
+
*
|
|
810
|
+
* try { role.parse("guest"); }
|
|
811
|
+
* catch (e) { e.issues[0].code; }
|
|
812
|
+
* // → "enum"
|
|
813
|
+
*
|
|
814
|
+
* // oneOf is the same primitive under a friendlier name.
|
|
815
|
+
* var same = s.oneOf(["a", "b"]);
|
|
816
|
+
* same.parse("a");
|
|
817
|
+
* // → "a"
|
|
818
|
+
*/
|
|
659
819
|
function enum_(values) {
|
|
660
820
|
if (!Array.isArray(values) || values.length === 0) {
|
|
661
821
|
throw new SafeSchemaError("safe-schema/bad-enum",
|
|
@@ -676,6 +836,29 @@ function enum_(values) {
|
|
|
676
836
|
|
|
677
837
|
// ---- null / undefined / any / unknown ----
|
|
678
838
|
|
|
839
|
+
/**
|
|
840
|
+
* @primitive b.safeSchema.null_
|
|
841
|
+
* @signature b.safeSchema.null_()
|
|
842
|
+
* @since 0.1.0
|
|
843
|
+
* @status stable
|
|
844
|
+
* @related b.safeSchema.undefined_, b.safeSchema.any
|
|
845
|
+
*
|
|
846
|
+
* Construct a schema that accepts only `null`. Trailing-underscore
|
|
847
|
+
* name because `null` is a reserved word. Useful inside unions
|
|
848
|
+
* (e.g. `s.union([s.string(), s.null_()])`), though
|
|
849
|
+
* `s.string().nullable()` is the more common idiom.
|
|
850
|
+
*
|
|
851
|
+
* @example
|
|
852
|
+
* var b = require("blamejs");
|
|
853
|
+
* var s = b.safeSchema;
|
|
854
|
+
*
|
|
855
|
+
* s.null_().parse(null);
|
|
856
|
+
* // → null
|
|
857
|
+
*
|
|
858
|
+
* try { s.null_().parse(0); }
|
|
859
|
+
* catch (e) { e.issues[0].code; }
|
|
860
|
+
* // → "type"
|
|
861
|
+
*/
|
|
679
862
|
function null_() {
|
|
680
863
|
return _baseSchema({
|
|
681
864
|
kind: "null",
|
|
@@ -687,6 +870,28 @@ function null_() {
|
|
|
687
870
|
});
|
|
688
871
|
}
|
|
689
872
|
|
|
873
|
+
/**
|
|
874
|
+
* @primitive b.safeSchema.undefined_
|
|
875
|
+
* @signature b.safeSchema.undefined_()
|
|
876
|
+
* @since 0.1.0
|
|
877
|
+
* @status stable
|
|
878
|
+
* @related b.safeSchema.null_, b.safeSchema.any
|
|
879
|
+
*
|
|
880
|
+
* Construct a schema that accepts only `undefined`. Trailing-
|
|
881
|
+
* underscore name because `undefined` shadows poorly. The schema is
|
|
882
|
+
* implicitly `optional` — `parse(undefined)` succeeds.
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* var b = require("blamejs");
|
|
886
|
+
* var s = b.safeSchema;
|
|
887
|
+
*
|
|
888
|
+
* s.undefined_().parse(undefined);
|
|
889
|
+
* // → undefined
|
|
890
|
+
*
|
|
891
|
+
* try { s.undefined_().parse(null); }
|
|
892
|
+
* catch (e) { e.issues[0].code; }
|
|
893
|
+
* // → "type"
|
|
894
|
+
*/
|
|
690
895
|
function undefined_() {
|
|
691
896
|
return _baseSchema({
|
|
692
897
|
kind: "undefined",
|
|
@@ -697,6 +902,28 @@ function undefined_() {
|
|
|
697
902
|
});
|
|
698
903
|
}
|
|
699
904
|
|
|
905
|
+
/**
|
|
906
|
+
* @primitive b.safeSchema.any
|
|
907
|
+
* @signature b.safeSchema.any()
|
|
908
|
+
* @since 0.1.0
|
|
909
|
+
* @status stable
|
|
910
|
+
* @related b.safeSchema.unknown, b.safeSchema.preprocess
|
|
911
|
+
*
|
|
912
|
+
* Construct a schema that accepts any value, including `null` and
|
|
913
|
+
* `undefined`. Useful as a placeholder while iterating on a schema
|
|
914
|
+
* shape, or inside `s.record(s.any())` when the operator wants the
|
|
915
|
+
* keys validated but not the values.
|
|
916
|
+
*
|
|
917
|
+
* @example
|
|
918
|
+
* var b = require("blamejs");
|
|
919
|
+
* var s = b.safeSchema;
|
|
920
|
+
*
|
|
921
|
+
* s.any().parse({ anything: "goes" });
|
|
922
|
+
* // → { anything: "goes" }
|
|
923
|
+
*
|
|
924
|
+
* s.any().parse(null);
|
|
925
|
+
* // → null
|
|
926
|
+
*/
|
|
700
927
|
function any() {
|
|
701
928
|
return _baseSchema({
|
|
702
929
|
kind: "any",
|
|
@@ -706,10 +933,73 @@ function any() {
|
|
|
706
933
|
});
|
|
707
934
|
}
|
|
708
935
|
|
|
936
|
+
/**
|
|
937
|
+
* @primitive b.safeSchema.unknown
|
|
938
|
+
* @signature b.safeSchema.unknown()
|
|
939
|
+
* @since 0.1.0
|
|
940
|
+
* @status stable
|
|
941
|
+
* @related b.safeSchema.any
|
|
942
|
+
*
|
|
943
|
+
* Alias for `b.safeSchema.any`. Some operators prefer the spelling
|
|
944
|
+
* `unknown` to signal "we accept anything but expect downstream
|
|
945
|
+
* code to narrow the type". Behavior is identical.
|
|
946
|
+
*
|
|
947
|
+
* @example
|
|
948
|
+
* var b = require("blamejs");
|
|
949
|
+
* var s = b.safeSchema;
|
|
950
|
+
*
|
|
951
|
+
* s.unknown().parse({ raw: 1 });
|
|
952
|
+
* // → { raw: 1 }
|
|
953
|
+
*/
|
|
709
954
|
function unknown() { return any(); }
|
|
710
955
|
|
|
711
956
|
// ---- object ----
|
|
712
957
|
|
|
958
|
+
/**
|
|
959
|
+
* @primitive b.safeSchema.object
|
|
960
|
+
* @signature b.safeSchema.object(shape)
|
|
961
|
+
* @since 0.1.0
|
|
962
|
+
* @status stable
|
|
963
|
+
* @related b.safeSchema.record, b.safeSchema.discriminatedUnion
|
|
964
|
+
*
|
|
965
|
+
* Construct an object schema from a `{ key: schema }` shape map.
|
|
966
|
+
* Strict by default — unknown keys produce an `object/unknown-key`
|
|
967
|
+
* issue. Chain `.passthrough()` to retain extras, `.strict()` to
|
|
968
|
+
* flip back, `.pick`, `.omit`, `.extend`, `.partial`, `.required`
|
|
969
|
+
* to derive related shapes.
|
|
970
|
+
*
|
|
971
|
+
* Prototype-pollution defense — `__proto__` / `constructor` /
|
|
972
|
+
* `prototype` keys are rejected at shape-construction time AND at
|
|
973
|
+
* parse time regardless of mode (`.passthrough()` does NOT permit
|
|
974
|
+
* them). Throws `SafeSchemaError`
|
|
975
|
+
* (`safe-schema/poisoned-shape-key` / `safe-schema/bad-shape`) on
|
|
976
|
+
* invalid input.
|
|
977
|
+
*
|
|
978
|
+
* @example
|
|
979
|
+
* var b = require("blamejs");
|
|
980
|
+
* var s = b.safeSchema;
|
|
981
|
+
*
|
|
982
|
+
* var user = s.object({
|
|
983
|
+
* email: s.string().email(),
|
|
984
|
+
* age: s.number().int().min(0).max(150),
|
|
985
|
+
* });
|
|
986
|
+
*
|
|
987
|
+
* user.parse({ email: "alice@example.com", age: 30 });
|
|
988
|
+
* // → { email: "alice@example.com", age: 30 }
|
|
989
|
+
*
|
|
990
|
+
* // Unknown keys rejected by default.
|
|
991
|
+
* try { user.parse({ email: "a@b.com", age: 30, extra: 1 }); }
|
|
992
|
+
* catch (e) { e.issues[0].code; }
|
|
993
|
+
* // → "object/unknown-key"
|
|
994
|
+
*
|
|
995
|
+
* // Prototype-pollution attempt rejected even with passthrough.
|
|
996
|
+
* var loose = user.passthrough();
|
|
997
|
+
* var report = loose.safeParse({ email: "a@b.com", age: 30, __proto__: { admin: true } });
|
|
998
|
+
* report.ok;
|
|
999
|
+
* // → false
|
|
1000
|
+
* report.errors[0].code;
|
|
1001
|
+
* // → "object/poisoned-key"
|
|
1002
|
+
*/
|
|
713
1003
|
function object(shape) {
|
|
714
1004
|
if (shape == null || typeof shape !== "object") {
|
|
715
1005
|
throw new SafeSchemaError("safe-schema/bad-shape",
|
|
@@ -861,6 +1151,33 @@ function _objectWithMode(shape, keys, mode) {
|
|
|
861
1151
|
|
|
862
1152
|
// ---- array ----
|
|
863
1153
|
|
|
1154
|
+
/**
|
|
1155
|
+
* @primitive b.safeSchema.array
|
|
1156
|
+
* @signature b.safeSchema.array(itemSchema)
|
|
1157
|
+
* @since 0.1.0
|
|
1158
|
+
* @status stable
|
|
1159
|
+
* @related b.safeSchema.tuple, b.safeSchema.record
|
|
1160
|
+
*
|
|
1161
|
+
* Construct an array schema where every element is validated
|
|
1162
|
+
* against `itemSchema`. Chain `.min`, `.max`, `.length`, `.nonempty`
|
|
1163
|
+
* to bound the length. Issues from individual items carry their
|
|
1164
|
+
* index in the path (e.g. `[3]`). Throws `SafeSchemaError`
|
|
1165
|
+
* (`safe-schema/bad-item`) when the item argument is not a schema.
|
|
1166
|
+
*
|
|
1167
|
+
* @example
|
|
1168
|
+
* var b = require("blamejs");
|
|
1169
|
+
* var s = b.safeSchema;
|
|
1170
|
+
*
|
|
1171
|
+
* var tags = s.array(s.string().min(1)).max(10);
|
|
1172
|
+
* tags.parse(["alpha", "beta"]);
|
|
1173
|
+
* // → ["alpha", "beta"]
|
|
1174
|
+
*
|
|
1175
|
+
* var report = tags.safeParse(["ok", "", "also-ok"]);
|
|
1176
|
+
* report.ok;
|
|
1177
|
+
* // → false
|
|
1178
|
+
* report.errors[0].path;
|
|
1179
|
+
* // → [1]
|
|
1180
|
+
*/
|
|
864
1181
|
function array(itemSchema) {
|
|
865
1182
|
if (!itemSchema || typeof itemSchema._run !== "function") {
|
|
866
1183
|
throw new SafeSchemaError("safe-schema/bad-item",
|
|
@@ -917,6 +1234,32 @@ function _arrayMethods(schema, spec) {
|
|
|
917
1234
|
|
|
918
1235
|
// ---- tuple ----
|
|
919
1236
|
|
|
1237
|
+
/**
|
|
1238
|
+
* @primitive b.safeSchema.tuple
|
|
1239
|
+
* @signature b.safeSchema.tuple(items)
|
|
1240
|
+
* @since 0.1.0
|
|
1241
|
+
* @status stable
|
|
1242
|
+
* @related b.safeSchema.array, b.safeSchema.union
|
|
1243
|
+
*
|
|
1244
|
+
* Construct a fixed-length heterogeneous array schema. `items` is
|
|
1245
|
+
* a non-empty array of schemas, one per slot. Chain `.rest(item)`
|
|
1246
|
+
* to allow a variadic tail (common for `[verb, ...args]` /
|
|
1247
|
+
* `[event, payload, ...metadata]` shapes). Throws
|
|
1248
|
+
* `SafeSchemaError` (`safe-schema/bad-tuple`) on invalid input.
|
|
1249
|
+
*
|
|
1250
|
+
* @example
|
|
1251
|
+
* var b = require("blamejs");
|
|
1252
|
+
* var s = b.safeSchema;
|
|
1253
|
+
*
|
|
1254
|
+
* var pair = s.tuple([s.string(), s.number()]);
|
|
1255
|
+
* pair.parse(["count", 42]);
|
|
1256
|
+
* // → ["count", 42]
|
|
1257
|
+
*
|
|
1258
|
+
* // Variadic tail via .rest()
|
|
1259
|
+
* var event = s.tuple([s.string()]).rest(s.number());
|
|
1260
|
+
* event.parse(["sum", 1, 2, 3]);
|
|
1261
|
+
* // → ["sum", 1, 2, 3]
|
|
1262
|
+
*/
|
|
920
1263
|
function tuple(items) {
|
|
921
1264
|
if (!Array.isArray(items) || items.length === 0) {
|
|
922
1265
|
throw new SafeSchemaError("safe-schema/bad-tuple",
|
|
@@ -983,6 +1326,35 @@ function _tupleWithRest(items, restSchema) {
|
|
|
983
1326
|
|
|
984
1327
|
// ---- union ----
|
|
985
1328
|
|
|
1329
|
+
/**
|
|
1330
|
+
* @primitive b.safeSchema.union
|
|
1331
|
+
* @signature b.safeSchema.union(options)
|
|
1332
|
+
* @since 0.1.0
|
|
1333
|
+
* @status stable
|
|
1334
|
+
* @related b.safeSchema.discriminatedUnion, b.safeSchema.enum_
|
|
1335
|
+
*
|
|
1336
|
+
* Construct a schema that accepts a value matching ANY of the
|
|
1337
|
+
* given option schemas. First match wins. When no option matches,
|
|
1338
|
+
* issues from every branch are collected in the failure for deep
|
|
1339
|
+
* diagnostics, plus a parent-level `union` issue summarizing the
|
|
1340
|
+
* miss. For tagged-variant shapes prefer
|
|
1341
|
+
* `s.discriminatedUnion` — it dispatches in O(1) on the tag and
|
|
1342
|
+
* produces clearer error messages.
|
|
1343
|
+
*
|
|
1344
|
+
* @example
|
|
1345
|
+
* var b = require("blamejs");
|
|
1346
|
+
* var s = b.safeSchema;
|
|
1347
|
+
*
|
|
1348
|
+
* var idOrName = s.union([s.number().int().positive(), s.string().min(1)]);
|
|
1349
|
+
* idOrName.parse(42);
|
|
1350
|
+
* // → 42
|
|
1351
|
+
* idOrName.parse("alice");
|
|
1352
|
+
* // → "alice"
|
|
1353
|
+
*
|
|
1354
|
+
* try { idOrName.parse(true); }
|
|
1355
|
+
* catch (e) { e.issues[0].code; }
|
|
1356
|
+
* // → "union"
|
|
1357
|
+
*/
|
|
986
1358
|
function union(options) {
|
|
987
1359
|
if (!Array.isArray(options) || options.length === 0) {
|
|
988
1360
|
throw new SafeSchemaError("safe-schema/bad-union",
|
|
@@ -1020,6 +1392,34 @@ function union(options) {
|
|
|
1020
1392
|
// record(value) — string keys, schema-typed values
|
|
1021
1393
|
// record(keySchema, value) — both keys and values are schema-validated
|
|
1022
1394
|
|
|
1395
|
+
/**
|
|
1396
|
+
* @primitive b.safeSchema.record
|
|
1397
|
+
* @signature b.safeSchema.record(a, b)
|
|
1398
|
+
* @since 0.1.0
|
|
1399
|
+
* @status stable
|
|
1400
|
+
* @related b.safeSchema.object, b.safeSchema.array
|
|
1401
|
+
*
|
|
1402
|
+
* Construct a schema for an object whose KEYS are arbitrary strings
|
|
1403
|
+
* and whose VALUES match a given schema. Two call shapes:
|
|
1404
|
+
* `record(valueSchema)` accepts any string key;
|
|
1405
|
+
* `record(keySchema, valueSchema)` validates both keys and values.
|
|
1406
|
+
* Prototype-pollution defense — `__proto__` / `constructor` /
|
|
1407
|
+
* `prototype` keys are rejected at parse time, mirroring `s.object`.
|
|
1408
|
+
* Throws `SafeSchemaError` on invalid arguments.
|
|
1409
|
+
*
|
|
1410
|
+
* @example
|
|
1411
|
+
* var bjs = require("blamejs");
|
|
1412
|
+
* var s = bjs.safeSchema;
|
|
1413
|
+
*
|
|
1414
|
+
* var counts = s.record(s.number().int().nonnegative());
|
|
1415
|
+
* counts.parse({ apples: 3, pears: 0 });
|
|
1416
|
+
* // → { apples: 3, pears: 0 }
|
|
1417
|
+
*
|
|
1418
|
+
* // Validate keys too: only ULIDs allowed.
|
|
1419
|
+
* var byId = s.record(s.string().ulid(), s.string());
|
|
1420
|
+
* byId.parse({ "01HF5Z6Q9P8R7S4T3V2W1X0Y9Z": "alice" });
|
|
1421
|
+
* // → { "01HF5Z6Q9P8R7S4T3V2W1X0Y9Z": "alice" }
|
|
1422
|
+
*/
|
|
1023
1423
|
function record(a, b) {
|
|
1024
1424
|
var keySchema, valueSchema;
|
|
1025
1425
|
if (b === undefined) {
|
|
@@ -1093,6 +1493,41 @@ function record(a, b) {
|
|
|
1093
1493
|
// literal schema. Mismatched discriminator fails fast with a clear
|
|
1094
1494
|
// "expected one of [...]" message rather than burying the operator in
|
|
1095
1495
|
// per-branch issues.
|
|
1496
|
+
/**
|
|
1497
|
+
* @primitive b.safeSchema.discriminatedUnion
|
|
1498
|
+
* @signature b.safeSchema.discriminatedUnion(discriminator, options)
|
|
1499
|
+
* @since 0.1.0
|
|
1500
|
+
* @status stable
|
|
1501
|
+
* @related b.safeSchema.union, b.safeSchema.literal
|
|
1502
|
+
*
|
|
1503
|
+
* Construct a tagged-union schema. `discriminator` names a key
|
|
1504
|
+
* present on every option as a `s.literal(...)` schema; the
|
|
1505
|
+
* validator dispatches in O(1) on that key's value rather than
|
|
1506
|
+
* trying every option in turn (faster + clearer error messages
|
|
1507
|
+
* than `s.union`). Every option must be an object schema whose
|
|
1508
|
+
* shape carries a literal at the discriminator key. The
|
|
1509
|
+
* discriminator name itself cannot be `__proto__` /
|
|
1510
|
+
* `constructor` / `prototype`. Throws `SafeSchemaError` on
|
|
1511
|
+
* invalid input.
|
|
1512
|
+
*
|
|
1513
|
+
* @example
|
|
1514
|
+
* var b = require("blamejs");
|
|
1515
|
+
* var s = b.safeSchema;
|
|
1516
|
+
*
|
|
1517
|
+
* var event = s.discriminatedUnion("kind", [
|
|
1518
|
+
* s.object({ kind: s.literal("created"), at: s.string().datetime() }),
|
|
1519
|
+
* s.object({ kind: s.literal("deleted"), reason: s.string() }),
|
|
1520
|
+
* ]);
|
|
1521
|
+
*
|
|
1522
|
+
* event.parse({ kind: "created", at: "2026-01-01T00:00:00Z" });
|
|
1523
|
+
* // → { kind: "created", at: "2026-01-01T00:00:00Z" }
|
|
1524
|
+
*
|
|
1525
|
+
* var report = event.safeParse({ kind: "unknown" });
|
|
1526
|
+
* report.ok;
|
|
1527
|
+
* // → false
|
|
1528
|
+
* report.errors[0].code;
|
|
1529
|
+
* // → "discriminated-union/no-match"
|
|
1530
|
+
*/
|
|
1096
1531
|
function discriminatedUnion(discriminator, options) {
|
|
1097
1532
|
if (typeof discriminator !== "string" || discriminator.length === 0) {
|
|
1098
1533
|
throw new SafeSchemaError("safe-schema/bad-discriminator",
|
|
@@ -1155,6 +1590,42 @@ function discriminatedUnion(discriminator, options) {
|
|
|
1155
1590
|
//
|
|
1156
1591
|
// fn errors propagate as a 'preprocess' issue at the parent path; they
|
|
1157
1592
|
// don't crash the validate call.
|
|
1593
|
+
/**
|
|
1594
|
+
* @primitive b.safeSchema.preprocess
|
|
1595
|
+
* @signature b.safeSchema.preprocess(fn, inner)
|
|
1596
|
+
* @since 0.1.0
|
|
1597
|
+
* @status stable
|
|
1598
|
+
* @related b.safeSchema.string, b.safeSchema.number
|
|
1599
|
+
*
|
|
1600
|
+
* Wrap a schema with a transform that runs BEFORE validation.
|
|
1601
|
+
* Common at HTTP boundaries where query strings arrive as strings
|
|
1602
|
+
* but the operator wants a number / boolean schema downstream.
|
|
1603
|
+
* Errors thrown by `fn` propagate as a `preprocess` issue at the
|
|
1604
|
+
* parent path rather than crashing the parse. Throws
|
|
1605
|
+
* `SafeSchemaError` (`safe-schema/bad-preprocess`) when args are
|
|
1606
|
+
* the wrong shape.
|
|
1607
|
+
*
|
|
1608
|
+
* Prefer `s.preprocess` over a hypothetical `.coerce` because
|
|
1609
|
+
* coercion ambiguity (`"0" → 0` vs `"0" → "0"`) is a security
|
|
1610
|
+
* foot-gun; `s.preprocess` makes the conversion explicit at the
|
|
1611
|
+
* call site.
|
|
1612
|
+
*
|
|
1613
|
+
* @example
|
|
1614
|
+
* var b = require("blamejs");
|
|
1615
|
+
* var s = b.safeSchema;
|
|
1616
|
+
*
|
|
1617
|
+
* var port = s.preprocess(
|
|
1618
|
+
* function (v) { return Number(v); },
|
|
1619
|
+
* s.number().int().min(1).max(65535)
|
|
1620
|
+
* );
|
|
1621
|
+
*
|
|
1622
|
+
* port.parse("8080");
|
|
1623
|
+
* // → 8080
|
|
1624
|
+
*
|
|
1625
|
+
* var report = port.safeParse("not-a-port");
|
|
1626
|
+
* report.ok;
|
|
1627
|
+
* // → false
|
|
1628
|
+
*/
|
|
1158
1629
|
function preprocess(fn, inner) {
|
|
1159
1630
|
if (typeof fn !== "function") {
|
|
1160
1631
|
throw new SafeSchemaError("safe-schema/bad-preprocess",
|
|
@@ -1192,6 +1663,34 @@ function preprocess(fn, inner) {
|
|
|
1192
1663
|
//
|
|
1193
1664
|
// The function is called lazily and cached per-call site; cycles in the
|
|
1194
1665
|
// returned schema are fine.
|
|
1666
|
+
/**
|
|
1667
|
+
* @primitive b.safeSchema.lazy
|
|
1668
|
+
* @signature b.safeSchema.lazy(getter)
|
|
1669
|
+
* @since 0.1.0
|
|
1670
|
+
* @status stable
|
|
1671
|
+
* @related b.safeSchema.object, b.safeSchema.array
|
|
1672
|
+
*
|
|
1673
|
+
* Defer schema construction until first parse, enabling recursive
|
|
1674
|
+
* shapes (comment threads, file-tree nodes, AST nodes). `getter` is
|
|
1675
|
+
* a no-arg function that returns the schema; it's called once on
|
|
1676
|
+
* the first parse and cached. The returned schema can reference
|
|
1677
|
+
* its enclosing variable, breaking the chicken-and-egg cycle.
|
|
1678
|
+
* Throws `SafeSchemaError` (`safe-schema/bad-lazy`) when `getter`
|
|
1679
|
+
* is not a function.
|
|
1680
|
+
*
|
|
1681
|
+
* @example
|
|
1682
|
+
* var b = require("blamejs");
|
|
1683
|
+
* var s = b.safeSchema;
|
|
1684
|
+
*
|
|
1685
|
+
* var commentSchema = s.object({
|
|
1686
|
+
* id: s.string(),
|
|
1687
|
+
* replies: s.array(s.lazy(function () { return commentSchema; })),
|
|
1688
|
+
* });
|
|
1689
|
+
*
|
|
1690
|
+
* var input = { id: "a", replies: [{ id: "b", replies: [] }] };
|
|
1691
|
+
* commentSchema.parse(input);
|
|
1692
|
+
* // → { id: "a", replies: [{ id: "b", replies: [] }] }
|
|
1693
|
+
*/
|
|
1195
1694
|
function lazy(getter) {
|
|
1196
1695
|
if (typeof getter !== "function") {
|
|
1197
1696
|
throw new SafeSchemaError("safe-schema/bad-lazy",
|
|
@@ -1215,7 +1714,52 @@ function lazy(getter) {
|
|
|
1215
1714
|
|
|
1216
1715
|
// ---- top-level modifier helpers ----
|
|
1217
1716
|
|
|
1717
|
+
/**
|
|
1718
|
+
* @primitive b.safeSchema.optional
|
|
1719
|
+
* @signature b.safeSchema.optional(inner)
|
|
1720
|
+
* @since 0.1.0
|
|
1721
|
+
* @status stable
|
|
1722
|
+
* @related b.safeSchema.nullable
|
|
1723
|
+
*
|
|
1724
|
+
* Composition-style alias for `inner.optional()`. Returns a schema
|
|
1725
|
+
* that accepts `undefined` in addition to whatever `inner` accepts.
|
|
1726
|
+
* Equivalent to chaining `.optional()` on the schema; provided for
|
|
1727
|
+
* operators who prefer composition over chaining (e.g. mapping over
|
|
1728
|
+
* a list of schemas).
|
|
1729
|
+
*
|
|
1730
|
+
* @example
|
|
1731
|
+
* var b = require("blamejs");
|
|
1732
|
+
* var s = b.safeSchema;
|
|
1733
|
+
*
|
|
1734
|
+
* var maybeName = s.optional(s.string().min(1));
|
|
1735
|
+
* maybeName.parse(undefined);
|
|
1736
|
+
* // → undefined
|
|
1737
|
+
* maybeName.parse("alice");
|
|
1738
|
+
* // → "alice"
|
|
1739
|
+
*/
|
|
1218
1740
|
function optional(inner) { return inner.optional(); }
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* @primitive b.safeSchema.nullable
|
|
1744
|
+
* @signature b.safeSchema.nullable(inner)
|
|
1745
|
+
* @since 0.1.0
|
|
1746
|
+
* @status stable
|
|
1747
|
+
* @related b.safeSchema.optional
|
|
1748
|
+
*
|
|
1749
|
+
* Composition-style alias for `inner.nullable()`. Returns a schema
|
|
1750
|
+
* that accepts `null` in addition to whatever `inner` accepts.
|
|
1751
|
+
* Compose with `s.optional` for "may be undefined OR null".
|
|
1752
|
+
*
|
|
1753
|
+
* @example
|
|
1754
|
+
* var b = require("blamejs");
|
|
1755
|
+
* var s = b.safeSchema;
|
|
1756
|
+
*
|
|
1757
|
+
* var maybeAge = s.nullable(s.number().int().min(0));
|
|
1758
|
+
* maybeAge.parse(null);
|
|
1759
|
+
* // → null
|
|
1760
|
+
* maybeAge.parse(30);
|
|
1761
|
+
* // → 30
|
|
1762
|
+
*/
|
|
1219
1763
|
function nullable(inner) { return inner.nullable(); }
|
|
1220
1764
|
|
|
1221
1765
|
module.exports = {
|