@blamejs/core 0.9.9 → 0.9.14
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 +5 -0
- package/lib/api-snapshot.js +4 -1
- package/lib/audit.js +29 -17
- package/lib/circuit-breaker.js +21 -6
- package/lib/crypto.js +145 -0
- package/lib/dsr.js +22 -15
- package/lib/inbox.js +21 -15
- package/lib/metrics.js +247 -0
- package/lib/middleware/idempotency-key.js +150 -0
- package/lib/pqc-agent.js +116 -26
- package/lib/retry.js +50 -0
- package/lib/self-update-standalone-verifier.js +280 -0
- package/lib/self-update.js +14 -8
- package/lib/vault/rotate.js +6 -2
- package/package.json +5 -3
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,11 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.9.x
|
|
10
10
|
|
|
11
|
+
- v0.9.14 (2026-05-13) — **Audit-existing-code sweep: `safeSql.quoteIdentifier` adopted across every framework primitive that interpolates SQL identifiers; new `raw-sql-identifier-interpolation` detector seals the bug class.** Codex P1 on PR #44 flagged that `dbStore.get`'s expired-row cleanup was an unconditional `DELETE WHERE k = ?` between SELECT and DELETE — in a multi-process deployment another process could upsert the same key in the race window, and the unconditional delete would erase the fresh row. Fix: scope the delete by the observed `expires_at`. While reviewing, a wider gap surfaced — every `db.prepare("CREATE TABLE " + tableName + ...)` site in the framework had been concatenating identifiers raw, sometimes with inline shape-regex validation that varied per module. The framework already shipped `b.safeSql.quoteIdentifier(name, dialect?)` for exactly this; per the audit-existing-code rule the same patch (a) routes every site through the helper (`lib/audit.js` segregation-of-duties trigger DDL, `lib/dsr.js` ticket store, `lib/inbox.js` message-receive table, `lib/middleware/idempotency-key.js` dbStore, `lib/vault/rotate.js` column-rotation DDL) and (b) adds a `raw-sql-identifier-interpolation` codebase-patterns detector so the bug class can't return. The detector skips variables whose names signal already-quoted identifiers (`q<X>` / `Q_<X>` / `quoted<X>` prefix), so future primitives that use the helper read naturally. **Plus: bounded JSON parse for two file-/DB-backed primitives.** `b.metrics.snapshot.read` and `b.middleware.idempotencyKey.dbStore.get` previously used bare `JSON.parse` with `allow:bare-json-parse` markers; both are read by processes separate from where they were written (CLI/sidecar reads daemon-written snapshot; multi-process fleet shares DB) where a hostile or misbehaving writer could plant a multi-GB value and OOM the reader. Both now route through `b.safeJson.parse(raw, { maxBytes: 4 MiB })`. **Three new primitives — `b.crypto.hashFilesParallel`, `b.pqcAgent.reload`, `b.middleware.idempotencyKey.dbStore` — plus republish of v0.9.13's surface**. v0.9.13's npm-publish workflow failed at the wiki-e2e gate (the Codex P1 fix removed the `opts` parameter from `b.selfUpdate.standaloneVerifier.verify` but the `@signature` JSDoc still declared four arguments — three vs. four arity drift). The v0.9.13 git tag + GH release reached operators but the npm tarball did not; the registry stayed at v0.9.12. v0.9.14 carries v0.9.13's entire shipped surface (circuitBreaker.create({...}) opts-object fix + `b.selfUpdate.standaloneVerifier` + `b.metrics.snapshot` + `b.retry.withBreaker`) plus three additional Tier 2 primitives surfaced by the same downstream consumer that flagged the v0.9.13 batch. (1) **`b.crypto.hashFilesParallel(filePaths, opts?)`** — parallel multi-digest hashing for many files in a single read pass per file. Worker-pool concurrency cap (default `min(8, paths.length)`, 1..256), operator-tunable `algorithms` list (default `["sha256", "sha3-512"]`), optional `onProgress(completed, total)` callback (throws swallowed). Returns rows in the same order as input. The common consumer-side reason to reach for this is SBOM regeneration / vendor-data integrity sweeps / release-asset bundling — situations where N files each need both SHA-256 (legacy compat) and SHA-3-512 (PQC-first) digests and rolling a worker pool by hand has cost a downstream consumer the same two-loop, capture-N-promises, settle-Q boilerplate every release. (2) **`b.pqcAgent.reload()`** — tear down the lazily-built default agent and reset to null so the next `b.pqcAgent.agent` access rebuilds against current TLS posture + `b.network.tls.applyToContext` output. Long-running daemons that rotate the framework's TLS posture (TLS-pinset reload, certificate-pinset refresh, `C.TLS_GROUP_PREFERENCE` update behind a feature flag) need a way to re-source the outbound `https.Agent` without forking a new process. `reload()` calls `.destroy()` on the existing default agent (Node closes idle keep-alive sockets, lets in-flight sockets complete) then nulls the cache. Agents handed out via explicit `b.pqcAgent.create()` are unaffected. Returns `{ destroyed: boolean }`. (3) **`b.middleware.idempotencyKey.dbStore({ db, tableName?, init? })`** — persistent-backed store for the `idempotencyKey` middleware. Same three-method interface as `memoryStore` (`get` / `set` / `delete`) but stores records in any sqlite-shaped database (`{ prepare(sql) → { run, get, all } }`) — the framework's internal `b.db`, an operator-supplied better-sqlite3 instance, or a custom adapter. Use this instead of `memoryStore` when (a) multiple processes share the request-handling fleet so retries can land on a different process than the original, (b) the daemon may restart between the original request and the retry (graceful rolling deploy, OOM kill), or (c) audit / compliance review needs to walk historic idempotency-cache decisions queryable via `SELECT * FROM <tableName>`. TTL is lazily enforced at read time; `set()` upserts on conflict so concurrent retries on different processes don't error. The `tableName` is validated via `b.safeSql.validateIdentifier` (ASCII identifier shape, 63-char cap, no reserved words). (4) **Internal fix**: `b.apiSnapshot.read` now passes `maxBytes: 64 MiB` to `safeJson.parse` — the framework-generated snapshot file outgrew safeJson's 1 MiB default after v0.9.13. Operators consuming `@blamejs/core` via npm jump from v0.9.12 directly to v0.9.14; v0.9.13's content is included.
|
|
12
|
+
- v0.9.13 (2026-05-13) — **Two `b.circuitBreaker.create` / `b.retry` bug fixes plus three new operator-facing primitives: `b.selfUpdate.standaloneVerifier`, `b.metrics.snapshot`, `b.retry.withBreaker`**. (1) `b.circuitBreaker.create({...})` was rejecting the documented opts-object call shape with `name must be a non-empty string, got object` — every consumer following the docstring hit a hard error. The factory now reads `opts.name` (defaulting to empty string) and forwards to the internal `CircuitBreaker(name, opts)` constructor. (2) The breaker's open-circuit error code was documented as `retry/circuit-open` but the runtime threw `CIRCUIT_OPEN`. The docstring is corrected to match the runtime; an alias rename will follow with a deprecation cycle in a future minor. (3) **`b.selfUpdate.standaloneVerifier`** — zero-dep verifier for install-pipeline contexts that run BEFORE the framework is installed (Dockerfile build stages, `install.sh`, `update.sh`, SEA-bundle verification at deploy time). Surface: `verify(assetPath, sigPath, pubkeyPem, opts?)` returns `{ ok, sha3_512, sha256, alg }`. Streams the asset in 64 KiB chunks through SHA-256 + SHA-3-512 + the signature verifier in parallel — multi-GB SEA bundles don't OOM the install runner. Supports ECDSA P-384 (both IEEE-P1363 96-byte and DER encodings), Ed25519, and ML-DSA-87. Detects signature format from length so `verifier.verify(...)` runs exactly once (calling it twice returns stale state and silently passes tampered assets). Module is hermetic: `node:crypto` + `node:fs` only, no framework imports. Operators copy the file via `cp "$(node -p "require('@blamejs/core').selfUpdate.standaloneVerifier.path")" install/standalone-verifier.js` into version control on their side. (4) **`b.metrics.snapshot`** — out-of-process metrics export for long-running daemons. `startWriter({ path, intervalMs, fields })` atomically flushes a JSON snapshot (first flush synchronous so the file exists by return-time; subsequent flushes on the interval with `.unref()`; `stop()` clears + final-flushes). `read(path)` parses with shape validation (writtenAt + fields). `render(snap, { format, prefix })` produces operator-readable text or Prometheus 0.0.4 exposition (gauge metrics, prefixed; only finite numeric scalars with prom-compatible names emit; invalid-name / non-numeric / non-finite fields skipped silently). Lets a CLI process scrape a daemon's live metrics without opening an HTTP port. (5) **`b.retry.withBreaker(fn, { retry, breaker })`** — composition primitive collapsing the two-line wrapper every consumer rolls: `breaker.wrap(() => retry.withRetry(fn, opts.retry))`. One breaker call per retry loop (the retry budget is INSIDE the breaker's accounting, so a single transient burst doesn't open the breaker spuriously). Throws on non-function `fn` or breaker without `.wrap`.
|
|
13
|
+
- v0.9.12 (2026-05-13) — **Republish of v0.9.10 / v0.9.11 — `npm audit signatures` grep widened for the npm-message variant that fires post-v0.9.11**. The publish workflow's "Verify npm registry signing chain" step treats an empty-tree result as success (the framework's zero-runtime-deps posture means `npm audit signatures --omit dev` finds nothing to audit). The exact phrasing has drifted across npm versions: older npm prints `found no installed dependencies to audit`; newer npm (the version on the GH Actions runner post-v0.9.11) prints `found no dependencies to audit that were installed from a supported registry`. The shell guard's grep only matched the older phrasing, so v0.9.11's publish failed at the audit-signatures gate even though every other step succeeded. The grep is now `no (installed )?dependencies to audit` — covers both known empty-tree variants. v0.9.10's broken-smoke fix (added `npm install` before smoke) plus v0.9.12's audit-signatures-grep fix together complete the publish-pipeline repair. v0.9.12 is functionally identical to v0.9.10's intended surface. Operators stuck at v0.9.9 (because v0.9.10 + v0.9.11 never reached the npm registry) jump directly to v0.9.12.
|
|
14
|
+
- v0.9.11 (2026-05-13) — **Republish of v0.9.10 — npm-publish.yml now installs devDependencies before smoke**. v0.9.10's tag was pushed and the GitHub release published, but `npm-publish.yml`'s Framework-smoke step ran `node test/smoke.js` without first running `npm install`. The new bundler-output gate added in v0.9.10 requires `esbuild` (a devDependency) to be present, so the publish workflow failed at the smoke step before reaching the publish step — the v0.9.10 npm tarball was never published. The fix adds `npm install --no-audit --no-fund` to `.github/workflows/npm-publish.yml` directly before the smoke step (mirroring the same fix already present in `.github/workflows/ci.yml`). Operators consuming via `npm install @blamejs/core` should pull v0.9.11; functionally identical to the intended-v0.9.10 surface. Zero runtime deps invariant preserved.
|
|
15
|
+
- v0.9.10 (2026-05-13) — **Bundler-output e2e gate** — `test/layer-5-integration/bundler-output.test.js`. Bundles the framework via `esbuild --bundle --platform=node` (also `--minify`), runs the bundled consumer, and asserts the four-layer vendor-data integrity surface (dual-hash + SLH-DSA signature + canary) survives bundling. The PSL canary roundtrips through `b.publicSuffix.isPublicSuffix(...)` after bundle exec — proves the `.data.js` payloads physically reached the bundle bytes, not just the runtime require shape. Plus a byte-search sentinel that grep's the produced bundle for the canary tokens directly (defense-in-depth, independent failure mode from the runtime path). Plus a SEA gate (Linux + Node >= 22 only) that runs `--experimental-sea-config` + `postject` to produce an actual single-executable binary and runs it. The whole class of bugs — dynamic-require breaks bundling, SEA `assets` map missing, esbuild static-trace failures — is now smoke-gated. Had this test existed when v0.9.8 published, it would have refused that release at smoke-time (the v0.9.8 dynamic-require defect produced bundles that exited with `vendor-data/module-missing` on first vendor-data access; this gate's `BUNDLE-OK psl=co.uk entries=3` stdout-check refuses that exit). No framework-surface changes.
|
|
11
16
|
- v0.9.9 (2026-05-13) — **`b.vendorData`: replace dynamic `require(variable)` with static literal-string requires so SEA / esbuild / pkg bundling actually works**. v0.9.8 shipped `b.vendorData` to remove `__dirname`-relative `fs.readFileSync` calls and make the loader packaging-mode-invariant. The implementation looked up each `.data.js` module via `require(entry.module)` where `entry.module` was read from a frozen lookup table — a *dynamic* require, opaque to every bundler's static-analysis pass. esbuild, webpack, ncc, rollup, pkg, nexe, Bun's bundler, and Deno's bundler all trace `require("./literal")` calls; none of them trace `require(variable)`. Result: the three `.data.js` payload modules never made it into SEA / pkg / esbuild bundles, defeating the v0.9.8 promise at boot ("vendor-data/module-missing" thrown by every consumer that bundled the framework). v0.9.9 replaces the lookup with a `_MODULES` table whose three values are each a top-level `var X = require("./vendor/<name>.data")` — literal string, statically traceable. Caught by hermitstash-sync operator review post-v0.9.8 publish. Net surface change: zero (the public `b.vendorData.get` / `getAsString` / `verifyAll` / `inventory` shape is identical); the fix is internal-only. **New codebase-patterns drift detector** `testNoDynamicRequires` refuses any future `require(variable)` in `lib/`; legitimate operator-extensibility points (`b.cli`, migrations, seeders) carry an explicit `allow:dynamic-require` marker with rationale. **Operators upgrade from v0.9.8 to v0.9.9 if they bundle the framework via SEA / esbuild / pkg / Bun-compile** — direct `node` consumers were unaffected (Node's runtime require always resolves dynamic strings correctly).
|
|
12
17
|
- v0.9.8 (2026-05-13) — **`b.vendorData` — packaging-mode-invariant + signed + canary-guarded loader for vendored data files**. The three plaintext vendor data files (`public-suffix-list.dat`, `common-passwords-top-10000.txt`, `bimi-trust-anchors.pem`) are now loaded via inline `Buffer.from(base64)` modules (`<name>.data.js`), eliminating the `__dirname`-relative `fs.readFileSync` paths that broke under Single Executable Application (SEA), `pkg`, `nexe`, esbuild, Bun compile, Deno compile, and AWS Lambda layer bundling. Every load runs four orthogonal integrity checks before returning a byte: SHA-256 + SHA3-512 + SLH-DSA-SHAKE-256f signature against the maintainer's pinned public key (`lib/vendor/.vendor-data-pubkey`) + in-payload canary entry that the parsed structure must surface. Tamper at any layer throws `VendorDataError` at module-load — fail-fast rather than first-request-touches-PSL surprise. **Public API**: `b.vendorData.get(name)` returns the verified Buffer; `b.vendorData.getAsString(name)` returns UTF-8 string; `b.vendorData.verifyAll()` runs all four layers across every registered vendor data file and is invoked at framework boot; `b.vendorData.inventory()` returns per-file metadata (name, source, fetchedAt, sha256, sha3_512, signedBy, canary, byteLength, description) for compliance reporting + SBOM emission. **Migrated call sites**: `b.publicSuffix` (PSL load), `b.auth.password._loadBundledCommon` (common-passwords), `b.mail.bimi` (trust anchors) now route through `b.vendorData` — removes any downstream consumer's need to patch the loader for SEA / bundler builds. **Maintainer signing infrastructure**: vendor data files signed at refresh time by a maintainer-held SLH-DSA-SHAKE-256f keypair (private key stays in `.keys/` and is never committed; public key ships in `lib/vendor/.vendor-data-pubkey` in every npm tarball). Adds a fourth orthogonal trust root alongside SSH-signed release tags + SLSA L3 npm provenance + Sigstore-keyless SBOM signatures. **MANIFEST.json**: per-vendor-data entry gains `runtime_artifact` + `integrity_layers` + dual-file `hashes` (raw `.dat/.txt/.pem` + companion `.data.js`). **New scripts**: `scripts/vendor-data-keygen.js` (one-time keypair generation), `scripts/vendor-data-gen.js` (generator invoked by `scripts/vendor-update.sh --refresh-data`).
|
|
13
18
|
- v0.9.7 (2026-05-13) — **SECURITY.md: release-tag verification path documented + signed-tag invariant from v0.9.7+**. SECURITY.md gains a "Verifying release authenticity" section documenting how operators verify a release tag's authenticity independently of GitHub's UI. The maintainer Ed25519 SSH signing key fingerprint (`SHA256:5oF/XWhFpMde9TRfEX2GAHiApAq/MXOS4vti5zQbD7g`) is published alongside the public-key retrieval URL (`https://github.com/dotCooCoo.keys`) and a `git tag -v` recipe that bypasses the "Verified" badge. From v0.9.7 onward, every release tag is an annotated SSH-signed tag; the repository's `release-tags` ruleset's `required_signatures` rule refuses any unsigned or lightweight tag push at the server side. Earlier tags (v0.9.6 and prior) remain as lightweight commits and don't verify via `git tag -v`; they continue to verify via the SLSA L3 npm provenance + Sigstore-keyless SBOM signatures already attached to those releases (the `cosign verify-blob` recipe is in the same SECURITY.md section). No framework-surface changes; this release ships the documentation + invariant only.
|
package/lib/api-snapshot.js
CHANGED
|
@@ -241,7 +241,10 @@ function read(filePath) {
|
|
|
241
241
|
"read: cannot read " + filePath + ": " + ((e && e.message) || String(e)));
|
|
242
242
|
}
|
|
243
243
|
var parsed;
|
|
244
|
-
|
|
244
|
+
// api-snapshot is framework-generated, not operator-supplied; grow
|
|
245
|
+
// the safeJson cap so wide-surface releases don't hit the 1 MiB
|
|
246
|
+
// default. Cap at the safeJson absolute max (64 MiB).
|
|
247
|
+
try { parsed = safeJson.parse(raw, { maxBytes: 64 * 1024 * 1024 }); } // allow:raw-byte-literal — internal-file maxBytes ceiling
|
|
245
248
|
catch (e) {
|
|
246
249
|
throw new ApiSnapshotError("api-snapshot/bad-json",
|
|
247
250
|
"read: not valid JSON: " + ((e && e.message) || String(e)));
|
package/lib/audit.js
CHANGED
|
@@ -55,6 +55,7 @@ var cluster = require("./cluster");
|
|
|
55
55
|
var clusterStorage = require("./cluster-storage");
|
|
56
56
|
var { generateToken } = require("./crypto");
|
|
57
57
|
var cryptoField = require("./crypto-field");
|
|
58
|
+
var safeSql = require("./safe-sql");
|
|
58
59
|
var dbRoleContext = require("./db-role-context");
|
|
59
60
|
var handlers = require("./handlers");
|
|
60
61
|
var { boot } = require("./log");
|
|
@@ -1334,37 +1335,48 @@ function bindActor(actorId, opts) {
|
|
|
1334
1335
|
*/
|
|
1335
1336
|
function generateActorBindingTriggerSql(opts) {
|
|
1336
1337
|
opts = opts || {};
|
|
1337
|
-
var
|
|
1338
|
-
var
|
|
1339
|
-
var allowRoles
|
|
1340
|
-
var
|
|
1341
|
-
var
|
|
1338
|
+
var columnRaw = opts.column || "actorUserId";
|
|
1339
|
+
var tableNameRaw = opts.tableName || "_blamejs_audit_log";
|
|
1340
|
+
var allowRoles = Array.isArray(opts.allowRoles) ? opts.allowRoles : [];
|
|
1341
|
+
var fnNameRaw = "_blamejs_audit_actor_binding_check";
|
|
1342
|
+
var trigNameRaw = "_blamejs_audit_actor_binding_trig";
|
|
1343
|
+
// Quote-and-validate every identifier through safeSql.quoteIdentifier
|
|
1344
|
+
// so operator-supplied opts.column / opts.tableName / opts.roleMappingFn
|
|
1345
|
+
// can't reach raw concatenation. PostgreSQL + SQLite both use the
|
|
1346
|
+
// double-quote dialect.
|
|
1347
|
+
var qColumn = safeSql.quoteIdentifier(columnRaw, "postgres");
|
|
1348
|
+
var qTable = safeSql.quoteIdentifier(tableNameRaw, "postgres");
|
|
1349
|
+
var qFn = safeSql.quoteIdentifier(fnNameRaw, "postgres");
|
|
1350
|
+
var qTrig = safeSql.quoteIdentifier(trigNameRaw, "postgres");
|
|
1351
|
+
var qRoleMapFn = opts.roleMappingFn
|
|
1352
|
+
? safeSql.quoteIdentifier(opts.roleMappingFn, "postgres")
|
|
1353
|
+
: null;
|
|
1342
1354
|
var allowList = allowRoles.length === 0 ? "" :
|
|
1343
1355
|
" IF current_user IN (" +
|
|
1344
1356
|
allowRoles.map(function (r) { return "'" + r.replace(/'/g, "''") + "'"; }).join(", ") +
|
|
1345
1357
|
") THEN RETURN NEW; END IF;\n";
|
|
1346
|
-
var roleMatch =
|
|
1347
|
-
? " IF " +
|
|
1348
|
-
: " IF NEW
|
|
1358
|
+
var roleMatch = qRoleMapFn
|
|
1359
|
+
? " IF " + qRoleMapFn + "(NEW." + qColumn + ") IS DISTINCT FROM current_user THEN\n"
|
|
1360
|
+
: " IF NEW." + qColumn + " IS DISTINCT FROM current_user THEN\n";
|
|
1349
1361
|
var up =
|
|
1350
|
-
"CREATE OR REPLACE FUNCTION " +
|
|
1362
|
+
"CREATE OR REPLACE FUNCTION " + qFn + "() RETURNS trigger AS $$\n" +
|
|
1351
1363
|
"BEGIN\n" +
|
|
1352
1364
|
allowList +
|
|
1353
1365
|
roleMatch +
|
|
1354
|
-
" RAISE EXCEPTION 'segregation-of-duties violation: actor=% does not match current_user=%', NEW
|
|
1366
|
+
" RAISE EXCEPTION 'segregation-of-duties violation: actor=% does not match current_user=%', NEW." + qColumn + ", current_user\n" +
|
|
1355
1367
|
" USING ERRCODE = 'P0001';\n" +
|
|
1356
1368
|
" END IF;\n" +
|
|
1357
1369
|
" RETURN NEW;\n" +
|
|
1358
1370
|
"END;\n" +
|
|
1359
1371
|
"$$ LANGUAGE plpgsql;\n" +
|
|
1360
|
-
"DROP TRIGGER IF EXISTS " +
|
|
1361
|
-
"CREATE TRIGGER " +
|
|
1362
|
-
" BEFORE INSERT ON " +
|
|
1363
|
-
" FOR EACH ROW EXECUTE FUNCTION " +
|
|
1372
|
+
"DROP TRIGGER IF EXISTS " + qTrig + " ON " + qTable + ";\n" +
|
|
1373
|
+
"CREATE TRIGGER " + qTrig + "\n" +
|
|
1374
|
+
" BEFORE INSERT ON " + qTable + "\n" +
|
|
1375
|
+
" FOR EACH ROW EXECUTE FUNCTION " + qFn + "();\n";
|
|
1364
1376
|
var down =
|
|
1365
|
-
"DROP TRIGGER IF EXISTS " +
|
|
1366
|
-
"DROP FUNCTION IF EXISTS " +
|
|
1367
|
-
return { up: up, down: down, functionName:
|
|
1377
|
+
"DROP TRIGGER IF EXISTS " + qTrig + " ON " + qTable + ";\n" +
|
|
1378
|
+
"DROP FUNCTION IF EXISTS " + qFn + "();\n";
|
|
1379
|
+
return { up: up, down: down, functionName: fnNameRaw, triggerName: trigNameRaw };
|
|
1368
1380
|
}
|
|
1369
1381
|
|
|
1370
1382
|
// Boot-time check operators wire under sox-404 / soc2 posture. Verifies
|
package/lib/circuit-breaker.js
CHANGED
|
@@ -34,11 +34,18 @@ var retry = require("./retry");
|
|
|
34
34
|
* @related b.retry, b.httpClient
|
|
35
35
|
*
|
|
36
36
|
* Build a circuit-breaker. Returns a CircuitBreaker instance with
|
|
37
|
-
* `wrap(fn)` (executes `fn` if the breaker is closed; throws
|
|
38
|
-
* with `code: "
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
37
|
+
* `wrap(fn)` (executes `fn` if the breaker is closed; throws an
|
|
38
|
+
* `Error` with `code: "CIRCUIT_OPEN"` + `isObjectStoreError: true` +
|
|
39
|
+
* `permanent: false` when open), `state()`, `reset()`, and
|
|
40
|
+
* `onStateChange(handler)` listener registration. Pass-through
|
|
41
|
+
* factory: identical instance shape to `b.retry.CircuitBreaker`,
|
|
42
|
+
* with the framework's `create(opts)` vocabulary.
|
|
43
|
+
*
|
|
44
|
+
* The `CIRCUIT_OPEN` error code is a pre-v1 artifact — every other
|
|
45
|
+
* framework error class uses namespaced codes (`retry/...`). The
|
|
46
|
+
* rename is deferred to v0.10 with a deprecation cycle so existing
|
|
47
|
+
* operators who match `err.code === "CIRCUIT_OPEN"` aren't broken
|
|
48
|
+
* in a patch.
|
|
42
49
|
*
|
|
43
50
|
* @opts
|
|
44
51
|
* name: string, // identifier used in audit + state-change events
|
|
@@ -65,7 +72,15 @@ var retry = require("./retry");
|
|
|
65
72
|
* result.value; // → 42
|
|
66
73
|
*/
|
|
67
74
|
function create(opts) {
|
|
68
|
-
|
|
75
|
+
// The CircuitBreaker class constructor is `(name, opts)` — passing a
|
|
76
|
+
// single opts object lands it in the positional `name` slot, and the
|
|
77
|
+
// validator throws "name must be a non-empty string, got object."
|
|
78
|
+
// The factory's documented shape is `create({ name, ...opts })`;
|
|
79
|
+
// split the name out of opts before invoking the constructor.
|
|
80
|
+
// Caught by hermitstash-sync operator review against v0.9.12.
|
|
81
|
+
opts = opts || {};
|
|
82
|
+
var name = (opts && typeof opts.name === "string") ? opts.name : "";
|
|
83
|
+
return new retry.CircuitBreaker(name, opts);
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
module.exports = {
|
package/lib/crypto.js
CHANGED
|
@@ -147,6 +147,150 @@ function hashFile(filePath, algorithm) {
|
|
|
147
147
|
return hashStream(nodeFs.createReadStream(filePath), algorithm);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
// _hashFileMulti — single-pass stream of a file through N hashers in
|
|
151
|
+
// parallel. Returns { path, byteLength, <alg>: hex } for every
|
|
152
|
+
// `algorithms` entry. Used by hashFilesParallel below; not exported
|
|
153
|
+
// directly because the common case is the parallel-many shape.
|
|
154
|
+
function _hashFileMulti(filePath, algorithms) {
|
|
155
|
+
return new Promise(function (resolve, reject) {
|
|
156
|
+
var hashers = new Array(algorithms.length);
|
|
157
|
+
for (var i = 0; i < algorithms.length; i += 1) {
|
|
158
|
+
try { hashers[i] = nodeCrypto.createHash(algorithms[i]); }
|
|
159
|
+
catch (e) {
|
|
160
|
+
reject(new Error("crypto.hashFilesParallel: unknown algorithm '" +
|
|
161
|
+
algorithms[i] + "': " + (e && e.message ? e.message : String(e))));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
var byteLength = 0;
|
|
166
|
+
var stream = nodeFs.createReadStream(filePath);
|
|
167
|
+
stream.on("error", reject);
|
|
168
|
+
stream.on("data", function (chunk) {
|
|
169
|
+
byteLength += chunk.length;
|
|
170
|
+
for (var j = 0; j < hashers.length; j += 1) hashers[j].update(chunk);
|
|
171
|
+
});
|
|
172
|
+
stream.on("end", function () {
|
|
173
|
+
var out = { path: filePath, byteLength: byteLength };
|
|
174
|
+
for (var k = 0; k < hashers.length; k += 1) {
|
|
175
|
+
// Field name = algorithm with `-` → `_` so "sha3-512" surfaces
|
|
176
|
+
// as `out.sha3_512` (matches the standalone-verifier shape).
|
|
177
|
+
out[algorithms[k].replace(/-/g, "_")] = hashers[k].digest("hex");
|
|
178
|
+
}
|
|
179
|
+
resolve(out);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @primitive b.crypto.hashFilesParallel
|
|
186
|
+
* @signature b.crypto.hashFilesParallel(filePaths, opts?)
|
|
187
|
+
* @since 0.9.14
|
|
188
|
+
* @status stable
|
|
189
|
+
* @related b.crypto.hashFile, b.crypto.hashStream
|
|
190
|
+
*
|
|
191
|
+
* Hash many files in parallel, streaming each one through one or
|
|
192
|
+
* more digest algorithms in a single read pass. Returns an array of
|
|
193
|
+
* `{ path, byteLength, sha256, sha3_512, ... }` records in the same
|
|
194
|
+
* order as `filePaths`. Concurrency is operator-tunable; the default
|
|
195
|
+
* (`min(8, filePaths.length)`) matches the framework's
|
|
196
|
+
* hash-while-streaming convention elsewhere without saturating the
|
|
197
|
+
* fs read queue on spinning-disk hosts.
|
|
198
|
+
*
|
|
199
|
+
* The common consumer-side reason to reach for this primitive is
|
|
200
|
+
* SBOM regeneration / vendor-data integrity sweeps / release-asset
|
|
201
|
+
* bundling — situations where N files each need both SHA-256 (legacy
|
|
202
|
+
* compat) and SHA-3-512 (PQC-first) digests and rolling a worker
|
|
203
|
+
* pool by hand has cost a downstream consumer (`hermitstash-sync`
|
|
204
|
+
* 2026-05-13) the same two-loop, capture-N-promises, settle-Q boilerplate
|
|
205
|
+
* every release.
|
|
206
|
+
*
|
|
207
|
+
* @opts
|
|
208
|
+
* algorithms?: string[], // default ["sha256", "sha3-512"]; any node:crypto-known digest
|
|
209
|
+
* concurrency?: number, // default min(8, filePaths.length); 1..256
|
|
210
|
+
* onProgress?: function (completed, total) // best-effort; thrown errors swallowed
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* var rows = await b.crypto.hashFilesParallel(
|
|
214
|
+
* ["/var/lib/blamejs/asset-a.bin",
|
|
215
|
+
* "/var/lib/blamejs/asset-b.bin"],
|
|
216
|
+
* { algorithms: ["sha256", "sha3-512"], concurrency: 4 }
|
|
217
|
+
* );
|
|
218
|
+
* // rows[0] → { path: "...asset-a.bin", byteLength: 4096,
|
|
219
|
+
* // sha256: "...", sha3_512: "..." }
|
|
220
|
+
*/
|
|
221
|
+
function hashFilesParallel(filePaths, opts) {
|
|
222
|
+
if (!Array.isArray(filePaths)) {
|
|
223
|
+
return Promise.reject(new TypeError(
|
|
224
|
+
"crypto.hashFilesParallel: filePaths must be an array of non-empty strings"
|
|
225
|
+
));
|
|
226
|
+
}
|
|
227
|
+
for (var i = 0; i < filePaths.length; i += 1) {
|
|
228
|
+
if (typeof filePaths[i] !== "string" || filePaths[i].length === 0) {
|
|
229
|
+
return Promise.reject(new TypeError(
|
|
230
|
+
"crypto.hashFilesParallel: filePaths[" + i + "] must be a non-empty string"
|
|
231
|
+
));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
opts = opts || {};
|
|
235
|
+
var algorithms = opts.algorithms !== undefined
|
|
236
|
+
? opts.algorithms : ["sha256", "sha3-512"];
|
|
237
|
+
if (!Array.isArray(algorithms) || algorithms.length === 0) {
|
|
238
|
+
return Promise.reject(new TypeError(
|
|
239
|
+
"crypto.hashFilesParallel: opts.algorithms must be a non-empty array"
|
|
240
|
+
));
|
|
241
|
+
}
|
|
242
|
+
for (var ai = 0; ai < algorithms.length; ai += 1) {
|
|
243
|
+
if (typeof algorithms[ai] !== "string" || algorithms[ai].length === 0) {
|
|
244
|
+
return Promise.reject(new TypeError(
|
|
245
|
+
"crypto.hashFilesParallel: opts.algorithms[" + ai + "] must be a non-empty string"
|
|
246
|
+
));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
var concurrency = opts.concurrency !== undefined
|
|
250
|
+
? opts.concurrency
|
|
251
|
+
: Math.min(8, Math.max(1, filePaths.length)); // allow:raw-byte-literal — worker fan-out cap, not bytes
|
|
252
|
+
if (typeof concurrency !== "number" || !isFinite(concurrency) ||
|
|
253
|
+
concurrency < 1 || concurrency > 256 || // allow:raw-byte-literal — concurrency upper cap
|
|
254
|
+
Math.floor(concurrency) !== concurrency) {
|
|
255
|
+
return Promise.reject(new TypeError(
|
|
256
|
+
"crypto.hashFilesParallel: opts.concurrency must be an integer in [1, 256], got " + concurrency
|
|
257
|
+
));
|
|
258
|
+
}
|
|
259
|
+
var onProgress = opts.onProgress;
|
|
260
|
+
if (onProgress !== undefined && typeof onProgress !== "function") {
|
|
261
|
+
return Promise.reject(new TypeError(
|
|
262
|
+
"crypto.hashFilesParallel: opts.onProgress must be a function when supplied"
|
|
263
|
+
));
|
|
264
|
+
}
|
|
265
|
+
if (filePaths.length === 0) return Promise.resolve([]);
|
|
266
|
+
|
|
267
|
+
var results = new Array(filePaths.length);
|
|
268
|
+
var nextIdx = 0;
|
|
269
|
+
var completed = 0;
|
|
270
|
+
var total = filePaths.length;
|
|
271
|
+
function _worker() {
|
|
272
|
+
function _step() {
|
|
273
|
+
var idx = nextIdx;
|
|
274
|
+
nextIdx += 1;
|
|
275
|
+
if (idx >= total) return Promise.resolve();
|
|
276
|
+
return _hashFileMulti(filePaths[idx], algorithms).then(function (rec) {
|
|
277
|
+
results[idx] = rec;
|
|
278
|
+
completed += 1;
|
|
279
|
+
if (onProgress) {
|
|
280
|
+
try { onProgress(completed, total); }
|
|
281
|
+
catch (_e) { /* progress callback errors are not fatal */ }
|
|
282
|
+
}
|
|
283
|
+
return _step();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return _step();
|
|
287
|
+
}
|
|
288
|
+
var workerCount = Math.min(concurrency, total);
|
|
289
|
+
var workers = new Array(workerCount);
|
|
290
|
+
for (var w = 0; w < workerCount; w += 1) workers[w] = _worker();
|
|
291
|
+
return Promise.all(workers).then(function () { return results; });
|
|
292
|
+
}
|
|
293
|
+
|
|
150
294
|
function random(byteLength) {
|
|
151
295
|
var n = byteLength || 32;
|
|
152
296
|
// SHAKE256 over OS-RNG bytes. The OS RNG (nodeCrypto.randomBytes) is
|
|
@@ -1374,6 +1518,7 @@ module.exports = {
|
|
|
1374
1518
|
sha3Hash: sha3Hash,
|
|
1375
1519
|
hmacSha3: hmacSha3,
|
|
1376
1520
|
hashFile: hashFile,
|
|
1521
|
+
hashFilesParallel: hashFilesParallel,
|
|
1377
1522
|
hashStream: hashStream,
|
|
1378
1523
|
namespaceHash: namespaceHash,
|
|
1379
1524
|
kdf: kdf,
|
package/lib/dsr.js
CHANGED
|
@@ -113,6 +113,7 @@ var C = require("./constants");
|
|
|
113
113
|
var bCrypto = require("./crypto");
|
|
114
114
|
var lazyRequire = require("./lazy-require");
|
|
115
115
|
var validateOpts = require("./validate-opts");
|
|
116
|
+
var safeSql = require("./safe-sql");
|
|
116
117
|
var { defineClass } = require("./framework-error");
|
|
117
118
|
|
|
118
119
|
var DsrError = defineClass("DsrError", { alwaysPermanent: true });
|
|
@@ -939,15 +940,21 @@ function dbTicketStore(opts) {
|
|
|
939
940
|
throw new DsrError("dsr/bad-db",
|
|
940
941
|
"dbTicketStore: opts.db must be a b.db-shaped handle (with runSql + prepare)");
|
|
941
942
|
}
|
|
942
|
-
var
|
|
943
|
-
|
|
943
|
+
var tableRaw = opts.table || "dsr_tickets";
|
|
944
|
+
var qTable, qEmailIdx, qStatusIdx;
|
|
945
|
+
try {
|
|
946
|
+
qTable = safeSql.quoteIdentifier(tableRaw, "sqlite");
|
|
947
|
+
qEmailIdx = safeSql.quoteIdentifier(tableRaw + "_email_idx", "sqlite");
|
|
948
|
+
qStatusIdx = safeSql.quoteIdentifier(tableRaw + "_status_idx", "sqlite");
|
|
949
|
+
} catch (sqlErr) {
|
|
944
950
|
throw new DsrError("dsr/bad-table",
|
|
945
|
-
"dbTicketStore: table must be a SQL identifier
|
|
951
|
+
"dbTicketStore: table must be a valid SQL identifier: " +
|
|
952
|
+
(sqlErr && sqlErr.message ? sqlErr.message : String(sqlErr)));
|
|
946
953
|
}
|
|
947
954
|
|
|
948
955
|
// Auto-provision schema if not already present. Idempotent.
|
|
949
956
|
function ensureSchema() {
|
|
950
|
-
db.runSql("CREATE TABLE IF NOT EXISTS " +
|
|
957
|
+
db.runSql("CREATE TABLE IF NOT EXISTS " + qTable + " (" +
|
|
951
958
|
"id TEXT PRIMARY KEY, " +
|
|
952
959
|
"type TEXT NOT NULL, " +
|
|
953
960
|
"status TEXT NOT NULL, " +
|
|
@@ -961,16 +968,16 @@ function dbTicketStore(opts) {
|
|
|
961
968
|
"posture TEXT, " +
|
|
962
969
|
"payload TEXT NOT NULL" +
|
|
963
970
|
")");
|
|
964
|
-
db.runSql("CREATE INDEX IF NOT EXISTS " +
|
|
965
|
-
|
|
966
|
-
db.runSql("CREATE INDEX IF NOT EXISTS " +
|
|
967
|
-
|
|
971
|
+
db.runSql("CREATE INDEX IF NOT EXISTS " + qEmailIdx + " ON " +
|
|
972
|
+
qTable + " (subject_email)");
|
|
973
|
+
db.runSql("CREATE INDEX IF NOT EXISTS " + qStatusIdx + " ON " +
|
|
974
|
+
qTable + " (status)");
|
|
968
975
|
}
|
|
969
976
|
ensureSchema();
|
|
970
977
|
|
|
971
978
|
return {
|
|
972
979
|
insert: async function (ticket) {
|
|
973
|
-
var stmt = db.prepare("INSERT INTO " +
|
|
980
|
+
var stmt = db.prepare("INSERT INTO " + qTable +
|
|
974
981
|
" (id, type, status, subject_id, subject_email, subject_phone, " +
|
|
975
982
|
" submitted_at, deadline_at, processed_at, verification_level, posture, payload) " +
|
|
976
983
|
" VALUES ($id, $type, $status, $sid, $email, $phone, $submittedAt, " +
|
|
@@ -991,14 +998,14 @@ function dbTicketStore(opts) {
|
|
|
991
998
|
});
|
|
992
999
|
},
|
|
993
1000
|
get: async function (id) {
|
|
994
|
-
var rows = db.prepare("SELECT payload FROM " +
|
|
1001
|
+
var rows = db.prepare("SELECT payload FROM " + qTable + " WHERE id = $id")
|
|
995
1002
|
.all({ $id: id });
|
|
996
1003
|
if (!rows || rows.length === 0) return null;
|
|
997
1004
|
return JSON.parse(rows[0].payload); // allow:bare-json-parse — payload was JSON.stringify-ed by this same store, never from operator/network input
|
|
998
1005
|
},
|
|
999
1006
|
list: async function (filter) {
|
|
1000
1007
|
filter = filter || {};
|
|
1001
|
-
var sql = "SELECT payload FROM " +
|
|
1008
|
+
var sql = "SELECT payload FROM " + qTable;
|
|
1002
1009
|
var conds = [];
|
|
1003
1010
|
var params = {};
|
|
1004
1011
|
if (filter.status) {
|
|
@@ -1021,7 +1028,7 @@ function dbTicketStore(opts) {
|
|
|
1021
1028
|
return rows.map(function (r) { return JSON.parse(r.payload); }); // allow:bare-json-parse — payload was JSON.stringify-ed by this same store, never from operator/network input
|
|
1022
1029
|
},
|
|
1023
1030
|
update: async function (id, ticket) {
|
|
1024
|
-
var stmt = db.prepare("UPDATE " +
|
|
1031
|
+
var stmt = db.prepare("UPDATE " + qTable + " SET " +
|
|
1025
1032
|
" type = $type, status = $status, subject_id = $sid, " +
|
|
1026
1033
|
" subject_email = $email, subject_phone = $phone, " +
|
|
1027
1034
|
" submitted_at = $submittedAt, deadline_at = $deadlineAt, " +
|
|
@@ -1051,10 +1058,10 @@ function dbTicketStore(opts) {
|
|
|
1051
1058
|
// Bulk-delete tickets in terminal states whose retentionUntil
|
|
1052
1059
|
// is in the past. Returns the number of rows removed.
|
|
1053
1060
|
var asOf = (typeof asOfMs === "number" && isFinite(asOfMs)) ? asOfMs : Date.now();
|
|
1054
|
-
var rows = db.prepare("SELECT id, payload FROM " +
|
|
1061
|
+
var rows = db.prepare("SELECT id, payload FROM " + qTable +
|
|
1055
1062
|
" WHERE status IN ('completed','partially_completed','cancelled','rejected','expired')").all({});
|
|
1056
1063
|
var purged = 0;
|
|
1057
|
-
var del = db.prepare("DELETE FROM " +
|
|
1064
|
+
var del = db.prepare("DELETE FROM " + qTable + " WHERE id = $id");
|
|
1058
1065
|
for (var i = 0; i < rows.length; i++) {
|
|
1059
1066
|
try {
|
|
1060
1067
|
var t = JSON.parse(rows[i].payload); // allow:bare-json-parse — payload was JSON.stringify-ed by this same store, never from operator/network input
|
|
@@ -1066,7 +1073,7 @@ function dbTicketStore(opts) {
|
|
|
1066
1073
|
}
|
|
1067
1074
|
return purged;
|
|
1068
1075
|
},
|
|
1069
|
-
_table:
|
|
1076
|
+
_table: tableRaw,
|
|
1070
1077
|
_ensureSchema: ensureSchema,
|
|
1071
1078
|
};
|
|
1072
1079
|
}
|
package/lib/inbox.js
CHANGED
|
@@ -143,7 +143,13 @@ function create(opts) {
|
|
|
143
143
|
_validateTableName(opts.table);
|
|
144
144
|
|
|
145
145
|
var externalDb = opts.externalDb;
|
|
146
|
-
var
|
|
146
|
+
var tableRaw = opts.table;
|
|
147
|
+
// Identifiers reach SQL through safeSql.quoteIdentifier — runs
|
|
148
|
+
// validateIdentifier internally + emits the dialect-correct quoted
|
|
149
|
+
// form. sqlite + postgres both use the double-quote dialect (per
|
|
150
|
+
// lib/safe-sql.js), so one quoted form serves both inbox paths.
|
|
151
|
+
var qTable = safeSql.quoteIdentifier(tableRaw, "sqlite");
|
|
152
|
+
var qIndex = safeSql.quoteIdentifier(tableRaw + "_received_at_idx", "sqlite");
|
|
147
153
|
var retentionDays = (typeof opts.retentionDays === "number" && opts.retentionDays > 0) // allow:numeric-opt-Infinity
|
|
148
154
|
? opts.retentionDays : 30; // allow:raw-byte-literal — default retention days
|
|
149
155
|
var auditOn = opts.audit !== false;
|
|
@@ -226,7 +232,7 @@ function create(opts) {
|
|
|
226
232
|
|
|
227
233
|
if (dialect === "postgres") {
|
|
228
234
|
var rs = await txn.query(
|
|
229
|
-
"INSERT INTO " +
|
|
235
|
+
"INSERT INTO " + qTable +
|
|
230
236
|
" (message_id, source, received_at, metadata_json) " +
|
|
231
237
|
" VALUES ($1, $2, " + nowExpr + ", $3::jsonb) " +
|
|
232
238
|
" ON CONFLICT (source, message_id) DO NOTHING " +
|
|
@@ -248,7 +254,7 @@ function create(opts) {
|
|
|
248
254
|
// that the framework can't prevent. RETURNING 1 collapses both
|
|
249
255
|
// round-trips into one and removes the changes() dependency.
|
|
250
256
|
var sqlInsert = await txn.query(
|
|
251
|
-
"INSERT OR IGNORE INTO " +
|
|
257
|
+
"INSERT OR IGNORE INTO " + qTable +
|
|
252
258
|
" (message_id, source, received_at, metadata_json) " +
|
|
253
259
|
" VALUES (?, ?, " + nowExpr + ", ?) RETURNING 1",
|
|
254
260
|
[receiveOpts.messageId, receiveOpts.source, metaJson]);
|
|
@@ -268,7 +274,7 @@ function create(opts) {
|
|
|
268
274
|
_validateReceiveOpts(receiveOpts, "markProcessed");
|
|
269
275
|
var nowExpr = _utcNowExpr(externalDb);
|
|
270
276
|
var dialect = (externalDb.dialect === "postgres") ? "postgres" : "sqlite";
|
|
271
|
-
var sql = "UPDATE " +
|
|
277
|
+
var sql = "UPDATE " + qTable +
|
|
272
278
|
" SET processed_at = " + nowExpr +
|
|
273
279
|
" WHERE source = " + (dialect === "postgres" ? "$1" : "?") +
|
|
274
280
|
" AND message_id = " + (dialect === "postgres" ? "$2" : "?");
|
|
@@ -318,7 +324,7 @@ function create(opts) {
|
|
|
318
324
|
var dialect = (xdb && xdb.dialect === "postgres") ? "postgres" : "sqlite";
|
|
319
325
|
if (dialect === "postgres") {
|
|
320
326
|
await xdb.query(
|
|
321
|
-
"CREATE TABLE IF NOT EXISTS " +
|
|
327
|
+
"CREATE TABLE IF NOT EXISTS " + qTable + " (" +
|
|
322
328
|
" message_id TEXT NOT NULL," +
|
|
323
329
|
" source TEXT NOT NULL," +
|
|
324
330
|
" received_at TIMESTAMPTZ NOT NULL DEFAULT NOW()," +
|
|
@@ -327,11 +333,11 @@ function create(opts) {
|
|
|
327
333
|
" PRIMARY KEY (source, message_id)" +
|
|
328
334
|
")");
|
|
329
335
|
await xdb.query(
|
|
330
|
-
"CREATE INDEX IF NOT EXISTS " +
|
|
331
|
-
"ON " +
|
|
336
|
+
"CREATE INDEX IF NOT EXISTS " + qIndex + " " +
|
|
337
|
+
"ON " + qTable + " (received_at)");
|
|
332
338
|
} else {
|
|
333
339
|
await xdb.query(
|
|
334
|
-
"CREATE TABLE IF NOT EXISTS " +
|
|
340
|
+
"CREATE TABLE IF NOT EXISTS " + qTable + " (" +
|
|
335
341
|
" message_id TEXT NOT NULL," +
|
|
336
342
|
" source TEXT NOT NULL," +
|
|
337
343
|
" received_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP," +
|
|
@@ -340,8 +346,8 @@ function create(opts) {
|
|
|
340
346
|
" PRIMARY KEY (source, message_id)" +
|
|
341
347
|
")");
|
|
342
348
|
await xdb.query(
|
|
343
|
-
"CREATE INDEX IF NOT EXISTS " +
|
|
344
|
-
"ON " +
|
|
349
|
+
"CREATE INDEX IF NOT EXISTS " + qIndex + " " +
|
|
350
|
+
"ON " + qTable + " (received_at)");
|
|
345
351
|
}
|
|
346
352
|
}
|
|
347
353
|
|
|
@@ -351,7 +357,7 @@ function create(opts) {
|
|
|
351
357
|
await externalDb.transaction(async function (xdb) {
|
|
352
358
|
if (dialect === "postgres") {
|
|
353
359
|
var rs = await xdb.query(
|
|
354
|
-
"DELETE FROM " +
|
|
360
|
+
"DELETE FROM " + qTable +
|
|
355
361
|
" WHERE received_at < NOW() - $1::interval " +
|
|
356
362
|
" AND (processed_at IS NOT NULL OR received_at < NOW() - $2::interval)",
|
|
357
363
|
[retentionDays + " days", (retentionDays * 2) + " days"]);
|
|
@@ -360,7 +366,7 @@ function create(opts) {
|
|
|
360
366
|
var staleDate = new Date(Date.now() - retentionDays * C.TIME.days(1)).toISOString();
|
|
361
367
|
var unprocStaleDate = new Date(Date.now() - retentionDays * 2 * C.TIME.days(1)).toISOString();
|
|
362
368
|
await xdb.query(
|
|
363
|
-
"DELETE FROM " +
|
|
369
|
+
"DELETE FROM " + qTable +
|
|
364
370
|
" WHERE received_at < ? " +
|
|
365
371
|
" AND (processed_at IS NOT NULL OR received_at < ?)",
|
|
366
372
|
[staleDate, unprocStaleDate]);
|
|
@@ -378,7 +384,7 @@ function create(opts) {
|
|
|
378
384
|
async function isFresh(receiveOpts) {
|
|
379
385
|
_validateReceiveOpts(receiveOpts, "isFresh");
|
|
380
386
|
var dialect = (externalDb.dialect === "postgres") ? "postgres" : "sqlite";
|
|
381
|
-
var sql = "SELECT 1 FROM " +
|
|
387
|
+
var sql = "SELECT 1 FROM " + qTable +
|
|
382
388
|
" WHERE source = " + (dialect === "postgres" ? "$1" : "?") +
|
|
383
389
|
" AND message_id = " + (dialect === "postgres" ? "$2" : "?");
|
|
384
390
|
var rs = await externalDb.transaction(async function (xdb) {
|
|
@@ -395,7 +401,7 @@ function create(opts) {
|
|
|
395
401
|
var stats = await externalDb.transaction(async function (xdb) {
|
|
396
402
|
var sql = "SELECT COUNT(*) AS total," +
|
|
397
403
|
" COUNT(processed_at) AS processed " +
|
|
398
|
-
" FROM " +
|
|
404
|
+
" FROM " + qTable +
|
|
399
405
|
(sourceFilter ? " WHERE source = " +
|
|
400
406
|
(dialect === "postgres" ? "$1" : "?") : "");
|
|
401
407
|
var args = sourceFilter ? [sourceFilter] : [];
|
|
@@ -418,7 +424,7 @@ function create(opts) {
|
|
|
418
424
|
sweep: sweep,
|
|
419
425
|
isFresh: isFresh,
|
|
420
426
|
getStats: getReceiveStats,
|
|
421
|
-
table:
|
|
427
|
+
table: tableRaw,
|
|
422
428
|
retentionDays: retentionDays,
|
|
423
429
|
};
|
|
424
430
|
}
|