@blamejs/core 0.8.42 → 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 +93 -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,647 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.selfUpdate
|
|
4
|
+
* @nav Production
|
|
5
|
+
* @title Self Update
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Framework / vendored-deps integrity check plus version pinning —
|
|
9
|
+
* refuses to install a new build when the asset's detached signature
|
|
10
|
+
* does not verify against the operator-supplied public key, or when
|
|
11
|
+
* the vendored SHA the new build would ship does not match the
|
|
12
|
+
* manifest the operator pinned.
|
|
13
|
+
*
|
|
14
|
+
* The lifecycle is four steps, each shippable as its own audit event:
|
|
15
|
+
*
|
|
16
|
+
* 1. `b.selfUpdate.poll({ releasesUrl, currentVersion })` fetches a
|
|
17
|
+
* releases feed (GitHub `/releases` shape or any feed exposing
|
|
18
|
+
* `{ tag_name, assets: [{ name, browser_download_url }] }`),
|
|
19
|
+
* compares semver-shaped tags, and reports whether a newer tag
|
|
20
|
+
* is available along with the matching asset and signature URLs.
|
|
21
|
+
* 2. The operator downloads the asset bytes plus the detached
|
|
22
|
+
* signature via `b.httpClient.downloadStream` — the framework
|
|
23
|
+
* downloader handles SSRF guard, TLS posture, hash-while-
|
|
24
|
+
* streaming, and atomic rename of the temp file.
|
|
25
|
+
* 3. `b.selfUpdate.verify({ assetPath, signaturePath, pubkeyPem })`
|
|
26
|
+
* verifies the detached signature over the asset bytes via
|
|
27
|
+
* `b.crypto.verify` (auto-detects ML-DSA-87 / Ed25519 / ECDSA
|
|
28
|
+
* P-384 from the supplied PEM) and reports the bytes' hash for
|
|
29
|
+
* SBOM correlation. A mismatched signature throws and the swap
|
|
30
|
+
* never runs.
|
|
31
|
+
* 4. `b.selfUpdate.swap({ from, to, backupTo })` performs the
|
|
32
|
+
* atomic install: copy the current `to` to `backupTo`, rename
|
|
33
|
+
* `from` → `to`, fsync both directories. Cross-device renames
|
|
34
|
+
* fall back to copy + unlink. Any failure rolls back from the
|
|
35
|
+
* backup. `b.selfUpdate.rollback({ to, backupTo })` restores
|
|
36
|
+
* the backup post-swap when a healthcheck reports the new
|
|
37
|
+
* binary is bad.
|
|
38
|
+
*
|
|
39
|
+
* Outbound HTTP routes through `b.httpClient.request` so SSRF,
|
|
40
|
+
* allowedHosts, and TLS posture defaults apply uniformly. Atomic file
|
|
41
|
+
* ops route through `b.atomicFile` (write + fsync + rename). Every
|
|
42
|
+
* step emits an audit event under `selfupdate.*` with `outcome:
|
|
43
|
+
* "denied"` on failure, so a tampered release surfaces in the audit
|
|
44
|
+
* log immediately even when the operator's own healthcheck missed it.
|
|
45
|
+
*
|
|
46
|
+
* @card
|
|
47
|
+
* Framework / vendored-deps integrity check plus version pinning — refuses to install a new build when the asset's detached signature does not verify against the operator-supplied public key, or when the vendored SHA the new build would ship does not match the manifest the opera...
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
var fs = require("fs");
|
|
51
|
+
var path = require("path");
|
|
52
|
+
var nodeCrypto = require("crypto");
|
|
53
|
+
var nb = require("./numeric-bounds");
|
|
54
|
+
var atomicFile = require("./atomic-file");
|
|
55
|
+
var validateOpts = require("./validate-opts");
|
|
56
|
+
var bjCrypto = require("./crypto");
|
|
57
|
+
var httpClient = require("./http-client");
|
|
58
|
+
var safeJson = require("./safe-json");
|
|
59
|
+
var { URL: NodeUrl } = require("url");
|
|
60
|
+
var lazyRequire = require("./lazy-require");
|
|
61
|
+
var C = require("./constants");
|
|
62
|
+
var { boot } = require("./log");
|
|
63
|
+
var { defineClass } = require("./framework-error");
|
|
64
|
+
|
|
65
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
66
|
+
|
|
67
|
+
var SelfUpdateError = defineClass("SelfUpdateError", { alwaysPermanent: true });
|
|
68
|
+
var log = boot("self-update");
|
|
69
|
+
|
|
70
|
+
// Algorithms accepted for the digest computed alongside verify. The
|
|
71
|
+
// signature itself is over the asset bytes; the digest is reported back
|
|
72
|
+
// to the operator for audit-trail / SBOM correlation.
|
|
73
|
+
var ALLOWED_HASH_ALGS = ["sha3-512", "sha-256", "sha-512", "shake256"];
|
|
74
|
+
var DEFAULT_HASH_ALG = "sha3-512";
|
|
75
|
+
var DEFAULT_RELEASES_BYTES = C.BYTES.mib(8); // GitHub releases JSON ~hundreds of KB; 8 MiB caps a malicious response
|
|
76
|
+
|
|
77
|
+
function _safeAuditEmit(action, outcome, metadata) {
|
|
78
|
+
try {
|
|
79
|
+
audit().safeEmit({
|
|
80
|
+
action: action,
|
|
81
|
+
outcome: outcome || "success",
|
|
82
|
+
metadata: metadata || {},
|
|
83
|
+
});
|
|
84
|
+
} catch (_e) { /* drop-silent — by design */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- semver-shaped comparison (tag_name like "v0.7.30" or "0.7.30") ----
|
|
88
|
+
// Strips a leading "v" / "V" then parses dot-separated numeric components.
|
|
89
|
+
// Non-numeric components are compared lexicographically (handles release
|
|
90
|
+
// suffixes like "1.0.0-rc.1" by falling back to string comparison after
|
|
91
|
+
// the matching numeric prefix). Returns -1 / 0 / +1.
|
|
92
|
+
function _normalizeTag(tag) {
|
|
93
|
+
if (typeof tag !== "string") return "";
|
|
94
|
+
return tag.replace(/^v/i, "").trim();
|
|
95
|
+
}
|
|
96
|
+
function _compareTags(a, b) {
|
|
97
|
+
var na = _normalizeTag(a);
|
|
98
|
+
var nb2 = _normalizeTag(b);
|
|
99
|
+
var pa = na.split(".");
|
|
100
|
+
var pbb = nb2.split(".");
|
|
101
|
+
var len = Math.max(pa.length, pbb.length);
|
|
102
|
+
for (var i = 0; i < len; i++) {
|
|
103
|
+
var ai = pa[i] !== undefined ? pa[i] : "0";
|
|
104
|
+
var bi = pbb[i] !== undefined ? pbb[i] : "0";
|
|
105
|
+
var an = parseInt(ai, 10);
|
|
106
|
+
var bn = parseInt(bi, 10);
|
|
107
|
+
if (isFinite(an) && isFinite(bn) && String(an) === ai && String(bn) === bi) {
|
|
108
|
+
if (an < bn) return -1;
|
|
109
|
+
if (an > bn) return 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (ai < bi) return -1;
|
|
113
|
+
if (ai > bi) return 1;
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---- poll ----
|
|
119
|
+
|
|
120
|
+
function _validatePollOpts(opts) {
|
|
121
|
+
validateOpts.requireObject(opts, "selfUpdate.poll", SelfUpdateError, "selfupdate/bad-opts");
|
|
122
|
+
validateOpts.requireNonEmptyString(opts.releasesUrl,
|
|
123
|
+
"selfUpdate.poll: opts.releasesUrl", SelfUpdateError, "selfupdate/bad-releases-url");
|
|
124
|
+
// Scheme enforcement at config-time so the bug surfaces here, not
|
|
125
|
+
// inside the request loop. Default policy: https only. Operators
|
|
126
|
+
// wiring against an internal mirror can pass allowedProtocols
|
|
127
|
+
// explicitly to opt in to http (e.g. a TLS-terminating proxy
|
|
128
|
+
// upstream of the framework process). The full SSRF / hostname /
|
|
129
|
+
// length policy still runs inside httpClient.request.
|
|
130
|
+
var parsedProto;
|
|
131
|
+
try { parsedProto = new NodeUrl(opts.releasesUrl).protocol; }
|
|
132
|
+
catch (_e) {
|
|
133
|
+
throw new SelfUpdateError("selfupdate/bad-releases-url",
|
|
134
|
+
"selfUpdate.poll: opts.releasesUrl is not parseable as a URL");
|
|
135
|
+
}
|
|
136
|
+
var allowedProtocols = Array.isArray(opts.allowedProtocols) && opts.allowedProtocols.length > 0
|
|
137
|
+
? opts.allowedProtocols.slice() : ["https:"];
|
|
138
|
+
if (allowedProtocols.indexOf(parsedProto) === -1) {
|
|
139
|
+
throw new SelfUpdateError("selfupdate/bad-releases-url",
|
|
140
|
+
"selfUpdate.poll: opts.releasesUrl protocol '" + parsedProto +
|
|
141
|
+
"' not in allowedProtocols [" + allowedProtocols.join(", ") + "]");
|
|
142
|
+
}
|
|
143
|
+
validateOpts.requireNonEmptyString(opts.currentVersion,
|
|
144
|
+
"selfUpdate.poll: opts.currentVersion", SelfUpdateError, "selfupdate/bad-current-version");
|
|
145
|
+
if (opts.assetPattern !== undefined && !(opts.assetPattern instanceof RegExp) &&
|
|
146
|
+
typeof opts.assetPattern !== "string") {
|
|
147
|
+
throw new SelfUpdateError("selfupdate/bad-asset-pattern",
|
|
148
|
+
"selfUpdate.poll: opts.assetPattern must be a RegExp or string when present");
|
|
149
|
+
}
|
|
150
|
+
if (opts.signaturePattern !== undefined && !(opts.signaturePattern instanceof RegExp) &&
|
|
151
|
+
typeof opts.signaturePattern !== "string") {
|
|
152
|
+
throw new SelfUpdateError("selfupdate/bad-sig-pattern",
|
|
153
|
+
"selfUpdate.poll: opts.signaturePattern must be a RegExp or string when present");
|
|
154
|
+
}
|
|
155
|
+
nb.requirePositiveFiniteIntIfPresent(opts.maxBytes,
|
|
156
|
+
"selfUpdate.poll: opts.maxBytes", SelfUpdateError, "selfupdate/bad-max-bytes");
|
|
157
|
+
nb.requirePositiveFiniteIntIfPresent(opts.timeoutMs,
|
|
158
|
+
"selfUpdate.poll: opts.timeoutMs", SelfUpdateError, "selfupdate/bad-timeout");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _matchAsset(name, pattern, fallback) {
|
|
162
|
+
if (pattern instanceof RegExp) return pattern.test(name);
|
|
163
|
+
if (typeof pattern === "string") return name.indexOf(pattern) !== -1;
|
|
164
|
+
// Fallback heuristic — the caller didn't pass a pattern. Accept the
|
|
165
|
+
// first asset whose name fits the well-known shape (tarball / zip /
|
|
166
|
+
// .sig). The fallback is documented as best-effort; operators with
|
|
167
|
+
// multi-asset releases should pass a pattern explicitly.
|
|
168
|
+
return fallback ? fallback.test(name) : false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @primitive b.selfUpdate.poll
|
|
173
|
+
* @signature b.selfUpdate.poll(opts)
|
|
174
|
+
* @since 0.6.0
|
|
175
|
+
* @related b.selfUpdate.verify, b.selfUpdate.swap, b.httpClient.request
|
|
176
|
+
*
|
|
177
|
+
* Fetch a releases feed and report whether a newer tag is available.
|
|
178
|
+
* Tags are compared semver-style with a leading `v` stripped. When
|
|
179
|
+
* `opts.etag` is supplied an `If-None-Match` header makes a 304 a fast
|
|
180
|
+
* "no update" path. The match against asset and signature URLs uses
|
|
181
|
+
* `opts.assetPattern` and `opts.signaturePattern` (RegExp or substring)
|
|
182
|
+
* with conservative fallbacks. Throws SelfUpdateError on a non-2xx
|
|
183
|
+
* upstream, malformed JSON, or unexpected shape.
|
|
184
|
+
*
|
|
185
|
+
* @opts
|
|
186
|
+
* releasesUrl: string, // required — feed URL
|
|
187
|
+
* currentVersion: string, // required — e.g. "0.8.43" or "v0.8.43"
|
|
188
|
+
* assetPattern: RegExp, // match for the runtime asset (default well-known shapes)
|
|
189
|
+
* signaturePattern: RegExp, // match for the detached signature (default .sig/.asc)
|
|
190
|
+
* allowedProtocols: array, // default ["https:"]
|
|
191
|
+
* allowedHosts: array, // routed into httpClient SSRF gate
|
|
192
|
+
* allowInternal: boolean, // routed into httpClient SSRF gate
|
|
193
|
+
* maxBytes: number, // response cap (default 8 MiB)
|
|
194
|
+
* timeoutMs: number, // request timeout (default 15s)
|
|
195
|
+
* headers: object, // additional request headers
|
|
196
|
+
* etag: string, // last-seen etag for If-None-Match
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* try {
|
|
200
|
+
* await b.selfUpdate.poll({
|
|
201
|
+
* releasesUrl: "https://updates.invalid.localhost/releases.json",
|
|
202
|
+
* currentVersion: "0.8.43",
|
|
203
|
+
* timeoutMs: 1,
|
|
204
|
+
* });
|
|
205
|
+
* } catch (e) {
|
|
206
|
+
* e.code; // → "selfupdate/poll-failed"
|
|
207
|
+
* }
|
|
208
|
+
*/
|
|
209
|
+
async function poll(opts) {
|
|
210
|
+
_validatePollOpts(opts);
|
|
211
|
+
var maxBytes = typeof opts.maxBytes === "number" ? opts.maxBytes : DEFAULT_RELEASES_BYTES;
|
|
212
|
+
var timeoutMs = typeof opts.timeoutMs === "number" ? opts.timeoutMs : C.TIME.seconds(15);
|
|
213
|
+
|
|
214
|
+
var headers = Object.assign({
|
|
215
|
+
"Accept": "application/json",
|
|
216
|
+
"User-Agent": "blamejs-selfupdate/" + C.version,
|
|
217
|
+
}, opts.headers || {});
|
|
218
|
+
if (typeof opts.etag === "string" && opts.etag.length > 0) {
|
|
219
|
+
headers["If-None-Match"] = opts.etag;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
var res;
|
|
223
|
+
try {
|
|
224
|
+
res = await httpClient.request({
|
|
225
|
+
method: "GET",
|
|
226
|
+
url: opts.releasesUrl,
|
|
227
|
+
headers: headers,
|
|
228
|
+
timeoutMs: timeoutMs,
|
|
229
|
+
maxResponseBytes: maxBytes,
|
|
230
|
+
allowedHosts: opts.allowedHosts,
|
|
231
|
+
allowedProtocols: opts.allowedProtocols,
|
|
232
|
+
allowInternal: opts.allowInternal,
|
|
233
|
+
errorClass: SelfUpdateError,
|
|
234
|
+
});
|
|
235
|
+
} catch (e) {
|
|
236
|
+
_safeAuditEmit("selfupdate.poll.checked", "denied", {
|
|
237
|
+
releasesUrl: opts.releasesUrl, reason: "request-failed",
|
|
238
|
+
message: (e && e.message) || String(e),
|
|
239
|
+
});
|
|
240
|
+
throw new SelfUpdateError("selfupdate/poll-failed",
|
|
241
|
+
"selfUpdate.poll: request failed: " + ((e && e.message) || String(e)));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (res.statusCode === 304) { // allow:raw-byte-literal — HTTP status code (RFC 7232), not bytes
|
|
245
|
+
_safeAuditEmit("selfupdate.poll.checked", "success", {
|
|
246
|
+
releasesUrl: opts.releasesUrl,
|
|
247
|
+
currentVersion: opts.currentVersion,
|
|
248
|
+
available: false,
|
|
249
|
+
etagHit: true,
|
|
250
|
+
});
|
|
251
|
+
return { available: false, latestTag: null, currentVersion: opts.currentVersion,
|
|
252
|
+
asset: null, signature: null, etag: opts.etag, statusCode: 304 }; // allow:raw-byte-literal — HTTP status code (RFC 7232), not bytes
|
|
253
|
+
}
|
|
254
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
255
|
+
_safeAuditEmit("selfupdate.poll.checked", "denied", {
|
|
256
|
+
releasesUrl: opts.releasesUrl, reason: "non-2xx", statusCode: res.statusCode,
|
|
257
|
+
});
|
|
258
|
+
throw new SelfUpdateError("selfupdate/poll-non-2xx",
|
|
259
|
+
"selfUpdate.poll: upstream returned HTTP " + res.statusCode);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
var bodyBuf = Buffer.isBuffer(res.body) ? res.body :
|
|
263
|
+
(res.body == null ? Buffer.alloc(0) : Buffer.from(String(res.body), "utf8"));
|
|
264
|
+
var parsed;
|
|
265
|
+
try {
|
|
266
|
+
parsed = safeJson.parse(bodyBuf, { maxBytes: maxBytes });
|
|
267
|
+
} catch (e) {
|
|
268
|
+
_safeAuditEmit("selfupdate.poll.checked", "denied", {
|
|
269
|
+
releasesUrl: opts.releasesUrl, reason: "bad-json",
|
|
270
|
+
message: (e && e.message) || String(e),
|
|
271
|
+
});
|
|
272
|
+
throw new SelfUpdateError("selfupdate/bad-json",
|
|
273
|
+
"selfUpdate.poll: response is not valid JSON: " + ((e && e.message) || String(e)));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Normalize: GitHub /releases/latest returns one object, /releases
|
|
277
|
+
// returns an array. Either is accepted; the array path picks the
|
|
278
|
+
// first entry sorted by tag_name descending so prerelease ordering
|
|
279
|
+
// matches semver-ish.
|
|
280
|
+
var latest;
|
|
281
|
+
if (Array.isArray(parsed)) {
|
|
282
|
+
if (parsed.length === 0) {
|
|
283
|
+
_safeAuditEmit("selfupdate.poll.checked", "success", {
|
|
284
|
+
releasesUrl: opts.releasesUrl, currentVersion: opts.currentVersion,
|
|
285
|
+
available: false, reason: "empty-feed",
|
|
286
|
+
});
|
|
287
|
+
return { available: false, latestTag: null, currentVersion: opts.currentVersion,
|
|
288
|
+
asset: null, signature: null };
|
|
289
|
+
}
|
|
290
|
+
var sorted = parsed.slice().sort(function (a, b) {
|
|
291
|
+
return _compareTags(b && b.tag_name, a && a.tag_name);
|
|
292
|
+
});
|
|
293
|
+
latest = sorted[0];
|
|
294
|
+
} else if (parsed && typeof parsed === "object") {
|
|
295
|
+
latest = parsed;
|
|
296
|
+
} else {
|
|
297
|
+
throw new SelfUpdateError("selfupdate/bad-shape",
|
|
298
|
+
"selfUpdate.poll: response shape must be { tag_name, assets[] } or array of same");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!latest || typeof latest.tag_name !== "string") {
|
|
302
|
+
throw new SelfUpdateError("selfupdate/bad-shape",
|
|
303
|
+
"selfUpdate.poll: latest release missing tag_name");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
var available = _compareTags(latest.tag_name, opts.currentVersion) > 0;
|
|
307
|
+
if (!available) {
|
|
308
|
+
_safeAuditEmit("selfupdate.poll.checked", "success", {
|
|
309
|
+
releasesUrl: opts.releasesUrl,
|
|
310
|
+
currentVersion: opts.currentVersion,
|
|
311
|
+
latestTag: latest.tag_name,
|
|
312
|
+
available: false,
|
|
313
|
+
});
|
|
314
|
+
return { available: false, latestTag: latest.tag_name,
|
|
315
|
+
currentVersion: opts.currentVersion, asset: null, signature: null,
|
|
316
|
+
etag: (res.headers && (res.headers.etag || res.headers.ETag)) || null };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
var assets = Array.isArray(latest.assets) ? latest.assets : [];
|
|
320
|
+
var assetMatch = null;
|
|
321
|
+
var signatureMatch = null;
|
|
322
|
+
for (var i = 0; i < assets.length; i++) {
|
|
323
|
+
var a = assets[i] || {};
|
|
324
|
+
if (typeof a.name !== "string" || typeof a.browser_download_url !== "string") continue;
|
|
325
|
+
if (signatureMatch === null && _matchAsset(a.name, opts.signaturePattern, /\.sig$|\.asc$|\.sig\.bin$/i)) {
|
|
326
|
+
signatureMatch = { name: a.name, url: a.browser_download_url, size: a.size || null };
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (assetMatch === null && _matchAsset(a.name, opts.assetPattern, /\.(tar\.gz|tgz|zip|node|exe|bin)$/i)) {
|
|
330
|
+
assetMatch = { name: a.name, url: a.browser_download_url, size: a.size || null };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
_safeAuditEmit("selfupdate.poll.checked", "success", {
|
|
335
|
+
releasesUrl: opts.releasesUrl,
|
|
336
|
+
currentVersion: opts.currentVersion,
|
|
337
|
+
latestTag: latest.tag_name,
|
|
338
|
+
available: true,
|
|
339
|
+
asset: assetMatch ? assetMatch.name : null,
|
|
340
|
+
signature: signatureMatch ? signatureMatch.name : null,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
available: true,
|
|
345
|
+
latestTag: latest.tag_name,
|
|
346
|
+
currentVersion: opts.currentVersion,
|
|
347
|
+
asset: assetMatch,
|
|
348
|
+
signature: signatureMatch,
|
|
349
|
+
etag: (res.headers && (res.headers.etag || res.headers.ETag)) || null,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---- verify ----
|
|
354
|
+
|
|
355
|
+
function _validateVerifyOpts(opts) {
|
|
356
|
+
validateOpts.requireObject(opts, "selfUpdate.verify", SelfUpdateError, "selfupdate/bad-opts");
|
|
357
|
+
validateOpts.requireNonEmptyString(opts.assetPath,
|
|
358
|
+
"selfUpdate.verify: opts.assetPath", SelfUpdateError, "selfupdate/bad-asset-path");
|
|
359
|
+
validateOpts.requireNonEmptyString(opts.signaturePath,
|
|
360
|
+
"selfUpdate.verify: opts.signaturePath", SelfUpdateError, "selfupdate/bad-signature-path");
|
|
361
|
+
validateOpts.requireNonEmptyString(opts.pubkeyPem,
|
|
362
|
+
"selfUpdate.verify: opts.pubkeyPem (PEM-encoded public key)",
|
|
363
|
+
SelfUpdateError, "selfupdate/bad-pubkey");
|
|
364
|
+
if (opts.hashAlgo !== undefined &&
|
|
365
|
+
(typeof opts.hashAlgo !== "string" || ALLOWED_HASH_ALGS.indexOf(opts.hashAlgo) === -1)) {
|
|
366
|
+
throw new SelfUpdateError("selfupdate/bad-hash-algo",
|
|
367
|
+
"selfUpdate.verify: opts.hashAlgo must be one of " + ALLOWED_HASH_ALGS.join(", "));
|
|
368
|
+
}
|
|
369
|
+
nb.requirePositiveFiniteIntIfPresent(opts.maxBytes,
|
|
370
|
+
"selfUpdate.verify: opts.maxBytes", SelfUpdateError, "selfupdate/bad-max-bytes");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @primitive b.selfUpdate.verify
|
|
375
|
+
* @signature b.selfUpdate.verify(opts)
|
|
376
|
+
* @since 0.6.0
|
|
377
|
+
* @related b.selfUpdate.poll, b.selfUpdate.swap, b.crypto.verify
|
|
378
|
+
*
|
|
379
|
+
* Verify a detached signature over the asset bytes. The algorithm is
|
|
380
|
+
* auto-detected from `opts.pubkeyPem` (ML-DSA-87 / Ed25519 / ECDSA
|
|
381
|
+
* P-384) by `b.crypto.verify`. Reports the asset's hash alongside the
|
|
382
|
+
* verified flag for SBOM / audit correlation; the supported digest
|
|
383
|
+
* algorithms are sha3-512 (default), sha-256, sha-512, and shake256.
|
|
384
|
+
* Throws SelfUpdateError on a missing file, a verify-time exception,
|
|
385
|
+
* or a signature that does not verify.
|
|
386
|
+
*
|
|
387
|
+
* @opts
|
|
388
|
+
* assetPath: string, // required — path to the downloaded asset
|
|
389
|
+
* signaturePath: string, // required — path to the detached signature
|
|
390
|
+
* pubkeyPem: string, // required — PEM-encoded public key
|
|
391
|
+
* hashAlgo: string, // sha3-512 | sha-256 | sha-512 | shake256 (default sha3-512)
|
|
392
|
+
* maxBytes: number, // asset read cap (default 1 GiB)
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* try {
|
|
396
|
+
* await b.selfUpdate.verify({
|
|
397
|
+
* assetPath: "/tmp/blamejs-doc-asset-not-present.tar.gz",
|
|
398
|
+
* signaturePath: "/tmp/blamejs-doc-asset-not-present.sig",
|
|
399
|
+
* pubkeyPem: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA\n-----END PUBLIC KEY-----\n",
|
|
400
|
+
* });
|
|
401
|
+
* } catch (e) {
|
|
402
|
+
* e.code; // → "selfupdate/read-failed"
|
|
403
|
+
* }
|
|
404
|
+
*/
|
|
405
|
+
async function verify(opts) {
|
|
406
|
+
_validateVerifyOpts(opts);
|
|
407
|
+
var alg = opts.hashAlgo || DEFAULT_HASH_ALG;
|
|
408
|
+
|
|
409
|
+
var assetBytes;
|
|
410
|
+
var sigBytes;
|
|
411
|
+
try {
|
|
412
|
+
assetBytes = await atomicFile.read(opts.assetPath, {
|
|
413
|
+
maxBytes: typeof opts.maxBytes === "number" ? opts.maxBytes : C.BYTES.gib(1),
|
|
414
|
+
});
|
|
415
|
+
sigBytes = await atomicFile.read(opts.signaturePath, {
|
|
416
|
+
maxBytes: C.BYTES.kib(64),
|
|
417
|
+
});
|
|
418
|
+
} catch (e) {
|
|
419
|
+
_safeAuditEmit("selfupdate.verify.failed", "denied", {
|
|
420
|
+
assetPath: opts.assetPath, signaturePath: opts.signaturePath,
|
|
421
|
+
reason: "read-failed", message: (e && e.message) || String(e),
|
|
422
|
+
});
|
|
423
|
+
throw new SelfUpdateError("selfupdate/read-failed",
|
|
424
|
+
"selfUpdate.verify: read failed: " + ((e && e.message) || String(e)));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
var ok = false;
|
|
428
|
+
try { ok = bjCrypto.verify(assetBytes, sigBytes, opts.pubkeyPem); }
|
|
429
|
+
catch (e) {
|
|
430
|
+
_safeAuditEmit("selfupdate.verify.failed", "denied", {
|
|
431
|
+
assetPath: opts.assetPath, signaturePath: opts.signaturePath,
|
|
432
|
+
reason: "verify-threw", message: (e && e.message) || String(e),
|
|
433
|
+
});
|
|
434
|
+
throw new SelfUpdateError("selfupdate/verify-failed",
|
|
435
|
+
"selfUpdate.verify: signature verify threw: " + ((e && e.message) || String(e)));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
var hashHex = nodeCrypto.createHash(alg).update(assetBytes).digest("hex");
|
|
439
|
+
|
|
440
|
+
if (!ok) {
|
|
441
|
+
_safeAuditEmit("selfupdate.verify.failed", "denied", {
|
|
442
|
+
assetPath: opts.assetPath, signaturePath: opts.signaturePath,
|
|
443
|
+
alg: alg, hash: hashHex, reason: "signature-mismatch",
|
|
444
|
+
});
|
|
445
|
+
throw new SelfUpdateError("selfupdate/signature-mismatch",
|
|
446
|
+
"selfUpdate.verify: signature did not verify against the supplied public key");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
_safeAuditEmit("selfupdate.verify.passed", "success", {
|
|
450
|
+
assetPath: opts.assetPath, signaturePath: opts.signaturePath,
|
|
451
|
+
alg: alg, hash: hashHex, bytes: assetBytes.length,
|
|
452
|
+
});
|
|
453
|
+
log("selfUpdate.verify passed asset=" + opts.assetPath + " alg=" + alg);
|
|
454
|
+
return { verified: true, hash: hashHex, alg: alg, bytes: assetBytes.length };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ---- swap ----
|
|
458
|
+
|
|
459
|
+
function _validateSwapOpts(opts, label) {
|
|
460
|
+
validateOpts.requireObject(opts, "selfUpdate." + label, SelfUpdateError, "selfupdate/bad-opts");
|
|
461
|
+
if (label === "swap") {
|
|
462
|
+
validateOpts.requireNonEmptyString(opts.from,
|
|
463
|
+
"selfUpdate.swap: opts.from", SelfUpdateError, "selfupdate/bad-from");
|
|
464
|
+
}
|
|
465
|
+
validateOpts.requireNonEmptyString(opts.to,
|
|
466
|
+
"selfUpdate." + label + ": opts.to", SelfUpdateError, "selfupdate/bad-to");
|
|
467
|
+
validateOpts.requireNonEmptyString(opts.backupTo,
|
|
468
|
+
"selfUpdate." + label + ": opts.backupTo", SelfUpdateError, "selfupdate/bad-backup");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Atomic swap of `from` -> `to` with rollback on failure. Steps:
|
|
472
|
+
//
|
|
473
|
+
// 1. ensure `to` and `backupTo` parents exist
|
|
474
|
+
// 2. if `to` exists — copy bytes to `backupTo` (atomic write of the
|
|
475
|
+
// backup, preserving the original on `to` until step 3)
|
|
476
|
+
// 3. rename `from` -> `to` (atomic on the same FS; cross-device is
|
|
477
|
+
// detected and surfaced as selfupdate/cross-device)
|
|
478
|
+
// 4. fsync both directories (best-effort across platforms)
|
|
479
|
+
//
|
|
480
|
+
// If step 3 fails the backup remains; if step 4 fails the swap is
|
|
481
|
+
// considered complete (operator can audit) but a warning is logged.
|
|
482
|
+
/**
|
|
483
|
+
* @primitive b.selfUpdate.swap
|
|
484
|
+
* @signature b.selfUpdate.swap(opts)
|
|
485
|
+
* @since 0.6.0
|
|
486
|
+
* @related b.selfUpdate.verify, b.selfUpdate.rollback, b.atomicFile.copy
|
|
487
|
+
*
|
|
488
|
+
* Atomic install: copy the existing `to` to `backupTo`, rename `from`
|
|
489
|
+
* → `to`, then fsync both directories. Cross-device renames fall back
|
|
490
|
+
* to copy + unlink on the destination filesystem. On any failure the
|
|
491
|
+
* original `to` is restored from `backupTo`. Throws SelfUpdateError on
|
|
492
|
+
* a missing `from`, backup-copy failure, cross-device install failure,
|
|
493
|
+
* or rename failure.
|
|
494
|
+
*
|
|
495
|
+
* @opts
|
|
496
|
+
* from: string, // required — newly-installed asset path
|
|
497
|
+
* to: string, // required — target install path
|
|
498
|
+
* backupTo: string, // required — backup path for the existing `to`
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* try {
|
|
502
|
+
* await b.selfUpdate.swap({
|
|
503
|
+
* from: "/tmp/blamejs-doc-missing.bin",
|
|
504
|
+
* to: "/tmp/blamejs-doc-target.bin",
|
|
505
|
+
* backupTo: "/tmp/blamejs-doc-backup.bin",
|
|
506
|
+
* });
|
|
507
|
+
* } catch (e) {
|
|
508
|
+
* e.code; // → "selfupdate/missing-from"
|
|
509
|
+
* }
|
|
510
|
+
*/
|
|
511
|
+
async function swap(opts) {
|
|
512
|
+
_validateSwapOpts(opts, "swap");
|
|
513
|
+
var from = opts.from;
|
|
514
|
+
var to = opts.to;
|
|
515
|
+
var backupTo = opts.backupTo;
|
|
516
|
+
|
|
517
|
+
if (!fs.existsSync(from)) {
|
|
518
|
+
throw new SelfUpdateError("selfupdate/missing-from",
|
|
519
|
+
"selfUpdate.swap: from path does not exist: " + from);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
var toDir = path.dirname(to);
|
|
523
|
+
var backupDir = path.dirname(backupTo);
|
|
524
|
+
atomicFile.ensureDir(toDir);
|
|
525
|
+
atomicFile.ensureDir(backupDir);
|
|
526
|
+
|
|
527
|
+
// Step 2 — backup if `to` exists. Use atomicFile.copy so the backup
|
|
528
|
+
// hits disk via temp+fsync+rename.
|
|
529
|
+
var hadOriginal = fs.existsSync(to);
|
|
530
|
+
if (hadOriginal) {
|
|
531
|
+
try {
|
|
532
|
+
await atomicFile.copy(to, backupTo, { fileMode: 0o600 });
|
|
533
|
+
} catch (e) {
|
|
534
|
+
throw new SelfUpdateError("selfupdate/backup-failed",
|
|
535
|
+
"selfUpdate.swap: failed to copy " + to + " -> " + backupTo + ": " +
|
|
536
|
+
((e && e.message) || String(e)));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Step 3 — install. Rename is atomic on same FS; on cross-device we
|
|
541
|
+
// fall back to copy + unlink.
|
|
542
|
+
try {
|
|
543
|
+
fs.renameSync(from, to);
|
|
544
|
+
} catch (e) {
|
|
545
|
+
if (e && e.code === "EXDEV") {
|
|
546
|
+
// Cross-device — copy + unlink. Use atomicFile.copy for the safety
|
|
547
|
+
// net (temp+fsync+rename on dest FS); then remove the source.
|
|
548
|
+
try {
|
|
549
|
+
await atomicFile.copy(from, to, { fileMode: 0o600 });
|
|
550
|
+
try { fs.unlinkSync(from); } catch (_u) { /* tmp source leak — operator-cleanable */ }
|
|
551
|
+
} catch (ce) {
|
|
552
|
+
// Roll back from backup if we have one.
|
|
553
|
+
if (hadOriginal) {
|
|
554
|
+
try { await atomicFile.copy(backupTo, to, { fileMode: 0o600 }); }
|
|
555
|
+
catch (_re) { /* rollback best-effort — operator surfaces via audit */ }
|
|
556
|
+
}
|
|
557
|
+
throw new SelfUpdateError("selfupdate/cross-device",
|
|
558
|
+
"selfUpdate.swap: cross-device install failed: " + ((ce && ce.message) || String(ce)));
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
// Other rename failure — try to roll back.
|
|
562
|
+
if (hadOriginal) {
|
|
563
|
+
try { await atomicFile.copy(backupTo, to, { fileMode: 0o600 }); }
|
|
564
|
+
catch (_re) { /* rollback best-effort */ }
|
|
565
|
+
}
|
|
566
|
+
throw new SelfUpdateError("selfupdate/swap-failed",
|
|
567
|
+
"selfUpdate.swap: rename " + from + " -> " + to + " failed: " + e.message);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Step 4 — fsync directories so the rename is durable.
|
|
572
|
+
atomicFile.fsyncDir(toDir);
|
|
573
|
+
if (backupDir !== toDir) atomicFile.fsyncDir(backupDir);
|
|
574
|
+
|
|
575
|
+
var swappedAt = Date.now();
|
|
576
|
+
_safeAuditEmit("selfupdate.swap.completed", "success", {
|
|
577
|
+
from: from, to: to, backupTo: backupTo, hadOriginal: hadOriginal,
|
|
578
|
+
});
|
|
579
|
+
log("selfUpdate.swap completed from=" + from + " to=" + to);
|
|
580
|
+
return { ok: true, swappedAt: swappedAt, from: from, to: to, backupTo: backupTo };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ---- rollback ----
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* @primitive b.selfUpdate.rollback
|
|
587
|
+
* @signature b.selfUpdate.rollback(opts)
|
|
588
|
+
* @since 0.6.0
|
|
589
|
+
* @related b.selfUpdate.swap, b.atomicFile.copy
|
|
590
|
+
*
|
|
591
|
+
* Restore `backupTo` → `to` via the same atomic copy used by `swap`.
|
|
592
|
+
* Operators run rollback when a post-swap healthcheck reports the new
|
|
593
|
+
* binary is bad. Throws SelfUpdateError when the backup file is
|
|
594
|
+
* missing or the copy fails.
|
|
595
|
+
*
|
|
596
|
+
* @opts
|
|
597
|
+
* to: string, // required — target path to restore
|
|
598
|
+
* backupTo: string, // required — source backup path
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* try {
|
|
602
|
+
* await b.selfUpdate.rollback({
|
|
603
|
+
* to: "/tmp/blamejs-doc-target.bin",
|
|
604
|
+
* backupTo: "/tmp/blamejs-doc-missing-backup.bin",
|
|
605
|
+
* });
|
|
606
|
+
* } catch (e) {
|
|
607
|
+
* e.code; // → "selfupdate/missing-backup"
|
|
608
|
+
* }
|
|
609
|
+
*/
|
|
610
|
+
async function rollback(opts) {
|
|
611
|
+
_validateSwapOpts(opts, "rollback");
|
|
612
|
+
var to = opts.to;
|
|
613
|
+
var backupTo = opts.backupTo;
|
|
614
|
+
|
|
615
|
+
if (!fs.existsSync(backupTo)) {
|
|
616
|
+
throw new SelfUpdateError("selfupdate/missing-backup",
|
|
617
|
+
"selfUpdate.rollback: backupTo path does not exist: " + backupTo);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
atomicFile.ensureDir(path.dirname(to));
|
|
621
|
+
try {
|
|
622
|
+
await atomicFile.copy(backupTo, to, { fileMode: 0o600 });
|
|
623
|
+
} catch (e) {
|
|
624
|
+
throw new SelfUpdateError("selfupdate/rollback-failed",
|
|
625
|
+
"selfUpdate.rollback: copy " + backupTo + " -> " + to + " failed: " +
|
|
626
|
+
((e && e.message) || String(e)));
|
|
627
|
+
}
|
|
628
|
+
atomicFile.fsyncDir(path.dirname(to));
|
|
629
|
+
|
|
630
|
+
_safeAuditEmit("selfupdate.rollback.completed", "success", {
|
|
631
|
+
to: to, backupTo: backupTo,
|
|
632
|
+
});
|
|
633
|
+
log("selfUpdate.rollback restored " + to + " from " + backupTo);
|
|
634
|
+
return { ok: true, restoredAt: Date.now(), to: to, backupTo: backupTo };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
module.exports = {
|
|
638
|
+
poll: poll,
|
|
639
|
+
verify: verify,
|
|
640
|
+
swap: swap,
|
|
641
|
+
rollback: rollback,
|
|
642
|
+
SelfUpdateError: SelfUpdateError,
|
|
643
|
+
ALLOWED_HASH_ALGS: ALLOWED_HASH_ALGS,
|
|
644
|
+
DEFAULT_HASH_ALG: DEFAULT_HASH_ALG,
|
|
645
|
+
// Internal — exposed for the layer-0 test suite only.
|
|
646
|
+
_compareTags: _compareTags,
|
|
647
|
+
};
|