@blamejs/core 0.14.27 → 0.15.0
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 +4 -0
- package/README.md +2 -2
- package/index.js +4 -0
- package/lib/ai-content-detect.js +9 -10
- package/lib/api-key.js +107 -74
- package/lib/atomic-file.js +29 -1
- package/lib/audit-chain.js +47 -11
- package/lib/audit-sign.js +77 -2
- package/lib/audit-tools.js +79 -51
- package/lib/audit.js +218 -100
- package/lib/backup/index.js +13 -10
- package/lib/break-glass.js +202 -144
- package/lib/cache.js +174 -105
- package/lib/chain-writer.js +38 -16
- package/lib/cli.js +19 -14
- package/lib/cluster-provider-db.js +130 -104
- package/lib/cluster-storage.js +119 -22
- package/lib/cluster.js +119 -71
- package/lib/compliance.js +22 -0
- package/lib/consent.js +73 -24
- package/lib/constants.js +16 -11
- package/lib/crypto-field.js +387 -91
- package/lib/db-declare-row-policy.js +35 -22
- package/lib/db-file-lifecycle.js +3 -2
- package/lib/db-query.js +497 -255
- package/lib/db-schema.js +209 -44
- package/lib/db.js +176 -95
- package/lib/external-db-migrate.js +229 -139
- package/lib/external-db.js +25 -15
- package/lib/framework-error.js +11 -0
- package/lib/framework-files.js +73 -0
- package/lib/framework-schema.js +695 -394
- package/lib/gate-contract.js +596 -1
- package/lib/guard-agent-registry.js +26 -44
- package/lib/guard-all.js +1 -0
- package/lib/guard-auth.js +42 -112
- package/lib/guard-cidr.js +33 -154
- package/lib/guard-csv.js +46 -113
- package/lib/guard-domain.js +34 -157
- package/lib/guard-dsn.js +27 -43
- package/lib/guard-email.js +47 -69
- package/lib/guard-envelope.js +19 -32
- package/lib/guard-event-bus-payload.js +24 -42
- package/lib/guard-event-bus-topic.js +25 -43
- package/lib/guard-filename.js +42 -106
- package/lib/guard-graphql.js +42 -123
- package/lib/guard-html.js +53 -108
- package/lib/guard-idempotency-key.js +24 -42
- package/lib/guard-image.js +46 -103
- package/lib/guard-imap-command.js +18 -32
- package/lib/guard-jmap.js +16 -30
- package/lib/guard-json.js +38 -108
- package/lib/guard-jsonpath.js +38 -171
- package/lib/guard-jwt.js +49 -179
- package/lib/guard-list-id.js +25 -41
- package/lib/guard-list-unsubscribe.js +27 -43
- package/lib/guard-mail-compose.js +24 -42
- package/lib/guard-mail-move.js +26 -44
- package/lib/guard-mail-query.js +28 -46
- package/lib/guard-mail-reply.js +24 -42
- package/lib/guard-mail-sieve.js +24 -42
- package/lib/guard-managesieve-command.js +17 -31
- package/lib/guard-markdown.js +37 -104
- package/lib/guard-message-id.js +26 -45
- package/lib/guard-mime.js +39 -151
- package/lib/guard-oauth.js +54 -135
- package/lib/guard-pdf.js +45 -101
- package/lib/guard-pop3-command.js +21 -31
- package/lib/guard-posture-chain.js +24 -42
- package/lib/guard-regex.js +33 -107
- package/lib/guard-saga-config.js +24 -42
- package/lib/guard-shell.js +42 -172
- package/lib/guard-smtp-command.js +48 -54
- package/lib/guard-snapshot-envelope.js +24 -42
- package/lib/guard-sql.js +1491 -0
- package/lib/guard-stream-args.js +24 -43
- package/lib/guard-svg.js +47 -65
- package/lib/guard-template.js +35 -172
- package/lib/guard-tenant-id.js +26 -45
- package/lib/guard-time.js +32 -154
- package/lib/guard-trace-context.js +25 -44
- package/lib/guard-uuid.js +32 -153
- package/lib/guard-xml.js +38 -113
- package/lib/guard-yaml.js +51 -163
- package/lib/http-client.js +14 -0
- package/lib/inbox.js +120 -107
- package/lib/legal-hold.js +107 -50
- package/lib/log-stream-cloudwatch.js +47 -31
- package/lib/log-stream-otlp.js +32 -18
- package/lib/mail-crypto-smime.js +2 -6
- package/lib/mail-greylist.js +2 -6
- package/lib/mail-helo.js +2 -6
- package/lib/mail-journal.js +85 -64
- package/lib/mail-rbl.js +2 -6
- package/lib/mail-scan.js +2 -6
- package/lib/mail-spam-score.js +2 -6
- package/lib/mail-store.js +287 -154
- package/lib/middleware/fetch-metadata.js +17 -7
- package/lib/middleware/idempotency-key.js +54 -38
- package/lib/middleware/rate-limit.js +102 -32
- package/lib/middleware/security-headers.js +21 -5
- package/lib/migrations.js +108 -66
- package/lib/network-heartbeat.js +7 -0
- package/lib/nonce-store.js +31 -9
- package/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/object-store/azure-blob.js +31 -3
- package/lib/object-store/sigv4.js +10 -0
- package/lib/outbox.js +136 -82
- package/lib/pqc-agent.js +44 -0
- package/lib/pubsub-cluster.js +42 -20
- package/lib/queue-local.js +202 -139
- package/lib/queue-redis.js +9 -1
- package/lib/queue-sqs.js +6 -0
- package/lib/retention.js +82 -39
- package/lib/safe-dns.js +29 -45
- package/lib/safe-ical.js +18 -33
- package/lib/safe-icap.js +27 -43
- package/lib/safe-sieve.js +21 -40
- package/lib/safe-sql.js +124 -3
- package/lib/safe-vcard.js +18 -33
- package/lib/scheduler.js +35 -12
- package/lib/seeders.js +122 -74
- package/lib/session-stores.js +42 -14
- package/lib/session.js +109 -72
- package/lib/sql.js +3885 -0
- package/lib/static.js +45 -7
- package/lib/subject.js +55 -17
- package/lib/vault/index.js +3 -2
- package/lib/vault/passphrase-ops.js +3 -2
- package/lib/vault/rotate.js +104 -64
- package/lib/vendor-data.js +2 -0
- package/lib/websocket.js +16 -0
- package/package.json +1 -1
- 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.
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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(
|
|
346
|
-
"k
|
|
347
|
-
"fingerprint
|
|
348
|
-
"status_code
|
|
349
|
-
"headers
|
|
350
|
-
"body
|
|
351
|
-
"expires_at
|
|
352
|
-
|
|
353
|
-
|
|
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.
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
"
|
|
368
|
-
"
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
"
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
"
|
|
376
|
-
|
|
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(
|
|
488
|
-
|
|
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
|
-
|
|
264
|
-
"
|
|
265
|
-
|
|
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
|
-
//
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
"
|
|
293
|
-
|
|
294
|
-
"
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
322
|
-
"
|
|
323
|
-
|
|
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:
|
|
14
|
-
*
|
|
13
|
+
* Cross-Origin-Embedder-Policy: credentialless (default-on — with 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,
|
|
177
|
-
*
|
|
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
|
-
|
|
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;
|