@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/cookies.js
CHANGED
|
@@ -1,315 +1,315 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* cookies — cookie parse/serialize + access-gated sealed cookies.
|
|
4
|
-
*
|
|
5
|
-
* RFC 6265 cookie plumbing the framework was duplicating across
|
|
6
|
-
* middleware: a parser in attach-user, ad-hoc Set-Cookie strings in
|
|
7
|
-
* route handlers, no shared place for attribute defaults. This is the
|
|
8
|
-
* single primitive.
|
|
9
|
-
*
|
|
10
|
-
* Two surfaces:
|
|
11
|
-
*
|
|
12
|
-
* 1. Module-level (stateless): cookies.parse / cookies.serialize.
|
|
13
|
-
* Useful in test fixtures and code that doesn't have a vault.
|
|
14
|
-
*
|
|
15
|
-
* 2. Instance (cookies.create): bound defaults for cookie attributes,
|
|
16
|
-
* a wired vault for sealed reads/writes, and req/res helpers.
|
|
17
|
-
*
|
|
18
|
-
* var cookies = b.cookies.create({
|
|
19
|
-
* vault: b.vault, // required for sealed* methods
|
|
20
|
-
* defaults: {
|
|
21
|
-
* httpOnly: true,
|
|
22
|
-
* secure: true, // default true; HTTPS expected
|
|
23
|
-
* sameSite: "Lax",
|
|
24
|
-
* path: "/",
|
|
25
|
-
* maxAge: 7 * 86400, // seconds
|
|
26
|
-
* },
|
|
27
|
-
* });
|
|
28
|
-
*
|
|
29
|
-
* cookies.parse("a=1; b=2") → { a: "1", b: "2" }
|
|
30
|
-
* cookies.serialize("name", "v",
|
|
31
|
-
* { maxAge: 3600 }) → "name=v; Max-Age=3600; Path=/; HttpOnly; SameSite=Lax; Secure"
|
|
32
|
-
*
|
|
33
|
-
* cookies.read(req, "name") → "v" or null
|
|
34
|
-
* cookies.write(res, "name", "v", {}) // appends to existing Set-Cookie
|
|
35
|
-
* cookies.clear(res, "name", {}) // expire by Max-Age=0
|
|
36
|
-
*
|
|
37
|
-
* cookies.writeSealed(res, "session", sid) // vault.seal then write
|
|
38
|
-
* cookies.readSealed(req, "session") // read then vault.unseal
|
|
39
|
-
*
|
|
40
|
-
* Sealed-cookie purpose: the cookie value is a vault.seal of the real
|
|
41
|
-
* value. Without the framework's vault key, no client can hand-craft a
|
|
42
|
-
* valid cookie value, so the API is unreachable via curl-with-arbitrary-
|
|
43
|
-
* cookies or any tool that hasn't been through the framework's crypto
|
|
44
|
-
* flow. The vault prefix is stripped on write and re-added on read so
|
|
45
|
-
* the cookie carries only the base64 envelope.
|
|
46
|
-
*
|
|
47
|
-
* Defense in serialize/parse:
|
|
48
|
-
* - Cookie name must be a valid token (no CTLs, no separator chars).
|
|
49
|
-
* - Cookie value must not contain CRLF, semicolon, or comma.
|
|
50
|
-
* - Value is percent-encoded on write, percent-decoded on read.
|
|
51
|
-
* - Domain / Path are CRLF-stripped to defeat header injection
|
|
52
|
-
* attempts via operator-controlled but improperly-escaped inputs.
|
|
53
|
-
*/
|
|
54
|
-
|
|
55
|
-
var C = require("./constants");
|
|
56
|
-
var validateOpts = require("./validate-opts");
|
|
57
|
-
var { FrameworkError } = require("./framework-error");
|
|
58
|
-
|
|
59
|
-
class CookieError extends FrameworkError {
|
|
60
|
-
constructor(code, message) {
|
|
61
|
-
super(message, code);
|
|
62
|
-
this.name = "CookieError";
|
|
63
|
-
this.permanent = true;
|
|
64
|
-
this.isCookieError = true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// RFC 6265 cookie-name token: VCHAR minus separators. Reject anything
|
|
69
|
-
// outside this range — embeddings of CTLs / separators / whitespace
|
|
70
|
-
// would break parsing on the next hop.
|
|
71
|
-
var TOKEN_RE = /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/;
|
|
72
|
-
// Reject CRLF, NUL, semicolon, comma in cookie value pre-encoding.
|
|
73
|
-
var FORBIDDEN_VALUE_RE = /[\r\n\0;,]/;
|
|
74
|
-
|
|
75
|
-
// Length caps applied before the regex tests so a hostile caller can't
|
|
76
|
-
// stage a pathological-length input against the engine. RFC 6265 doesn't
|
|
77
|
-
// pin a name/value length; browsers vary, with a practical 4 KiB total
|
|
78
|
-
// envelope. The framework caps name + value to leave headroom.
|
|
79
|
-
var MAX_NAME_LENGTH = C.BYTES.bytes(256);
|
|
80
|
-
var MAX_VALUE_LENGTH = C.BYTES.kib(4);
|
|
81
|
-
|
|
82
|
-
function _validateName(name) {
|
|
83
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
84
|
-
throw new CookieError("cookies/invalid-name",
|
|
85
|
-
"cookie name must be a non-empty string");
|
|
86
|
-
}
|
|
87
|
-
// Length cap before the regex test — bound the engine on hostile
|
|
88
|
-
// input lengths even though the regex is anchored.
|
|
89
|
-
if (name.length > MAX_NAME_LENGTH || !TOKEN_RE.test(name)) {
|
|
90
|
-
throw new CookieError("cookies/invalid-name",
|
|
91
|
-
"cookie name '" + name + "' is empty, too long, or contains forbidden characters");
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function _validateValue(value) {
|
|
96
|
-
if (typeof value !== "string") {
|
|
97
|
-
throw new CookieError("cookies/invalid-value",
|
|
98
|
-
"cookie value must be a string");
|
|
99
|
-
}
|
|
100
|
-
// Length cap before the regex test (defense in depth — the regex
|
|
101
|
-
// here is a simple character class, but the same discipline that
|
|
102
|
-
// bounds longer regexes elsewhere applies).
|
|
103
|
-
if (value.length > MAX_VALUE_LENGTH || FORBIDDEN_VALUE_RE.test(value)) {
|
|
104
|
-
throw new CookieError("cookies/invalid-value",
|
|
105
|
-
"cookie value is too long or contains forbidden control character (CRLF/NUL/;/,)");
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Strip CRLF/NUL defensively from operator-supplied attribute strings
|
|
110
|
-
// (Domain, Path, SameSite). Even if the attribute is constant in the
|
|
111
|
-
// caller's code, attribute strings flow into Set-Cookie which is a
|
|
112
|
-
// header — never trust unscrubbed values reach the wire.
|
|
113
|
-
function _scrubAttr(s) {
|
|
114
|
-
if (typeof s !== "string") return s;
|
|
115
|
-
return s.replace(/[\r\n\0]/g, "");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function parse(cookieHeader) {
|
|
119
|
-
var out = {};
|
|
120
|
-
if (typeof cookieHeader !== "string" || cookieHeader.length === 0) return out;
|
|
121
|
-
var pairs = cookieHeader.split(/;\s*/);
|
|
122
|
-
for (var i = 0; i < pairs.length; i++) {
|
|
123
|
-
var pair = pairs[i];
|
|
124
|
-
if (!pair) continue;
|
|
125
|
-
var eq = pair.indexOf("=");
|
|
126
|
-
if (eq < 0) continue;
|
|
127
|
-
var k = pair.slice(0, eq).trim();
|
|
128
|
-
if (!k) continue;
|
|
129
|
-
var v = pair.slice(eq + 1).trim();
|
|
130
|
-
// Strip surrounding double-quotes per RFC 6265 §5.2.
|
|
131
|
-
if (v.length >= 2 && v.charAt(0) === '"' && v.charAt(v.length - 1) === '"') {
|
|
132
|
-
v = v.slice(1, -1);
|
|
133
|
-
}
|
|
134
|
-
try { v = decodeURIComponent(v); }
|
|
135
|
-
catch (_e) { /* malformed encoding — keep raw */ }
|
|
136
|
-
// Last write wins per RFC; matches every browser's behavior.
|
|
137
|
-
out[k] = v;
|
|
138
|
-
}
|
|
139
|
-
return out;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function serialize(name, value, attrs) {
|
|
143
|
-
_validateName(name);
|
|
144
|
-
_validateValue(value);
|
|
145
|
-
attrs = attrs || {};
|
|
146
|
-
|
|
147
|
-
var parts = [name + "=" + encodeURIComponent(value)];
|
|
148
|
-
|
|
149
|
-
if (attrs.maxAge !== undefined && attrs.maxAge !== null) {
|
|
150
|
-
var maxAge = Number(attrs.maxAge);
|
|
151
|
-
if (!Number.isFinite(maxAge) || Math.floor(maxAge) !== maxAge) {
|
|
152
|
-
throw new CookieError("cookies/invalid-attr",
|
|
153
|
-
"cookie attr maxAge must be an integer (seconds)");
|
|
154
|
-
}
|
|
155
|
-
parts.push("Max-Age=" + maxAge);
|
|
156
|
-
}
|
|
157
|
-
if (attrs.expires !== undefined && attrs.expires !== null) {
|
|
158
|
-
var d = attrs.expires instanceof Date ? attrs.expires : new Date(attrs.expires);
|
|
159
|
-
if (isNaN(d.getTime())) {
|
|
160
|
-
throw new CookieError("cookies/invalid-attr",
|
|
161
|
-
"cookie attr expires must be a Date or parseable date string");
|
|
162
|
-
}
|
|
163
|
-
parts.push("Expires=" + d.toUTCString());
|
|
164
|
-
}
|
|
165
|
-
if (attrs.domain) {
|
|
166
|
-
var dom = _scrubAttr(String(attrs.domain));
|
|
167
|
-
// RFC 6265 §4.1.2.3 + §5.1.3: Domain attribute is a host name.
|
|
168
|
-
// A leading "." is tolerated by browsers (legacy form). Anything
|
|
169
|
-
// else (URLs, paths, spaces, scheme prefixes) makes browsers
|
|
170
|
-
// either ignore the cookie or apply it inconsistently.
|
|
171
|
-
if (!/^\.?[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/i.test(dom)) {
|
|
172
|
-
throw new CookieError("cookies/invalid-attr",
|
|
173
|
-
"cookie attr domain must be a valid host name " +
|
|
174
|
-
"(letters/digits/dots/hyphens, optional leading dot), got " + JSON.stringify(dom));
|
|
175
|
-
}
|
|
176
|
-
parts.push("Domain=" + dom);
|
|
177
|
-
}
|
|
178
|
-
if (attrs.path !== undefined && attrs.path !== null) {
|
|
179
|
-
parts.push("Path=" + _scrubAttr(String(attrs.path)));
|
|
180
|
-
}
|
|
181
|
-
if (attrs.httpOnly) parts.push("HttpOnly");
|
|
182
|
-
if (attrs.sameSite) {
|
|
183
|
-
var ss = String(attrs.sameSite);
|
|
184
|
-
var ssLow = ss.toLowerCase();
|
|
185
|
-
var ssNorm;
|
|
186
|
-
if (ssLow === "strict") ssNorm = "Strict";
|
|
187
|
-
else if (ssLow === "lax") ssNorm = "Lax";
|
|
188
|
-
else if (ssLow === "none") ssNorm = "None";
|
|
189
|
-
else throw new CookieError("cookies/invalid-attr",
|
|
190
|
-
"cookie attr sameSite must be Strict, Lax, or None");
|
|
191
|
-
parts.push("SameSite=" + ssNorm);
|
|
192
|
-
// SameSite=None requires Secure per spec; force it on so operators
|
|
193
|
-
// don't ship a cookie that browsers silently drop.
|
|
194
|
-
if (ssNorm === "None") attrs = Object.assign({}, attrs, { secure: true });
|
|
195
|
-
}
|
|
196
|
-
if (attrs.secure) parts.push("Secure");
|
|
197
|
-
if (attrs.partitioned) parts.push("Partitioned");
|
|
198
|
-
if (attrs.priority) {
|
|
199
|
-
var p = String(attrs.priority);
|
|
200
|
-
var pLow = p.toLowerCase();
|
|
201
|
-
var pNorm;
|
|
202
|
-
if (pLow === "low") pNorm = "Low";
|
|
203
|
-
else if (pLow === "medium") pNorm = "Medium";
|
|
204
|
-
else if (pLow === "high") pNorm = "High";
|
|
205
|
-
else throw new CookieError("cookies/invalid-attr",
|
|
206
|
-
"cookie attr priority must be Low, Medium, or High");
|
|
207
|
-
parts.push("Priority=" + pNorm);
|
|
208
|
-
}
|
|
209
|
-
return parts.join("; ");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Append a Set-Cookie header preserving any already on the response.
|
|
213
|
-
function _appendSetCookie(res, header) {
|
|
214
|
-
if (!res || typeof res.setHeader !== "function") {
|
|
215
|
-
throw new CookieError("cookies/no-set-header",
|
|
216
|
-
"response object has no setHeader (not a Node http.ServerResponse?)");
|
|
217
|
-
}
|
|
218
|
-
var existing;
|
|
219
|
-
if (typeof res.getHeader === "function") existing = res.getHeader("Set-Cookie");
|
|
220
|
-
var arr;
|
|
221
|
-
if (Array.isArray(existing)) arr = existing.slice();
|
|
222
|
-
else if (existing !== undefined) arr = [existing];
|
|
223
|
-
else arr = [];
|
|
224
|
-
arr.push(header);
|
|
225
|
-
res.setHeader("Set-Cookie", arr);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function _readCookieFromReq(req, name) {
|
|
229
|
-
if (!req || !req.headers) return null;
|
|
230
|
-
var header = req.headers.cookie;
|
|
231
|
-
if (!header) return null;
|
|
232
|
-
var jar = parse(header);
|
|
233
|
-
return Object.prototype.hasOwnProperty.call(jar, name) ? jar[name] : null;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function create(opts) {
|
|
237
|
-
opts = opts || {};
|
|
238
|
-
validateOpts(opts, ["vault", "defaults"], "b.cookies");
|
|
239
|
-
var vault = opts.vault || null;
|
|
240
|
-
// Defaults applied on every write unless the per-call attrs override.
|
|
241
|
-
// secure defaults to true: cookies should never be sent in cleartext;
|
|
242
|
-
// operators developing locally over http opt out explicitly.
|
|
243
|
-
var defaults = Object.assign({
|
|
244
|
-
httpOnly: true,
|
|
245
|
-
secure: true,
|
|
246
|
-
sameSite: "Lax",
|
|
247
|
-
path: "/",
|
|
248
|
-
}, opts.defaults || {});
|
|
249
|
-
|
|
250
|
-
function _mergeAttrs(callerAttrs) {
|
|
251
|
-
return Object.assign({}, defaults, callerAttrs || {});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function read(req, name) { return _readCookieFromReq(req, name); }
|
|
255
|
-
function write(res, name, value, attrs) {
|
|
256
|
-
_appendSetCookie(res, serialize(name, value, _mergeAttrs(attrs)));
|
|
257
|
-
}
|
|
258
|
-
function clear(res, name, attrs) {
|
|
259
|
-
// Expire-now cookie. Domain + Path must match the original write
|
|
260
|
-
// for the browser to actually delete it — operators pass the same
|
|
261
|
-
// attrs they used on write (or rely on the same defaults).
|
|
262
|
-
var attrsExp = Object.assign({}, _mergeAttrs(attrs), { maxAge: 0 });
|
|
263
|
-
delete attrsExp.expires;
|
|
264
|
-
_appendSetCookie(res, serialize(name, "", attrsExp));
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function _requireVault() {
|
|
268
|
-
if (!vault || typeof vault.seal !== "function" || typeof vault.unseal !== "function") {
|
|
269
|
-
throw new CookieError("cookies/no-vault",
|
|
270
|
-
"sealed cookies require opts.vault (a value with .seal/.unseal)");
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Vault.seal returns "vault:<base64>". We strip the constant prefix
|
|
275
|
-
// on the wire to keep cookies short, and re-add it before unseal.
|
|
276
|
-
// Within a vault major version the prefix is stable for the cookie's
|
|
277
|
-
// entire Max-Age window, so this is safe.
|
|
278
|
-
function writeSealed(res, name, value, attrs) {
|
|
279
|
-
_requireVault();
|
|
280
|
-
if (typeof value !== "string") {
|
|
281
|
-
throw new CookieError("cookies/invalid-value",
|
|
282
|
-
"sealed cookie value must be a string before sealing");
|
|
283
|
-
}
|
|
284
|
-
var sealed = vault.seal(value);
|
|
285
|
-
var stripped = sealed.startsWith(C.VAULT_PREFIX)
|
|
286
|
-
? sealed.substring(C.VAULT_PREFIX.length)
|
|
287
|
-
: sealed;
|
|
288
|
-
write(res, name, stripped, attrs);
|
|
289
|
-
}
|
|
290
|
-
function readSealed(req, name) {
|
|
291
|
-
_requireVault();
|
|
292
|
-
var raw = _readCookieFromReq(req, name);
|
|
293
|
-
if (raw === null) return null;
|
|
294
|
-
try { return vault.unseal(C.VAULT_PREFIX + raw); }
|
|
295
|
-
catch (_e) { return null; }
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return {
|
|
299
|
-
parse: parse,
|
|
300
|
-
serialize: function (n, v, a) { return serialize(n, v, _mergeAttrs(a)); },
|
|
301
|
-
read: read,
|
|
302
|
-
write: write,
|
|
303
|
-
clear: clear,
|
|
304
|
-
writeSealed: writeSealed,
|
|
305
|
-
readSealed: readSealed,
|
|
306
|
-
defaults: defaults,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
module.exports = {
|
|
311
|
-
create: create,
|
|
312
|
-
parse: parse,
|
|
313
|
-
serialize: serialize,
|
|
314
|
-
CookieError: CookieError,
|
|
315
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* cookies — cookie parse/serialize + access-gated sealed cookies.
|
|
4
|
+
*
|
|
5
|
+
* RFC 6265 cookie plumbing the framework was duplicating across
|
|
6
|
+
* middleware: a parser in attach-user, ad-hoc Set-Cookie strings in
|
|
7
|
+
* route handlers, no shared place for attribute defaults. This is the
|
|
8
|
+
* single primitive.
|
|
9
|
+
*
|
|
10
|
+
* Two surfaces:
|
|
11
|
+
*
|
|
12
|
+
* 1. Module-level (stateless): cookies.parse / cookies.serialize.
|
|
13
|
+
* Useful in test fixtures and code that doesn't have a vault.
|
|
14
|
+
*
|
|
15
|
+
* 2. Instance (cookies.create): bound defaults for cookie attributes,
|
|
16
|
+
* a wired vault for sealed reads/writes, and req/res helpers.
|
|
17
|
+
*
|
|
18
|
+
* var cookies = b.cookies.create({
|
|
19
|
+
* vault: b.vault, // required for sealed* methods
|
|
20
|
+
* defaults: {
|
|
21
|
+
* httpOnly: true,
|
|
22
|
+
* secure: true, // default true; HTTPS expected
|
|
23
|
+
* sameSite: "Lax",
|
|
24
|
+
* path: "/",
|
|
25
|
+
* maxAge: 7 * 86400, // seconds
|
|
26
|
+
* },
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* cookies.parse("a=1; b=2") → { a: "1", b: "2" }
|
|
30
|
+
* cookies.serialize("name", "v",
|
|
31
|
+
* { maxAge: 3600 }) → "name=v; Max-Age=3600; Path=/; HttpOnly; SameSite=Lax; Secure"
|
|
32
|
+
*
|
|
33
|
+
* cookies.read(req, "name") → "v" or null
|
|
34
|
+
* cookies.write(res, "name", "v", {}) // appends to existing Set-Cookie
|
|
35
|
+
* cookies.clear(res, "name", {}) // expire by Max-Age=0
|
|
36
|
+
*
|
|
37
|
+
* cookies.writeSealed(res, "session", sid) // vault.seal then write
|
|
38
|
+
* cookies.readSealed(req, "session") // read then vault.unseal
|
|
39
|
+
*
|
|
40
|
+
* Sealed-cookie purpose: the cookie value is a vault.seal of the real
|
|
41
|
+
* value. Without the framework's vault key, no client can hand-craft a
|
|
42
|
+
* valid cookie value, so the API is unreachable via curl-with-arbitrary-
|
|
43
|
+
* cookies or any tool that hasn't been through the framework's crypto
|
|
44
|
+
* flow. The vault prefix is stripped on write and re-added on read so
|
|
45
|
+
* the cookie carries only the base64 envelope.
|
|
46
|
+
*
|
|
47
|
+
* Defense in serialize/parse:
|
|
48
|
+
* - Cookie name must be a valid token (no CTLs, no separator chars).
|
|
49
|
+
* - Cookie value must not contain CRLF, semicolon, or comma.
|
|
50
|
+
* - Value is percent-encoded on write, percent-decoded on read.
|
|
51
|
+
* - Domain / Path are CRLF-stripped to defeat header injection
|
|
52
|
+
* attempts via operator-controlled but improperly-escaped inputs.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
var C = require("./constants");
|
|
56
|
+
var validateOpts = require("./validate-opts");
|
|
57
|
+
var { FrameworkError } = require("./framework-error");
|
|
58
|
+
|
|
59
|
+
class CookieError extends FrameworkError {
|
|
60
|
+
constructor(code, message) {
|
|
61
|
+
super(message, code);
|
|
62
|
+
this.name = "CookieError";
|
|
63
|
+
this.permanent = true;
|
|
64
|
+
this.isCookieError = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// RFC 6265 cookie-name token: VCHAR minus separators. Reject anything
|
|
69
|
+
// outside this range — embeddings of CTLs / separators / whitespace
|
|
70
|
+
// would break parsing on the next hop.
|
|
71
|
+
var TOKEN_RE = /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/;
|
|
72
|
+
// Reject CRLF, NUL, semicolon, comma in cookie value pre-encoding.
|
|
73
|
+
var FORBIDDEN_VALUE_RE = /[\r\n\0;,]/;
|
|
74
|
+
|
|
75
|
+
// Length caps applied before the regex tests so a hostile caller can't
|
|
76
|
+
// stage a pathological-length input against the engine. RFC 6265 doesn't
|
|
77
|
+
// pin a name/value length; browsers vary, with a practical 4 KiB total
|
|
78
|
+
// envelope. The framework caps name + value to leave headroom.
|
|
79
|
+
var MAX_NAME_LENGTH = C.BYTES.bytes(256);
|
|
80
|
+
var MAX_VALUE_LENGTH = C.BYTES.kib(4);
|
|
81
|
+
|
|
82
|
+
function _validateName(name) {
|
|
83
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
84
|
+
throw new CookieError("cookies/invalid-name",
|
|
85
|
+
"cookie name must be a non-empty string");
|
|
86
|
+
}
|
|
87
|
+
// Length cap before the regex test — bound the engine on hostile
|
|
88
|
+
// input lengths even though the regex is anchored.
|
|
89
|
+
if (name.length > MAX_NAME_LENGTH || !TOKEN_RE.test(name)) {
|
|
90
|
+
throw new CookieError("cookies/invalid-name",
|
|
91
|
+
"cookie name '" + name + "' is empty, too long, or contains forbidden characters");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function _validateValue(value) {
|
|
96
|
+
if (typeof value !== "string") {
|
|
97
|
+
throw new CookieError("cookies/invalid-value",
|
|
98
|
+
"cookie value must be a string");
|
|
99
|
+
}
|
|
100
|
+
// Length cap before the regex test (defense in depth — the regex
|
|
101
|
+
// here is a simple character class, but the same discipline that
|
|
102
|
+
// bounds longer regexes elsewhere applies).
|
|
103
|
+
if (value.length > MAX_VALUE_LENGTH || FORBIDDEN_VALUE_RE.test(value)) {
|
|
104
|
+
throw new CookieError("cookies/invalid-value",
|
|
105
|
+
"cookie value is too long or contains forbidden control character (CRLF/NUL/;/,)");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Strip CRLF/NUL defensively from operator-supplied attribute strings
|
|
110
|
+
// (Domain, Path, SameSite). Even if the attribute is constant in the
|
|
111
|
+
// caller's code, attribute strings flow into Set-Cookie which is a
|
|
112
|
+
// header — never trust unscrubbed values reach the wire.
|
|
113
|
+
function _scrubAttr(s) {
|
|
114
|
+
if (typeof s !== "string") return s;
|
|
115
|
+
return s.replace(/[\r\n\0]/g, "");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parse(cookieHeader) {
|
|
119
|
+
var out = {};
|
|
120
|
+
if (typeof cookieHeader !== "string" || cookieHeader.length === 0) return out;
|
|
121
|
+
var pairs = cookieHeader.split(/;\s*/);
|
|
122
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
123
|
+
var pair = pairs[i];
|
|
124
|
+
if (!pair) continue;
|
|
125
|
+
var eq = pair.indexOf("=");
|
|
126
|
+
if (eq < 0) continue;
|
|
127
|
+
var k = pair.slice(0, eq).trim();
|
|
128
|
+
if (!k) continue;
|
|
129
|
+
var v = pair.slice(eq + 1).trim();
|
|
130
|
+
// Strip surrounding double-quotes per RFC 6265 §5.2.
|
|
131
|
+
if (v.length >= 2 && v.charAt(0) === '"' && v.charAt(v.length - 1) === '"') {
|
|
132
|
+
v = v.slice(1, -1);
|
|
133
|
+
}
|
|
134
|
+
try { v = decodeURIComponent(v); }
|
|
135
|
+
catch (_e) { /* malformed encoding — keep raw */ }
|
|
136
|
+
// Last write wins per RFC; matches every browser's behavior.
|
|
137
|
+
out[k] = v;
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function serialize(name, value, attrs) {
|
|
143
|
+
_validateName(name);
|
|
144
|
+
_validateValue(value);
|
|
145
|
+
attrs = attrs || {};
|
|
146
|
+
|
|
147
|
+
var parts = [name + "=" + encodeURIComponent(value)];
|
|
148
|
+
|
|
149
|
+
if (attrs.maxAge !== undefined && attrs.maxAge !== null) {
|
|
150
|
+
var maxAge = Number(attrs.maxAge);
|
|
151
|
+
if (!Number.isFinite(maxAge) || Math.floor(maxAge) !== maxAge) {
|
|
152
|
+
throw new CookieError("cookies/invalid-attr",
|
|
153
|
+
"cookie attr maxAge must be an integer (seconds)");
|
|
154
|
+
}
|
|
155
|
+
parts.push("Max-Age=" + maxAge);
|
|
156
|
+
}
|
|
157
|
+
if (attrs.expires !== undefined && attrs.expires !== null) {
|
|
158
|
+
var d = attrs.expires instanceof Date ? attrs.expires : new Date(attrs.expires);
|
|
159
|
+
if (isNaN(d.getTime())) {
|
|
160
|
+
throw new CookieError("cookies/invalid-attr",
|
|
161
|
+
"cookie attr expires must be a Date or parseable date string");
|
|
162
|
+
}
|
|
163
|
+
parts.push("Expires=" + d.toUTCString());
|
|
164
|
+
}
|
|
165
|
+
if (attrs.domain) {
|
|
166
|
+
var dom = _scrubAttr(String(attrs.domain));
|
|
167
|
+
// RFC 6265 §4.1.2.3 + §5.1.3: Domain attribute is a host name.
|
|
168
|
+
// A leading "." is tolerated by browsers (legacy form). Anything
|
|
169
|
+
// else (URLs, paths, spaces, scheme prefixes) makes browsers
|
|
170
|
+
// either ignore the cookie or apply it inconsistently.
|
|
171
|
+
if (!/^\.?[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/i.test(dom)) {
|
|
172
|
+
throw new CookieError("cookies/invalid-attr",
|
|
173
|
+
"cookie attr domain must be a valid host name " +
|
|
174
|
+
"(letters/digits/dots/hyphens, optional leading dot), got " + JSON.stringify(dom));
|
|
175
|
+
}
|
|
176
|
+
parts.push("Domain=" + dom);
|
|
177
|
+
}
|
|
178
|
+
if (attrs.path !== undefined && attrs.path !== null) {
|
|
179
|
+
parts.push("Path=" + _scrubAttr(String(attrs.path)));
|
|
180
|
+
}
|
|
181
|
+
if (attrs.httpOnly) parts.push("HttpOnly");
|
|
182
|
+
if (attrs.sameSite) {
|
|
183
|
+
var ss = String(attrs.sameSite);
|
|
184
|
+
var ssLow = ss.toLowerCase();
|
|
185
|
+
var ssNorm;
|
|
186
|
+
if (ssLow === "strict") ssNorm = "Strict";
|
|
187
|
+
else if (ssLow === "lax") ssNorm = "Lax";
|
|
188
|
+
else if (ssLow === "none") ssNorm = "None";
|
|
189
|
+
else throw new CookieError("cookies/invalid-attr",
|
|
190
|
+
"cookie attr sameSite must be Strict, Lax, or None");
|
|
191
|
+
parts.push("SameSite=" + ssNorm);
|
|
192
|
+
// SameSite=None requires Secure per spec; force it on so operators
|
|
193
|
+
// don't ship a cookie that browsers silently drop.
|
|
194
|
+
if (ssNorm === "None") attrs = Object.assign({}, attrs, { secure: true });
|
|
195
|
+
}
|
|
196
|
+
if (attrs.secure) parts.push("Secure");
|
|
197
|
+
if (attrs.partitioned) parts.push("Partitioned");
|
|
198
|
+
if (attrs.priority) {
|
|
199
|
+
var p = String(attrs.priority);
|
|
200
|
+
var pLow = p.toLowerCase();
|
|
201
|
+
var pNorm;
|
|
202
|
+
if (pLow === "low") pNorm = "Low";
|
|
203
|
+
else if (pLow === "medium") pNorm = "Medium";
|
|
204
|
+
else if (pLow === "high") pNorm = "High";
|
|
205
|
+
else throw new CookieError("cookies/invalid-attr",
|
|
206
|
+
"cookie attr priority must be Low, Medium, or High");
|
|
207
|
+
parts.push("Priority=" + pNorm);
|
|
208
|
+
}
|
|
209
|
+
return parts.join("; ");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Append a Set-Cookie header preserving any already on the response.
|
|
213
|
+
function _appendSetCookie(res, header) {
|
|
214
|
+
if (!res || typeof res.setHeader !== "function") {
|
|
215
|
+
throw new CookieError("cookies/no-set-header",
|
|
216
|
+
"response object has no setHeader (not a Node http.ServerResponse?)");
|
|
217
|
+
}
|
|
218
|
+
var existing;
|
|
219
|
+
if (typeof res.getHeader === "function") existing = res.getHeader("Set-Cookie");
|
|
220
|
+
var arr;
|
|
221
|
+
if (Array.isArray(existing)) arr = existing.slice();
|
|
222
|
+
else if (existing !== undefined) arr = [existing];
|
|
223
|
+
else arr = [];
|
|
224
|
+
arr.push(header);
|
|
225
|
+
res.setHeader("Set-Cookie", arr);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function _readCookieFromReq(req, name) {
|
|
229
|
+
if (!req || !req.headers) return null;
|
|
230
|
+
var header = req.headers.cookie;
|
|
231
|
+
if (!header) return null;
|
|
232
|
+
var jar = parse(header);
|
|
233
|
+
return Object.prototype.hasOwnProperty.call(jar, name) ? jar[name] : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function create(opts) {
|
|
237
|
+
opts = opts || {};
|
|
238
|
+
validateOpts(opts, ["vault", "defaults"], "b.cookies");
|
|
239
|
+
var vault = opts.vault || null;
|
|
240
|
+
// Defaults applied on every write unless the per-call attrs override.
|
|
241
|
+
// secure defaults to true: cookies should never be sent in cleartext;
|
|
242
|
+
// operators developing locally over http opt out explicitly.
|
|
243
|
+
var defaults = Object.assign({
|
|
244
|
+
httpOnly: true,
|
|
245
|
+
secure: true,
|
|
246
|
+
sameSite: "Lax",
|
|
247
|
+
path: "/",
|
|
248
|
+
}, opts.defaults || {});
|
|
249
|
+
|
|
250
|
+
function _mergeAttrs(callerAttrs) {
|
|
251
|
+
return Object.assign({}, defaults, callerAttrs || {});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function read(req, name) { return _readCookieFromReq(req, name); }
|
|
255
|
+
function write(res, name, value, attrs) {
|
|
256
|
+
_appendSetCookie(res, serialize(name, value, _mergeAttrs(attrs)));
|
|
257
|
+
}
|
|
258
|
+
function clear(res, name, attrs) {
|
|
259
|
+
// Expire-now cookie. Domain + Path must match the original write
|
|
260
|
+
// for the browser to actually delete it — operators pass the same
|
|
261
|
+
// attrs they used on write (or rely on the same defaults).
|
|
262
|
+
var attrsExp = Object.assign({}, _mergeAttrs(attrs), { maxAge: 0 });
|
|
263
|
+
delete attrsExp.expires;
|
|
264
|
+
_appendSetCookie(res, serialize(name, "", attrsExp));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function _requireVault() {
|
|
268
|
+
if (!vault || typeof vault.seal !== "function" || typeof vault.unseal !== "function") {
|
|
269
|
+
throw new CookieError("cookies/no-vault",
|
|
270
|
+
"sealed cookies require opts.vault (a value with .seal/.unseal)");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Vault.seal returns "vault:<base64>". We strip the constant prefix
|
|
275
|
+
// on the wire to keep cookies short, and re-add it before unseal.
|
|
276
|
+
// Within a vault major version the prefix is stable for the cookie's
|
|
277
|
+
// entire Max-Age window, so this is safe.
|
|
278
|
+
function writeSealed(res, name, value, attrs) {
|
|
279
|
+
_requireVault();
|
|
280
|
+
if (typeof value !== "string") {
|
|
281
|
+
throw new CookieError("cookies/invalid-value",
|
|
282
|
+
"sealed cookie value must be a string before sealing");
|
|
283
|
+
}
|
|
284
|
+
var sealed = vault.seal(value);
|
|
285
|
+
var stripped = sealed.startsWith(C.VAULT_PREFIX)
|
|
286
|
+
? sealed.substring(C.VAULT_PREFIX.length)
|
|
287
|
+
: sealed;
|
|
288
|
+
write(res, name, stripped, attrs);
|
|
289
|
+
}
|
|
290
|
+
function readSealed(req, name) {
|
|
291
|
+
_requireVault();
|
|
292
|
+
var raw = _readCookieFromReq(req, name);
|
|
293
|
+
if (raw === null) return null;
|
|
294
|
+
try { return vault.unseal(C.VAULT_PREFIX + raw); }
|
|
295
|
+
catch (_e) { return null; }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
parse: parse,
|
|
300
|
+
serialize: function (n, v, a) { return serialize(n, v, _mergeAttrs(a)); },
|
|
301
|
+
read: read,
|
|
302
|
+
write: write,
|
|
303
|
+
clear: clear,
|
|
304
|
+
writeSealed: writeSealed,
|
|
305
|
+
readSealed: readSealed,
|
|
306
|
+
defaults: defaults,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = {
|
|
311
|
+
create: create,
|
|
312
|
+
parse: parse,
|
|
313
|
+
serialize: serialize,
|
|
314
|
+
CookieError: CookieError,
|
|
315
|
+
};
|