@blamejs/core 0.4.1

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.
Files changed (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. package/package.json +39 -0
package/lib/cookies.js ADDED
@@ -0,0 +1,293 @@
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
+ function _validateName(name) {
76
+ if (typeof name !== "string" || name.length === 0) {
77
+ throw new CookieError("cookies/invalid-name",
78
+ "cookie name must be a non-empty string");
79
+ }
80
+ if (!TOKEN_RE.test(name)) {
81
+ throw new CookieError("cookies/invalid-name",
82
+ "cookie name '" + name + "' contains forbidden characters");
83
+ }
84
+ }
85
+
86
+ function _validateValue(value) {
87
+ if (typeof value !== "string") {
88
+ throw new CookieError("cookies/invalid-value",
89
+ "cookie value must be a string");
90
+ }
91
+ if (FORBIDDEN_VALUE_RE.test(value)) {
92
+ throw new CookieError("cookies/invalid-value",
93
+ "cookie value contains forbidden control character (CRLF/NUL/;/,)");
94
+ }
95
+ }
96
+
97
+ // Strip CRLF/NUL defensively from operator-supplied attribute strings
98
+ // (Domain, Path, SameSite). Even if the attribute is constant in the
99
+ // caller's code, attribute strings flow into Set-Cookie which is a
100
+ // header — never trust unscrubbed values reach the wire.
101
+ function _scrubAttr(s) {
102
+ if (typeof s !== "string") return s;
103
+ return s.replace(/[\r\n\0]/g, "");
104
+ }
105
+
106
+ function parse(cookieHeader) {
107
+ var out = {};
108
+ if (typeof cookieHeader !== "string" || cookieHeader.length === 0) return out;
109
+ var pairs = cookieHeader.split(/;\s*/);
110
+ for (var i = 0; i < pairs.length; i++) {
111
+ var pair = pairs[i];
112
+ if (!pair) continue;
113
+ var eq = pair.indexOf("=");
114
+ if (eq < 0) continue;
115
+ var k = pair.slice(0, eq).trim();
116
+ if (!k) continue;
117
+ var v = pair.slice(eq + 1).trim();
118
+ // Strip surrounding double-quotes per RFC 6265 §5.2.
119
+ if (v.length >= 2 && v.charAt(0) === '"' && v.charAt(v.length - 1) === '"') {
120
+ v = v.slice(1, -1);
121
+ }
122
+ try { v = decodeURIComponent(v); }
123
+ catch (_e) { /* malformed encoding — keep raw */ }
124
+ // Last write wins per RFC; matches every browser's behavior.
125
+ out[k] = v;
126
+ }
127
+ return out;
128
+ }
129
+
130
+ function serialize(name, value, attrs) {
131
+ _validateName(name);
132
+ _validateValue(value);
133
+ attrs = attrs || {};
134
+
135
+ var parts = [name + "=" + encodeURIComponent(value)];
136
+
137
+ if (attrs.maxAge !== undefined && attrs.maxAge !== null) {
138
+ var maxAge = Number(attrs.maxAge);
139
+ if (!Number.isFinite(maxAge) || Math.floor(maxAge) !== maxAge) {
140
+ throw new CookieError("cookies/invalid-attr",
141
+ "cookie attr maxAge must be an integer (seconds)");
142
+ }
143
+ parts.push("Max-Age=" + maxAge);
144
+ }
145
+ if (attrs.expires !== undefined && attrs.expires !== null) {
146
+ var d = attrs.expires instanceof Date ? attrs.expires : new Date(attrs.expires);
147
+ if (isNaN(d.getTime())) {
148
+ throw new CookieError("cookies/invalid-attr",
149
+ "cookie attr expires must be a Date or parseable date string");
150
+ }
151
+ parts.push("Expires=" + d.toUTCString());
152
+ }
153
+ if (attrs.domain) {
154
+ parts.push("Domain=" + _scrubAttr(String(attrs.domain)));
155
+ }
156
+ if (attrs.path !== undefined && attrs.path !== null) {
157
+ parts.push("Path=" + _scrubAttr(String(attrs.path)));
158
+ }
159
+ if (attrs.httpOnly) parts.push("HttpOnly");
160
+ if (attrs.sameSite) {
161
+ var ss = String(attrs.sameSite);
162
+ var ssLow = ss.toLowerCase();
163
+ var ssNorm;
164
+ if (ssLow === "strict") ssNorm = "Strict";
165
+ else if (ssLow === "lax") ssNorm = "Lax";
166
+ else if (ssLow === "none") ssNorm = "None";
167
+ else throw new CookieError("cookies/invalid-attr",
168
+ "cookie attr sameSite must be Strict, Lax, or None");
169
+ parts.push("SameSite=" + ssNorm);
170
+ // SameSite=None requires Secure per spec; force it on so operators
171
+ // don't ship a cookie that browsers silently drop.
172
+ if (ssNorm === "None") attrs = Object.assign({}, attrs, { secure: true });
173
+ }
174
+ if (attrs.secure) parts.push("Secure");
175
+ if (attrs.partitioned) parts.push("Partitioned");
176
+ if (attrs.priority) {
177
+ var p = String(attrs.priority);
178
+ var pLow = p.toLowerCase();
179
+ var pNorm;
180
+ if (pLow === "low") pNorm = "Low";
181
+ else if (pLow === "medium") pNorm = "Medium";
182
+ else if (pLow === "high") pNorm = "High";
183
+ else throw new CookieError("cookies/invalid-attr",
184
+ "cookie attr priority must be Low, Medium, or High");
185
+ parts.push("Priority=" + pNorm);
186
+ }
187
+ return parts.join("; ");
188
+ }
189
+
190
+ // Append a Set-Cookie header preserving any already on the response.
191
+ function _appendSetCookie(res, header) {
192
+ if (!res || typeof res.setHeader !== "function") {
193
+ throw new CookieError("cookies/no-set-header",
194
+ "response object has no setHeader (not a Node http.ServerResponse?)");
195
+ }
196
+ var existing;
197
+ if (typeof res.getHeader === "function") existing = res.getHeader("Set-Cookie");
198
+ var arr;
199
+ if (Array.isArray(existing)) arr = existing.slice();
200
+ else if (existing !== undefined) arr = [existing];
201
+ else arr = [];
202
+ arr.push(header);
203
+ res.setHeader("Set-Cookie", arr);
204
+ }
205
+
206
+ function _readCookieFromReq(req, name) {
207
+ if (!req || !req.headers) return null;
208
+ var header = req.headers.cookie;
209
+ if (!header) return null;
210
+ var jar = parse(header);
211
+ return Object.prototype.hasOwnProperty.call(jar, name) ? jar[name] : null;
212
+ }
213
+
214
+ function create(opts) {
215
+ opts = opts || {};
216
+ validateOpts(opts, ["vault", "defaults"], "b.cookies");
217
+ var vault = opts.vault || null;
218
+ // Defaults applied on every write unless the per-call attrs override.
219
+ // secure defaults to true: cookies should never be sent in cleartext;
220
+ // operators developing locally over http opt out explicitly.
221
+ var defaults = Object.assign({
222
+ httpOnly: true,
223
+ secure: true,
224
+ sameSite: "Lax",
225
+ path: "/",
226
+ }, opts.defaults || {});
227
+
228
+ function _mergeAttrs(callerAttrs) {
229
+ return Object.assign({}, defaults, callerAttrs || {});
230
+ }
231
+
232
+ function read(req, name) { return _readCookieFromReq(req, name); }
233
+ function write(res, name, value, attrs) {
234
+ _appendSetCookie(res, serialize(name, value, _mergeAttrs(attrs)));
235
+ }
236
+ function clear(res, name, attrs) {
237
+ // Expire-now cookie. Domain + Path must match the original write
238
+ // for the browser to actually delete it — operators pass the same
239
+ // attrs they used on write (or rely on the same defaults).
240
+ var attrsExp = Object.assign({}, _mergeAttrs(attrs), { maxAge: 0 });
241
+ delete attrsExp.expires;
242
+ _appendSetCookie(res, serialize(name, "", attrsExp));
243
+ }
244
+
245
+ function _requireVault() {
246
+ if (!vault || typeof vault.seal !== "function" || typeof vault.unseal !== "function") {
247
+ throw new CookieError("cookies/no-vault",
248
+ "sealed cookies require opts.vault (a value with .seal/.unseal)");
249
+ }
250
+ }
251
+
252
+ // Vault.seal returns "vault:<base64>". We strip the constant prefix
253
+ // on the wire to keep cookies short, and re-add it before unseal.
254
+ // Within a vault major version the prefix is stable for the cookie's
255
+ // entire Max-Age window, so this is safe.
256
+ function writeSealed(res, name, value, attrs) {
257
+ _requireVault();
258
+ if (typeof value !== "string") {
259
+ throw new CookieError("cookies/invalid-value",
260
+ "sealed cookie value must be a string before sealing");
261
+ }
262
+ var sealed = vault.seal(value);
263
+ var stripped = sealed.startsWith(C.VAULT_PREFIX)
264
+ ? sealed.substring(C.VAULT_PREFIX.length)
265
+ : sealed;
266
+ write(res, name, stripped, attrs);
267
+ }
268
+ function readSealed(req, name) {
269
+ _requireVault();
270
+ var raw = _readCookieFromReq(req, name);
271
+ if (raw === null) return null;
272
+ try { return vault.unseal(C.VAULT_PREFIX + raw); }
273
+ catch (_e) { return null; }
274
+ }
275
+
276
+ return {
277
+ parse: parse,
278
+ serialize: function (n, v, a) { return serialize(n, v, _mergeAttrs(a)); },
279
+ read: read,
280
+ write: write,
281
+ clear: clear,
282
+ writeSealed: writeSealed,
283
+ readSealed: readSealed,
284
+ defaults: defaults,
285
+ };
286
+ }
287
+
288
+ module.exports = {
289
+ create: create,
290
+ parse: parse,
291
+ serialize: serialize,
292
+ CookieError: CookieError,
293
+ };
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ /**
3
+ * b.credentialHash — envelope-versioned credential hashing.
4
+ *
5
+ * Stores a verifiable digest of a credential (API key secret, shared
6
+ * bearer token, etc.) as a base64-encoded envelope:
7
+ *
8
+ * byte 0: 0xC1 (CREDENTIAL_MAGIC)
9
+ * byte 1: <algorithm ID>
10
+ * bytes 2..N: algorithm-specific payload
11
+ *
12
+ * The verify path dispatches on byte 1, so old credentials remain
13
+ * verifiable regardless of what ACTIVE.CRED_HASH points at today.
14
+ * When a new algorithm becomes the framework default, existing rows
15
+ * surface via `needsRehash()` and the next successful verify rotates
16
+ * them transparently — same pattern as `b.auth.password.needsRehash`.
17
+ *
18
+ * var env = await b.credentialHash.hash(secretBytes);
19
+ * // → "wQEx..." (base64)
20
+ *
21
+ * var ok = await b.credentialHash.verify(secretBytes, env);
22
+ * // → true / false
23
+ *
24
+ * var info = b.credentialHash.inspect(env);
25
+ * // → { algoId, algoName, payloadBytes }
26
+ *
27
+ * if (b.credentialHash.needsRehash(env)) {
28
+ * await db.update({ credentialHash: await b.credentialHash.hash(secretBytes) });
29
+ * }
30
+ *
31
+ * Active algorithm: SHAKE256 (0x01). Suitable for high-entropy random
32
+ * secrets (≥ 128 bits) — fast verify (microseconds), brute-force
33
+ * infeasible at the entropy level the framework generates. SHAKE256
34
+ * is an XOF: the envelope payload length drives the digest size, so
35
+ * a future operator can request a 96-byte (or 32-byte) digest without
36
+ * a primitive change — the same algorithm ID covers all output sizes.
37
+ * Operators with low-entropy or operator-supplied secrets should pin
38
+ * Argon2id (0x02) per-registry: `hash(s, { algo: "argon2id" })`.
39
+ *
40
+ * Why SHAKE256 over SHA3-512 as the active:
41
+ * - SHAKE256 is an extensible-output function (XOF). The envelope
42
+ * payload's actual byte length tells the verify path how many
43
+ * bytes to recompute. Changing digest size = no algo rotation.
44
+ * - Same family as the framework KDF (`crypto.kdf`), so one
45
+ * primitive does double duty.
46
+ * - SHA-3 family fixed-size mode locks the byte count at 64 — the
47
+ * moment we want a different size, we'd have to rotate algos.
48
+ *
49
+ * Why not Argon2id by default for api-key:
50
+ * - Argon2id at framework defaults costs ~250ms per verify call.
51
+ * For request-path verification that's a real latency hit.
52
+ * SHAKE256 is microseconds.
53
+ * - For ≥128-bit random secrets, the memory-hard property buys
54
+ * nothing — brute force is infeasible regardless of hash.
55
+ *
56
+ * Why the envelope still matters with SHAKE256 as active:
57
+ * - Algorithm agility — when SHA-3 family ever shows weakness, or
58
+ * a stronger XOF lands, ACTIVE.CRED_HASH rotates with no need
59
+ * to re-issue every credential. Old rows verify under their
60
+ * stored algo byte; new rows use the active.
61
+ * - Transparent rehash via needsRehash() drains old algos at the
62
+ * pace of organic verify traffic.
63
+ *
64
+ * Validation tiers:
65
+ *
66
+ * - hash() opts (algo, params) → Tier A (throw)
67
+ * - hash() secret type / length → Tier A (throw)
68
+ * - verify() envelope shape unparsable → Tier C (return false)
69
+ * - verify() unknown algo ID → Tier C (return false)
70
+ * - inspect() bad envelope → Tier C (return null)
71
+ */
72
+
73
+ var crypto = require("./crypto");
74
+ var C = require("./constants");
75
+ var lazyRequire = require("./lazy-require");
76
+ var { FrameworkError } = require("./framework-error");
77
+
78
+ var observability = lazyRequire(function () { return require("./observability"); });
79
+
80
+ function _emitEvent(name, value, labels) {
81
+ try { observability().event(name, value, labels || {}); }
82
+ catch (_e) { /* Tier B: hot-path observability sink */ }
83
+ }
84
+
85
+ // Default SHAKE256 output size for newly-issued credentials. 128
86
+ // bytes (1024 bits) is double the SHAKE256 capacity (512 bits, which
87
+ // caps collision-resistance); the extra bytes don't add to that
88
+ // guarantee but cost nothing — base64 expansion is ~172 chars in a
89
+ // TEXT column, verify is sub-microsecond, and a longer digest hurts
90
+ // no foreseeable attack while protecting against any unforeseen
91
+ // partial-collision attack class. Per the framework's modernity
92
+ // posture (highest practical bar where cost is negligible), default
93
+ // to the larger size; operators pass `{ params: { length: 64 } }`
94
+ // to opt into the SHA3-512-comparable byte count if they need it.
95
+ var SHAKE256_DEFAULT_LENGTH = 128;
96
+
97
+ function _shake256(secret, length) {
98
+ // crypto.kdf wraps SHAKE256 with arbitrary output length. That's the
99
+ // exact primitive we need — the framework's KDF and credential-hash
100
+ // share one underlying XOF.
101
+ return crypto.kdf(secret, length);
102
+ }
103
+
104
+
105
+ // auth/password is required lazily because it imports the (large)
106
+ // argon2 vendor; loading it for SHA3-only callers is wasted work.
107
+ var passwordPrimitive = lazyRequire(function () { return require("./auth/password"); });
108
+
109
+ class CredentialHashError extends FrameworkError {
110
+ constructor(message, code) {
111
+ super(message, code || "credential-hash/invalid");
112
+ this.name = "CredentialHashError";
113
+ this.isCredentialHashError = true;
114
+ }
115
+ }
116
+
117
+ var ALGOS = Object.freeze({
118
+ SHAKE256: "shake256",
119
+ ARGON2ID: "argon2id",
120
+ });
121
+
122
+ var DEFAULTS = Object.freeze({
123
+ algo: ALGOS.SHAKE256,
124
+ });
125
+
126
+ // Map between human-readable algo names (used in opts) and the wire
127
+ // algorithm IDs (used in the envelope byte). The envelope byte is
128
+ // what's persisted; the string is what operators read in code.
129
+ var NAME_TO_ID = Object.freeze({
130
+ "shake256": C.CRED_HASH_IDS.SHAKE256,
131
+ "argon2id": C.CRED_HASH_IDS.ARGON2ID,
132
+ });
133
+ var ID_TO_NAME = Object.freeze({
134
+ 0x01: "shake256",
135
+ 0x02: "argon2id",
136
+ });
137
+
138
+ // ---- Tier-A validation ----
139
+
140
+ function _validateSecret(secret) {
141
+ if (typeof secret !== "string" && !Buffer.isBuffer(secret)) {
142
+ throw new CredentialHashError(
143
+ "credentialHash: secret must be a string or Buffer, got " + typeof secret,
144
+ "credential-hash/bad-secret");
145
+ }
146
+ var len = Buffer.isBuffer(secret) ? secret.length : Buffer.byteLength(secret, "utf8");
147
+ if (len === 0) {
148
+ throw new CredentialHashError(
149
+ "credentialHash: secret must be non-empty",
150
+ "credential-hash/bad-secret");
151
+ }
152
+ }
153
+
154
+ function _validateOpts(opts) {
155
+ if (!opts) return;
156
+ if (opts.algo !== undefined && typeof opts.algo !== "string") {
157
+ throw new CredentialHashError(
158
+ "credentialHash: algo must be a string ('sha3-512' or 'argon2id')",
159
+ "credential-hash/bad-opt");
160
+ }
161
+ if (opts.algo !== undefined && !Object.prototype.hasOwnProperty.call(NAME_TO_ID, opts.algo)) {
162
+ throw new CredentialHashError(
163
+ "credentialHash: unknown algo '" + opts.algo + "', expected one of " +
164
+ JSON.stringify(Object.keys(NAME_TO_ID)),
165
+ "credential-hash/bad-opt");
166
+ }
167
+ if (opts.params !== undefined && (typeof opts.params !== "object" || Array.isArray(opts.params))) {
168
+ throw new CredentialHashError(
169
+ "credentialHash: params must be a plain object",
170
+ "credential-hash/bad-opt");
171
+ }
172
+ }
173
+
174
+ // ---- Envelope (de)serialization ----
175
+
176
+ function _envelope(algoId, payload) {
177
+ var head = Buffer.from([C.CREDENTIAL_MAGIC, algoId]);
178
+ return Buffer.concat([head, payload]).toString("base64");
179
+ }
180
+
181
+ function _decodeEnvelope(env) {
182
+ if (typeof env !== "string" || env.length === 0) return null;
183
+ var buf;
184
+ try { buf = Buffer.from(env, "base64"); }
185
+ catch (_e) { return null; }
186
+ if (buf.length < 2) return null;
187
+ if (buf[0] !== C.CREDENTIAL_MAGIC) return null;
188
+ var algoId = buf[1];
189
+ if (!Object.prototype.hasOwnProperty.call(ID_TO_NAME, algoId)) return null;
190
+ return { algoId: algoId, payload: buf.slice(2) };
191
+ }
192
+
193
+ // ---- Public surface ----
194
+
195
+ async function hash(secret, opts) {
196
+ _validateSecret(secret);
197
+ _validateOpts(opts);
198
+ var algoName = (opts && opts.algo) || DEFAULTS.algo;
199
+ var algoId = NAME_TO_ID[algoName];
200
+
201
+ if (algoId === C.CRED_HASH_IDS.SHAKE256) {
202
+ var length = (opts && opts.params && opts.params.length) || SHAKE256_DEFAULT_LENGTH;
203
+ if (typeof length !== "number" || !isFinite(length) || length < 16 || Math.floor(length) !== length) {
204
+ throw new CredentialHashError(
205
+ "credentialHash.hash: SHAKE256 length must be an integer >= 16, got " + JSON.stringify(length),
206
+ "credential-hash/bad-opt");
207
+ }
208
+ var env = _envelope(algoId, _shake256(secret, length));
209
+ _emitEvent("credentialHash.hash", 1, { algo: algoName });
210
+ return env;
211
+ }
212
+ if (algoId === C.CRED_HASH_IDS.ARGON2ID) {
213
+ var plain = Buffer.isBuffer(secret) ? secret.toString("utf8") : secret;
214
+ var phc = await passwordPrimitive().hash(plain, opts && opts.params);
215
+ var argonEnv = _envelope(algoId, Buffer.from(phc, "utf8"));
216
+ _emitEvent("credentialHash.hash", 1, { algo: algoName });
217
+ return argonEnv;
218
+ }
219
+ // Unreachable — _validateOpts rejects unknown algos.
220
+ throw new CredentialHashError(
221
+ "credentialHash.hash: unsupported algo id 0x" + algoId.toString(16),
222
+ "credential-hash/unsupported");
223
+ }
224
+
225
+ async function verify(secret, envelope) {
226
+ // Tier C: any malformed input → false. Lets operators write
227
+ // if (!await ch.verify(s, row.hash)) return res.status(401);
228
+ // without try/catch ceremony. We still reject obvious caller bugs
229
+ // (non-string-or-Buffer secret) loudly because that signals broken
230
+ // request plumbing rather than a bad credential.
231
+ if (typeof secret !== "string" && !Buffer.isBuffer(secret)) {
232
+ _emitEvent("credentialHash.verify", 1, { outcome: "failure", reason: "bad-secret-type" });
233
+ return false;
234
+ }
235
+ var len = Buffer.isBuffer(secret) ? secret.length : Buffer.byteLength(secret, "utf8");
236
+ if (len === 0) {
237
+ _emitEvent("credentialHash.verify", 1, { outcome: "failure", reason: "empty-secret" });
238
+ return false;
239
+ }
240
+
241
+ var decoded = _decodeEnvelope(envelope);
242
+ if (!decoded) {
243
+ _emitEvent("credentialHash.verify", 1, { outcome: "failure", reason: "bad-envelope" });
244
+ return false;
245
+ }
246
+ var algoName = ID_TO_NAME[decoded.algoId];
247
+
248
+ if (decoded.algoId === C.CRED_HASH_IDS.SHAKE256) {
249
+ var expected = _shake256(secret, decoded.payload.length);
250
+ var ok = crypto.timingSafeEqual(expected, decoded.payload);
251
+ _emitEvent("credentialHash.verify", 1,
252
+ { outcome: ok ? "success" : "failure", algo: algoName });
253
+ return ok;
254
+ }
255
+ if (decoded.algoId === C.CRED_HASH_IDS.ARGON2ID) {
256
+ var phc = decoded.payload.toString("utf8");
257
+ var plain = Buffer.isBuffer(secret) ? secret.toString("utf8") : secret;
258
+ var argOk = false;
259
+ try { argOk = await passwordPrimitive().verify(phc, plain); }
260
+ catch (_e) { argOk = false; }
261
+ _emitEvent("credentialHash.verify", 1,
262
+ { outcome: argOk ? "success" : "failure", algo: algoName });
263
+ return argOk;
264
+ }
265
+ _emitEvent("credentialHash.verify", 1, { outcome: "failure", reason: "unknown-algo" });
266
+ return false;
267
+ }
268
+
269
+ function inspect(envelope) {
270
+ var decoded = _decodeEnvelope(envelope);
271
+ if (!decoded) return null;
272
+ return {
273
+ algoId: decoded.algoId,
274
+ algoName: ID_TO_NAME[decoded.algoId],
275
+ payloadBytes: decoded.payload.length,
276
+ };
277
+ }
278
+
279
+ function needsRehash(envelope, opts) {
280
+ var decoded = _decodeEnvelope(envelope);
281
+ if (!decoded) return true; // unrecognized → migrate aggressively
282
+ var targetAlgoName = (opts && opts.algo) || DEFAULTS.algo;
283
+ var targetId = NAME_TO_ID[targetAlgoName] || C.ACTIVE.CRED_HASH;
284
+ if (decoded.algoId !== targetId) return true;
285
+ if (decoded.algoId === C.CRED_HASH_IDS.ARGON2ID) {
286
+ // Defer the parameter-lag check to the password primitive's
287
+ // own needsRehash so the threshold stays in one place.
288
+ var phc = decoded.payload.toString("utf8");
289
+ try { return passwordPrimitive().needsRehash(phc, opts && opts.params); }
290
+ catch (_e) { return true; }
291
+ }
292
+ return false;
293
+ }
294
+
295
+ module.exports = {
296
+ hash: hash,
297
+ verify: verify,
298
+ inspect: inspect,
299
+ needsRehash: needsRehash,
300
+ ALGOS: ALGOS,
301
+ DEFAULTS: DEFAULTS,
302
+ CredentialHashError: CredentialHashError,
303
+ };