@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
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+ /**
3
+ * mtls-engine-default — pure-JS X.509 engine wired into b.mtlsCa.
4
+ *
5
+ * Implements the engine contract documented at the top of lib/mtls-ca.js:
6
+ * generateCa({ generation }) -> { caCertPem, caKeyPem }
7
+ * signClientCert({ cn, validityDays,
8
+ * caCertPem, caKeyPem }) -> { cert, key, ca, issuedAt, expiresAt }
9
+ * packageP12({ cn, password, validityDays,
10
+ * caCertPem, caKeyPem }) -> { p12, certPem, issuedAt, expiresAt }
11
+ *
12
+ * Backed by lib/vendor/pki.cjs (vendored @peculiar/x509 + pkijs +
13
+ * reflect-metadata + ASN.1 schema chain). node:crypto.webcrypto is bound
14
+ * inside the bundle entry; nothing here calls openssl CLI.
15
+ *
16
+ * Algorithm envelope:
17
+ * CA + leaf signatures: ECDSA P-384 + SHA-384
18
+ * PKCS#12 key bag : PBES2 + AES-256-CBC + PBKDF2-HMAC-SHA-512, 2,000,000 iter
19
+ * PKCS#12 cert bag : same as key bag
20
+ * PKCS#12 outer MAC : HMAC-SHA-512 + PBKDF2, 2,000,000 iter
21
+ *
22
+ * The X.509 ecosystem doesn't yet accept SLH-DSA / ML-DSA on shipping
23
+ * client certs, so the cert sigs stay classical ECDSA-P384 — matching
24
+ * the framework's hybrid KEM posture rather than its standalone PQ
25
+ * signing posture. Swap atomically when browsers + OS cert stores can
26
+ * verify a PQ algorithm; bump CA_GENERATION on the same release so
27
+ * b.mtlsCa.status reports legacy correctly.
28
+ */
29
+
30
+ var nodeCrypto = require("node:crypto");
31
+
32
+ var pki = require("./vendor/pki.cjs");
33
+
34
+ var C = require("./constants");
35
+ var { FrameworkError } = require("./framework-error");
36
+
37
+ var x509 = pki.x509;
38
+ var pkijs = pki.pkijs;
39
+ var webcrypto = pki.crypto;
40
+
41
+ class MtlsEngineError extends FrameworkError {
42
+ constructor(code, message) {
43
+ super(message, code);
44
+ this.name = "MtlsEngineError";
45
+ this.permanent = true;
46
+ this.isMtlsEngineError = true;
47
+ }
48
+ }
49
+
50
+ var CA_KEY_ALG = { name: "ECDSA", namedCurve: "P-384" };
51
+ var CA_SIG_ALG = { name: "ECDSA", hash: "SHA-384" };
52
+ var CA_KEY_USAGES = ["sign", "verify"];
53
+
54
+ var P12_CONTENT_ENC = { name: "AES-CBC", length: 256 };
55
+ var P12_KDF_HASH = "SHA-512";
56
+ var P12_MAC_HASH = "SHA-512";
57
+ var P12_ITER = 2000000;
58
+
59
+ var CA_VALIDITY_DAYS = 10 * 365; // 10y CA lifetime
60
+ var LEAF_DEFAULT_DAYS = 365;
61
+ var DEFAULT_CA_NAME = "blamejs CA";
62
+ var BAG_ID_KEY = "1.2.840.113549.1.12.10.1.2"; // pkcs-12-pkcs-8ShroudedKeyBag
63
+ var BAG_ID_CERT = "1.2.840.113549.1.12.10.1.3"; // pkcs-12-certBag
64
+ var EKU_CLIENT_AUTH_OID = "1.3.6.1.5.5.7.3.2";
65
+
66
+ function _pemBlock(label, der) {
67
+ var b64 = Buffer.from(der).toString("base64");
68
+ return "-----BEGIN " + label + "-----\n" + b64.match(/.{1,64}/g).join("\n") + "\n-----END " + label + "-----\n";
69
+ }
70
+
71
+ async function _exportKeyPairToPem(keyPair) {
72
+ var pkcs8 = await webcrypto.subtle.exportKey("pkcs8", keyPair.privateKey);
73
+ var spki = await webcrypto.subtle.exportKey("spki", keyPair.publicKey);
74
+ return {
75
+ privatePem: _pemBlock("PRIVATE KEY", pkcs8),
76
+ publicPem: _pemBlock("PUBLIC KEY", spki),
77
+ };
78
+ }
79
+
80
+ // Import a PEM private key regardless of its on-disk encoding.
81
+ // Existing keys may be SEC1, PKCS#1, or PKCS#8 — Node's createPrivateKey
82
+ // normalises all three; webcrypto.importKey then reads the PKCS#8 DER.
83
+ async function _importPemPrivateKey(pem, alg, usages, extractable) {
84
+ var keyObj = nodeCrypto.createPrivateKey(pem);
85
+ var pkcs8 = keyObj.export({ format: "der", type: "pkcs8" });
86
+ return webcrypto.subtle.importKey("pkcs8", pkcs8, alg, !!extractable, usages);
87
+ }
88
+
89
+ function _parseCertPem(pem) {
90
+ return new x509.X509Certificate(pem);
91
+ }
92
+
93
+ function _normaliseCn(cn) {
94
+ var s = String(cn || "").replace(/[^a-zA-Z0-9_.-]/g, "").slice(0, 63);
95
+ if (!s) {
96
+ throw new MtlsEngineError("mtls-engine/bad-cn",
97
+ "cn must contain at least one [A-Za-z0-9._-] character (post-sanitisation)");
98
+ }
99
+ return s;
100
+ }
101
+
102
+ async function generateCa(opts) {
103
+ opts = opts || {};
104
+ var generation = (typeof opts.generation === "number" && opts.generation >= 1)
105
+ ? Math.floor(opts.generation) : 1;
106
+ var caName = opts.name || DEFAULT_CA_NAME;
107
+
108
+ var keys = await webcrypto.subtle.generateKey(CA_KEY_ALG, true, CA_KEY_USAGES);
109
+ var now = new Date();
110
+ var ca = await x509.X509CertificateGenerator.createSelfSigned({
111
+ serialNumber: nodeCrypto.randomBytes(16).toString("hex"),
112
+ name: "CN=" + caName + ",OU=CAv" + generation,
113
+ notBefore: now,
114
+ notAfter: new Date(now.getTime() + C.TIME.days(CA_VALIDITY_DAYS)),
115
+ signingAlgorithm: CA_SIG_ALG,
116
+ keys: keys,
117
+ extensions: [
118
+ new x509.BasicConstraintsExtension(true, 0, true),
119
+ new x509.KeyUsagesExtension(
120
+ x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign,
121
+ true
122
+ ),
123
+ ],
124
+ });
125
+ var pem = await _exportKeyPairToPem(keys);
126
+ return { caCertPem: ca.toString("pem"), caKeyPem: pem.privatePem };
127
+ }
128
+
129
+ async function signClientCert(opts) {
130
+ opts = opts || {};
131
+ if (typeof opts.cn !== "string" || !opts.caCertPem || !opts.caKeyPem) {
132
+ throw new MtlsEngineError("mtls-engine/missing-arg",
133
+ "signClientCert requires { cn, caCertPem, caKeyPem }");
134
+ }
135
+ var validityDays = (typeof opts.validityDays === "number" && opts.validityDays > 0)
136
+ ? Math.floor(opts.validityDays) : LEAF_DEFAULT_DAYS;
137
+ var cn = _normaliseCn(opts.cn);
138
+
139
+ var caKey = await _importPemPrivateKey(opts.caKeyPem, CA_KEY_ALG, ["sign"]);
140
+ var caCert = _parseCertPem(opts.caCertPem);
141
+ var clientKeys = await webcrypto.subtle.generateKey(CA_KEY_ALG, true, CA_KEY_USAGES);
142
+
143
+ var now = new Date();
144
+ var notAfter = new Date(now.getTime() + C.TIME.days(validityDays));
145
+ var clientCert = await x509.X509CertificateGenerator.create({
146
+ serialNumber: nodeCrypto.randomBytes(16).toString("hex"),
147
+ subject: "CN=" + cn,
148
+ issuer: caCert.subject,
149
+ notBefore: now,
150
+ notAfter: notAfter,
151
+ signingAlgorithm: CA_SIG_ALG,
152
+ publicKey: clientKeys.publicKey,
153
+ signingKey: caKey,
154
+ extensions: [
155
+ new x509.BasicConstraintsExtension(false, undefined, true),
156
+ new x509.KeyUsagesExtension(
157
+ x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment,
158
+ true
159
+ ),
160
+ new x509.ExtendedKeyUsageExtension([EKU_CLIENT_AUTH_OID], true),
161
+ ],
162
+ });
163
+ var pem = await _exportKeyPairToPem(clientKeys);
164
+ return {
165
+ cert: clientCert.toString("pem"),
166
+ key: pem.privatePem,
167
+ ca: opts.caCertPem,
168
+ issuedAt: now.toISOString(),
169
+ expiresAt: notAfter.toISOString(),
170
+ };
171
+ }
172
+
173
+ async function packageP12(opts) {
174
+ opts = opts || {};
175
+ if (typeof opts.password !== "string" || opts.password.length < 1) {
176
+ throw new MtlsEngineError("mtls-engine/no-password",
177
+ "packageP12 requires opts.password (non-empty string)");
178
+ }
179
+ var leaf = await signClientCert(opts);
180
+
181
+ // Re-import the leaf key as extractable so we can re-export PKCS#8 DER
182
+ // for the shrouded key bag.
183
+ var leafKey = await _importPemPrivateKey(leaf.key, CA_KEY_ALG, ["sign"], true);
184
+ var leafPkcs8 = await webcrypto.subtle.exportKey("pkcs8", leafKey);
185
+ var privateKeyInfo = pkijs.PrivateKeyInfo.fromBER(leafPkcs8);
186
+
187
+ var leafX509 = _parseCertPem(leaf.cert);
188
+ var caX509 = _parseCertPem(leaf.ca);
189
+ var leafPkijsCert = pkijs.Certificate.fromBER(leafX509.rawData);
190
+ var caPkijsCert = pkijs.Certificate.fromBER(caX509.rawData);
191
+
192
+ var passwordBuf = Buffer.from(opts.password, "utf8");
193
+
194
+ var pfx = new pkijs.PFX({
195
+ parsedValue: {
196
+ integrityMode: 0, // PasswordMode (outer HMAC-PBKDF2)
197
+ authenticatedSafe: new pkijs.AuthenticatedSafe({
198
+ parsedValue: {
199
+ safeContents: [
200
+ {
201
+ privacyMode: 1, // PasswordPrivacyMode (PBES2)
202
+ value: new pkijs.SafeContents({
203
+ safeBags: [
204
+ new pkijs.SafeBag({
205
+ bagId: BAG_ID_KEY,
206
+ bagValue: new pkijs.PKCS8ShroudedKeyBag({ parsedValue: privateKeyInfo }),
207
+ }),
208
+ ],
209
+ }),
210
+ },
211
+ {
212
+ privacyMode: 1,
213
+ value: new pkijs.SafeContents({
214
+ safeBags: [
215
+ new pkijs.SafeBag({
216
+ bagId: BAG_ID_CERT,
217
+ bagValue: new pkijs.CertBag({ parsedValue: leafPkijsCert }),
218
+ }),
219
+ new pkijs.SafeBag({
220
+ bagId: BAG_ID_CERT,
221
+ bagValue: new pkijs.CertBag({ parsedValue: caPkijsCert }),
222
+ }),
223
+ ],
224
+ }),
225
+ },
226
+ ],
227
+ },
228
+ }),
229
+ },
230
+ });
231
+
232
+ // Inner protection on the shrouded-key bag itself.
233
+ await pfx.parsedValue.authenticatedSafe.parsedValue.safeContents[0]
234
+ .value.safeBags[0].bagValue.makeInternalValues({
235
+ password: passwordBuf,
236
+ contentEncryptionAlgorithm: P12_CONTENT_ENC,
237
+ hmacHashAlgorithm: P12_KDF_HASH,
238
+ iterationCount: P12_ITER,
239
+ });
240
+
241
+ // Encrypt each SafeContents envelope.
242
+ await pfx.parsedValue.authenticatedSafe.makeInternalValues({
243
+ safeContents: [
244
+ { password: passwordBuf, contentEncryptionAlgorithm: P12_CONTENT_ENC, hmacHashAlgorithm: P12_KDF_HASH, iterationCount: P12_ITER },
245
+ { password: passwordBuf, contentEncryptionAlgorithm: P12_CONTENT_ENC, hmacHashAlgorithm: P12_KDF_HASH, iterationCount: P12_ITER },
246
+ ],
247
+ });
248
+
249
+ // Outer integrity MAC.
250
+ await pfx.makeInternalValues({
251
+ password: passwordBuf,
252
+ iterations: P12_ITER,
253
+ pbkdf2HashAlgorithm: P12_KDF_HASH,
254
+ hmacHashAlgorithm: P12_MAC_HASH,
255
+ });
256
+
257
+ return {
258
+ p12: Buffer.from(pfx.toSchema().toBER(false)),
259
+ certPem: leaf.cert,
260
+ issuedAt: leaf.issuedAt,
261
+ expiresAt: leaf.expiresAt,
262
+ };
263
+ }
264
+
265
+ function algorithmEnvelope() {
266
+ return {
267
+ cert: { keyAlg: CA_KEY_ALG, sigAlg: CA_SIG_ALG },
268
+ p12: {
269
+ contentEncryption: P12_CONTENT_ENC,
270
+ kdfHash: P12_KDF_HASH,
271
+ macHash: P12_MAC_HASH,
272
+ iterationCount: P12_ITER,
273
+ },
274
+ caValidityDays: CA_VALIDITY_DAYS,
275
+ leafDefaultDays: LEAF_DEFAULT_DAYS,
276
+ };
277
+ }
278
+
279
+ module.exports = {
280
+ generateCa: generateCa,
281
+ signClientCert: signClientCert,
282
+ packageP12: packageP12,
283
+ algorithmEnvelope: algorithmEnvelope,
284
+ MtlsEngineError: MtlsEngineError,
285
+ };
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ /**
3
+ * nonce-store — pluggable replay-protection store.
4
+ *
5
+ * The api-encrypt middleware and any other surface that needs
6
+ * "first-time-only" semantics for a request-supplied nonce uses this
7
+ * primitive. The store is shaped around two operations:
8
+ *
9
+ * await store.checkAndInsert(nonce, expireAt) → boolean
10
+ * Returns true if the nonce was unseen (and is now recorded);
11
+ * returns false if it was already present (= replay attempt).
12
+ * The check + insert MUST be atomic — checking first and
13
+ * inserting second would race on concurrent requests carrying
14
+ * the same nonce.
15
+ *
16
+ * await store.purgeExpired() → number
17
+ * Removes entries past their expireAt. Called periodically by the
18
+ * middleware that uses the store. Returns the count purged.
19
+ *
20
+ * store.close()
21
+ * Releases any timer / pool resources. Memory backend stops its
22
+ * periodic sweep; cluster backend is a no-op (table prune is
23
+ * called explicitly).
24
+ *
25
+ * Backends:
26
+ *
27
+ * 'memory' (default) — Map-backed; periodic sweep evicts expired
28
+ * entries. Single-process accuracy only — a request hitting
29
+ * node A and a replay hitting node B will NOT be caught.
30
+ *
31
+ * 'cluster' — INSERT...ON CONFLICT DO NOTHING into the framework
32
+ * table _blamejs_api_encrypt_nonces. The PRIMARY KEY race is what
33
+ * makes the check + insert atomic across nodes.
34
+ *
35
+ * { checkAndInsert, purgeExpired, close } — operator-supplied
36
+ * custom backend (Redis SETNX, Memcached add, etc.). Use this
37
+ * for cross-process accuracy without a per-request SQL hop.
38
+ *
39
+ * Sweep cadence: memory backend sweeps every opts.sweepIntervalMs
40
+ * (default 5 minutes). Cluster backend's purgeExpired is invoked by
41
+ * the api-encrypt middleware on a rate-limited schedule.
42
+ */
43
+
44
+ var clusterStorage = require("./cluster-storage");
45
+ var C = require("./constants");
46
+ var { defineClass } = require("./framework-error");
47
+
48
+ var NonceStoreError = defineClass("NonceStoreError");
49
+
50
+ var DEFAULT_SWEEP_INTERVAL_MS = C.TIME.minutes(5);
51
+
52
+ function _err(code, message) {
53
+ return new NonceStoreError(code, message, true);
54
+ }
55
+
56
+ // ---- Memory backend ----
57
+
58
+ function _memoryBackend(opts) {
59
+ var sweepIntervalMs = opts.sweepIntervalMs || DEFAULT_SWEEP_INTERVAL_MS;
60
+ var seen = new Map(); // nonce -> expireAt
61
+
62
+ var sweepTimer = setInterval(function () {
63
+ var now = Date.now();
64
+ for (var entry of seen) {
65
+ if (entry[1] <= now) seen.delete(entry[0]);
66
+ }
67
+ }, sweepIntervalMs);
68
+ if (typeof sweepTimer.unref === "function") sweepTimer.unref();
69
+
70
+ function checkAndInsert(nonce, expireAt) {
71
+ if (typeof nonce !== "string" || nonce.length === 0) {
72
+ return Promise.reject(_err("INVALID_NONCE", "nonce must be a non-empty string"));
73
+ }
74
+ if (typeof expireAt !== "number" || !Number.isFinite(expireAt)) {
75
+ return Promise.reject(_err("INVALID_EXPIRE", "expireAt must be a finite number (unix ms)"));
76
+ }
77
+ var existing = seen.get(nonce);
78
+ if (existing !== undefined && existing > Date.now()) {
79
+ return Promise.resolve(false); // replay
80
+ }
81
+ seen.set(nonce, expireAt);
82
+ return Promise.resolve(true);
83
+ }
84
+
85
+ function purgeExpired() {
86
+ var now = Date.now();
87
+ var removed = 0;
88
+ for (var entry of seen) {
89
+ if (entry[1] <= now) { seen.delete(entry[0]); removed++; }
90
+ }
91
+ return Promise.resolve(removed);
92
+ }
93
+
94
+ function close() {
95
+ if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
96
+ seen.clear();
97
+ }
98
+
99
+ return {
100
+ name: "memory",
101
+ checkAndInsert: checkAndInsert,
102
+ purgeExpired: purgeExpired,
103
+ close: close,
104
+ // Test hook — direct read of the underlying Map size
105
+ _size: function () { return seen.size; },
106
+ };
107
+ }
108
+
109
+ // ---- Cluster backend ----
110
+
111
+ function _clusterBackend(_opts) {
112
+ async function checkAndInsert(nonce, expireAt) {
113
+ if (typeof nonce !== "string" || nonce.length === 0) {
114
+ throw _err("INVALID_NONCE", "nonce must be a non-empty string");
115
+ }
116
+ if (typeof expireAt !== "number" || !Number.isFinite(expireAt)) {
117
+ throw _err("INVALID_EXPIRE", "expireAt must be a finite number (unix ms)");
118
+ }
119
+ // INSERT ... ON CONFLICT DO NOTHING is the atomic primitive.
120
+ // rowCount === 1 → won (first sighting); rowCount === 0 → lost
121
+ // (someone else already inserted the same nonce, i.e. replay).
122
+ // The middleware hashes the raw nonce before passing it here so
123
+ // the table only ever sees hashes, not the originals.
124
+ var result = await clusterStorage.execute(
125
+ "INSERT INTO _blamejs_api_encrypt_nonces (nonceHash, expireAt) " +
126
+ "VALUES (?, ?) ON CONFLICT (nonceHash) DO NOTHING",
127
+ [nonce, expireAt]
128
+ );
129
+ return (result && result.rowCount > 0);
130
+ }
131
+
132
+ async function purgeExpired() {
133
+ var result = await clusterStorage.execute(
134
+ "DELETE FROM _blamejs_api_encrypt_nonces WHERE expireAt <= ?",
135
+ [Date.now()]
136
+ );
137
+ return (result && result.rowCount) || 0;
138
+ }
139
+
140
+ function close() { /* no resources held */ }
141
+
142
+ return {
143
+ name: "cluster",
144
+ checkAndInsert: checkAndInsert,
145
+ purgeExpired: purgeExpired,
146
+ close: close,
147
+ };
148
+ }
149
+
150
+ // ---- Resolution ----
151
+
152
+ function create(opts) {
153
+ opts = opts || {};
154
+ var backend = opts.backend;
155
+ if (backend && typeof backend === "object" && typeof backend.checkAndInsert === "function") {
156
+ // Operator-supplied custom backend (Redis, Memcached, etc.). Fill
157
+ // in any missing optional methods so the rest of the framework
158
+ // can call them unconditionally.
159
+ return Object.assign({
160
+ name: "custom",
161
+ purgeExpired: function () { return Promise.resolve(0); },
162
+ close: function () {},
163
+ }, backend);
164
+ }
165
+ if (backend === "cluster") return _clusterBackend(opts);
166
+ if (!backend || backend === "memory") return _memoryBackend(opts);
167
+ throw _err("UNKNOWN_BACKEND",
168
+ "nonce-store: unknown backend '" + backend +
169
+ "' (must be 'memory', 'cluster', or { checkAndInsert, purgeExpired, close })");
170
+ }
171
+
172
+ module.exports = {
173
+ create: create,
174
+ NonceStoreError: NonceStoreError,
175
+ _memoryBackend: _memoryBackend,
176
+ _clusterBackend: _clusterBackend,
177
+ };