@blamejs/core 0.6.20 → 0.6.22
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 +2 -0
- package/README.md +18 -10
- package/lib/archive.js +8 -7
- package/lib/bundler.js +8 -8
- package/lib/cache.js +105 -20
- package/lib/db-query.js +21 -2
- package/lib/db.js +15 -0
- package/lib/framework-schema.js +38 -6
- package/lib/http-client-cookie-jar.js +117 -17
- package/lib/http-client.js +4 -3
- package/lib/mail.js +5 -4
- package/lib/network-dns.js +138 -43
- package/lib/pagination.js +136 -76
- package/lib/parsers/index.js +16 -2
- package/lib/parsers/safe-ini.js +273 -0
- package/lib/queue-local.js +12 -1
- package/lib/vault/index.js +3 -3
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.6.x
|
|
10
10
|
|
|
11
|
+
- **0.6.22** (2026-05-02) — `b.queue.enqueue({ availableAt })` is now honoured. Previously the local-protocol enqueue() ignored opts.availableAt entirely, recomputing from `Date.now() + delaySeconds*1000`. The cron-repeat path passes both fields (the exact next-fire ms in availableAt + the floored seconds in delaySeconds), and the enqueue's recomputation lost sub-second precision plus drifted on the internal clock-vs-caller delta. Symptoms: cron-scheduled jobs landed up to 999ms off the intended boundary; the queue-flow-repeat smoke test was intermittently flaky on slow CI runners (caught by ubuntu-latest on the v0.6.21 commit). Fix: enqueue() honours opts.availableAt directly when finite; falls back to delaySeconds-based shorthand otherwise. Operators relying on `enqueue({ availableAt: T })` for non-cron scheduled jobs (e.g. "deliver this notification at exactly 09:00 tomorrow") now get the requested time instead of nowMs+0.
|
|
12
|
+
- **0.6.21** (2026-05-02) — closes the medium / low audit findings flagged after v0.6.18. **Cluster cache `invalidateTag`** now actually works — the cluster backend gained a `_blamejs_cache_tags` junction table (`(cacheKey, tag)` PK + index on `tag`), tag-aware `set` / `del` / `clear` / `_sweep`, plus `getTags(key)`. The old NOT_SUPPORTED-on-cluster path is gone; multi-tag rotation, mid-flight tag replacement on update, and namespace-scoped sweeps all covered. **Multi-column cursor pagination** — `b.pagination.cursor({ orderBy: [{column,direction},...] })` accepts a string (single column), an array of strings (multi, all using `opts.direction`), or an array of `{column,direction}` objects (mixed directions). The keyset WHERE expands to the standard OR cascade so successive pages can't skip or repeat rows when ties on the leading columns are broken by trailing ones. `_id` is appended as a tiebreaker if not in the chain. The Query class also gained chained `orderBy(col, dir)` calls — second-and-later calls extend a multi-column ORDER BY in the SQL. Cursor format is bumped to encode `{ orderKey, vals, forward }` instead of the old `{ orderBy, dir, orderByVal, id, forward }` — pre-1.0 break, no compat shim. **DoH POST mode** (RFC 8484 §4.1) — `b.network.dns.useDnsOverHttps({ method })` accepts `"GET" | "POST" | undefined` (auto). Auto switches to POST when the GET URL would exceed 2048 bytes (long DNS names). **DoT connection pooling** — per-`(host:port)` cached TLS socket with a 2-minute idle timeout, serialized in-flight queries per socket. Eliminates the per-query handshake. **INI parser** shipped as `b.parsers.ini` — covers Windows .ini / .gitconfig / systemd-unit / php.ini / tox.ini shape: sections (incl. `[parent.child]` / `[parent "child"]` nesting), `;` and `#` comments (inline + leading), single + double quoting with `\n` `\t` `\\` `\"` `\'` escapes, boolean coercion (`true`/`false`/`yes`/`no`/`on`/`off`), decimal + hex integers + floats. Prototype-pollution defense (`__proto__` / `constructor` / `prototype` rejected). Duplicate-key policy throws by default; `onDuplicate: "first" | "last"` opts in to silent shadowing. Section / per-section key / value-bytes caps configurable. **Cookie-jar file persistence** — `b.httpClient.cookieJar.create({ persist: "file", file: "/abs/path", vault: b.vault })` loads at construct, debounce-flushes on every set/clear, plus `flush()` and `close()` for explicit lifecycle. With `vault`, on-disk bytes are sealed; without, plaintext JSON (operator chooses). **Comment cleanups per rule §4** — `vault/index.js` stale `// later` removed, `mail.js` "future patch" wording rewritten as scope, `bundler.js` "What it does NOT do today (deliberately deferred)" rewritten as "Out of scope", `archive.js` "v1 scope cuts (deferred)" rewritten as "Out of scope", `framework-schema.js` "next release" wording rewritten to describe what's actually shipped, `http-client.js` "out of reach today" / "we'll plumb it through when h3 lands" rewritten as scope statements. **README CLI section** updated to reflect the v0.6.17 + v0.6.18 + v0.6.19 + v0.6.20 additions (security / config-drift / file-type / password / erase / retention) — was missing 6 subcommands.
|
|
11
13
|
- **0.6.20** (2026-05-02) — CI / packaging fix-up. The npm-publish workflow's "Attach SBOM as release asset" step started failing with HTTP 422 ("Cannot upload assets to an immutable release") because the operator's manual `gh release create` had already been published when the workflow ran. Two changes: (1) `sbom.cyclonedx.json` is now bundled into the npm tarball (`files` block in package.json), so `npm install @blamejs/core && cat node_modules/@blamejs/core/sbom.cyclonedx.json` is the canonical SBOM access path. The prepack guard's known-allowed list covers the just-in-time generation. (2) The workflow's GH-release-attach step is now non-fatal: it tries to upload, logs a warning if the release is immutable, and lets the publish proceed regardless. The npm tarball is the load-bearing artifact; the GitHub release attachment was only ever supplementary. `.gitignore` adds `sbom.cyclonedx.json` so a stray local `npm sbom` doesn't pollute the repo.
|
|
12
14
|
- **0.6.19** (2026-05-02) — closes the critical + high gaps surfaced by the v0.6.18 audit. **Critical**: `b.network.ntp.nts.querySingle` now actually verifies the server reply with the s2cKey — extracts the AUTHENTICATOR_AND_ENC extension, AEAD-decrypts with AAD = bytes-before-authenticator, fails closed (`nts/auth-failed` / `nts/no-authenticator`) when verification fails. Server-supplied new cookies in the encrypted plaintext are appended to the cookie pool and the consumed cookie popped (real RFC 8915 cookie rotation). Previously the function returned `authenticated: true` while only checking the unique-identifier echo — any MITM that mirrored the request's 32-byte unique field could spoof timestamps. **High**: `azure-blob.presignedUploadPolicy` now throws `PRESIGN_NOT_SUPPORTED` instead of silently returning a SAS PUT URL when operators asked for POST policy semantics — Azure SAS has no body-size cap and the previous shape was a misleading mismatch (operator error message points at presignedUploadUrl + post-upload HEAD as the alternative). `b.auth.password.policy` now ships the SecLists top-10000 common-password list bundled (CC-BY-3.0, `lib/vendor/common-passwords-top-10000.txt`), loaded lazily on first `policy.check()` call; `password` / `dragon` / `qwerty` / etc. now reject with `policy/forbidden-common`; `useBundledCommon: false` per-policy bypasses if operator ships their own list. `b.network.tls` adds `removeCa(fingerprint256)` / `removeCaByLabel(label)` / `clearAll()` / `purgeExpired()` / `expiringSoon(windowMs)` so operators can rotate corp DPI CAs without process restart; every removal audits with subject + fingerprint + reason. `b.network.dns.setResultOrder("ipv6first")` now flips the order on the DoH / DoT dual-stack fallback paths too (was only sorting OS-resolver results). `b.network.dns.resolve4` / `resolve6` / `resolveAaaa` now use real DNS-protocol queries (`dns.promises.resolve4` / `_dohLookup` / `_dotLookup`) instead of aliasing `lookup()` — operator semantics now match Node's standard library (skips `/etc/hosts`, mDNS). `b.network.socket.setDefaultLinger` removed from the silent no-op path; now throws `socket/linger-not-supported` with operator guidance to use `socket.destroy()` (abort) vs `socket.end()` (graceful) since Node's public `net.Socket` has no `setLinger()`. **Internal**: every `new XxxError(...)` call across `lib/network*.js` was passing args in the wrong order (message-then-code instead of code-then-message), making `e.code` return human messages and `e.message` return slash-codes — operators relying on `e.code` for error handling got the wrong field. All 50+ throws fixed across `lib/network.js` / `network-dns.js` / `network-proxy.js` / `network-tls.js` / `network-heartbeat.js` / `network-nts.js`. **CI**: `.gitleaks.toml` adds `test/smoke.js` to the path allowlist and pins the historical commit + fingerprint that tripped the jwt rule on a `REDACTED` placeholder; `npm-publish.yml` job permissions bumped from `contents: read` to `contents: write` so `gh release upload sbom.cyclonedx.json` no longer 403s.
|
|
13
15
|
- **0.6.18** (2026-05-02) — `b.network` primitive — single namespace for runtime-configurable network behaviour. `b.network.ntp` adds tunable warn / fatal drift thresholds, env-var bindings (`BLAMEJS_NTP_SERVERS` / `BLAMEJS_NTP_TIMEOUT_MS` / `BLAMEJS_NTP_DRIFT_WARN_MS` / `BLAMEJS_NTP_DRIFT_FATAL_MS`), and authenticated-time support: `b.network.ntp.nts.query(opts)` performs an NTS-KE handshake (RFC 8915) over TLS 1.3 with the framework's PQC-hybrid group preference, extracts C2S / S2C keys via the standardized TLS exporter, and authenticates NTPv4 packets with AES-SIV-CMAC-256 (mandatory-to-implement, in-house) or AEAD-CHACHA20-POLY1305 — no extra vendored deps. `b.network.dns` exposes operator-pinned resolvers, IPv4 / IPv6 / dual-stack family selection, ipv4first / verbatim / ipv6first ordering, DNS lookup timeout (Node's native `dns.lookup` has none), in-memory positive + negative cache, and DoH / DoT (cloudflare / google / quad9 / custom URL) — `b.ssrfGuard` and `b.httpClient` route through it when configured. `b.network.proxy` honours `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` / `ALL_PROXY` (lower- and upper-case) with CIDR + suffix + wildcard `NO_PROXY` matching, basic-auth via `BLAMEJS_PROXY_AUTH`, CONNECT tunnels for HTTPS through HTTP proxies; `b.httpClient` picks up the agent automatically. `b.network.tls` is a runtime-overridable trust store: `addCa(pemOrPath)` / `addCaBundle(path)` / `useSystemTrust()` / `getTrustStore()` for deep-packet-inspection deploys behind Zscaler / Netskope / corporate Squid + custom CA — Node's `NODE_EXTRA_CA_CERTS` only works at boot; this primitive accepts adds at any time and `b.pqcAgent` picks them up immediately. Every `addCa` audits with subject + issuer + fingerprint256 + validity + isSelfSigned. `b.security.assertProduction({ allowDpiTrust })` refuses to boot in production with installed CAs unless explicitly allowed. `b.network.heartbeat` adds application-level liveness probes for upstream services (HTTP / TCP / NTP probe types, healthy → degraded → down state machine with consecutive-failure threshold, audit on state change, observability counters per probe). `b.network.socket` gives operator-tunable defaults for `TCP_NODELAY` / `SO_KEEPALIVE` / `SO_LINGER`. `b.network.bootFromEnv()` reads every supported env var at startup and applies in the right order so configuration takes effect before the first outbound socket; the wiki app's docker-compose configs ship every knob with a default-empty value (production overlay tightens DNS lookup timeout + cache TTL + `NTP_STRICT=1` + `SOCKET_NO_DELAY=1`). `.gitleaks.toml` was previously gitignored by the deny-all-dotfiles allowlist; it is now allowlisted so CI's secret-scan job loads the framework's allowlist and stops failing on `.gitleaks.toml: no such file or directory`. New wiki page `/network-config` documents all six sub-primitives.
|
package/README.md
CHANGED
|
@@ -71,18 +71,26 @@ Full primitive-by-primitive docs live at [blamejs.com](https://blamejs.com), whi
|
|
|
71
71
|
`blamejs` ships an operator-facing CLI for the recurring ops work. Each subcommand boots a headless app instance from `--data-dir` (no HTTP listener), runs the operation, and shuts down. Same vault + DB + audit chain the running app uses.
|
|
72
72
|
|
|
73
73
|
```
|
|
74
|
-
blamejs migrate
|
|
75
|
-
blamejs seed
|
|
76
|
-
blamejs
|
|
77
|
-
blamejs
|
|
78
|
-
blamejs api-key
|
|
79
|
-
blamejs
|
|
80
|
-
blamejs
|
|
81
|
-
blamejs
|
|
82
|
-
blamejs
|
|
74
|
+
blamejs migrate up | down | status --db <path> [--dir <path>]
|
|
75
|
+
blamejs seed run | status --db <path> --env <name> [--dir <path>]
|
|
76
|
+
blamejs dev --command <cmd> [--watch <dir>...]
|
|
77
|
+
blamejs api-snapshot capture | compare --file <path>
|
|
78
|
+
blamejs api-key issue | revoke | list | rotate | verify --data-dir <path> --namespace <ns>
|
|
79
|
+
blamejs audit archive | export | verify | purge --data-dir <path>
|
|
80
|
+
blamejs backup inspect | verify | extract --bundle <path>
|
|
81
|
+
blamejs mtls status | show-cert | init | issue | issue-p12 --data-dir <path>
|
|
82
|
+
blamejs vault status | seal | unseal | rotate --data-dir <path>
|
|
83
|
+
blamejs security assert --data-dir <path>
|
|
84
|
+
blamejs config-drift inspect | verify --data-dir <path>
|
|
85
|
+
blamejs file-type detect <file> [--allowlist image,pdf,...]
|
|
86
|
+
blamejs password check --plaintext "..." [--profile pci-4.0|nist-aal2|hipaa-aal2] [--breach-check] [--email <e>] [--username <u>]
|
|
87
|
+
blamejs erase --table <t> --row-id <id> --confirm --data-dir <path>
|
|
88
|
+
blamejs retention preview | run --data-dir <path> --table <t> --age-field <col> --ttl-ms <n> [--action soft-delete|delete|erase] [--soft-delete-field <col>]
|
|
89
|
+
blamejs version
|
|
90
|
+
blamejs help [<command>]
|
|
83
91
|
```
|
|
84
92
|
|
|
85
|
-
Pass `--help` to any subcommand for the full flag list. Passphrases for crypto-backed operations resolve from the appropriate env var (`BLAMEJS_VAULT_PASSPHRASE`, `BLAMEJS_BACKUP_PASSPHRASE`, `BLAMEJS_AUDIT_PASSPHRASE`) so they don't end up in shell history.
|
|
93
|
+
Pass `--help` to any subcommand for the full flag list (`blamejs api-key --help` etc.). Passphrases for crypto-backed operations resolve from the appropriate env var (`BLAMEJS_VAULT_PASSPHRASE`, `BLAMEJS_BACKUP_PASSPHRASE`, `BLAMEJS_AUDIT_PASSPHRASE`) so they don't end up in shell history.
|
|
86
94
|
|
|
87
95
|
## Reference app + deployment
|
|
88
96
|
|
package/lib/archive.js
CHANGED
|
@@ -21,16 +21,17 @@
|
|
|
21
21
|
* - UTF-8 file names (sets the EFS bit per APPNOTE 6.3.4)
|
|
22
22
|
* - Modification time defaults to "now"; operators override per file
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
* - ZIP64 (>4 GiB archives, >65535 files) — operators
|
|
26
|
-
*
|
|
24
|
+
* Out of scope:
|
|
25
|
+
* - ZIP64 (>4 GiB archives, >65535 files) — operators at that scale
|
|
26
|
+
* bring their own toolset
|
|
27
27
|
* - Encryption — `b.crypto.encryptPacked` produces a sealed bundle
|
|
28
28
|
* for the operator's encryption-at-rest needs; ZIP-native
|
|
29
29
|
* password encryption is broken-by-design
|
|
30
|
-
* - Streaming write (toStream) — toBuffer()
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* - Reading / extraction — write-only
|
|
30
|
+
* - Streaming write (toStream) — toBuffer() covers the "download my
|
|
31
|
+
* data" shape; operators streaming gigabytes use the operator-side
|
|
32
|
+
* toolset
|
|
33
|
+
* - Reading / extraction — write-only; operators use node:zlib +
|
|
34
|
+
* yauzl (or unzip in shell) for read paths
|
|
34
35
|
*/
|
|
35
36
|
var zlib = require("node:zlib");
|
|
36
37
|
var fs = require("node:fs");
|
package/lib/bundler.js
CHANGED
|
@@ -2,24 +2,24 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* bundler — content-hashed asset pipeline + manifest.
|
|
4
4
|
*
|
|
5
|
-
* What this primitive does
|
|
5
|
+
* What this primitive does:
|
|
6
6
|
* - Reads each named entry from disk
|
|
7
7
|
* - Computes a content hash (SHA3-512, first 16 hex chars)
|
|
8
8
|
* - Writes the entry to outdir/<name>.<hash>.<ext> for cache-busting
|
|
9
9
|
* - Emits manifest.json mapping logical name → hashed filename
|
|
10
10
|
* - Optionally watches entries and rebuilds on change
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Out of scope (operators bring their own tool when they need these):
|
|
13
13
|
* - Module-graph resolution (esbuild-style multi-file bundling)
|
|
14
14
|
* - Tree shaking, dead-code elimination, AST transforms
|
|
15
15
|
* - Source maps
|
|
16
|
-
* - Minification (
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* - Minification (an AST-based pass would need a vendored ESM parser;
|
|
17
|
+
* the framework's `b.bundler` is the cache-bust + manifest layer,
|
|
18
|
+
* not a full bundler)
|
|
19
19
|
*
|
|
20
|
-
* Operators with multi-file ESM source
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Operators with multi-file ESM source pre-concat manually and point
|
|
21
|
+
* bundler at the result, or use an external bundler that emits the
|
|
22
|
+
* concatenated entry — `b.bundler` then hashes + manifests the output.
|
|
23
23
|
*
|
|
24
24
|
* var bundler = b.bundler.create({
|
|
25
25
|
* entries: { app: "./public/js/app.js", styles: "./public/css/app.css" },
|
package/lib/cache.js
CHANGED
|
@@ -74,11 +74,6 @@
|
|
|
74
74
|
*
|
|
75
75
|
* What is NOT in the box:
|
|
76
76
|
*
|
|
77
|
-
* - Tag invalidation on the cluster backend — invalidating tagged
|
|
78
|
-
* entries across cluster nodes ties to a future distributed-pubsub
|
|
79
|
-
* slice. invalidateTag against a cluster-backend cache throws
|
|
80
|
-
* NOT_SUPPORTED today; operators wanting cluster-scope tag wipe
|
|
81
|
-
* run their own DELETE against _blamejs_cache.
|
|
82
77
|
* - maxBytes on the cluster backend — per-row size accounting against
|
|
83
78
|
* a shared table would mean an aggregate query on every set. The
|
|
84
79
|
* operator controls cluster-table size with their own pruning if
|
|
@@ -509,28 +504,98 @@ function _clusterBackend(cfg) {
|
|
|
509
504
|
catch (_e) { return undefined; }
|
|
510
505
|
}
|
|
511
506
|
|
|
512
|
-
async function set(key, value, expiresAt) {
|
|
507
|
+
async function set(key, value, expiresAt, meta) {
|
|
513
508
|
var json = JSON.stringify(value);
|
|
514
509
|
var storedExpires = (expiresAt === Infinity) ? Number.MAX_SAFE_INTEGER : expiresAt;
|
|
515
510
|
var now = clock();
|
|
511
|
+
var ck = _composedKey(key);
|
|
516
512
|
// SQLite + Postgres both honor ON CONFLICT (cacheKey) DO UPDATE.
|
|
517
513
|
await clusterStorage.execute(
|
|
518
514
|
"INSERT INTO _blamejs_cache (cacheKey, valueJson, expiresAt, updatedAt) " +
|
|
519
515
|
"VALUES (?, ?, ?, ?) " +
|
|
520
516
|
"ON CONFLICT (cacheKey) DO UPDATE SET " +
|
|
521
517
|
"valueJson = ?, expiresAt = ?, updatedAt = ?",
|
|
522
|
-
[
|
|
518
|
+
[ck, json, storedExpires, now, json, storedExpires, now]
|
|
519
|
+
);
|
|
520
|
+
// Tag handling: drop any prior tags for this key (tags can change
|
|
521
|
+
// across sets), then INSERT the new ones. The PRIMARY KEY on
|
|
522
|
+
// (cacheKey, tag) makes the INSERT idempotent if duplicate tags
|
|
523
|
+
// sneak in.
|
|
524
|
+
var tags = meta && Array.isArray(meta.tags) ? meta.tags : null;
|
|
525
|
+
await clusterStorage.execute(
|
|
526
|
+
"DELETE FROM _blamejs_cache_tags WHERE cacheKey = ?",
|
|
527
|
+
[ck]
|
|
523
528
|
);
|
|
529
|
+
if (tags && tags.length > 0) {
|
|
530
|
+
for (var i = 0; i < tags.length; i++) {
|
|
531
|
+
await clusterStorage.execute(
|
|
532
|
+
"INSERT INTO _blamejs_cache_tags (cacheKey, tag) VALUES (?, ?) " +
|
|
533
|
+
"ON CONFLICT (cacheKey, tag) DO NOTHING",
|
|
534
|
+
[ck, tags[i]]
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
524
538
|
}
|
|
525
539
|
|
|
526
540
|
async function del(key) {
|
|
541
|
+
var ck = _composedKey(key);
|
|
527
542
|
var result = await clusterStorage.execute(
|
|
528
543
|
"DELETE FROM _blamejs_cache WHERE cacheKey = ?",
|
|
529
|
-
[
|
|
544
|
+
[ck]
|
|
530
545
|
);
|
|
546
|
+
// Drop any matching tag rows. Best-effort: a stale tag row pointing
|
|
547
|
+
// at a non-existent cacheKey is dropped on the next invalidateTag
|
|
548
|
+
// sweep (by the JOIN-shape DELETE) anyway.
|
|
549
|
+
await clusterStorage.execute(
|
|
550
|
+
"DELETE FROM _blamejs_cache_tags WHERE cacheKey = ?",
|
|
551
|
+
[ck]
|
|
552
|
+
).catch(function () { /* best-effort */ });
|
|
531
553
|
return !!(result && result.rowCount && result.rowCount > 0);
|
|
532
554
|
}
|
|
533
555
|
|
|
556
|
+
async function invalidateTag(tag) {
|
|
557
|
+
// Find every cacheKey carrying the tag (namespace-scoped via the LIKE
|
|
558
|
+
// on the composed key), delete from the cache table + the junction.
|
|
559
|
+
var like = namespace + ":%";
|
|
560
|
+
var keysResult = await clusterStorage.execute(
|
|
561
|
+
"SELECT cacheKey FROM _blamejs_cache_tags WHERE tag = ? AND cacheKey LIKE ?",
|
|
562
|
+
[tag, like]
|
|
563
|
+
);
|
|
564
|
+
var keys = (keysResult && keysResult.rows) || [];
|
|
565
|
+
if (keys.length === 0) {
|
|
566
|
+
// Nothing to invalidate; still drop any orphan tag rows for
|
|
567
|
+
// this tag scoped to our namespace.
|
|
568
|
+
await clusterStorage.execute(
|
|
569
|
+
"DELETE FROM _blamejs_cache_tags WHERE tag = ? AND cacheKey LIKE ?",
|
|
570
|
+
[tag, like]
|
|
571
|
+
);
|
|
572
|
+
return 0;
|
|
573
|
+
}
|
|
574
|
+
var purged = 0;
|
|
575
|
+
for (var i = 0; i < keys.length; i++) {
|
|
576
|
+
var ck = keys[i].cacheKey;
|
|
577
|
+
var r = await clusterStorage.execute(
|
|
578
|
+
"DELETE FROM _blamejs_cache WHERE cacheKey = ?",
|
|
579
|
+
[ck]
|
|
580
|
+
);
|
|
581
|
+
if (r && r.rowCount > 0) purged += r.rowCount;
|
|
582
|
+
await clusterStorage.execute(
|
|
583
|
+
"DELETE FROM _blamejs_cache_tags WHERE cacheKey = ?",
|
|
584
|
+
[ck]
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
return purged;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function getTags(key) {
|
|
591
|
+
var result = await clusterStorage.execute(
|
|
592
|
+
"SELECT tag FROM _blamejs_cache_tags WHERE cacheKey = ?",
|
|
593
|
+
[_composedKey(key)]
|
|
594
|
+
);
|
|
595
|
+
if (!result || !result.rows) return [];
|
|
596
|
+
return result.rows.map(function (r) { return r.tag; });
|
|
597
|
+
}
|
|
598
|
+
|
|
534
599
|
async function has(key) {
|
|
535
600
|
// Existence check without recency bump — cluster backend doesn't
|
|
536
601
|
// track LRU at all, so "without bumping" is automatic. Honors
|
|
@@ -551,6 +616,11 @@ function _clusterBackend(cfg) {
|
|
|
551
616
|
"DELETE FROM _blamejs_cache WHERE cacheKey LIKE ?",
|
|
552
617
|
[like]
|
|
553
618
|
);
|
|
619
|
+
// Drop matching tag rows in the same namespace.
|
|
620
|
+
await clusterStorage.execute(
|
|
621
|
+
"DELETE FROM _blamejs_cache_tags WHERE cacheKey LIKE ?",
|
|
622
|
+
[like]
|
|
623
|
+
).catch(function () { /* best-effort */ });
|
|
554
624
|
return (result && result.rowCount) || 0;
|
|
555
625
|
}
|
|
556
626
|
|
|
@@ -568,10 +638,24 @@ function _clusterBackend(cfg) {
|
|
|
568
638
|
async function _sweep() {
|
|
569
639
|
var now = clock();
|
|
570
640
|
var like = namespace + ":%";
|
|
641
|
+
// Capture the to-be-purged keys first so we can drop matching tag
|
|
642
|
+
// rows in the same sweep — keeps the junction table free of orphans
|
|
643
|
+
// pointing at expired cacheKeys.
|
|
644
|
+
var expiredResult = await clusterStorage.execute(
|
|
645
|
+
"SELECT cacheKey FROM _blamejs_cache WHERE cacheKey LIKE ? AND expiresAt <= ?",
|
|
646
|
+
[like, now]
|
|
647
|
+
);
|
|
648
|
+
var expiredKeys = (expiredResult && expiredResult.rows) || [];
|
|
571
649
|
await clusterStorage.execute(
|
|
572
650
|
"DELETE FROM _blamejs_cache WHERE cacheKey LIKE ? AND expiresAt <= ?",
|
|
573
651
|
[like, now]
|
|
574
652
|
);
|
|
653
|
+
for (var i = 0; i < expiredKeys.length; i++) {
|
|
654
|
+
await clusterStorage.execute(
|
|
655
|
+
"DELETE FROM _blamejs_cache_tags WHERE cacheKey = ?",
|
|
656
|
+
[expiredKeys[i].cacheKey]
|
|
657
|
+
).catch(function () { /* best-effort */ });
|
|
658
|
+
}
|
|
575
659
|
}
|
|
576
660
|
|
|
577
661
|
function _startSweep(intervalMs) {
|
|
@@ -583,15 +667,17 @@ function _clusterBackend(cfg) {
|
|
|
583
667
|
}
|
|
584
668
|
|
|
585
669
|
return {
|
|
586
|
-
name:
|
|
587
|
-
get:
|
|
588
|
-
set:
|
|
589
|
-
del:
|
|
590
|
-
has:
|
|
591
|
-
clear:
|
|
592
|
-
size:
|
|
593
|
-
close:
|
|
594
|
-
|
|
670
|
+
name: "cluster",
|
|
671
|
+
get: get,
|
|
672
|
+
set: set,
|
|
673
|
+
del: del,
|
|
674
|
+
has: has,
|
|
675
|
+
clear: clear,
|
|
676
|
+
size: size,
|
|
677
|
+
close: close,
|
|
678
|
+
invalidateTag: invalidateTag,
|
|
679
|
+
getTags: getTags,
|
|
680
|
+
_startSweep: _startSweep,
|
|
595
681
|
};
|
|
596
682
|
}
|
|
597
683
|
|
|
@@ -865,9 +951,8 @@ function create(opts) {
|
|
|
865
951
|
if (typeof backend.invalidateTag !== "function") {
|
|
866
952
|
throw _err("NOT_SUPPORTED",
|
|
867
953
|
"cache.invalidateTag: backend '" + (backend.name || "custom") +
|
|
868
|
-
"' does not
|
|
869
|
-
"
|
|
870
|
-
"the distributed-invalidation slice.");
|
|
954
|
+
"' does not implement invalidateTag. Operator-supplied custom backends " +
|
|
955
|
+
"must export invalidateTag(tag) → number to participate in tag-based wipes.");
|
|
871
956
|
}
|
|
872
957
|
var purged;
|
|
873
958
|
try { purged = await backend.invalidateTag(tag); }
|
package/lib/db-query.js
CHANGED
|
@@ -148,7 +148,21 @@ class Query {
|
|
|
148
148
|
if (direction !== "asc" && direction !== "desc") {
|
|
149
149
|
throw new Error("orderBy direction must be 'asc' or 'desc'");
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
var entry = { field: field, direction: direction.toUpperCase() };
|
|
152
|
+
if (this._orderBy === null) {
|
|
153
|
+
// First call — keep the back-compat single-object shape so any
|
|
154
|
+
// legacy reader that does `query._orderBy.field` keeps working.
|
|
155
|
+
this._orderBy = entry;
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
// Second-or-later call — promote to an array. Multi-column ORDER BY
|
|
159
|
+
// is the keyset-pagination tiebreaker pattern: ORDER BY createdAt
|
|
160
|
+
// DESC, _id DESC means same-second rows still have a total order.
|
|
161
|
+
if (Array.isArray(this._orderBy)) {
|
|
162
|
+
this._orderBy.push(entry);
|
|
163
|
+
} else {
|
|
164
|
+
this._orderBy = [this._orderBy, entry];
|
|
165
|
+
}
|
|
152
166
|
return this;
|
|
153
167
|
}
|
|
154
168
|
|
|
@@ -173,7 +187,12 @@ class Query {
|
|
|
173
187
|
_orderLimitOffset() {
|
|
174
188
|
var s = "";
|
|
175
189
|
if (this._orderBy) {
|
|
176
|
-
|
|
190
|
+
var entries = Array.isArray(this._orderBy) ? this._orderBy : [this._orderBy];
|
|
191
|
+
var fragments = [];
|
|
192
|
+
for (var i = 0; i < entries.length; i++) {
|
|
193
|
+
fragments.push('"' + entries[i].field + '" ' + entries[i].direction);
|
|
194
|
+
}
|
|
195
|
+
s += " ORDER BY " + fragments.join(", ");
|
|
177
196
|
}
|
|
178
197
|
if (this._limit !== null) s += " LIMIT " + this._limit;
|
|
179
198
|
if (this._offset !== null) s += " OFFSET " + this._offset;
|
package/lib/db.js
CHANGED
|
@@ -399,6 +399,21 @@ var FRAMEWORK_SCHEMA = [
|
|
|
399
399
|
indexes: ["expiresAt"],
|
|
400
400
|
sealedFields: [],
|
|
401
401
|
},
|
|
402
|
+
{
|
|
403
|
+
// _blamejs_cache_tags — junction table for tag→cacheKey lookup
|
|
404
|
+
// backing b.cache.invalidateTag(t) on the cluster backend. Composite
|
|
405
|
+
// PK (cacheKey, tag) lets one cacheKey carry many tags; index on
|
|
406
|
+
// tag makes invalidation a single indexed scan. Cleared together
|
|
407
|
+
// with the matching _blamejs_cache rows on del / clear / sweep.
|
|
408
|
+
name: "_blamejs_cache_tags",
|
|
409
|
+
columns: {
|
|
410
|
+
cacheKey: "TEXT NOT NULL",
|
|
411
|
+
tag: "TEXT NOT NULL",
|
|
412
|
+
},
|
|
413
|
+
primaryKey: ["cacheKey", "tag"],
|
|
414
|
+
indexes: ["tag"],
|
|
415
|
+
sealedFields: [],
|
|
416
|
+
},
|
|
402
417
|
{
|
|
403
418
|
// _blamejs_seeders — registry of applied seed files for the
|
|
404
419
|
// b.seeders primitive (lib/seeders.js). Composite PK (env, name)
|
package/lib/framework-schema.js
CHANGED
|
@@ -16,20 +16,26 @@
|
|
|
16
16
|
* _blamejs_audit_log external-db name
|
|
17
17
|
*
|
|
18
18
|
* The mapping is exposed via tableName(local) so write-dispatch code
|
|
19
|
-
* (
|
|
19
|
+
* (cluster-storage.js) uses a single name reference and the
|
|
20
|
+
* dialect-aware ensureSchema fans out the DDL to either the local-
|
|
21
|
+
* SQLite (db.js's FRAMEWORK_SCHEMA) or the external-db backend.
|
|
20
22
|
*
|
|
21
23
|
* Dialects: Postgres + SQLite. Both support CREATE TABLE IF NOT EXISTS,
|
|
22
24
|
* CREATE INDEX IF NOT EXISTS, and the same column types modulo
|
|
23
|
-
* INTEGER/BIGINT and BLOB/BYTEA differences. MySQL is not
|
|
24
|
-
* supported — operators on MySQL must
|
|
25
|
+
* INTEGER/BIGINT and BLOB/BYTEA differences. MySQL is not currently
|
|
26
|
+
* supported — operators on MySQL must use one of the supported
|
|
27
|
+
* dialects until a MySQL adapter ships.
|
|
25
28
|
*
|
|
26
29
|
* What ensureSchema does NOT do:
|
|
27
30
|
* - Migrate existing audit_log rows from local SQLite into external-db.
|
|
28
31
|
* That migration belongs to a separate operator-driven tool.
|
|
29
32
|
* - Verify chain integrity in external-db. That happens at boot via
|
|
30
|
-
* the audit module's regular verify() path
|
|
31
|
-
* - Install append-only triggers.
|
|
32
|
-
*
|
|
33
|
+
* the audit module's regular verify() path on every read.
|
|
34
|
+
* - Install append-only triggers. The framework's tamper-evidence
|
|
35
|
+
* model is the audit chain's hash linkage + SLH-DSA-signed
|
|
36
|
+
* checkpoints — triggers would add a defense-in-depth layer
|
|
37
|
+
* but they're not load-bearing for the threat model. Operators
|
|
38
|
+
* who want triggers add them per their dialect's syntax.
|
|
33
39
|
*
|
|
34
40
|
* Public API:
|
|
35
41
|
* await frameworkSchema.ensureSchema({ externalDbBackend, dialect })
|
|
@@ -114,6 +120,11 @@ var LOCAL_TO_EXTERNAL = Object.freeze({
|
|
|
114
120
|
// values, BIGINT expiresAt for ttl. Indexed on expiresAt for the
|
|
115
121
|
// periodic prune query.
|
|
116
122
|
_blamejs_cache: "_blamejs_cache",
|
|
123
|
+
// _blamejs_cache_tags — junction table for tag-based cache
|
|
124
|
+
// invalidation on the cluster backend. Composite PK
|
|
125
|
+
// (cacheKey, tag) lets a single cacheKey carry many tags;
|
|
126
|
+
// index on tag makes invalidateTag(t) a single indexed scan.
|
|
127
|
+
_blamejs_cache_tags: "_blamejs_cache_tags",
|
|
117
128
|
// _blamejs_seeders — registry of applied seed files for b.seeders
|
|
118
129
|
// (lib/seeders.js). Composite PK (env, name) lets the same filename
|
|
119
130
|
// apply per env. Mirrors the local-SQLite shape in db.js
|
|
@@ -561,6 +572,26 @@ function _cacheDDL(dialect) {
|
|
|
561
572
|
};
|
|
562
573
|
}
|
|
563
574
|
|
|
575
|
+
// _blamejs_cache_tags — tag→cacheKey junction for cluster-backend
|
|
576
|
+
// tag invalidation. b.cache.invalidateTag(t) finds matching cacheKeys
|
|
577
|
+
// via the indexed `tag` column, deletes them from _blamejs_cache, and
|
|
578
|
+
// drops the junction rows. Cleared on cache.clear() and del() too.
|
|
579
|
+
function _cacheTagsDDL(_dialect) {
|
|
580
|
+
// Junction table is TEXT-only — no dialect-specific INT / BLOB needed.
|
|
581
|
+
var name = LOCAL_TO_EXTERNAL._blamejs_cache_tags;
|
|
582
|
+
return {
|
|
583
|
+
create:
|
|
584
|
+
"CREATE TABLE IF NOT EXISTS " + name + " (" +
|
|
585
|
+
" cacheKey TEXT NOT NULL," +
|
|
586
|
+
" tag TEXT NOT NULL," +
|
|
587
|
+
" PRIMARY KEY (cacheKey, tag)" +
|
|
588
|
+
")",
|
|
589
|
+
indexes: [
|
|
590
|
+
"CREATE INDEX IF NOT EXISTS idx_" + name + "_tag ON " + name + " (tag)",
|
|
591
|
+
],
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
564
595
|
// _blamejs_break_glass_policies — column-level break-glass policy
|
|
565
596
|
// registry. One row per (table) declares which columns are
|
|
566
597
|
// glass-locked + the operator's grant rules. Sealed columns hide
|
|
@@ -658,6 +689,7 @@ async function ensureSchema(opts) {
|
|
|
658
689
|
_sessionsDDL(dialect),
|
|
659
690
|
_jobsDDL(dialect),
|
|
660
691
|
_cacheDDL(dialect),
|
|
692
|
+
_cacheTagsDDL(dialect),
|
|
661
693
|
_seedersDDL(dialect),
|
|
662
694
|
_seedersLockDDL(dialect),
|
|
663
695
|
_breakGlassPoliciesDDL(dialect),
|
|
@@ -6,24 +6,41 @@
|
|
|
6
6
|
* (login → list → mutate → logout, OAuth code-exchange → userinfo, etc.)
|
|
7
7
|
* carry the right Cookie header without operators threading it by hand.
|
|
8
8
|
* RFC 6265 attribute coverage: Domain / Path / Expires / Max-Age /
|
|
9
|
-
* HttpOnly / Secure / SameSite. Public Suffix List awareness is
|
|
10
|
-
*
|
|
11
|
-
* domains don't need it;
|
|
9
|
+
* HttpOnly / Secure / SameSite. Public Suffix List (PSL) awareness is
|
|
10
|
+
* out of scope — operators wiring jars against trusted upstream
|
|
11
|
+
* domains don't need it; for cross-eTLD safety in untrusted contexts,
|
|
12
|
+
* use a per-domain jar and validate the host against an allowlist.
|
|
12
13
|
*
|
|
13
14
|
* var jar = b.httpClient.cookieJar.create(); // in-memory
|
|
14
15
|
* await b.httpClient.request({ url: loginUrl, method: "POST", body, jar });
|
|
15
16
|
* await b.httpClient.request({ url: meUrl, jar }); // session cookie attaches
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
* b.vault.seal before it lands in the jar's store, so a memory dump or
|
|
19
|
-
* core file doesn't expose plaintext values:
|
|
18
|
+
* Three persistence modes:
|
|
20
19
|
*
|
|
21
|
-
*
|
|
20
|
+
* memory — in-process Map. Restart loses everything.
|
|
21
|
+
* vault — every cookie value is sealed via b.vault.seal before it
|
|
22
|
+
* lands in the in-process Map, so a memory dump or core
|
|
23
|
+
* file doesn't expose plaintext values:
|
|
22
24
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* b.httpClient.cookieJar.create({ persist: "vault", vault: b.vault })
|
|
26
|
+
*
|
|
27
|
+
* file — on-disk persistence at opts.file (absolute path). Loaded
|
|
28
|
+
* at create() if the file exists; flushed (debounced via
|
|
29
|
+
* opts.flushDebounceMs, default 100ms) on every set / clear
|
|
30
|
+
* / setFromSerialized. Pass `vault` alongside `file` to
|
|
31
|
+
* seal the on-disk bytes; without vault the file is
|
|
32
|
+
* plaintext JSON (operator chose the threat model).
|
|
33
|
+
*
|
|
34
|
+
* b.httpClient.cookieJar.create({
|
|
35
|
+
* persist: "file",
|
|
36
|
+
* file: "/var/lib/myapp/jar.json",
|
|
37
|
+
* vault: b.vault, // optional but recommended
|
|
38
|
+
* })
|
|
39
|
+
*
|
|
40
|
+
* The file mode survives process restart. Cluster-shared persistence
|
|
41
|
+
* (multiple nodes sharing one jar) is out of scope; operators with
|
|
42
|
+
* that need wire a custom jar via the same shape as the returned
|
|
43
|
+
* object (setFromResponse / cookieHeaderFor / getAll / etc.).
|
|
27
44
|
*
|
|
28
45
|
* Outbound filtering follows RFC 6265 §5.4:
|
|
29
46
|
* - Domain: exact-host match by default; Domain attribute allows
|
|
@@ -43,6 +60,8 @@
|
|
|
43
60
|
* }
|
|
44
61
|
*/
|
|
45
62
|
|
|
63
|
+
var fs = require("node:fs");
|
|
64
|
+
var path = require("node:path");
|
|
46
65
|
var C = require("./constants");
|
|
47
66
|
var safeUrl = require("./safe-url");
|
|
48
67
|
var { defineClass } = require("./framework-error");
|
|
@@ -51,10 +70,11 @@ var CookieJarError = defineClass("CookieJarError", { alwaysPermanent: true });
|
|
|
51
70
|
var _err = CookieJarError.factory;
|
|
52
71
|
|
|
53
72
|
var DEFAULTS = Object.freeze({
|
|
54
|
-
persist:
|
|
73
|
+
persist: "memory",
|
|
74
|
+
flushDebounceMs: 100,
|
|
55
75
|
});
|
|
56
76
|
|
|
57
|
-
var VALID_PERSIST = new Set(["memory", "vault"]);
|
|
77
|
+
var VALID_PERSIST = new Set(["memory", "vault", "file"]);
|
|
58
78
|
var VALID_SAMESITE = new Set(["Strict", "Lax", "None"]);
|
|
59
79
|
|
|
60
80
|
// ---- Set-Cookie parser ----
|
|
@@ -130,7 +150,7 @@ function create(opts) {
|
|
|
130
150
|
opts = opts || {};
|
|
131
151
|
var persist = opts.persist === undefined ? DEFAULTS.persist : opts.persist;
|
|
132
152
|
if (!VALID_PERSIST.has(persist)) {
|
|
133
|
-
throw _err("BAD_OPT", "cookieJar.create: persist must be 'memory'
|
|
153
|
+
throw _err("BAD_OPT", "cookieJar.create: persist must be 'memory' | 'vault' | 'file', got " +
|
|
134
154
|
JSON.stringify(persist));
|
|
135
155
|
}
|
|
136
156
|
var vault = opts.vault || null;
|
|
@@ -140,6 +160,22 @@ function create(opts) {
|
|
|
140
160
|
"cookieJar.create: persist: 'vault' requires opts.vault with seal/unseal (pass b.vault)");
|
|
141
161
|
}
|
|
142
162
|
}
|
|
163
|
+
var filePath = null;
|
|
164
|
+
if (persist === "file") {
|
|
165
|
+
if (typeof opts.file !== "string" || opts.file.length === 0) {
|
|
166
|
+
throw _err("BAD_OPT",
|
|
167
|
+
"cookieJar.create: persist: 'file' requires opts.file (absolute path)");
|
|
168
|
+
}
|
|
169
|
+
filePath = opts.file;
|
|
170
|
+
// Refuse relative paths so a process running in a different cwd
|
|
171
|
+
// doesn't accidentally serialize to a sibling directory.
|
|
172
|
+
if (!path.isAbsolute(filePath)) {
|
|
173
|
+
throw _err("BAD_OPT",
|
|
174
|
+
"cookieJar.create: opts.file must be an absolute path, got " + JSON.stringify(filePath));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
var flushDebounceMs = (typeof opts.flushDebounceMs === "number" && opts.flushDebounceMs >= 0)
|
|
178
|
+
? opts.flushDebounceMs : DEFAULTS.flushDebounceMs;
|
|
143
179
|
var clock = typeof opts.clock === "function" ? opts.clock : Date.now;
|
|
144
180
|
|
|
145
181
|
// Storage map keyed by `<domain>|<path>|<name>` so a (domain, path)
|
|
@@ -377,14 +413,78 @@ function create(opts) {
|
|
|
377
413
|
return rows;
|
|
378
414
|
}
|
|
379
415
|
|
|
416
|
+
// ---- File persistence ----
|
|
417
|
+
// When persist === "file", load on construct + flush on every write
|
|
418
|
+
// (debounced). On-disk format is JSON of getAll() output. If a vault
|
|
419
|
+
// is also passed, the file is sealed via vault.seal so the on-disk
|
|
420
|
+
// bytes are encrypted at rest; otherwise plaintext (operator chose
|
|
421
|
+
// the threat model by passing or omitting vault).
|
|
422
|
+
var flushTimer = null;
|
|
423
|
+
function _flushSync() {
|
|
424
|
+
if (!filePath) return;
|
|
425
|
+
var rows = getAll();
|
|
426
|
+
var serialized = JSON.stringify(rows);
|
|
427
|
+
var blob = vault ? vault.seal(serialized) : serialized;
|
|
428
|
+
fs.writeFileSync(filePath, blob);
|
|
429
|
+
}
|
|
430
|
+
function _scheduleFlush() {
|
|
431
|
+
if (!filePath) return;
|
|
432
|
+
if (flushTimer) return;
|
|
433
|
+
flushTimer = setTimeout(function () {
|
|
434
|
+
flushTimer = null;
|
|
435
|
+
try { _flushSync(); } catch (_e) { /* operator can call flush() to retry */ }
|
|
436
|
+
}, flushDebounceMs);
|
|
437
|
+
if (flushTimer.unref) flushTimer.unref();
|
|
438
|
+
}
|
|
439
|
+
function flush() {
|
|
440
|
+
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
441
|
+
_flushSync();
|
|
442
|
+
}
|
|
443
|
+
function close() {
|
|
444
|
+
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
445
|
+
if (filePath) try { _flushSync(); } catch (_e) { /* best-effort */ }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Wrap mutating entrypoints so each write schedules a flush. The
|
|
449
|
+
// wrappers go on the returned object — the underlying function
|
|
450
|
+
// declarations stay intact so other internal callers reach them.
|
|
451
|
+
var setFromResponseAndFlush = function (reqUrl, hdr) {
|
|
452
|
+
setFromResponse(reqUrl, hdr); _scheduleFlush();
|
|
453
|
+
};
|
|
454
|
+
var clearAndFlush = function (filter) {
|
|
455
|
+
var n = clear(filter); _scheduleFlush(); return n;
|
|
456
|
+
};
|
|
457
|
+
var setFromSerializedAndFlush = function (rows) {
|
|
458
|
+
setFromSerialized(rows); _scheduleFlush();
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Initial load from file. Missing file is fine (first run).
|
|
462
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
463
|
+
try {
|
|
464
|
+
var raw = fs.readFileSync(filePath, "utf8");
|
|
465
|
+
var serialized = vault ? vault.unseal(raw) : raw;
|
|
466
|
+
if (serialized && serialized.length > 0) {
|
|
467
|
+
var rows = JSON.parse(serialized);
|
|
468
|
+
setFromSerialized(rows);
|
|
469
|
+
}
|
|
470
|
+
} catch (e) {
|
|
471
|
+
throw _err("LOAD_FAILED",
|
|
472
|
+
"cookieJar.create: failed to load persist file '" + filePath + "': " +
|
|
473
|
+
(e.message || String(e)));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
380
477
|
return {
|
|
381
|
-
setFromResponse: setFromResponse,
|
|
478
|
+
setFromResponse: filePath ? setFromResponseAndFlush : setFromResponse,
|
|
382
479
|
cookieHeaderFor: cookieHeaderFor,
|
|
383
480
|
getAll: getAll,
|
|
384
|
-
clear: clear,
|
|
481
|
+
clear: filePath ? clearAndFlush : clear,
|
|
385
482
|
size: size,
|
|
386
|
-
setFromSerialized: setFromSerialized,
|
|
483
|
+
setFromSerialized: filePath ? setFromSerializedAndFlush : setFromSerialized,
|
|
484
|
+
flush: flush,
|
|
485
|
+
close: close,
|
|
387
486
|
persist: persist,
|
|
487
|
+
file: filePath,
|
|
388
488
|
_storeForTest: _storeForTest,
|
|
389
489
|
};
|
|
390
490
|
}
|