@blamejs/core 0.14.27 → 0.15.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 (134) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/index.js +4 -0
  4. package/lib/ai-content-detect.js +9 -10
  5. package/lib/api-key.js +158 -77
  6. package/lib/atomic-file.js +29 -1
  7. package/lib/audit-chain.js +47 -11
  8. package/lib/audit-sign.js +77 -2
  9. package/lib/audit-tools.js +79 -51
  10. package/lib/audit.js +228 -100
  11. package/lib/backup/index.js +13 -10
  12. package/lib/break-glass.js +202 -144
  13. package/lib/cache.js +174 -105
  14. package/lib/chain-writer.js +38 -16
  15. package/lib/cli.js +19 -14
  16. package/lib/cluster-provider-db.js +130 -104
  17. package/lib/cluster-storage.js +119 -22
  18. package/lib/cluster.js +119 -71
  19. package/lib/compliance.js +22 -0
  20. package/lib/consent.js +82 -29
  21. package/lib/constants.js +16 -11
  22. package/lib/crypto-field.js +387 -91
  23. package/lib/db-declare-row-policy.js +35 -22
  24. package/lib/db-file-lifecycle.js +3 -2
  25. package/lib/db-query.js +517 -256
  26. package/lib/db-schema.js +209 -44
  27. package/lib/db.js +202 -95
  28. package/lib/external-db-migrate.js +229 -139
  29. package/lib/external-db.js +25 -15
  30. package/lib/framework-error.js +11 -0
  31. package/lib/framework-files.js +73 -0
  32. package/lib/framework-schema.js +695 -394
  33. package/lib/gate-contract.js +596 -1
  34. package/lib/guard-agent-registry.js +26 -44
  35. package/lib/guard-all.js +1 -0
  36. package/lib/guard-auth.js +42 -112
  37. package/lib/guard-cidr.js +33 -154
  38. package/lib/guard-csv.js +46 -113
  39. package/lib/guard-domain.js +34 -157
  40. package/lib/guard-dsn.js +27 -43
  41. package/lib/guard-email.js +47 -69
  42. package/lib/guard-envelope.js +19 -32
  43. package/lib/guard-event-bus-payload.js +24 -42
  44. package/lib/guard-event-bus-topic.js +25 -43
  45. package/lib/guard-filename.js +42 -106
  46. package/lib/guard-graphql.js +42 -123
  47. package/lib/guard-html.js +53 -108
  48. package/lib/guard-idempotency-key.js +24 -42
  49. package/lib/guard-image.js +46 -103
  50. package/lib/guard-imap-command.js +18 -32
  51. package/lib/guard-jmap.js +16 -30
  52. package/lib/guard-json.js +38 -108
  53. package/lib/guard-jsonpath.js +38 -171
  54. package/lib/guard-jwt.js +49 -179
  55. package/lib/guard-list-id.js +25 -41
  56. package/lib/guard-list-unsubscribe.js +27 -43
  57. package/lib/guard-mail-compose.js +24 -42
  58. package/lib/guard-mail-move.js +26 -44
  59. package/lib/guard-mail-query.js +28 -46
  60. package/lib/guard-mail-reply.js +24 -42
  61. package/lib/guard-mail-sieve.js +24 -42
  62. package/lib/guard-managesieve-command.js +17 -31
  63. package/lib/guard-markdown.js +37 -104
  64. package/lib/guard-message-id.js +26 -45
  65. package/lib/guard-mime.js +39 -151
  66. package/lib/guard-oauth.js +54 -135
  67. package/lib/guard-pdf.js +45 -101
  68. package/lib/guard-pop3-command.js +21 -31
  69. package/lib/guard-posture-chain.js +24 -42
  70. package/lib/guard-regex.js +33 -107
  71. package/lib/guard-saga-config.js +24 -42
  72. package/lib/guard-shell.js +42 -172
  73. package/lib/guard-smtp-command.js +48 -54
  74. package/lib/guard-snapshot-envelope.js +24 -42
  75. package/lib/guard-sql.js +1491 -0
  76. package/lib/guard-stream-args.js +24 -43
  77. package/lib/guard-svg.js +47 -65
  78. package/lib/guard-template.js +35 -172
  79. package/lib/guard-tenant-id.js +26 -45
  80. package/lib/guard-time.js +32 -154
  81. package/lib/guard-trace-context.js +25 -44
  82. package/lib/guard-uuid.js +32 -153
  83. package/lib/guard-xml.js +38 -113
  84. package/lib/guard-yaml.js +51 -163
  85. package/lib/http-client.js +14 -0
  86. package/lib/inbox.js +120 -107
  87. package/lib/legal-hold.js +107 -50
  88. package/lib/log-stream-cloudwatch.js +47 -31
  89. package/lib/log-stream-otlp.js +32 -18
  90. package/lib/mail-crypto-smime.js +2 -6
  91. package/lib/mail-greylist.js +2 -6
  92. package/lib/mail-helo.js +2 -6
  93. package/lib/mail-journal.js +85 -64
  94. package/lib/mail-rbl.js +2 -6
  95. package/lib/mail-scan.js +2 -6
  96. package/lib/mail-spam-score.js +2 -6
  97. package/lib/mail-store.js +293 -154
  98. package/lib/middleware/fetch-metadata.js +17 -7
  99. package/lib/middleware/idempotency-key.js +54 -38
  100. package/lib/middleware/rate-limit.js +102 -32
  101. package/lib/middleware/security-headers.js +21 -5
  102. package/lib/migrations.js +108 -66
  103. package/lib/network-heartbeat.js +7 -0
  104. package/lib/nonce-store.js +31 -9
  105. package/lib/object-store/azure-blob-bucket-ops.js +9 -4
  106. package/lib/object-store/azure-blob.js +31 -3
  107. package/lib/object-store/sigv4.js +10 -0
  108. package/lib/outbox.js +136 -82
  109. package/lib/pqc-agent.js +44 -0
  110. package/lib/pubsub-cluster.js +42 -20
  111. package/lib/queue-local.js +202 -139
  112. package/lib/queue-redis.js +9 -1
  113. package/lib/queue-sqs.js +6 -0
  114. package/lib/retention.js +82 -39
  115. package/lib/safe-dns.js +29 -45
  116. package/lib/safe-ical.js +18 -33
  117. package/lib/safe-icap.js +27 -43
  118. package/lib/safe-sieve.js +21 -40
  119. package/lib/safe-sql.js +124 -3
  120. package/lib/safe-vcard.js +18 -33
  121. package/lib/scheduler.js +35 -12
  122. package/lib/seeders.js +122 -74
  123. package/lib/session-stores.js +42 -14
  124. package/lib/session.js +116 -72
  125. package/lib/sql.js +3885 -0
  126. package/lib/static.js +45 -7
  127. package/lib/subject.js +89 -49
  128. package/lib/vault/index.js +3 -2
  129. package/lib/vault/passphrase-ops.js +3 -2
  130. package/lib/vault/rotate.js +104 -64
  131. package/lib/vendor-data.js +2 -0
  132. package/lib/websocket.js +16 -0
  133. package/package.json +1 -1
  134. package/sbom.cdx.json +6 -6
@@ -125,11 +125,13 @@ function _writeReject(req, res, message, reason, onDeny, problemMode) {
125
125
  * destination list, including `webidentity` (FedCM credentialed
126
126
  * requests). `deniedDest` refuses chosen destinations outright on the
127
127
  * gated methods — a FedCM `webidentity` Sec-Fetch-Dest hitting a route
128
- * that is not an identity endpoint is refused. `allowStorageAccess:
129
- * false` refuses the Storage Access API escalation (a cross-site request
130
- * carrying `Sec-Fetch-Storage-Access: active` / `inactive`) on routes
131
- * that do not participate in the Storage Access flow. Both are opt-in;
132
- * leaving them unset preserves the prior behavior exactly.
128
+ * that is not an identity endpoint is refused. The Storage Access API
129
+ * escalation (a cross-site request carrying `Sec-Fetch-Storage-Access:
130
+ * active` / `inactive`) is REFUSED BY DEFAULT (v0.15.0) on routes that do
131
+ * not participate in the Storage Access flow; operators running an
132
+ * embedded-iframe SaaS that legitimately uses the API opt back in with
133
+ * `allowStorageAccess: true`. `deniedDest` stays opt-in (unset = no
134
+ * destination is denied outright).
133
135
  *
134
136
  * @opts
135
137
  * {
@@ -138,7 +140,7 @@ function _writeReject(req, res, message, reason, onDeny, problemMode) {
138
140
  * allowMissing: boolean, // default true
139
141
  * allowedDest: string[], // cross-site allowlist of Sec-Fetch-Dest values
140
142
  * deniedDest: string[], // Sec-Fetch-Dest values refused on gated methods regardless of site (e.g. ["webidentity"])
141
- * allowStorageAccess: boolean, // default truefalse refuses Sec-Fetch-Storage-Access: active|inactive
143
+ * allowStorageAccess: boolean, // default false — refuses Sec-Fetch-Storage-Access: active|inactive; pass true to opt back in for Storage-Access-flow routes
142
144
  * strictDest: boolean, // default false — true throws at config time on an allowedDest/deniedDest value outside the known Sec-Fetch-Dest vocabulary
143
145
  * allowedNavigate: boolean, // default true
144
146
  * methods: string[], // default POST/PUT/DELETE/PATCH
@@ -178,7 +180,15 @@ function create(opts) {
178
180
  var allowCrossSite = opts.allowCrossSite === true;
179
181
  var allowMissing = opts.allowMissing !== false;
180
182
  var allowedDest = Array.isArray(opts.allowedDest) ? opts.allowedDest.slice() : null;
181
- var allowStorageAccess = opts.allowStorageAccess !== false;
183
+ // Storage Access escalation default-deny (v0.15.0): a cross-site
184
+ // credentialed request carrying Sec-Fetch-Storage-Access: active|inactive
185
+ // is REFUSED by default on the gated methods, because that header signals
186
+ // the embedded context can reach unpartitioned cross-site cookies — a
187
+ // capability a route that does not participate in the Storage Access flow
188
+ // should not silently honor. Operators running an embedded-iframe SaaS
189
+ // that legitimately uses the Storage Access API opt back in with
190
+ // allowStorageAccess: true.
191
+ var allowStorageAccess = opts.allowStorageAccess === true;
182
192
  // deniedDest → a null-prototype membership map; an operator-supplied
183
193
  // destination string is never assigned onto a plain object, so no
184
194
  // reserved name (__proto__ / constructor / prototype) can pollute it.
@@ -47,6 +47,7 @@ var validateOpts = require("../validate-opts");
47
47
  var safeBuffer = require("../safe-buffer");
48
48
  var safeJson = require("../safe-json");
49
49
  var safeSql = require("../safe-sql");
50
+ var sql = require("../sql");
50
51
  var bCrypto = require("../crypto");
51
52
  var cryptoField = require("../crypto-field");
52
53
  var vault = require("../vault");
@@ -250,17 +251,23 @@ function dbStore(opts) {
250
251
  "dbStore: opts.db must be a sqlite-shaped database with a `prepare(sql)` method", true);
251
252
  }
252
253
  var tableNameRaw = opts.tableName !== undefined ? opts.tableName : "blamejs_idempotency_keys";
253
- // Quote-and-validate via safeSql.quoteIdentifier runs
254
- // validateIdentifier internally + emits the dialect-correct quoted
255
- // form. Identifier always reaches SQL through the quoted form.
256
- var qTable;
257
- try { qTable = safeSql.quoteIdentifier(tableNameRaw, "sqlite"); }
254
+ // Validate the operator-supplied table name up front so a bad
255
+ // identifier fails at construction with the stable
256
+ // idempotency/bad-table-name code (b.sql would otherwise raise its own
257
+ // SqlBuilderError deeper in the first build). b.sql then quotes the
258
+ // name by construction in every statement it emits for this store
259
+ // (quoteName:true — this is a direct sqlite handle, not a
260
+ // clusterStorage rewrite target, so the name is quoted, not left bare).
261
+ try { safeSql.validateIdentifier(tableNameRaw, { allowReserved: true }); }
258
262
  catch (sqlErr) {
259
263
  throw new IdempotencyError("idempotency/bad-table-name",
260
264
  "dbStore: opts.tableName is not a valid SQL identifier: " +
261
265
  (sqlErr && sqlErr.message ? sqlErr.message : String(sqlErr)), true);
262
266
  }
263
- var qIndex = safeSql.quoteIdentifier(tableNameRaw + "_expires_idx", "sqlite");
267
+ // b.sql opts for every statement this store builds against the local
268
+ // sqlite handle: sqlite dialect (native `?` placeholders, double-quoted
269
+ // identifiers) + quoteName so the operator table name is emitted quoted.
270
+ var sqlOpts = { dialect: "sqlite", quoteName: true };
264
271
  var doInit = opts.init !== false;
265
272
  var hashKeys = opts.hashKeys !== false;
266
273
  var sealReq = opts.seal !== false;
@@ -342,38 +349,46 @@ function dbStore(opts) {
342
349
  }
343
350
 
344
351
  if (doInit) {
345
- db.prepare("CREATE TABLE IF NOT EXISTS " + qTable + " (" +
346
- "k TEXT PRIMARY KEY, " +
347
- "fingerprint TEXT NOT NULL, " +
348
- "status_code INTEGER NOT NULL, " +
349
- "headers TEXT NOT NULL, " +
350
- "body TEXT NOT NULL, " +
351
- "expires_at INTEGER NOT NULL)").run();
352
- db.prepare("CREATE INDEX IF NOT EXISTS " + qIndex + " ON " +
353
- qTable + "(expires_at)").run();
352
+ db.prepare(sql.createTable(tableNameRaw, [
353
+ { name: "k", type: "text", primaryKey: true },
354
+ { name: "fingerprint", type: "text", notNull: true },
355
+ { name: "status_code", type: "int", notNull: true },
356
+ { name: "headers", type: "text", notNull: true },
357
+ { name: "body", type: "text", notNull: true },
358
+ { name: "expires_at", type: "int", notNull: true },
359
+ ], sqlOpts).sql).run();
360
+ db.prepare(sql.createIndex(tableNameRaw + "_expires_idx", tableNameRaw,
361
+ ["expires_at"], sqlOpts).sql).run();
354
362
  }
355
363
 
356
- // Prepared statements. status_code + expires_at stay non-sealed
357
- // so audit/forensic SELECTs don't have to unseal-everything. The
358
- // `k` column is selected even when not strictly needed for read
359
- // because cryptoField.unsealRow uses it as the rowId in AAD when
360
- // the table is AAD-bound.
361
- var stmtGet = db.prepare(
362
- "SELECT k, fingerprint, status_code, headers, body, expires_at FROM " +
363
- qTable + " WHERE k = ?");
364
- var stmtUpsert = db.prepare(
365
- "INSERT INTO " + qTable +
366
- "(k, fingerprint, status_code, headers, body, expires_at) " +
367
- "VALUES (?, ?, ?, ?, ?, ?) " +
368
- "ON CONFLICT(k) DO UPDATE SET " +
369
- " fingerprint = excluded.fingerprint, " +
370
- " status_code = excluded.status_code, " +
371
- " headers = excluded.headers, " +
372
- " body = excluded.body, " +
373
- " expires_at = excluded.expires_at");
374
- var stmtDeleteStale = db.prepare("DELETE FROM " + qTable +
375
- " WHERE k = ? AND expires_at <= ?");
376
- var stmtDelete = db.prepare("DELETE FROM " + qTable + " WHERE k = ?");
364
+ // Prepared statements, composed once through b.sql and reused per call.
365
+ // b.sql binds concrete values into a params array; for a reusable
366
+ // prepared statement we keep only the emitted SQL text (its `?`
367
+ // placeholders) and bind fresh values at run time, so a build-time
368
+ // sentinel value is just a placeholder slot. status_code + expires_at
369
+ // stay non-sealed so audit/forensic SELECTs don't have to unseal-
370
+ // everything. The `k` column is selected even when not strictly needed
371
+ // for read because cryptoField.unsealRow uses it as the rowId in AAD
372
+ // when the table is AAD-bound.
373
+ var _slot = 0; // sentinel bind value; the prepared statement rebinds at call time
374
+ var stmtGet = db.prepare(sql.select(tableNameRaw, sqlOpts)
375
+ .columns(["k", "fingerprint", "status_code", "headers", "body", "expires_at"])
376
+ .where("k", _slot)
377
+ .toSql().sql);
378
+ var stmtUpsert = db.prepare(sql.upsert(tableNameRaw, sqlOpts)
379
+ .columns(["k", "fingerprint", "status_code", "headers", "body", "expires_at"])
380
+ .values({ k: _slot, fingerprint: _slot, status_code: _slot,
381
+ headers: _slot, body: _slot, expires_at: _slot })
382
+ .onConflict(["k"])
383
+ .doUpdateFromExcluded(["fingerprint", "status_code", "headers", "body", "expires_at"])
384
+ .toSql().sql);
385
+ var stmtDeleteStale = db.prepare(sql.delete(tableNameRaw, sqlOpts)
386
+ .where("k", _slot)
387
+ .where("expires_at", "<=", _slot)
388
+ .toSql().sql);
389
+ var stmtDelete = db.prepare(sql.delete(tableNameRaw, sqlOpts)
390
+ .where("k", _slot)
391
+ .toSql().sql);
377
392
 
378
393
  function _k(rawKey) {
379
394
  if (!hashKeys) return rawKey;
@@ -484,8 +499,9 @@ function dbStore(opts) {
484
499
  }
485
500
  var migrated = 0;
486
501
  var skipped = 0;
487
- var rows = db.prepare("SELECT k, fingerprint, status_code, headers, body, expires_at FROM " +
488
- qTable).all();
502
+ var rows = db.prepare(sql.select(tableNameRaw, sqlOpts)
503
+ .columns(["k", "fingerprint", "status_code", "headers", "body", "expires_at"])
504
+ .toSql().sql).all();
489
505
  for (var i = 0; i < rows.length; i += 1) {
490
506
  var r = rows[i];
491
507
  // If headers/body already start with vault.aad: this row is
@@ -57,13 +57,63 @@
57
57
  * Audit: every limit hit emits system.ratelimit.block with the key + path.
58
58
  */
59
59
  var C = require("../constants");
60
+ var frameworkSchema = require("../framework-schema");
60
61
  var lazyRequire = require("../lazy-require");
61
62
  var requestHelpers = require("../request-helpers");
62
63
  var safeAsync = require("../safe-async");
64
+ var sql = require("../sql");
63
65
  var validateOpts = require("../validate-opts");
64
66
  var clusterStorage = require("../cluster-storage");
65
67
  var denyResponse = require("./deny-response").denyResponse;
66
68
 
69
+ // Cluster-backend table — resolved through frameworkSchema.tableName so a
70
+ // configured table prefix (b.frameworkSchema.setTablePrefix) is honored.
71
+ // The name is identity-mapped in LOCAL_TO_EXTERNAL, so clusterStorage's
72
+ // resolveTables leaves it untouched at dispatch and the resolved name is
73
+ // what reaches the backend on both single-node + cluster sides.
74
+ var RATE_LIMIT_TABLE = "_blamejs_rate_limit_counters"; // allow:hand-rolled-sql — canonical logical table-name declaration
75
+ function _rateLimitSqlTable() { return frameworkSchema.tableName(RATE_LIMIT_TABLE); }
76
+
77
+ // b.sql opts for every cluster-backend statement: thread the ACTIVE backend
78
+ // dialect (clusterStorage.dialect() — "sqlite" single-node, "postgres" |
79
+ // "mysql" in cluster mode) so the emitted identifier quoting and dialect
80
+ // idioms (ON CONFLICT ... DO UPDATE vs ON DUPLICATE KEY UPDATE) match the
81
+ // backend the SQL dispatches to. b.sql defaults to "sqlite", which works on
82
+ // Postgres only by accident (both double-quote identifiers) and emits the
83
+ // wrong quoting + ON CONFLICT (which MySQL rejects) on MySQL.
84
+ // clusterStorage.execute still rewrites table names + translates `?`
85
+ // placeholders at dispatch; this controls only the builder-side quoting +
86
+ // idiom selection.
87
+ function _rateLimitSqlOpts() { return { dialect: clusterStorage.dialect() }; }
88
+
89
+ // Dialect-aware references for the conflict-action CASE expressions in
90
+ // take(). The fixed-window counter's update is per-column conditional (a new
91
+ // window resets count to 1; the same window increments), so it can't reduce
92
+ // to doUpdateFromExcluded — it needs a CASE that reads BOTH the proposed row
93
+ // and the existing row. Those two references are spelled differently per
94
+ // dialect and b.sql passes a doUpdate({col: rawExpr}) expression through
95
+ // verbatim (it is NOT EXCLUDED->VALUES translated on MySQL), so the caller
96
+ // must emit the dialect-correct tokens itself:
97
+ // - proposed-row column: EXCLUDED."<col>" (Postgres/SQLite) vs
98
+ // VALUES(`<col>`) (MySQL ON DUPLICATE KEY UPDATE)
99
+ // - existing-row column: "<table>"."<col>" (Postgres/SQLite) vs
100
+ // `<table>`.`<col>` (MySQL)
101
+ // Identifiers here are framework-controlled constants (the table name + the
102
+ // three counter columns), never operator input, so the inline quoting is
103
+ // closed over a fixed set of names.
104
+ function _conflictRefs(dialect, table) {
105
+ if (dialect === "mysql") {
106
+ return {
107
+ proposed: function (col) { return "VALUES(`" + col + "`)"; },
108
+ existing: function (col) { return "`" + table + "`.`" + col + "`"; },
109
+ };
110
+ }
111
+ return {
112
+ proposed: function (col) { return "EXCLUDED.\"" + col + "\""; },
113
+ existing: function (col) { return "\"" + table + "\".\"" + col + "\""; },
114
+ };
115
+ }
116
+
67
117
  var audit = lazyRequire(function () { return require("../audit"); });
68
118
  var logger = lazyRequire(function () { return require("../log").boot("rate-limit"); });
69
119
 
@@ -260,10 +310,10 @@ function _clusterBackend(opts) {
260
310
  if (now - lastPruneAt < pruneIntervalMs) return;
261
311
  lastPruneAt = now;
262
312
  var cutoff = now - windowMs;
263
- clusterStorage.execute(
264
- "DELETE FROM _blamejs_rate_limit_counters WHERE windowStart < ?",
265
- [cutoff]
266
- ).catch(function (e) {
313
+ var built = sql.delete(_rateLimitSqlTable(), _rateLimitSqlOpts())
314
+ .where("windowStart", "<", cutoff)
315
+ .toSql();
316
+ clusterStorage.execute(built.sql, built.params).catch(function (e) {
267
317
  try {
268
318
  logger().warn("rate-limit prune failed: " + ((e && e.message) || String(e)));
269
319
  } catch (_e) { /* logger best-effort */ }
@@ -274,29 +324,49 @@ function _clusterBackend(opts) {
274
324
  var now = Date.now();
275
325
  var windowStart = Math.floor(now / windowMs) * windowMs;
276
326
 
277
- // Atomic increment: a fresh window resets count to 1; an existing
278
- // row in the same window gets count + 1. Postgres + SQLite both
279
- // support ON CONFLICT...DO UPDATE...RETURNING.
280
- var result = await clusterStorage.execute(
281
- "INSERT INTO _blamejs_rate_limit_counters (key, windowStart, count) " +
282
- "VALUES (?, ?, 1) " +
283
- "ON CONFLICT (key) DO UPDATE SET " +
284
- " count = CASE " +
285
- " WHEN excluded.windowStart > _blamejs_rate_limit_counters.windowStart " +
286
- " THEN 1 " +
287
- " ELSE _blamejs_rate_limit_counters.count + 1 " +
288
- " END, " +
289
- " windowStart = CASE " +
290
- " WHEN excluded.windowStart > _blamejs_rate_limit_counters.windowStart " +
291
- " THEN excluded.windowStart " +
292
- " ELSE _blamejs_rate_limit_counters.windowStart " +
293
- " END " +
294
- "RETURNING count, windowStart",
295
- [key, windowStart]
296
- );
297
- var row = result.rows && result.rows[0];
298
- var count = row ? row.count : 1;
299
- var rowWindow = row ? row.windowStart : windowStart;
327
+ // Atomic increment: a fresh window resets count to 1; an existing row in
328
+ // the same window gets count + 1. The per-column conflict action is a
329
+ // CASE that reads the proposed row AND the existing row, so it goes
330
+ // through the STRUCTURED upsert().doUpdate({...}) form with the dialect
331
+ // threaded b.sql then renders ON CONFLICT...DO UPDATE...RETURNING
332
+ // (Postgres/SQLite) or ON DUPLICATE KEY UPDATE + a readback SELECT
333
+ // (MySQL). The CASE bodies spell the proposed-row (EXCLUDED / VALUES())
334
+ // and existing-row (table self-reference) tokens per dialect via
335
+ // _conflictRefs so the same logic compiles on every backend. No `?` in
336
+ // the CASE bodies; the count seed of 1 binds as the third inserted value.
337
+ var t = _rateLimitSqlTable();
338
+ var dialect = clusterStorage.dialect();
339
+ var refs = _conflictRefs(dialect, t);
340
+ var newerWindow = refs.proposed("windowStart") + " > " + refs.existing("windowStart");
341
+ var countExpr = "CASE WHEN " + newerWindow + " THEN 1 ELSE " +
342
+ refs.existing("count") + " + 1 END";
343
+ var windowExpr = "CASE WHEN " + newerWindow + " THEN " + refs.proposed("windowStart") +
344
+ " ELSE " + refs.existing("windowStart") + " END";
345
+ var built = sql.upsert(t, _rateLimitSqlOpts())
346
+ .columns(["key", "windowStart", "count"])
347
+ .values({ key: key, windowStart: windowStart, count: 1 })
348
+ .onConflict(["key"])
349
+ .doUpdate({ count: countExpr, windowStart: windowExpr })
350
+ .returning(["count", "windowStart"])
351
+ .toSql();
352
+ var row;
353
+ if (built.readbackSql) {
354
+ // MySQL: ON DUPLICATE KEY UPDATE has no RETURNING. Run the upsert,
355
+ // then the readback SELECT b.sql emits (keyed on the conflict key) to
356
+ // learn the post-upsert count/windowStart. clusterStorage.execute
357
+ // coerces the framework int columns (count/windowStart) back to JS
358
+ // numbers on both reads.
359
+ await clusterStorage.execute(built.sql, built.params);
360
+ var readback = await clusterStorage.execute(built.readbackSql.sql, built.readbackSql.params);
361
+ row = readback.rows && readback.rows[0];
362
+ } else {
363
+ var result = await clusterStorage.execute(built.sql, built.params);
364
+ row = result.rows && result.rows[0];
365
+ }
366
+ // count/windowStart are framework int columns coerced to JS numbers by
367
+ // clusterStorage; the absent-row fall-back keeps the verdict math finite.
368
+ var count = row ? Number(row.count) : 1;
369
+ var rowWindow = row ? Number(row.windowStart) : windowStart;
300
370
 
301
371
  _maybePrune();
302
372
 
@@ -318,10 +388,10 @@ function _clusterBackend(opts) {
318
388
  }
319
389
 
320
390
  async function reset(key) {
321
- await clusterStorage.execute(
322
- "DELETE FROM _blamejs_rate_limit_counters WHERE key = ?",
323
- [key]
324
- );
391
+ var built = sql.delete(_rateLimitSqlTable(), _rateLimitSqlOpts())
392
+ .where("key", key)
393
+ .toSql();
394
+ await clusterStorage.execute(built.sql, built.params);
325
395
  }
326
396
 
327
397
  function close() { /* no resources to release */ }
@@ -417,7 +487,7 @@ function create(opts) {
417
487
  // pass "RateLimit-", or a gateway's own prefix. Kept as a matched pair.
418
488
  var headerPrefix = (typeof opts.headerPrefix === "string" && opts.headerPrefix.length > 0)
419
489
  ? opts.headerPrefix : "X-RateLimit-";
420
- var limitHeader = headerPrefix + "Limit";
490
+ var limitHeader = headerPrefix + "Limit"; // allow:hand-rolled-sql — HTTP response-header name (X-RateLimit-Limit), not a SQL LIMIT clause
421
491
  var remainingHeader = headerPrefix + "Remaining";
422
492
  var skipPaths = opts.skipPaths || [];
423
493
  // Throw at create(): each entry must be a string prefix or a RegExp.
@@ -10,8 +10,12 @@
10
10
  * Referrer-Policy: no-referrer — don't leak full URL to outbound links
11
11
  * Permissions-Policy — disable common-attack APIs (camera, geolocation, payment, etc.)
12
12
  * Cross-Origin-Opener-Policy: same-origin
13
- * Cross-Origin-Embedder-Policy: require-corp / credentialless (off by default — breaks images from CDNs; credentialless is the
14
- * CR 2024-12 relaxed mode that lets cross-origin no-cors requests load without CORP markers as long as they don't carry credentials)
13
+ * Cross-Origin-Embedder-Policy: credentialless (default-onwith COOP
14
+ * same-origin this yields cross-origin isolation; credentialless is the
15
+ * relaxed enforcing mode that lets cross-origin no-cors requests load
16
+ * without CORP markers as long as they don't carry credentials, so CDN
17
+ * images/fonts keep working. Pass coep: "require-corp" to tighten, or
18
+ * coep: false to disable.)
15
19
  * Cross-Origin-Resource-Policy: same-origin
16
20
  * Origin-Agent-Cluster: ?1 — origin-keyed agent cluster; extra process isolation
17
21
  * X-DNS-Prefetch-Control: off — don't pre-resolve DNS for off-page links
@@ -173,8 +177,9 @@ function _validatePermissionsPolicy(value) {
173
177
  * nosniff, X-Frame-Options DENY, Referrer-Policy no-referrer, an
174
178
  * extensive Permissions-Policy denylist (camera / geolocation /
175
179
  * payment / Privacy-Sandbox attribution-reporting / bluetooth /
176
- * etc.), COOP same-origin, CORP same-origin, Origin-Agent-Cluster
177
- * `?1`, and a strict default CSP with `require-trusted-types-for
180
+ * etc.), COOP same-origin, COEP credentialless (cross-origin isolation
181
+ * on by default; pass `coep: false` to disable), CORP same-origin,
182
+ * Origin-Agent-Cluster `?1`, and a strict default CSP with `require-trusted-types-for
178
183
  * 'script'`. Each header can be softened by passing the option
179
184
  * value or disabled by passing `false`. Mount FIRST (after
180
185
  * `requestId`) so headers are set before any response could be
@@ -233,7 +238,18 @@ function create(opts) {
233
238
  var refPolicy = opts.referrerPolicy === undefined ? "no-referrer" : opts.referrerPolicy;
234
239
  var permPolicy = opts.permissionsPolicy === undefined ? DEFAULT_PERMISSIONS.join(", ") : opts.permissionsPolicy;
235
240
  var coop = opts.coop === undefined ? "same-origin" : opts.coop;
236
- var coep = opts.coep === undefined ? false : opts.coep;
241
+ // COEP default-on (v0.15.0): emit Cross-Origin-Embedder-Policy:
242
+ // credentialless. With COOP same-origin this completes cross-origin
243
+ // isolation (crossOriginIsolated === true), re-enabling SharedArrayBuffer
244
+ // / high-resolution timers while closing the Spectre-class cross-origin
245
+ // read surface. `credentialless` (HTML spec, shipped Chrome 110+) is the
246
+ // least-breaking enforcing mode: cross-origin no-cors subresources (CDN
247
+ // images, fonts) still load — they're fetched WITHOUT credentials rather
248
+ // than requiring an explicit CORP/CORS opt-in, so existing pages keep
249
+ // working where `require-corp` would have broken them. Operators serving
250
+ // credentialed cross-origin subresources pass coep: "require-corp" (and
251
+ // add CORP/CORS headers), or coep: false to opt out of COEP entirely.
252
+ var coep = opts.coep === undefined ? "credentialless" : opts.coep;
237
253
  var corp = opts.corp === undefined ? "same-origin" : opts.corp;
238
254
  var oac = opts.originAgentCluster === undefined ? "?1" : opts.originAgentCluster;
239
255
  var dpc = opts.dnsPrefetchControl === undefined ? "off" : opts.dnsPrefetchControl;