@blamejs/core 0.8.43 → 0.8.49
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 +92 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* legal-hold — subject-level litigation/regulatory hold registry.
|
|
4
|
+
*
|
|
5
|
+
* A legal hold (a.k.a. litigation hold, preservation order, regulatory
|
|
6
|
+
* hold) freezes a named subject's data so retention sweeps, RTBF
|
|
7
|
+
* erasures, and routine purges cannot remove records pending the
|
|
8
|
+
* resolution of an investigation, lawsuit, audit, or regulatory
|
|
9
|
+
* inquiry. Per FRCP Rule 26 / Rule 37(e) (US Federal Rules of Civil
|
|
10
|
+
* Procedure), GDPR Art. 17(3)(e) (right-to-erasure exception for
|
|
11
|
+
* legal claims), SEC Rule 17a-4 (broker-dealer record preservation),
|
|
12
|
+
* and HIPAA §164.530(j)(2) (six-year retention of HIPAA records),
|
|
13
|
+
* once an organization knows or should reasonably know that records
|
|
14
|
+
* are relevant to anticipated litigation or regulatory action, it
|
|
15
|
+
* must suspend routine destruction.
|
|
16
|
+
*
|
|
17
|
+
* Per-rule `legalHoldField` in b.retention already lets an operator
|
|
18
|
+
* gate retention skips on a column value. This module is the central
|
|
19
|
+
* registry that:
|
|
20
|
+
*
|
|
21
|
+
* 1. Records each placement / release as a hash-chained audit row
|
|
22
|
+
* (`legalhold.placed` / `legalhold.released` events).
|
|
23
|
+
* 2. Persists subject-id-keyed entries in `_blamejs_legal_hold` so
|
|
24
|
+
* the framework can answer `isHeld(subjectId)` in O(1) without
|
|
25
|
+
* the operator having to plumb a flag column into every table.
|
|
26
|
+
* 3. Wires into b.subject.erase + b.retention so a placed hold
|
|
27
|
+
* refuses erasure even when the operator-supplied
|
|
28
|
+
* acknowledgements would otherwise pass.
|
|
29
|
+
*
|
|
30
|
+
* var holds = b.legalHold.create({
|
|
31
|
+
* db: b.db,
|
|
32
|
+
* audit: b.audit,
|
|
33
|
+
* signWith: b.auditSign, // optional — sign every event
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* await holds.place("user-42", {
|
|
37
|
+
* reason: "SEC subpoena 2026-03-12 case 24-cv-01933",
|
|
38
|
+
* custodian: "legal@example.com",
|
|
39
|
+
* citation: "FRCP-26",
|
|
40
|
+
* retainUntil: Date.now() + C.TIME.days(365), // optional sunset
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* await holds.release("user-42", {
|
|
44
|
+
* reason: "case dismissed; preservation duty ended",
|
|
45
|
+
* approver: "legal@example.com",
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* holds.isHeld("user-42"); // → boolean (sync read)
|
|
49
|
+
* holds.list(); // → [{ subjectId, ... }]
|
|
50
|
+
* holds.history("user-42"); // → [{ at, action, ... }]
|
|
51
|
+
*
|
|
52
|
+
* Storage shape: `_blamejs_legal_hold` is the active registry (one
|
|
53
|
+
* row per held subject); placement + release history is preserved in
|
|
54
|
+
* the framework audit_log via `legalhold.placed` / `.released`
|
|
55
|
+
* events. Operators wanting a flat history table re-derive it from
|
|
56
|
+
* the audit chain.
|
|
57
|
+
*
|
|
58
|
+
* Validation tier: throw at config-time on bad opts; throw on
|
|
59
|
+
* missing/garbage subjectId at the API; emit + return shaped error
|
|
60
|
+
* on policy denials (already-held / not-held / invalid-citation).
|
|
61
|
+
*/
|
|
62
|
+
var crypto = require("./crypto");
|
|
63
|
+
var lazyRequire = require("./lazy-require");
|
|
64
|
+
var safeJson = require("./safe-json");
|
|
65
|
+
var validateOpts = require("./validate-opts");
|
|
66
|
+
var { defineClass } = require("./framework-error");
|
|
67
|
+
|
|
68
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
69
|
+
|
|
70
|
+
var LegalHoldError = defineClass("LegalHoldError", { alwaysPermanent: true });
|
|
71
|
+
var _err = LegalHoldError.factory;
|
|
72
|
+
|
|
73
|
+
// Per FRCP Rule 26(f) / 37(e), GDPR Art. 17(3)(e), SEC Rule 17a-4(b),
|
|
74
|
+
// HIPAA §164.530(j)(2), 21 CFR Part 11 §11.10(c). Operator-supplied
|
|
75
|
+
// citation should match one of these or be a free-form reference; the
|
|
76
|
+
// framework only sanity-checks shape, not value.
|
|
77
|
+
var KNOWN_CITATIONS = Object.freeze([
|
|
78
|
+
"FRCP-26", "FRCP-37(e)",
|
|
79
|
+
"GDPR-Art-17-3-e",
|
|
80
|
+
"SEC-Rule-17a-4", "SEC-Rule-17a-4(f)",
|
|
81
|
+
"FINRA-4511",
|
|
82
|
+
"HIPAA-164.530(j)(2)",
|
|
83
|
+
"21-CFR-Part-11", "21-CFR-Part-11-11.10(c)",
|
|
84
|
+
"operator-defined",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
function _subjectIdString(subjectId) {
|
|
88
|
+
if (subjectId === null || subjectId === undefined) {
|
|
89
|
+
throw _err("BAD_ARG", "subjectId must be a non-empty string");
|
|
90
|
+
}
|
|
91
|
+
var s = String(subjectId);
|
|
92
|
+
if (s.length === 0) {
|
|
93
|
+
throw _err("BAD_ARG", "subjectId must be a non-empty string");
|
|
94
|
+
}
|
|
95
|
+
return s;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _hashSubject(subjectId) {
|
|
99
|
+
return crypto.sha3Hash("bj-legal-hold:" + subjectId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function create(opts) {
|
|
103
|
+
opts = opts || {};
|
|
104
|
+
validateOpts(opts, ["db", "audit", "signWith"], "legalHold");
|
|
105
|
+
if (!opts.db || typeof opts.db.prepare !== "function") {
|
|
106
|
+
throw _err("BAD_OPT", "create: opts.db is required (a b.db handle)");
|
|
107
|
+
}
|
|
108
|
+
var db = opts.db;
|
|
109
|
+
var auditOn = opts.audit !== false && opts.audit != null;
|
|
110
|
+
var auditInstance = (opts.audit && opts.audit !== true) ? opts.audit : null;
|
|
111
|
+
// signWith is reserved for future per-event detached signatures;
|
|
112
|
+
// currently the audit chain itself is hash-linked + the sidecar
|
|
113
|
+
// signing on audit_checkpoints covers tamper detection. Validate
|
|
114
|
+
// shape so a caller passing it gets a typo-surfacing throw, but
|
|
115
|
+
// don't require it.
|
|
116
|
+
validateOpts.optionalObjectWithMethod(opts.signWith, "sign",
|
|
117
|
+
"create: opts.signWith", LegalHoldError, "BAD_OPT", "b.auditSign-shaped object");
|
|
118
|
+
|
|
119
|
+
function _emit(action, info, outcome) {
|
|
120
|
+
if (!auditOn) return;
|
|
121
|
+
var sink = auditInstance || audit();
|
|
122
|
+
try {
|
|
123
|
+
sink.safeEmit({
|
|
124
|
+
action: action,
|
|
125
|
+
outcome: outcome,
|
|
126
|
+
resource: { kind: "legal-hold", id: info && info.subjectId },
|
|
127
|
+
reason: (info && info.reason) || null,
|
|
128
|
+
metadata: info || {},
|
|
129
|
+
});
|
|
130
|
+
} catch (_e) { /* best-effort */ }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _ensureSchema() {
|
|
134
|
+
// Idempotent migration. The framework SQLite path installs the
|
|
135
|
+
// table via FRAMEWORK_SCHEMA at boot; this guard is for the
|
|
136
|
+
// external-db / cluster path where schema migrations are operator-
|
|
137
|
+
// driven. Either way the IF NOT EXISTS shape is safe to re-run.
|
|
138
|
+
var fn = db.runSql || db.execRaw;
|
|
139
|
+
if (typeof fn === "function") {
|
|
140
|
+
fn(
|
|
141
|
+
'CREATE TABLE IF NOT EXISTS "_blamejs_legal_hold" (' +
|
|
142
|
+
'"subjectIdHash" TEXT PRIMARY KEY,' +
|
|
143
|
+
'"placedAt" INTEGER NOT NULL,' +
|
|
144
|
+
'"placedBy" TEXT,' +
|
|
145
|
+
'"reason" TEXT NOT NULL,' +
|
|
146
|
+
'"custodian" TEXT,' +
|
|
147
|
+
'"citation" TEXT,' +
|
|
148
|
+
'"retainUntil" INTEGER' +
|
|
149
|
+
')'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function place(subjectId, args) {
|
|
155
|
+
var sid = _subjectIdString(subjectId);
|
|
156
|
+
args = args || {};
|
|
157
|
+
if (typeof args.reason !== "string" || args.reason.length === 0) {
|
|
158
|
+
throw _err("BAD_ARG", "place: args.reason is required (non-empty string)");
|
|
159
|
+
}
|
|
160
|
+
if (args.citation !== undefined && args.citation !== null) {
|
|
161
|
+
if (typeof args.citation !== "string" || args.citation.length === 0) {
|
|
162
|
+
throw _err("BAD_ARG", "place: args.citation must be a non-empty string");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (args.retainUntil !== undefined && args.retainUntil !== null) {
|
|
166
|
+
if (typeof args.retainUntil !== "number" ||
|
|
167
|
+
!isFinite(args.retainUntil) ||
|
|
168
|
+
args.retainUntil <= 0) {
|
|
169
|
+
throw _err("BAD_ARG", "place: args.retainUntil must be a positive finite ms-epoch");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
_ensureSchema();
|
|
173
|
+
var hash = _hashSubject(sid);
|
|
174
|
+
var existing = db.prepare(
|
|
175
|
+
'SELECT placedAt FROM "_blamejs_legal_hold" WHERE subjectIdHash = ?'
|
|
176
|
+
).get(hash);
|
|
177
|
+
if (existing) {
|
|
178
|
+
_emit("legalhold.place_rejected",
|
|
179
|
+
{ subjectId: sid, reason: "already-held",
|
|
180
|
+
existingSince: existing.placedAt },
|
|
181
|
+
"denied");
|
|
182
|
+
return { error: "already-held", placedAt: existing.placedAt };
|
|
183
|
+
}
|
|
184
|
+
var nowMs = Date.now();
|
|
185
|
+
db.prepare(
|
|
186
|
+
'INSERT INTO "_blamejs_legal_hold" ' +
|
|
187
|
+
'(subjectIdHash, placedAt, placedBy, reason, custodian, citation, retainUntil) ' +
|
|
188
|
+
'VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
189
|
+
).run(
|
|
190
|
+
hash, nowMs,
|
|
191
|
+
args.placedBy || null,
|
|
192
|
+
args.reason,
|
|
193
|
+
args.custodian || null,
|
|
194
|
+
args.citation || null,
|
|
195
|
+
args.retainUntil || null
|
|
196
|
+
);
|
|
197
|
+
_emit("legalhold.placed",
|
|
198
|
+
{ subjectId: sid, reason: args.reason,
|
|
199
|
+
custodian: args.custodian || null,
|
|
200
|
+
citation: args.citation || null,
|
|
201
|
+
retainUntil: args.retainUntil || null,
|
|
202
|
+
placedBy: args.placedBy || null,
|
|
203
|
+
knownCitation: args.citation && KNOWN_CITATIONS.indexOf(args.citation) !== -1 },
|
|
204
|
+
"success");
|
|
205
|
+
return { placed: true, placedAt: nowMs };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function release(subjectId, args) {
|
|
209
|
+
var sid = _subjectIdString(subjectId);
|
|
210
|
+
args = args || {};
|
|
211
|
+
if (typeof args.reason !== "string" || args.reason.length === 0) {
|
|
212
|
+
throw _err("BAD_ARG", "release: args.reason is required (non-empty string)");
|
|
213
|
+
}
|
|
214
|
+
if (typeof args.approver !== "string" || args.approver.length === 0) {
|
|
215
|
+
throw _err("BAD_ARG", "release: args.approver is required (non-empty string)");
|
|
216
|
+
}
|
|
217
|
+
_ensureSchema();
|
|
218
|
+
var hash = _hashSubject(sid);
|
|
219
|
+
var existing = db.prepare(
|
|
220
|
+
'SELECT placedAt, reason FROM "_blamejs_legal_hold" WHERE subjectIdHash = ?'
|
|
221
|
+
).get(hash);
|
|
222
|
+
if (!existing) {
|
|
223
|
+
_emit("legalhold.release_rejected",
|
|
224
|
+
{ subjectId: sid, reason: "not-held" },
|
|
225
|
+
"denied");
|
|
226
|
+
return { error: "not-held" };
|
|
227
|
+
}
|
|
228
|
+
db.prepare(
|
|
229
|
+
'DELETE FROM "_blamejs_legal_hold" WHERE subjectIdHash = ?'
|
|
230
|
+
).run(hash);
|
|
231
|
+
_emit("legalhold.released",
|
|
232
|
+
{ subjectId: sid, reason: args.reason,
|
|
233
|
+
approver: args.approver,
|
|
234
|
+
originalReason: existing.reason,
|
|
235
|
+
heldSince: existing.placedAt },
|
|
236
|
+
"success");
|
|
237
|
+
return { released: true, heldSince: existing.placedAt };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isHeld(subjectId) {
|
|
241
|
+
var sid = _subjectIdString(subjectId);
|
|
242
|
+
_ensureSchema();
|
|
243
|
+
var hash = _hashSubject(sid);
|
|
244
|
+
var row = db.prepare(
|
|
245
|
+
'SELECT retainUntil FROM "_blamejs_legal_hold" WHERE subjectIdHash = ?'
|
|
246
|
+
).get(hash);
|
|
247
|
+
if (!row) return false;
|
|
248
|
+
// retainUntil expiry — when the operator pinned a sunset and it
|
|
249
|
+
// has passed, the hold has lapsed and isHeld returns false. The
|
|
250
|
+
// row stays so audit/history reads can see when the sunset hit;
|
|
251
|
+
// operators wanting the row gone call release() explicitly.
|
|
252
|
+
if (row.retainUntil && row.retainUntil < Date.now()) return false;
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function get(subjectId) {
|
|
257
|
+
var sid = _subjectIdString(subjectId);
|
|
258
|
+
_ensureSchema();
|
|
259
|
+
var hash = _hashSubject(sid);
|
|
260
|
+
var row = db.prepare(
|
|
261
|
+
'SELECT subjectIdHash, placedAt, placedBy, reason, custodian, citation, retainUntil ' +
|
|
262
|
+
'FROM "_blamejs_legal_hold" WHERE subjectIdHash = ?'
|
|
263
|
+
).get(hash);
|
|
264
|
+
if (!row) return null;
|
|
265
|
+
return {
|
|
266
|
+
subjectId: sid,
|
|
267
|
+
placedAt: row.placedAt,
|
|
268
|
+
placedBy: row.placedBy,
|
|
269
|
+
reason: row.reason,
|
|
270
|
+
custodian: row.custodian,
|
|
271
|
+
citation: row.citation,
|
|
272
|
+
retainUntil: row.retainUntil,
|
|
273
|
+
lapsed: !!(row.retainUntil && row.retainUntil < Date.now()),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function list() {
|
|
278
|
+
_ensureSchema();
|
|
279
|
+
var rows = db.prepare(
|
|
280
|
+
'SELECT subjectIdHash, placedAt, placedBy, reason, custodian, citation, retainUntil ' +
|
|
281
|
+
'FROM "_blamejs_legal_hold" ORDER BY placedAt'
|
|
282
|
+
).all();
|
|
283
|
+
var nowMs = Date.now();
|
|
284
|
+
return rows.map(function (r) {
|
|
285
|
+
return {
|
|
286
|
+
subjectIdHash: r.subjectIdHash,
|
|
287
|
+
placedAt: r.placedAt,
|
|
288
|
+
placedBy: r.placedBy,
|
|
289
|
+
reason: r.reason,
|
|
290
|
+
custodian: r.custodian,
|
|
291
|
+
citation: r.citation,
|
|
292
|
+
retainUntil: r.retainUntil,
|
|
293
|
+
lapsed: !!(r.retainUntil && r.retainUntil < nowMs),
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function history(subjectId) {
|
|
299
|
+
// Re-derive the placement/release history from the audit chain.
|
|
300
|
+
// We don't store a separate history table — the audit_log is
|
|
301
|
+
// already tamper-evident and chain-verified.
|
|
302
|
+
var sid = _subjectIdString(subjectId);
|
|
303
|
+
var rows = [];
|
|
304
|
+
try {
|
|
305
|
+
var auditQuery = db.prepare(
|
|
306
|
+
'SELECT recordedAt, action, metadata, outcome ' +
|
|
307
|
+
'FROM audit_log ' +
|
|
308
|
+
'WHERE action LIKE ? AND resourceKind = ? ' +
|
|
309
|
+
'ORDER BY recordedAt'
|
|
310
|
+
);
|
|
311
|
+
// resourceId is sealed, so match on resourceKind + post-filter
|
|
312
|
+
// by parsed metadata.
|
|
313
|
+
var raw = auditQuery.all("legalhold.%", "legal-hold");
|
|
314
|
+
for (var i = 0; i < raw.length; i++) {
|
|
315
|
+
var meta = null;
|
|
316
|
+
try { meta = safeJson.parse(raw[i].metadata || "{}"); } catch (_e) { meta = null; }
|
|
317
|
+
if (meta && meta.subjectId === sid) {
|
|
318
|
+
rows.push({
|
|
319
|
+
at: raw[i].recordedAt,
|
|
320
|
+
action: raw[i].action,
|
|
321
|
+
outcome: raw[i].outcome,
|
|
322
|
+
metadata: meta,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch (_e) { /* drop-silent: audit_log may be sealed-metadata in cluster mode */ }
|
|
327
|
+
return rows;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
var instance = {
|
|
331
|
+
place: place,
|
|
332
|
+
release: release,
|
|
333
|
+
isHeld: isHeld,
|
|
334
|
+
get: get,
|
|
335
|
+
list: list,
|
|
336
|
+
history: history,
|
|
337
|
+
KNOWN_CITATIONS: KNOWN_CITATIONS,
|
|
338
|
+
};
|
|
339
|
+
// Auto-register the most recent instance as the framework singleton
|
|
340
|
+
// so b.subject.erase / b.retention can consult b.legalHold.isHeld
|
|
341
|
+
// without each operator threading the instance through. Operators
|
|
342
|
+
// building multiple registries (multi-tenant, test isolation) call
|
|
343
|
+
// create() once per tenant; the last create() wins for the global
|
|
344
|
+
// gate, and per-tenant code holds its own instance reference.
|
|
345
|
+
_registerSingleton(instance);
|
|
346
|
+
return instance;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Singleton convenience — many primitives (b.subject.erase,
|
|
350
|
+
// b.retention) need to consult the registry without each operator
|
|
351
|
+
// passing a holds instance through. The first create() under a
|
|
352
|
+
// db handle wins; subsequent calls return the active singleton.
|
|
353
|
+
var _singleton = null;
|
|
354
|
+
|
|
355
|
+
function _registerSingleton(instance) {
|
|
356
|
+
_singleton = instance;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function _getSingleton() {
|
|
360
|
+
return _singleton;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function _resetForTest() {
|
|
364
|
+
_singleton = null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
module.exports = {
|
|
368
|
+
create: create,
|
|
369
|
+
KNOWN_CITATIONS: KNOWN_CITATIONS,
|
|
370
|
+
LegalHoldError: LegalHoldError,
|
|
371
|
+
_registerSingleton: _registerSingleton,
|
|
372
|
+
_getSingleton: _getSingleton,
|
|
373
|
+
_resetForTest: _resetForTest,
|
|
374
|
+
};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.localDb.thin — lightweight node:sqlite wrapper for desktop-daemon-
|
|
4
|
+
* shaped local state.
|
|
5
|
+
*
|
|
6
|
+
* The framework's full b.db is the right tool when the workload needs
|
|
7
|
+
* vault-sealed columns, the audit chain, sealed-by-default schema
|
|
8
|
+
* governance, framework-schema bootstrap, derived-hash lookup, sealed-
|
|
9
|
+
* fields rotation, and the cross-cutting "encrypted at rest" envelope.
|
|
10
|
+
* That stack costs vault keys, a tmpfs sealed-file dance, schema
|
|
11
|
+
* declarations, and a startup audit chain — overkill for a daemon
|
|
12
|
+
* keeping a hundred-row local registry on the operator's laptop.
|
|
13
|
+
*
|
|
14
|
+
* b.localDb.thin keeps the parts a daemon does need:
|
|
15
|
+
*
|
|
16
|
+
* - A node:sqlite handle opened in WAL mode with sane busy-timeout.
|
|
17
|
+
* - Boot-time `PRAGMA integrity_check` (cheap + closes the partial-
|
|
18
|
+
* write data-loss class).
|
|
19
|
+
* - Optional corrupt-rename-and-recreate recovery: if the file fails
|
|
20
|
+
* integrity_check or open() raises SQLITE_CORRUPT, rename it to
|
|
21
|
+
* `<file>.corrupt-<unix-ms>` and start fresh against the operator-
|
|
22
|
+
* supplied schema.
|
|
23
|
+
* - LRU-bounded prepared-statement cache (matches b.db's shape so
|
|
24
|
+
* long-running daemons with diverse query shapes don't leak
|
|
25
|
+
* statement handles).
|
|
26
|
+
* - Audit hooks on open / recover / close so operators can wire
|
|
27
|
+
* the same incident review pipeline they already have for b.db.
|
|
28
|
+
*
|
|
29
|
+
* What it deliberately does NOT do:
|
|
30
|
+
*
|
|
31
|
+
* - No vault, no field encryption, no per-row keys, no SHA3 derived
|
|
32
|
+
* hashes — operators with PHI / PCI go straight to b.db.
|
|
33
|
+
* - No audit chain — there is no server-side compliance posture for
|
|
34
|
+
* a desktop daemon's sqlite cache.
|
|
35
|
+
* - No schema governance — `schemaSql` is the operator's `CREATE
|
|
36
|
+
* TABLE IF NOT EXISTS ...` script, run verbatim at open.
|
|
37
|
+
* - No background flush / no encrypted-at-rest tmpfs — the file is
|
|
38
|
+
* opened in place. Operators wanting at-rest encryption use full
|
|
39
|
+
* b.db.
|
|
40
|
+
*
|
|
41
|
+
* Public surface:
|
|
42
|
+
* localDbThin.thin({
|
|
43
|
+
* file: string, // required absolute path
|
|
44
|
+
* schemaSql: string, // required CREATE TABLE / INDEX script
|
|
45
|
+
* recovery: "refuse" | "rename-and-recreate", // default: "refuse"
|
|
46
|
+
* pragmas: object, // optional extra PRAGMA overrides
|
|
47
|
+
* audit: boolean, // default: true
|
|
48
|
+
* }) -> { db, prepare, run, query, close, file }
|
|
49
|
+
*
|
|
50
|
+
* Audit emits:
|
|
51
|
+
* localdb.thin.opened { file }
|
|
52
|
+
* localdb.thin.recovered { file, renamedTo }
|
|
53
|
+
* localdb.thin.closed { file }
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
var fs = require("fs");
|
|
57
|
+
var path = require("path");
|
|
58
|
+
var lazyRequire = require("./lazy-require");
|
|
59
|
+
var validateOpts = require("./validate-opts");
|
|
60
|
+
var safeSql = require("./safe-sql");
|
|
61
|
+
var { LocalDbThinError } = require("./framework-error");
|
|
62
|
+
|
|
63
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
64
|
+
|
|
65
|
+
// LRU prepared-statement cache cap — same magnitude as lib/db.js's full
|
|
66
|
+
// variant. Daemons issuing more than this many distinct SQL strings
|
|
67
|
+
// likely have a string-concat bug rather than a legitimate need.
|
|
68
|
+
var PREPARE_CACHE_MAX = 256; // allow:raw-byte-literal — distinct-statement cache cap
|
|
69
|
+
|
|
70
|
+
var ALLOWED_RECOVERY = ["refuse", "rename-and-recreate"];
|
|
71
|
+
|
|
72
|
+
// Bare NUL byte, expressed via String.fromCharCode so the source file
|
|
73
|
+
// itself stays pure ASCII (the codebase-patterns gate also guarantees
|
|
74
|
+
// this won't be confused with a literal embedded NUL during diffs).
|
|
75
|
+
var NUL_BYTE = String.fromCharCode(0);
|
|
76
|
+
|
|
77
|
+
function _validateOpts(opts) {
|
|
78
|
+
validateOpts.requireObject(opts, "localDb.thin", LocalDbThinError, "localdb-thin/bad-opts");
|
|
79
|
+
validateOpts.requireNonEmptyString(opts.file, "file", LocalDbThinError, "localdb-thin/bad-file");
|
|
80
|
+
// `file` is operator-supplied (daemon's chosen storage path), not
|
|
81
|
+
// request-driven input. Reject NUL bytes defensively — Node's path
|
|
82
|
+
// routines silently truncate at the first NUL, which would let a
|
|
83
|
+
// typo open a different file than the operator intended.
|
|
84
|
+
if (opts.file.indexOf(NUL_BYTE) !== -1) {
|
|
85
|
+
throw new LocalDbThinError("localdb-thin/bad-file",
|
|
86
|
+
"localDb.thin: file path must not contain NUL bytes");
|
|
87
|
+
}
|
|
88
|
+
validateOpts.requireNonEmptyString(opts.schemaSql, "schemaSql",
|
|
89
|
+
LocalDbThinError, "localdb-thin/bad-schema-sql");
|
|
90
|
+
var recovery = opts.recovery || "refuse";
|
|
91
|
+
if (ALLOWED_RECOVERY.indexOf(recovery) === -1) {
|
|
92
|
+
throw new LocalDbThinError("localdb-thin/bad-recovery",
|
|
93
|
+
"localDb.thin: recovery must be one of " + ALLOWED_RECOVERY.join(", ") +
|
|
94
|
+
" (got '" + recovery + "')");
|
|
95
|
+
}
|
|
96
|
+
if (opts.pragmas !== undefined &&
|
|
97
|
+
(typeof opts.pragmas !== "object" || Array.isArray(opts.pragmas))) {
|
|
98
|
+
throw new LocalDbThinError("localdb-thin/bad-pragmas",
|
|
99
|
+
"localDb.thin: pragmas must be an object mapping pragma name -> value");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _runPragmas(database, extra) {
|
|
104
|
+
database.exec("PRAGMA journal_mode=WAL");
|
|
105
|
+
database.exec("PRAGMA synchronous=NORMAL");
|
|
106
|
+
database.exec("PRAGMA busy_timeout=5000");
|
|
107
|
+
database.exec("PRAGMA foreign_keys=ON");
|
|
108
|
+
database.exec("PRAGMA secure_delete=ON");
|
|
109
|
+
try { database.exec("PRAGMA trusted_schema=OFF"); } catch (_e) { /* sqlite < 3.31 */ }
|
|
110
|
+
try { database.exec("PRAGMA cell_size_check=ON"); } catch (_e) { /* sqlite < 3.26 */ }
|
|
111
|
+
if (extra && typeof extra === "object") {
|
|
112
|
+
var keys = Object.keys(extra);
|
|
113
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
114
|
+
var name = keys[i];
|
|
115
|
+
// PRAGMA names are operator-supplied keys; reject anything that
|
|
116
|
+
// isn't a bare SQL identifier so this never becomes a SQL-injection
|
|
117
|
+
// vector even at config time. Composes the same identifier shape
|
|
118
|
+
// safeSql.validateIdentifier enforces elsewhere.
|
|
119
|
+
if (!safeSql.DEFAULT_IDENTIFIER_RE.test(name) ||
|
|
120
|
+
name.length > safeSql.MAX_IDENTIFIER_LENGTH) {
|
|
121
|
+
throw new LocalDbThinError("localdb-thin/bad-pragma-name",
|
|
122
|
+
"localDb.thin: pragma name '" + name + "' must be a bare identifier");
|
|
123
|
+
}
|
|
124
|
+
var value = extra[name];
|
|
125
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
126
|
+
throw new LocalDbThinError("localdb-thin/bad-pragma-value",
|
|
127
|
+
"localDb.thin: pragma '" + name + "' value must be string|number|boolean");
|
|
128
|
+
}
|
|
129
|
+
database.exec("PRAGMA " + name + "=" + String(value));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _integrityOk(database) {
|
|
135
|
+
try {
|
|
136
|
+
var rows = database.prepare("PRAGMA integrity_check").all();
|
|
137
|
+
return rows.length === 1 && rows[0] && rows[0].integrity_check === "ok";
|
|
138
|
+
} catch (_e) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function thin(opts) {
|
|
144
|
+
_validateOpts(opts);
|
|
145
|
+
|
|
146
|
+
var auditOn = opts.audit !== false;
|
|
147
|
+
// opts.file is operator-config (daemon-author chosen storage path),
|
|
148
|
+
// not request-driven input. Validation above already rejected non-
|
|
149
|
+
// strings and NUL bytes. The operator picks the file location; the
|
|
150
|
+
// wrapper opens it as-is.
|
|
151
|
+
var file = opts.file;
|
|
152
|
+
var recovery = opts.recovery || "refuse";
|
|
153
|
+
|
|
154
|
+
function _safeEmitAudit(action, metadata) {
|
|
155
|
+
if (!auditOn) return;
|
|
156
|
+
try { audit().safeEmit({ action: action, outcome: "success", metadata: metadata || {} }); }
|
|
157
|
+
catch (_e) { /* drop-silent — audit best-effort */ }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// node:sqlite is required lazily so a process never importing localDb
|
|
161
|
+
// doesn't pay the cost of resolving it at module load.
|
|
162
|
+
var nodeSqlite = require("node:sqlite");
|
|
163
|
+
var DatabaseSync = nodeSqlite.DatabaseSync;
|
|
164
|
+
if (typeof DatabaseSync !== "function") {
|
|
165
|
+
throw new LocalDbThinError("localdb-thin/sqlite-missing",
|
|
166
|
+
"localDb.thin: node:sqlite is unavailable on this Node build (requires Node 24.14+)");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Ensure parent directory exists — operators commonly point this at
|
|
170
|
+
// an OS app-data path that may not exist on first daemon launch.
|
|
171
|
+
try { fs.mkdirSync(path.dirname(file), { recursive: true }); } catch (_e) { /* best-effort */ }
|
|
172
|
+
|
|
173
|
+
var database = null;
|
|
174
|
+
var renamedTo = null;
|
|
175
|
+
|
|
176
|
+
function _attemptOpen() {
|
|
177
|
+
var db = new DatabaseSync(file);
|
|
178
|
+
_runPragmas(db, opts.pragmas);
|
|
179
|
+
if (!_integrityOk(db)) {
|
|
180
|
+
try { db.close(); } catch (_e) { /* best-effort */ }
|
|
181
|
+
throw new LocalDbThinError("localdb-thin/corrupt",
|
|
182
|
+
"localDb.thin: PRAGMA integrity_check failed for '" + file + "'");
|
|
183
|
+
}
|
|
184
|
+
db.exec(opts.schemaSql);
|
|
185
|
+
return db;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
database = _attemptOpen();
|
|
190
|
+
} catch (e) {
|
|
191
|
+
var corrupt = (e && e.code === "localdb-thin/corrupt") ||
|
|
192
|
+
(e && typeof e.message === "string" &&
|
|
193
|
+
/SQLITE_CORRUPT|malformed|not a database/i.test(e.message));
|
|
194
|
+
if (corrupt && recovery === "rename-and-recreate") {
|
|
195
|
+
var stamp = String(Date.now());
|
|
196
|
+
renamedTo = file + ".corrupt-" + stamp;
|
|
197
|
+
// Bounded rename retry — Windows holds a file lock briefly after
|
|
198
|
+
// DatabaseSync.close() returns. Linux/macOS land on the first
|
|
199
|
+
// attempt. Capped at ~250ms total so a genuinely-stuck handle
|
|
200
|
+
// still surfaces as recovery-failed rather than hanging.
|
|
201
|
+
var renamed = false;
|
|
202
|
+
var lastRenameErr = null;
|
|
203
|
+
for (var attempt = 0; attempt < 5 && !renamed; attempt += 1) {
|
|
204
|
+
try {
|
|
205
|
+
if (fs.existsSync(file)) fs.renameSync(file, renamedTo);
|
|
206
|
+
renamed = true;
|
|
207
|
+
} catch (re) {
|
|
208
|
+
lastRenameErr = re;
|
|
209
|
+
if (re && (re.code === "EBUSY" || re.code === "EPERM")) {
|
|
210
|
+
// Synchronous spin — don't reach for setTimeout in a
|
|
211
|
+
// boot-time path. 50ms × 5 = 250ms upper bound.
|
|
212
|
+
var until = Date.now() + 50;
|
|
213
|
+
while (Date.now() < until) { /* spin */ }
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
throw new LocalDbThinError("localdb-thin/recovery-failed",
|
|
217
|
+
"localDb.thin: rename of corrupt file failed: " + ((re && re.message) || String(re)));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!renamed) {
|
|
221
|
+
throw new LocalDbThinError("localdb-thin/recovery-failed",
|
|
222
|
+
"localDb.thin: rename of corrupt file failed: " +
|
|
223
|
+
((lastRenameErr && lastRenameErr.message) || "unknown"));
|
|
224
|
+
}
|
|
225
|
+
// Also move WAL/SHM siblings if present so the fresh DB doesn't
|
|
226
|
+
// re-attach a half-open journal.
|
|
227
|
+
["-wal", "-shm"].forEach(function (suffix) {
|
|
228
|
+
var sibling = file + suffix;
|
|
229
|
+
if (fs.existsSync(sibling)) {
|
|
230
|
+
try { fs.renameSync(sibling, sibling + ".corrupt-" + stamp); }
|
|
231
|
+
catch (_se) { /* best-effort */ }
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
database = _attemptOpen();
|
|
235
|
+
_safeEmitAudit("localdb.thin.recovered", { file: file, renamedTo: renamedTo });
|
|
236
|
+
} else if (corrupt) {
|
|
237
|
+
throw new LocalDbThinError("localdb-thin/corrupt",
|
|
238
|
+
"localDb.thin: file '" + file + "' is corrupt; pass recovery: 'rename-and-recreate' to auto-recover");
|
|
239
|
+
} else if (e && e.isLocalDbThinError) {
|
|
240
|
+
// Bad-pragma / bad-shape errors bubble up from _runPragmas with
|
|
241
|
+
// their own typed code already attached — re-throw verbatim
|
|
242
|
+
// rather than re-wrapping behind open-failed.
|
|
243
|
+
throw e;
|
|
244
|
+
} else {
|
|
245
|
+
throw new LocalDbThinError("localdb-thin/open-failed",
|
|
246
|
+
"localDb.thin: open of '" + file + "' failed: " + ((e && e.message) || String(e)));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_safeEmitAudit("localdb.thin.opened", { file: file });
|
|
251
|
+
|
|
252
|
+
// ---- Prepared-statement cache ----
|
|
253
|
+
var prepareCache = new Map();
|
|
254
|
+
var closed = false;
|
|
255
|
+
|
|
256
|
+
function _ensureOpen() {
|
|
257
|
+
if (closed) {
|
|
258
|
+
throw new LocalDbThinError("localdb-thin/closed",
|
|
259
|
+
"localDb.thin: handle is closed");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function prepare(sql) {
|
|
264
|
+
_ensureOpen();
|
|
265
|
+
validateOpts.requireNonEmptyString(sql, "sql",
|
|
266
|
+
LocalDbThinError, "localdb-thin/bad-sql");
|
|
267
|
+
if (prepareCache.has(sql)) {
|
|
268
|
+
// Touch for LRU
|
|
269
|
+
var hit = prepareCache.get(sql);
|
|
270
|
+
prepareCache.delete(sql);
|
|
271
|
+
prepareCache.set(sql, hit);
|
|
272
|
+
return hit;
|
|
273
|
+
}
|
|
274
|
+
var stmt = database.prepare(sql);
|
|
275
|
+
prepareCache.set(sql, stmt);
|
|
276
|
+
if (prepareCache.size > PREPARE_CACHE_MAX) {
|
|
277
|
+
var oldest = prepareCache.keys().next().value;
|
|
278
|
+
prepareCache.delete(oldest);
|
|
279
|
+
}
|
|
280
|
+
return stmt;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function run(sql /* , ...params */) {
|
|
284
|
+
_ensureOpen();
|
|
285
|
+
var params = Array.prototype.slice.call(arguments, 1);
|
|
286
|
+
var stmt = prepare(sql);
|
|
287
|
+
return stmt.run.apply(stmt, params);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function query(sql /* , ...params */) {
|
|
291
|
+
_ensureOpen();
|
|
292
|
+
var params = Array.prototype.slice.call(arguments, 1);
|
|
293
|
+
var stmt = prepare(sql);
|
|
294
|
+
return stmt.all.apply(stmt, params);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function close() {
|
|
298
|
+
if (closed) return;
|
|
299
|
+
closed = true;
|
|
300
|
+
prepareCache.clear();
|
|
301
|
+
try { database.close(); } catch (_e) { /* best-effort */ }
|
|
302
|
+
_safeEmitAudit("localdb.thin.closed", { file: file });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
db: database,
|
|
307
|
+
prepare: prepare,
|
|
308
|
+
run: run,
|
|
309
|
+
query: query,
|
|
310
|
+
close: close,
|
|
311
|
+
file: file,
|
|
312
|
+
recovered: !!renamedTo,
|
|
313
|
+
recoveredTo: renamedTo,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = {
|
|
318
|
+
thin: thin,
|
|
319
|
+
LocalDbThinError: LocalDbThinError,
|
|
320
|
+
};
|