@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.
Files changed (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. 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
+ };