@blamejs/core 0.14.26 → 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 +6 -0
- package/README.md +2 -2
- package/index.js +4 -0
- package/lib/agent-envelope-mac.js +104 -0
- package/lib/agent-event-bus.js +105 -4
- package/lib/agent-posture-chain.js +8 -42
- package/lib/ai-content-detect.js +9 -10
- package/lib/api-key.js +107 -74
- package/lib/atomic-file.js +62 -4
- 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 +249 -123
- package/lib/auth/openid-federation.js +108 -47
- 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 +169 -4
- package/lib/consent.js +73 -24
- package/lib/constants.js +16 -11
- package/lib/crypto-field.js +474 -92
- 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/error-page.js +14 -1
- package/lib/external-db-migrate.js +229 -139
- package/lib/external-db.js +25 -15
- package/lib/file-upload.js +52 -7
- package/lib/framework-error.js +14 -1
- package/lib/framework-files.js +73 -0
- package/lib/framework-schema.js +695 -394
- package/lib/gate-contract.js +649 -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 +37 -9
- 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-server-jmap.js +117 -12
- package/lib/mail-spam-score.js +2 -6
- package/lib/mail-store.js +287 -154
- package/lib/middleware/body-parser.js +71 -25
- package/lib/middleware/csrf-protect.js +19 -8
- 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 +57 -3
- package/lib/object-store/sigv4.js +10 -0
- package/lib/observability.js +87 -0
- package/lib/otel-export.js +25 -1
- package/lib/outbox.js +136 -82
- package/lib/parsers/safe-xml.js +47 -7
- 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/redact.js +68 -11
- package/lib/redis-client.js +160 -31
- package/lib/retention.js +82 -39
- package/lib/router.js +212 -5
- 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/ssrf-guard.js +51 -4
- package/lib/static.js +177 -34
- 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 +35 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cache.js
CHANGED
|
@@ -74,15 +74,38 @@ var cacheRedis = require("./cache-redis");
|
|
|
74
74
|
var redisClient = require("./redis-client");
|
|
75
75
|
var clusterStorage = require("./cluster-storage");
|
|
76
76
|
var C = require("./constants");
|
|
77
|
+
var frameworkSchema = require("./framework-schema");
|
|
77
78
|
var lazyRequire = require("./lazy-require");
|
|
78
79
|
var { boot } = require("./log");
|
|
79
80
|
var numericChecks = require("./numeric-checks");
|
|
80
81
|
var requestHelpers = require("./request-helpers");
|
|
81
82
|
var safeAsync = require("./safe-async");
|
|
82
83
|
var safeJson = require("./safe-json");
|
|
84
|
+
var sql = require("./sql");
|
|
83
85
|
var validateOpts = require("./validate-opts");
|
|
84
86
|
var { CacheError } = require("./framework-error");
|
|
85
87
|
|
|
88
|
+
// Cluster-backend tables — resolved through frameworkSchema.tableName so a
|
|
89
|
+
// configured table prefix (b.frameworkSchema.setTablePrefix) is honored.
|
|
90
|
+
// Both names are identity-mapped in LOCAL_TO_EXTERNAL, so clusterStorage's
|
|
91
|
+
// resolveTables leaves them untouched at dispatch and the resolved name is
|
|
92
|
+
// what reaches the backend on both single-node + cluster sides.
|
|
93
|
+
var CACHE_TABLE = "_blamejs_cache"; // allow:hand-rolled-sql — canonical logical table-name declaration
|
|
94
|
+
var CACHE_TAGS_TABLE = "_blamejs_cache_tags"; // allow:hand-rolled-sql — canonical logical table-name declaration
|
|
95
|
+
function _cacheSqlTable() { return frameworkSchema.tableName(CACHE_TABLE); }
|
|
96
|
+
function _cacheTagsSqlTable() { return frameworkSchema.tableName(CACHE_TAGS_TABLE); }
|
|
97
|
+
// b.sql opts for every cluster-backend statement: thread the ACTIVE
|
|
98
|
+
// backend dialect (clusterStorage.dialect() — "sqlite" single-node,
|
|
99
|
+
// "postgres" | "mysql" in cluster mode) so the emitted identifier quoting
|
|
100
|
+
// and dialect idioms (ON CONFLICT vs ON DUPLICATE KEY) match the backend
|
|
101
|
+
// the SQL dispatches to. Defaulting to "sqlite" works on Postgres only by
|
|
102
|
+
// accident (both double-quote identifiers) and emits the wrong quoting on
|
|
103
|
+
// MySQL, so this is the canonical resolver the data-layer threads into
|
|
104
|
+
// b.sql. clusterStorage.execute still rewrites table names + translates
|
|
105
|
+
// `?` placeholders at dispatch; this controls only the builder-side
|
|
106
|
+
// quoting + idiom selection.
|
|
107
|
+
function _cacheSqlOpts() { return { dialect: clusterStorage.dialect() }; }
|
|
108
|
+
|
|
86
109
|
var log = boot("cache");
|
|
87
110
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
88
111
|
// Opt-in vault seal for cluster-backend cache values. Lazy so
|
|
@@ -471,19 +494,21 @@ function _clusterBackend(cfg) {
|
|
|
471
494
|
|
|
472
495
|
async function get(key) {
|
|
473
496
|
var now = clock();
|
|
474
|
-
var
|
|
475
|
-
"
|
|
476
|
-
|
|
477
|
-
|
|
497
|
+
var getBuilt = sql.select(_cacheSqlTable(), _cacheSqlOpts())
|
|
498
|
+
.columns(["valueJson", "expiresAt"])
|
|
499
|
+
.where("cacheKey", _composedKey(key))
|
|
500
|
+
.toSql();
|
|
501
|
+
var result = await clusterStorage.execute(getBuilt.sql, getBuilt.params);
|
|
478
502
|
if (!result || !result.rows || result.rows.length === 0) return undefined;
|
|
479
503
|
var row = result.rows[0];
|
|
480
504
|
if (row.expiresAt <= now) {
|
|
481
505
|
// Lazy purge: opportunistic delete on stale read.
|
|
482
506
|
try {
|
|
483
|
-
|
|
484
|
-
"
|
|
485
|
-
|
|
486
|
-
|
|
507
|
+
var delBuilt = sql.delete(_cacheSqlTable(), _cacheSqlOpts())
|
|
508
|
+
.where("cacheKey", _composedKey(key))
|
|
509
|
+
.where("expiresAt", "<=", now)
|
|
510
|
+
.toSql();
|
|
511
|
+
await clusterStorage.execute(delBuilt.sql, delBuilt.params);
|
|
487
512
|
} catch (_e) { /* sweeper will catch it next pass */ }
|
|
488
513
|
emitObs("cache.eviction.expired", { namespace: namespace });
|
|
489
514
|
return undefined;
|
|
@@ -494,11 +519,13 @@ function _clusterBackend(cfg) {
|
|
|
494
519
|
// Fire-and-forget — best-effort lifetime extension.
|
|
495
520
|
if (slidingTtl && defaultTtlMs !== Infinity && typeof defaultTtlMs === "number") {
|
|
496
521
|
var newExpires = now + defaultTtlMs;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
"
|
|
500
|
-
|
|
501
|
-
|
|
522
|
+
var slideBuilt = sql.update(_cacheSqlTable(), _cacheSqlOpts())
|
|
523
|
+
.set({ expiresAt: newExpires, updatedAt: now })
|
|
524
|
+
.where("cacheKey", _composedKey(key))
|
|
525
|
+
.where("expiresAt", ">", now)
|
|
526
|
+
.toSql();
|
|
527
|
+
clusterStorage.execute(slideBuilt.sql, slideBuilt.params)
|
|
528
|
+
.catch(function () { /* best-effort */ });
|
|
502
529
|
}
|
|
503
530
|
var stored = row.valueJson;
|
|
504
531
|
// Sealed-row decode. Sealed entries are prefixed at write
|
|
@@ -541,28 +568,30 @@ function _clusterBackend(cfg) {
|
|
|
541
568
|
// Wrapping them in a transaction makes a concurrent set see either the
|
|
542
569
|
// whole prior state or the whole new state, never a mix.
|
|
543
570
|
// SQLite + Postgres both honor ON CONFLICT (cacheKey) DO UPDATE.
|
|
571
|
+
var upsertBuilt = sql.upsert(_cacheSqlTable(), _cacheSqlOpts())
|
|
572
|
+
.columns(["cacheKey", "valueJson", "expiresAt", "updatedAt"])
|
|
573
|
+
.values({ cacheKey: ck, valueJson: json, expiresAt: storedExpires, updatedAt: now })
|
|
574
|
+
.onConflict(["cacheKey"])
|
|
575
|
+
.doUpdate({ valueJson: "?", expiresAt: "?", updatedAt: "?" }, [json, storedExpires, now])
|
|
576
|
+
.toSql();
|
|
577
|
+
var tagsDelBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
|
|
578
|
+
.where("cacheKey", ck)
|
|
579
|
+
.toSql();
|
|
544
580
|
await clusterStorage.transaction(async function (tx) {
|
|
545
|
-
await tx.execute(
|
|
546
|
-
"INSERT INTO _blamejs_cache (cacheKey, valueJson, expiresAt, updatedAt) " +
|
|
547
|
-
"VALUES (?, ?, ?, ?) " +
|
|
548
|
-
"ON CONFLICT (cacheKey) DO UPDATE SET " +
|
|
549
|
-
"valueJson = ?, expiresAt = ?, updatedAt = ?",
|
|
550
|
-
[ck, json, storedExpires, now, json, storedExpires, now]
|
|
551
|
-
);
|
|
581
|
+
await tx.execute(upsertBuilt.sql, upsertBuilt.params);
|
|
552
582
|
// Drop any prior tags for this key (tags can change across sets),
|
|
553
583
|
// then INSERT the new ones. The PRIMARY KEY on (cacheKey, tag) makes
|
|
554
584
|
// the INSERT idempotent if duplicate tags sneak in.
|
|
555
|
-
await tx.execute(
|
|
556
|
-
"DELETE FROM _blamejs_cache_tags WHERE cacheKey = ?",
|
|
557
|
-
[ck]
|
|
558
|
-
);
|
|
585
|
+
await tx.execute(tagsDelBuilt.sql, tagsDelBuilt.params);
|
|
559
586
|
if (tags && tags.length > 0) {
|
|
560
587
|
for (var i = 0; i < tags.length; i++) {
|
|
561
|
-
|
|
562
|
-
"
|
|
563
|
-
|
|
564
|
-
[
|
|
565
|
-
|
|
588
|
+
var tagInsBuilt = sql.upsert(_cacheTagsSqlTable(), _cacheSqlOpts())
|
|
589
|
+
.columns(["cacheKey", "tag"])
|
|
590
|
+
.values({ cacheKey: ck, tag: tags[i] })
|
|
591
|
+
.onConflict(["cacheKey", "tag"])
|
|
592
|
+
.doNothing()
|
|
593
|
+
.toSql();
|
|
594
|
+
await tx.execute(tagInsBuilt.sql, tagInsBuilt.params);
|
|
566
595
|
}
|
|
567
596
|
}
|
|
568
597
|
});
|
|
@@ -583,8 +612,11 @@ function _clusterBackend(cfg) {
|
|
|
583
612
|
for (var attempt = 0; attempt < maxRetries; attempt++) {
|
|
584
613
|
var outcome = await clusterStorage.transaction(async function (tx) {
|
|
585
614
|
var now = clock();
|
|
586
|
-
var
|
|
587
|
-
"
|
|
615
|
+
var rowSelBuilt = sql.select(_cacheSqlTable(), _cacheSqlOpts())
|
|
616
|
+
.columns(["valueJson", "expiresAt"])
|
|
617
|
+
.where("cacheKey", ck)
|
|
618
|
+
.toSql();
|
|
619
|
+
var row = await tx.executeOne(rowSelBuilt.sql, rowSelBuilt.params);
|
|
588
620
|
var oldRaw = null;
|
|
589
621
|
var current = null;
|
|
590
622
|
if (row && row.expiresAt > now) {
|
|
@@ -599,8 +631,15 @@ function _clusterBackend(cfg) {
|
|
|
599
631
|
if (decision && decision.abort !== undefined) return { aborted: decision.abort };
|
|
600
632
|
if (decision && decision.delete === true) {
|
|
601
633
|
if (oldRaw !== null) {
|
|
602
|
-
|
|
603
|
-
|
|
634
|
+
var casDelBuilt = sql.delete(_cacheSqlTable(), _cacheSqlOpts())
|
|
635
|
+
.where("cacheKey", ck)
|
|
636
|
+
.where("valueJson", oldRaw)
|
|
637
|
+
.toSql();
|
|
638
|
+
await tx.execute(casDelBuilt.sql, casDelBuilt.params);
|
|
639
|
+
var casTagDelBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
|
|
640
|
+
.where("cacheKey", ck)
|
|
641
|
+
.toSql();
|
|
642
|
+
await tx.execute(casTagDelBuilt.sql, casTagDelBuilt.params);
|
|
604
643
|
}
|
|
605
644
|
return { updated: true, deleted: true };
|
|
606
645
|
}
|
|
@@ -614,17 +653,22 @@ function _clusterBackend(cfg) {
|
|
|
614
653
|
if (oldRaw === null) {
|
|
615
654
|
// Row was absent/expired — insert, but lose the race if another
|
|
616
655
|
// writer inserted concurrently (ON CONFLICT DO NOTHING → 0 rows).
|
|
617
|
-
var
|
|
618
|
-
"
|
|
619
|
-
|
|
620
|
-
[
|
|
656
|
+
var insBuilt = sql.upsert(_cacheSqlTable(), _cacheSqlOpts())
|
|
657
|
+
.columns(["cacheKey", "valueJson", "expiresAt", "updatedAt"])
|
|
658
|
+
.values({ cacheKey: ck, valueJson: json, expiresAt: storedExpires, updatedAt: now })
|
|
659
|
+
.onConflict(["cacheKey"])
|
|
660
|
+
.doNothing()
|
|
661
|
+
.toSql();
|
|
662
|
+
var ins = await tx.execute(insBuilt.sql, insBuilt.params);
|
|
621
663
|
if (!ins || ins.rowCount !== 1) return { conflict: true };
|
|
622
664
|
} else {
|
|
623
665
|
// CAS: only commit if the row still holds the exact bytes we read.
|
|
624
|
-
var
|
|
625
|
-
|
|
626
|
-
"
|
|
627
|
-
|
|
666
|
+
var updBuilt = sql.update(_cacheSqlTable(), _cacheSqlOpts())
|
|
667
|
+
.set({ valueJson: json, expiresAt: storedExpires, updatedAt: now })
|
|
668
|
+
.where("cacheKey", ck)
|
|
669
|
+
.where("valueJson", oldRaw)
|
|
670
|
+
.toSql();
|
|
671
|
+
var upd = await tx.execute(updBuilt.sql, updBuilt.params);
|
|
628
672
|
if (!upd || upd.rowCount !== 1) return { conflict: true };
|
|
629
673
|
}
|
|
630
674
|
return { updated: true, value: decision.value };
|
|
@@ -638,59 +682,67 @@ function _clusterBackend(cfg) {
|
|
|
638
682
|
|
|
639
683
|
async function del(key) {
|
|
640
684
|
var ck = _composedKey(key);
|
|
641
|
-
var
|
|
642
|
-
"
|
|
643
|
-
|
|
644
|
-
);
|
|
685
|
+
var delBuilt = sql.delete(_cacheSqlTable(), _cacheSqlOpts())
|
|
686
|
+
.where("cacheKey", ck)
|
|
687
|
+
.toSql();
|
|
688
|
+
var result = await clusterStorage.execute(delBuilt.sql, delBuilt.params);
|
|
645
689
|
// Drop any matching tag rows. Best-effort: a stale tag row pointing
|
|
646
690
|
// at a non-existent cacheKey is dropped on the next invalidateTag
|
|
647
691
|
// sweep (by the JOIN-shape DELETE) anyway.
|
|
648
|
-
|
|
649
|
-
"
|
|
650
|
-
|
|
651
|
-
|
|
692
|
+
var tagDelBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
|
|
693
|
+
.where("cacheKey", ck)
|
|
694
|
+
.toSql();
|
|
695
|
+
await clusterStorage.execute(tagDelBuilt.sql, tagDelBuilt.params)
|
|
696
|
+
.catch(function () { /* best-effort */ });
|
|
652
697
|
return !!(result && result.rowCount && result.rowCount > 0);
|
|
653
698
|
}
|
|
654
699
|
|
|
655
700
|
async function invalidateTag(tag) {
|
|
656
|
-
// Find every cacheKey carrying the tag (namespace-scoped via
|
|
657
|
-
// on the composed key), delete from the
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
)
|
|
701
|
+
// Find every cacheKey carrying the tag (namespace-scoped via a prefix
|
|
702
|
+
// LIKE on the composed "<namespace>:<key>" form), delete from the
|
|
703
|
+
// cache table + the junction. whereLike(..., "prefix") escapes the
|
|
704
|
+
// namespace's own metacharacters and appends the live wildcard, so the
|
|
705
|
+
// scope stays "this namespace's keys" without a hand-rolled LIKE.
|
|
706
|
+
var prefix = namespace + ":";
|
|
707
|
+
var keysBuilt = sql.select(_cacheTagsSqlTable(), _cacheSqlOpts())
|
|
708
|
+
.columns(["cacheKey"])
|
|
709
|
+
.where("tag", tag)
|
|
710
|
+
.whereLike("cacheKey", prefix, "prefix")
|
|
711
|
+
.toSql();
|
|
712
|
+
var keysResult = await clusterStorage.execute(keysBuilt.sql, keysBuilt.params);
|
|
663
713
|
var keys = (keysResult && keysResult.rows) || [];
|
|
664
714
|
if (keys.length === 0) {
|
|
665
715
|
// Nothing to invalidate; still drop any orphan tag rows for
|
|
666
716
|
// this tag scoped to our namespace.
|
|
667
|
-
|
|
668
|
-
"
|
|
669
|
-
|
|
670
|
-
|
|
717
|
+
var orphanBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
|
|
718
|
+
.where("tag", tag)
|
|
719
|
+
.whereLike("cacheKey", prefix, "prefix")
|
|
720
|
+
.toSql();
|
|
721
|
+
await clusterStorage.execute(orphanBuilt.sql, orphanBuilt.params);
|
|
671
722
|
return 0;
|
|
672
723
|
}
|
|
673
724
|
var purged = 0;
|
|
674
725
|
for (var i = 0; i < keys.length; i++) {
|
|
675
726
|
var ck = keys[i].cacheKey;
|
|
676
|
-
var
|
|
677
|
-
"
|
|
678
|
-
|
|
679
|
-
);
|
|
727
|
+
var rBuilt = sql.delete(_cacheSqlTable(), _cacheSqlOpts())
|
|
728
|
+
.where("cacheKey", ck)
|
|
729
|
+
.toSql();
|
|
730
|
+
var r = await clusterStorage.execute(rBuilt.sql, rBuilt.params);
|
|
680
731
|
if (r && r.rowCount > 0) purged += r.rowCount;
|
|
681
|
-
|
|
682
|
-
"
|
|
683
|
-
|
|
684
|
-
);
|
|
732
|
+
var tagDelBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
|
|
733
|
+
.where("cacheKey", ck)
|
|
734
|
+
.toSql();
|
|
735
|
+
await clusterStorage.execute(tagDelBuilt.sql, tagDelBuilt.params);
|
|
685
736
|
}
|
|
686
737
|
return purged;
|
|
687
738
|
}
|
|
688
739
|
|
|
689
740
|
async function getTags(key) {
|
|
690
|
-
var
|
|
691
|
-
"
|
|
692
|
-
|
|
693
|
-
|
|
741
|
+
var built = sql.select(_cacheTagsSqlTable(), _cacheSqlOpts())
|
|
742
|
+
.columns(["tag"])
|
|
743
|
+
.where("cacheKey", _composedKey(key))
|
|
744
|
+
.toSql();
|
|
745
|
+
var result = await clusterStorage.execute(built.sql, built.params);
|
|
694
746
|
if (!result || !result.rows) return [];
|
|
695
747
|
return result.rows.map(function (r) { return r.tag; });
|
|
696
748
|
}
|
|
@@ -700,60 +752,77 @@ function _clusterBackend(cfg) {
|
|
|
700
752
|
// track LRU at all, so "without bumping" is automatic. Honors
|
|
701
753
|
// expiresAt the same as get().
|
|
702
754
|
var now = clock();
|
|
703
|
-
var
|
|
704
|
-
"
|
|
705
|
-
|
|
706
|
-
|
|
755
|
+
var built = sql.select(_cacheSqlTable(), _cacheSqlOpts())
|
|
756
|
+
.columns(["expiresAt"])
|
|
757
|
+
.where("cacheKey", _composedKey(key))
|
|
758
|
+
.where("expiresAt", ">", now)
|
|
759
|
+
.toSql();
|
|
760
|
+
var result = await clusterStorage.execute(built.sql, built.params);
|
|
707
761
|
return !!(result && result.rows && result.rows.length > 0);
|
|
708
762
|
}
|
|
709
763
|
|
|
710
764
|
async function clear() {
|
|
711
765
|
// Namespace-scoped wipe so two CacheInstance instances sharing the
|
|
712
|
-
// table don't cross-purge each other.
|
|
713
|
-
|
|
714
|
-
var
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
766
|
+
// table don't cross-purge each other. Prefix LIKE on the composed
|
|
767
|
+
// "<namespace>:<key>" form scopes the wipe to this instance.
|
|
768
|
+
var prefix = namespace + ":";
|
|
769
|
+
var clrBuilt = sql.delete(_cacheSqlTable(), _cacheSqlOpts())
|
|
770
|
+
.whereLike("cacheKey", prefix, "prefix")
|
|
771
|
+
.toSql();
|
|
772
|
+
var result = await clusterStorage.execute(clrBuilt.sql, clrBuilt.params);
|
|
718
773
|
// Drop matching tag rows in the same namespace.
|
|
719
|
-
|
|
720
|
-
"
|
|
721
|
-
|
|
722
|
-
|
|
774
|
+
var tagClrBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
|
|
775
|
+
.whereLike("cacheKey", prefix, "prefix")
|
|
776
|
+
.toSql();
|
|
777
|
+
await clusterStorage.execute(tagClrBuilt.sql, tagClrBuilt.params)
|
|
778
|
+
.catch(function () { /* best-effort */ });
|
|
723
779
|
return (result && result.rowCount) || 0;
|
|
724
780
|
}
|
|
725
781
|
|
|
726
782
|
async function size() {
|
|
727
783
|
var now = clock();
|
|
728
|
-
var
|
|
729
|
-
var
|
|
730
|
-
"
|
|
731
|
-
|
|
732
|
-
|
|
784
|
+
var prefix = namespace + ":";
|
|
785
|
+
var built = sql.select(_cacheSqlTable(), _cacheSqlOpts())
|
|
786
|
+
.count("*", "n")
|
|
787
|
+
.whereLike("cacheKey", prefix, "prefix")
|
|
788
|
+
.where("expiresAt", ">", now)
|
|
789
|
+
.toSql();
|
|
790
|
+
var result = await clusterStorage.execute(built.sql, built.params);
|
|
733
791
|
if (!result || !result.rows || result.rows.length === 0) return 0;
|
|
734
|
-
|
|
792
|
+
// COUNT(*) comes back as a BIGINT — node-postgres / mysql2 hand that
|
|
793
|
+
// back as a decimal STRING, and "5" || 0 stays the string "5". A caller
|
|
794
|
+
// comparing size() numerically (>= maxEntries, capacity math) would
|
|
795
|
+
// then compare a string and silently mis-decide. The COUNT alias `n`
|
|
796
|
+
// is not a framework-schema column, so coerceRows does not touch it —
|
|
797
|
+
// coerce it here at the read boundary.
|
|
798
|
+
var n = result.rows[0].n;
|
|
799
|
+
return Number(n) || 0;
|
|
735
800
|
}
|
|
736
801
|
|
|
737
802
|
async function _sweep() {
|
|
738
803
|
var now = clock();
|
|
739
|
-
var
|
|
804
|
+
var prefix = namespace + ":";
|
|
740
805
|
// Capture the to-be-purged keys first so we can drop matching tag
|
|
741
806
|
// rows in the same sweep — keeps the junction table free of orphans
|
|
742
807
|
// pointing at expired cacheKeys.
|
|
743
|
-
var
|
|
744
|
-
"
|
|
745
|
-
|
|
746
|
-
|
|
808
|
+
var expiredBuilt = sql.select(_cacheSqlTable(), _cacheSqlOpts())
|
|
809
|
+
.columns(["cacheKey"])
|
|
810
|
+
.whereLike("cacheKey", prefix, "prefix")
|
|
811
|
+
.where("expiresAt", "<=", now)
|
|
812
|
+
.toSql();
|
|
813
|
+
var expiredResult = await clusterStorage.execute(expiredBuilt.sql, expiredBuilt.params);
|
|
747
814
|
var expiredKeys = (expiredResult && expiredResult.rows) || [];
|
|
748
|
-
|
|
749
|
-
"
|
|
750
|
-
|
|
751
|
-
|
|
815
|
+
var sweepDelBuilt = sql.delete(_cacheSqlTable(), _cacheSqlOpts())
|
|
816
|
+
.whereLike("cacheKey", prefix, "prefix")
|
|
817
|
+
.where("expiresAt", "<=", now)
|
|
818
|
+
.toSql();
|
|
819
|
+
await clusterStorage.execute(sweepDelBuilt.sql, sweepDelBuilt.params);
|
|
752
820
|
for (var i = 0; i < expiredKeys.length; i++) {
|
|
753
|
-
|
|
754
|
-
"
|
|
755
|
-
|
|
756
|
-
|
|
821
|
+
var tagSweepBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
|
|
822
|
+
.where("cacheKey", expiredKeys[i].cacheKey)
|
|
823
|
+
.toSql();
|
|
824
|
+
await clusterStorage.execute(tagSweepBuilt.sql, tagSweepBuilt.params)
|
|
825
|
+
.catch(function () { /* best-effort */ });
|
|
757
826
|
}
|
|
758
827
|
}
|
|
759
828
|
|
package/lib/chain-writer.js
CHANGED
|
@@ -43,6 +43,7 @@ var cluster = require("./cluster");
|
|
|
43
43
|
var clusterStorage = require("./cluster-storage");
|
|
44
44
|
var safeAsync = require("./safe-async");
|
|
45
45
|
var safeSql = require("./safe-sql");
|
|
46
|
+
var sql = require("./sql");
|
|
46
47
|
var C = require("./constants");
|
|
47
48
|
var { FrameworkError } = require("./framework-error");
|
|
48
49
|
|
|
@@ -54,6 +55,16 @@ var ALLOWED_CHAIN_TABLES = new Set(["audit_log", "consent_log"]);
|
|
|
54
55
|
|
|
55
56
|
var FRAMEWORK_SQL_TIMEOUT_MS = C.TIME.seconds(30);
|
|
56
57
|
|
|
58
|
+
// b.sql opts for every chain-table statement: thread the ACTIVE backend
|
|
59
|
+
// dialect (clusterStorage.dialect() — "sqlite" single-node, "postgres" |
|
|
60
|
+
// "mysql" in cluster mode) so the emitted identifier quoting + dialect
|
|
61
|
+
// idioms match the backend the SQL dispatches to. Defaulting to "sqlite"
|
|
62
|
+
// works on Postgres only by accident (both double-quote identifiers) and
|
|
63
|
+
// emits the wrong quoting on MySQL. clusterStorage.execute still rewrites
|
|
64
|
+
// table names + translates `?` placeholders at dispatch; this controls only
|
|
65
|
+
// the builder-side quoting + idiom selection.
|
|
66
|
+
function _sqlOpts() { return { dialect: clusterStorage.dialect() }; }
|
|
67
|
+
|
|
57
68
|
class ChainWriterError extends FrameworkError {
|
|
58
69
|
constructor(message, code) {
|
|
59
70
|
super(message);
|
|
@@ -140,11 +151,15 @@ function create(opts) {
|
|
|
140
151
|
function _ensureCounterInit() {
|
|
141
152
|
if (!_counterInit) {
|
|
142
153
|
_counterInit = new safeAsync.Once(async function () {
|
|
154
|
+
// BARE logical table name — clusterStorage rewrites the framework
|
|
155
|
+
// name to the configured-prefix form and placeholderizes; b.sql
|
|
156
|
+
// quotes the camelCase column + emits the MAX aggregate.
|
|
157
|
+
var maxBuilt = sql.select(table, _sqlOpts())
|
|
158
|
+
.max("monotonicCounter", "m")
|
|
159
|
+
.toSql();
|
|
143
160
|
var row = await safeAsync.withTimeout(
|
|
144
161
|
safeAsync.asyncRetry(function () {
|
|
145
|
-
return clusterStorage.executeOne(
|
|
146
|
-
"SELECT MAX(monotonicCounter) AS m FROM " + safeSql.quoteIdentifier(table)
|
|
147
|
-
);
|
|
162
|
+
return clusterStorage.executeOne(maxBuilt.sql, maxBuilt.params);
|
|
148
163
|
}),
|
|
149
164
|
FRAMEWORK_SQL_TIMEOUT_MS,
|
|
150
165
|
{ name: table + ".readMaxCounter" }
|
|
@@ -156,12 +171,14 @@ function create(opts) {
|
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
async function _readChainTipRow() {
|
|
174
|
+
var tipBuilt = sql.select(table, _sqlOpts())
|
|
175
|
+
.columns(["rowHash"])
|
|
176
|
+
.orderBy("monotonicCounter", "desc")
|
|
177
|
+
.limit(1)
|
|
178
|
+
.toSql();
|
|
159
179
|
return await safeAsync.withTimeout(
|
|
160
180
|
safeAsync.asyncRetry(function () {
|
|
161
|
-
return clusterStorage.executeOne(
|
|
162
|
-
"SELECT rowHash FROM " + safeSql.quoteIdentifier(table) +
|
|
163
|
-
" ORDER BY monotonicCounter DESC LIMIT 1"
|
|
164
|
-
);
|
|
181
|
+
return clusterStorage.executeOne(tipBuilt.sql, tipBuilt.params);
|
|
165
182
|
}),
|
|
166
183
|
FRAMEWORK_SQL_TIMEOUT_MS,
|
|
167
184
|
{ name: table + ".readChainTip" }
|
|
@@ -169,16 +186,21 @@ function create(opts) {
|
|
|
169
186
|
}
|
|
170
187
|
|
|
171
188
|
async function _insertRow(values) {
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
189
|
+
// b.sql INSERT: map each column (identifier-validated at create()) to
|
|
190
|
+
// its positional value and bind as a row object — the unambiguous form
|
|
191
|
+
// (a flat value array whose first element is a Buffer/object would be
|
|
192
|
+
// misread as an array-of-rows). BARE logical table name — clusterStorage
|
|
193
|
+
// rewrites + placeholderizes per dialect.
|
|
194
|
+
var rowObj = {};
|
|
195
|
+
for (var ci = 0; ci < columnsForInsert.length; ci++) {
|
|
196
|
+
rowObj[columnsForInsert[ci]] = values[ci];
|
|
197
|
+
}
|
|
198
|
+
var insBuilt = sql.insert(table, _sqlOpts())
|
|
199
|
+
.columns(columnsForInsert)
|
|
200
|
+
.values(rowObj)
|
|
201
|
+
.toSql();
|
|
176
202
|
return await safeAsync.withTimeout(
|
|
177
|
-
clusterStorage.execute(
|
|
178
|
-
"INSERT INTO " + safeSql.quoteIdentifier(table) +
|
|
179
|
-
" (" + quoted + ") VALUES (" + placeholders + ")",
|
|
180
|
-
values
|
|
181
|
-
),
|
|
203
|
+
clusterStorage.execute(insBuilt.sql, insBuilt.params),
|
|
182
204
|
FRAMEWORK_SQL_TIMEOUT_MS,
|
|
183
205
|
{ name: table + ".insertRow" }
|
|
184
206
|
);
|
package/lib/cli.js
CHANGED
|
@@ -2030,7 +2030,14 @@ async function _runErase(args, ctx) {
|
|
|
2030
2030
|
}
|
|
2031
2031
|
var row;
|
|
2032
2032
|
try {
|
|
2033
|
-
|
|
2033
|
+
// Compose the lookup through b.sql so the identifier is quoted by
|
|
2034
|
+
// construction and the _id binds as a placeholder. quoteName: true
|
|
2035
|
+
// emits the local-sqlite `"table"` form (this runs against the
|
|
2036
|
+
// bootstrapped single-node b.db handle, no clusterStorage rewrite).
|
|
2037
|
+
var selBuilt = b.sql.select(safeTable, { quoteName: true })
|
|
2038
|
+
.where("_id", String(rowId)).toSql();
|
|
2039
|
+
var selStmt = b.db.prepare(selBuilt.sql);
|
|
2040
|
+
row = selStmt.get.apply(selStmt, selBuilt.params);
|
|
2034
2041
|
} catch (e) {
|
|
2035
2042
|
return report.error("row lookup failed: " + ((e && e.message) || String(e)));
|
|
2036
2043
|
}
|
|
@@ -2044,20 +2051,18 @@ async function _runErase(args, ctx) {
|
|
|
2044
2051
|
return report.error("table " + safeTable + " has no sealed columns or derived hashes; " +
|
|
2045
2052
|
"use a regular DELETE for non-sealed rows");
|
|
2046
2053
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
for (var di = 0; di < derivedHashes.length; di++)
|
|
2054
|
-
setClauses.push('"' + derivedHashes[di] + '" = ?');
|
|
2055
|
-
values.push(null);
|
|
2056
|
-
}
|
|
2057
|
-
values.push(String(rowId));
|
|
2054
|
+
// NULL every sealed column + derived hash. Build the SET map for
|
|
2055
|
+
// b.sql.update — each column binds NULL as a placeholder, the
|
|
2056
|
+
// identifiers quote by construction, and the WHERE keeps the write
|
|
2057
|
+
// scoped to the single _id (b.sql refuses an unconditional update).
|
|
2058
|
+
var eraseSet = {};
|
|
2059
|
+
for (var si = 0; si < sealedFields.length; si++) eraseSet[sealedFields[si]] = null;
|
|
2060
|
+
for (var di = 0; di < derivedHashes.length; di++) eraseSet[derivedHashes[di]] = null;
|
|
2058
2061
|
try {
|
|
2059
|
-
var
|
|
2060
|
-
|
|
2062
|
+
var updBuilt = b.sql.update(safeTable, { quoteName: true })
|
|
2063
|
+
.set(eraseSet).where("_id", String(rowId)).toSql();
|
|
2064
|
+
var upd = b.db.prepare(updBuilt.sql);
|
|
2065
|
+
upd.run.apply(upd, updBuilt.params);
|
|
2061
2066
|
} catch (e) {
|
|
2062
2067
|
return report.error("UPDATE failed: " + ((e && e.message) || String(e)));
|
|
2063
2068
|
}
|