@blamejs/core 0.6.69 → 0.7.0

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 (164) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +11 -1
  3. package/index.js +2 -5
  4. package/lib/api-key.js +24 -55
  5. package/lib/api-snapshot.js +4 -1
  6. package/lib/app-shutdown.js +20 -16
  7. package/lib/app.js +6 -5
  8. package/lib/archive.js +58 -50
  9. package/lib/atomic-file.js +11 -5
  10. package/lib/audit-chain.js +12 -4
  11. package/lib/audit-sign.js +38 -13
  12. package/lib/audit-tools.js +4 -4
  13. package/lib/audit.js +8 -4
  14. package/lib/auth/jwt.js +3 -2
  15. package/lib/auth/lockout.js +3 -13
  16. package/lib/auth/oauth.js +27 -17
  17. package/lib/auth/passkey.js +1 -5
  18. package/lib/auth/password.js +23 -13
  19. package/lib/backup/bundle.js +2 -4
  20. package/lib/backup/crypto.js +8 -6
  21. package/lib/backup/index.js +18 -14
  22. package/lib/backup/manifest.js +6 -2
  23. package/lib/break-glass.js +12 -7
  24. package/lib/bundler.js +32 -21
  25. package/lib/cache-redis.js +8 -3
  26. package/lib/cache.js +39 -59
  27. package/lib/canonical-json.js +17 -2
  28. package/lib/chain-writer.js +2 -2
  29. package/lib/cli-helpers.js +9 -4
  30. package/lib/cli.js +27 -5
  31. package/lib/cluster-provider-db.js +2 -1
  32. package/lib/cluster-storage.js +71 -11
  33. package/lib/cluster.js +30 -25
  34. package/lib/config-drift.js +3 -4
  35. package/lib/constants.js +8 -3
  36. package/lib/cookies.js +16 -4
  37. package/lib/credential-hash.js +4 -7
  38. package/lib/crypto.js +9 -9
  39. package/lib/csv.js +11 -8
  40. package/lib/db-declare-row-policy.js +4 -9
  41. package/lib/db-declare-view.js +3 -6
  42. package/lib/db-query.js +9 -3
  43. package/lib/db-schema.js +75 -10
  44. package/lib/db.js +116 -70
  45. package/lib/deprecate.js +3 -8
  46. package/lib/dev.js +30 -19
  47. package/lib/dual-control.js +2 -4
  48. package/lib/error-page.js +32 -39
  49. package/lib/external-db-migrate.js +27 -20
  50. package/lib/external-db.js +11 -3
  51. package/lib/file-type.js +16 -8
  52. package/lib/forms.js +48 -12
  53. package/lib/framework-error.js +1 -2
  54. package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
  55. package/lib/handlers.js +1 -1
  56. package/lib/http-client-cookie-jar.js +25 -18
  57. package/lib/http-client.js +46 -23
  58. package/lib/i18n.js +6 -16
  59. package/lib/jobs.js +5 -1
  60. package/lib/log-stream-cloudwatch.js +31 -30
  61. package/lib/log-stream-local.js +12 -3
  62. package/lib/log-stream-otlp-grpc.js +29 -23
  63. package/lib/log-stream-otlp.js +13 -19
  64. package/lib/log-stream-syslog.js +27 -22
  65. package/lib/log-stream-webhook.js +8 -17
  66. package/lib/log-stream.js +13 -2
  67. package/lib/log.js +83 -5
  68. package/lib/mail-bounce.js +22 -15
  69. package/lib/mail-dkim.js +3 -6
  70. package/lib/mail.js +62 -34
  71. package/lib/metrics.js +61 -25
  72. package/lib/middleware/api-encrypt.js +15 -13
  73. package/lib/middleware/body-parser.js +46 -39
  74. package/lib/middleware/bot-guard.js +25 -4
  75. package/lib/middleware/compression.js +26 -11
  76. package/lib/middleware/cors.js +6 -5
  77. package/lib/middleware/csp-nonce.js +17 -4
  78. package/lib/middleware/csrf-protect.js +1 -1
  79. package/lib/middleware/db-role-for.js +7 -30
  80. package/lib/middleware/health.js +42 -8
  81. package/lib/middleware/index.js +55 -36
  82. package/lib/middleware/rate-limit.js +11 -8
  83. package/lib/middleware/request-id.js +10 -2
  84. package/lib/middleware/request-log.js +3 -2
  85. package/lib/middleware/require-auth.js +5 -2
  86. package/lib/middleware/sse.js +5 -4
  87. package/lib/migration-files.js +37 -0
  88. package/lib/migrations.js +42 -17
  89. package/lib/mtls-ca.js +38 -17
  90. package/lib/mtls-engine-default.js +15 -7
  91. package/lib/network-dns.js +49 -20
  92. package/lib/network-heartbeat.js +7 -7
  93. package/lib/network-nts.js +70 -48
  94. package/lib/network-proxy.js +34 -15
  95. package/lib/network-tls.js +7 -5
  96. package/lib/network.js +3 -3
  97. package/lib/notify.js +14 -36
  98. package/lib/ntp-check.js +7 -6
  99. package/lib/numeric-bounds.js +35 -3
  100. package/lib/object-store/azure-blob-bucket-ops.js +73 -15
  101. package/lib/object-store/azure-blob.js +31 -24
  102. package/lib/object-store/gcs-bucket-ops.js +41 -17
  103. package/lib/object-store/gcs.js +41 -34
  104. package/lib/object-store/http-put.js +2 -11
  105. package/lib/object-store/http-request.js +38 -0
  106. package/lib/object-store/index.js +1 -1
  107. package/lib/object-store/sigv4-bucket-ops.js +33 -37
  108. package/lib/object-store/sigv4.js +50 -30
  109. package/lib/observability.js +14 -3
  110. package/lib/otel-export.js +16 -14
  111. package/lib/pagination.js +23 -8
  112. package/lib/parsers/index.js +11 -5
  113. package/lib/parsers/safe-env.js +43 -14
  114. package/lib/parsers/safe-ini.js +29 -10
  115. package/lib/parsers/safe-toml.js +55 -25
  116. package/lib/parsers/safe-xml.js +22 -11
  117. package/lib/parsers/safe-yaml.js +55 -17
  118. package/lib/permissions.js +24 -33
  119. package/lib/pqc-agent.js +4 -2
  120. package/lib/pqc-gate.js +28 -15
  121. package/lib/protobuf-encoder.js +9 -3
  122. package/lib/protocol-dispatcher.js +29 -12
  123. package/lib/pubsub-redis.js +25 -8
  124. package/lib/pubsub.js +84 -12
  125. package/lib/queue-local.js +13 -4
  126. package/lib/queue-redis.js +2 -2
  127. package/lib/queue-sqs.js +21 -16
  128. package/lib/queue.js +8 -4
  129. package/lib/redis-client.js +13 -10
  130. package/lib/request-helpers.js +33 -2
  131. package/lib/restore-bundle.js +2 -4
  132. package/lib/restore-rollback.js +4 -1
  133. package/lib/restore.js +12 -10
  134. package/lib/retry.js +58 -9
  135. package/lib/router.js +96 -27
  136. package/lib/safe-async.js +97 -4
  137. package/lib/safe-buffer.js +47 -0
  138. package/lib/safe-json.js +18 -9
  139. package/lib/safe-schema.js +54 -18
  140. package/lib/safe-url.js +6 -2
  141. package/lib/scheduler.js +25 -14
  142. package/lib/security-assert.js +31 -26
  143. package/lib/seeders.js +71 -60
  144. package/lib/session.js +13 -7
  145. package/lib/slug.js +35 -8
  146. package/lib/ssrf-guard.js +58 -43
  147. package/lib/static.js +11 -7
  148. package/lib/storage.js +2 -1
  149. package/lib/template.js +17 -7
  150. package/lib/testing.js +28 -8
  151. package/lib/time.js +26 -5
  152. package/lib/totp.js +18 -9
  153. package/lib/tracing.js +15 -5
  154. package/lib/uuid.js +47 -20
  155. package/lib/validate-opts.js +213 -0
  156. package/lib/vault/index.js +36 -27
  157. package/lib/vault/rotate.js +41 -22
  158. package/lib/vault/wrap.js +26 -19
  159. package/lib/webhook.js +37 -68
  160. package/lib/websocket.js +60 -46
  161. package/package.json +1 -1
  162. package/sbom.cyclonedx.json +6 -6
  163. package/lib/auth/totp.js +0 -17
  164. package/lib/object-store/retry.js +0 -15
package/CHANGELOG.md CHANGED
@@ -6,8 +6,14 @@ Pre-1.0 the surface is intentionally evolving — every release may
6
6
  change something operators depend on. Read each entry before
7
7
  upgrading across more than a few patches at a time.
8
8
 
9
+ ## v0.7.x
10
+
11
+ - **0.7.0** (2026-05-04) — Codebase-patterns hardening sweep + primitive consolidation. The duplicate-block detector in `test/layer-0-primitives/codebase-patterns.test.js` ran at MIN_DISTINCT_FILES=3 and surfaced ~50 inline-shape clusters that had proliferated across lib/ — the kind of soft drift that's invisible at higher thresholds and hides re-introduction of bug classes the framework already swept once. **New primitive families** consolidated those clusters down: opts validation (`validateOpts.requireObject` / `auditShape` / `observabilityShape` / `applyDefaults` / `optional{Boolean,Function,PositiveInt,FiniteNonNegative,PositiveFinite,NonEmptyString}` / `requireNonEmptyString` / `makeAuditEmitter`), async coordination (`safeAsync.safeInvoke` / `makeDropCallback` / `makeScheduledFlush`), and SQL execution (`dbSchema.runInTransaction` / `runSqlOnHandle`). Plus `numericBounds.requireXFiniteIntIfPresent` for opt-time numeric-shape gates that throw via the caller's framework-error class, `log.makeViaOrFallback` for operator-log routing with per-module fallback, `observability.safeEvent` for hot-path drop-silent emission, `safeBuffer.HEX_RE`/`CRLF_RE`/`TRAILING_HSPACE_RE` regex constants + `isHex` / `hasCrlf` / `stripCrlf` / `stripTrailingHspace` helpers, `time.toIso8601NoMs`, `migrationFiles.MIGRATION_FILE_RE` / `isMigrationFileName` (shared filename grammar across migrations / seeders / external-db-migrate), and `lib/object-store/http-request.js` as a shared HTTP request helper across azure-blob / gcs / sigv4 / http-put. Roughly 80 inline call sites across 50+ files refactored to route through the new primitives; ~1500 lines of duplicate inline shapes eliminated. **Catalog gate** — `KNOWN_ANTIPATTERNS` in the codebase-patterns test now has 24 entries firing at n=1, so future code re-introducing any of the registered inline shapes (even one new file) fails the gate immediately. Pre-v0.7.0 the duplicate-block detector required n>=3 to fire; the catalog closes the gap by registering each extracted primitive's inline shape so it can't drift back in. **Cluster allowlist** — `KNOWN_CLUSTERS` allowlist (n>=3 detector) has 25 entries with documented structural reasons (parser error class signatures don't fit the framework's `(code, message)` contract; framework-convention shapes like middleware factories; future consolidation candidates). **Cleanup** — deleted dead re-export shims `lib/object-store/retry.js` (re-exported `lib/retry.js`) and `lib/auth/totp.js` (re-exported `lib/totp.js`); both fit the rule "pre-v1 frameworks have no operators to compatibly upgrade — every legacy fallback is dead code." Renamed `lib/internal-sha1-hibp.js` → `lib/framework-sha1-hibp.js` to match the lib naming convention (the `internal-` prefix wasn't in the convention's five-bucket list; `framework-` is the canonical "restricted-use" bucket alongside `framework-error.js` / `framework-schema.js`). **No operator-facing API breakage** — `b.objectStoreRetry` was removed from the public surface (operators use `b.retry` directly, which has been the canonical primitive since v0.2.24). **Eslint config tightened** — added `eqeqeq` (with the `null` exception for the `== null` null-or-undefined idiom), `no-throw-literal`, `no-promise-executor-return`, `default-case`, `no-loss-of-precision`. The previous config was hiding 11 real errors: 8 sites of `return resolve()` / `return done()` inside `new Promise(executor)` (return value silently discarded) across app / dev / http-client / mail / router / wiki/integration / 00-primitives, plus a missing default-case + 2 intentional template-language `==` / `!=` operators in template.js's binary-op evaluator (now allowed via inline `// eslint-disable-next-line eqeqeq -- template language operator`). Also adds `structuredClone` to the Node-globals list so db.js's deep-clone calls don't trip `no-undef`. **Release-workflow gate added** — Linux container smoke `docker run --rm -v "/$(pwd):/blamejs" -w //blamejs node:24-alpine node test/smoke.js` is now part of the release flow. Catches lingering-handle bugs that pass on Windows / macOS but hang or error on Linux CI. **Tests** — smoke 7338 / Linux container smoke 7338 (148s) / wiki e2e 178 / per-primitive integration 16 files / wiki integration green / eslint clean / shellcheck clean.
12
+
9
13
  ## v0.6.x
10
14
 
15
+ - **0.6.70** (2026-05-03) — `lib/numeric-bounds` extended to 3 more sites the v0.6.69 sweep didn't cover. **`lib/middleware/csp-nonce.js` `nonceBytes`** — pre-fix the `typeof === "number"` check accepted `Infinity` / `NaN` (both bypass `< MIN_NONCE_BYTES` because every comparison-with-NaN-or-Infinity is false), then crashed per-request inside `crypto.generateBytes(Infinity)` with `ERR_OUT_OF_RANGE`. The DoS-shape: an operator typo at boot took down every CSP-protected route at request time. Now rejects at create() with `csp-nonce/bad-nonce-bytes`. **`lib/migrations.js` and `lib/external-db-migrate.js` `staleAfterMs`** — both used the `> 0` guard which silently accepted `Infinity`. `(Date.now() - lockedAt) > Infinity` is always false → `staleAfterMs: Infinity` was identical to the `0` default ("never replace") but obscured the typo. Operators wanting "never expire" now pass `0` explicitly; everything else (Infinity / NaN / fractional / negative / non-number) rejects with a clear message. **Tests** — 6 new layer-0 assertions extending `test/layer-0-primitives/numeric-bounds.test.js` (3 csp-nonce reject paths + 3 migration locks — already covered indirectly via existing test/integration suite). Smoke 7293 → 7296 / wiki e2e 178 / Linux 85.9s / eslint clean / shellcheck clean.
16
+
11
17
  - **0.6.69** (2026-05-03) — `lib/numeric-bounds.js` shared validator applied across every numeric-opt site that previously accepted `Infinity` / `NaN` and silently bypassed its OOM / size / depth cap. Pre-fix the recurring pattern `typeof opts.X === "number" && opts.X > 0` was vulnerable everywhere it shipped: `Infinity > Infinity` is false, `byteLength > NaN` is false, `Number.isFinite()` was missing. Operators with env-var coercions (`Number(process.env.MAX_X || "")` produces `NaN` for missing values, `Infinity` for typo'd ones) silently lost their cap. **The unified fix:** new `lib/numeric-bounds.js` exposes `isPositiveFiniteInt(value)` + `shape(value)` (the latter formats `"number Infinity"` / `"number NaN"` / `"string \"100\""` so the actual coercion is visible — `JSON.stringify` collapses Infinity/NaN to `"null"` and hides the typo). Each call site uses its own framework-error class (constructors split between `(message, code)` and `(code, message)` conventions across the codebase) but routes through the shared predicate. **Sites swept:** `lib/safe-buffer.js` (`boundedChunkCollector`, `toBuffer`, `normalizeText` — the v0.6.57 fix only covered the first), `lib/atomic-file.js` (refactored from v0.6.68's inline check), `lib/csv.js` (parse `maxBytes` was operator-overridable to Infinity → multi-megabyte CSV bodies through unbounded), `lib/safe-url.js` (`maxUrlLength` opt was the v0.6.62 fix's escape hatch — now also bounded), `lib/mail-bounce.js` (`maxBytes` for inbound webhook body — DoS-shape on bounce intake). **Tests** — new `test/layer-0-primitives/numeric-bounds.test.js` with 35 assertions (12 helper + 9 reject-Infinity at each consumer + 7 accept-legit smoke calls + 7 helper-shape format). Smoke 7258 → 7293 / wiki e2e 178 / Linux 86.0s / eslint clean / shellcheck clean.
12
18
 
13
19
  - **0.6.68** (2026-05-03) — `b.atomicFile.read` / `readSync` enforce strict `maxBytes` validation. Same bug class as v0.6.57's `safeBuffer.boundedChunkCollector` fix: pre-fix the size check was `if (stat.size > opts.maxBytes)`, which silently accepted `Infinity` (`stat.size > Infinity` always false → reads any file regardless of size, defeating the OOM cap). Operators with `Number(env.MAX_READ_BYTES || "")` coercion bugs got `NaN` on missing values and unbounded reads on Infinity-typo'd values. New `_validateMaxBytes` runs before the size check and rejects `Infinity` / `NaN` / non-integer / negative / zero / non-number with `atomic-file/bad-opt`. Real-world consumers (vault.initPlaintext, audit-sign, db.loadOrCreateDbKey, etc.) all pass real positive integers via `C.BYTES.*` helpers and are unaffected. **Tests** — 7 new layer-0 boot-validation assertions in `test/00-primitives.js` (Infinity, NaN, 0, -1, 3.5, "100", null all reject). Smoke 7251 → 7258 / wiki e2e 178 / Linux 86.0s / eslint clean / shellcheck clean.
package/README.md CHANGED
@@ -64,7 +64,7 @@ Full primitive-by-primitive docs live at [blamejs.com](https://blamejs.com), whi
64
64
  - **Communication** — [WebSockets](https://blamejs.com/websockets) · [Mail](https://blamejs.com/mail) · [Notifications](https://blamejs.com/notifications)
65
65
  - **Tools** — [Observability](https://blamejs.com/observability) · [Testing](https://blamejs.com/testing) · [i18n & Locale](https://blamejs.com/i18n-locale) · [Format Helpers](https://blamejs.com/format-helpers)
66
66
  - **Compliance** — [Compliance Patterns](https://blamejs.com/compliance-patterns)
67
- - **Production** — [Cluster Mode](https://blamejs.com/cluster) · [Reliability](https://blamejs.com/reliability) · [Backup & Restore](https://blamejs.com/backup-restore)
67
+ - **Production** — [Cluster Mode](https://blamejs.com/cluster) · [Reliability](https://blamejs.com/reliability) · [Backup & Restore](https://blamejs.com/backup-restore) · [Quality Contract](https://blamejs.com/quality-contract)
68
68
 
69
69
  ## CLI
70
70
 
@@ -124,6 +124,16 @@ These libraries are exceptional work — blamejs wouldn't exist without them. Al
124
124
 
125
125
  Because when something breaks, `blame` should know exactly where it lives. We own the stack so you don't have to chase the fault across an ecosystem.
126
126
 
127
+ ## Quality contract
128
+
129
+ Every release passes a layered gate at `test/layer-0-primitives/codebase-patterns.test.js` that operates on lib/ source:
130
+
131
+ - **Bug-class detectors** — raw byte / time literals, `JSON.parse` on operator input without size cap, numeric opts that silently accept `Infinity` / `NaN`, ReDoS-risky regex without length cap, hash / token compares without `timingSafeEqual`, raw `new URL` skipping the SSRF gate, `Math.random()` in security-sensitive paths, and a couple dozen others — each a bug class the framework already swept once and won't re-introduce.
132
+ - **Inline-shape catalog (n=1)** — every primitive that's been extracted (`validateOpts.requireNonEmptyString`, `safeAsync.makeScheduledFlush`, `dbSchema.runInTransaction`, etc.) registers the inline shape it replaced; new code that re-implements the shape fails the gate even if it's the only file matching.
133
+ - **Cluster allowlist (n>=3)** — duplicate-block detection across files. Genuine new clusters get extracted; clusters that resist extraction (parser error class signature mismatches, framework-convention shapes, cross-domain coincidences) get an entry with a documented structural reason. No silent allowlisting.
134
+
135
+ The gate is part of `node test/smoke.js`; the framework refuses to release without it green.
136
+
127
137
  ## Contributing
128
138
 
129
139
  Patches welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the dev setup, house rules (zero npm runtime deps, PQC-only crypto, audit-on-every-action, ship-complete-not-incremental), and the PR loop. New to the codebase? Start with [ARCHITECTURE.md](ARCHITECTURE.md) for the orientation map.
package/index.js CHANGED
@@ -57,9 +57,6 @@ var ntpCheck = require("./lib/ntp-check");
57
57
  var auditSign = require("./lib/audit-sign");
58
58
  var objectStore = require("./lib/object-store");
59
59
  var retry = require("./lib/retry");
60
- // objectStoreRetry is preserved as a re-export of the canonical b.retry
61
- // for backward compatibility with consumers of the pre-v0.2.24 surface.
62
- var objectStoreRetry = require("./lib/object-store/retry");
63
60
  var queue = require("./lib/queue");
64
61
  var logStream = require("./lib/log-stream");
65
62
  var redact = require("./lib/redact");
@@ -90,7 +87,7 @@ var ssrfGuard = require("./lib/ssrf-guard");
90
87
  var authHeader = require("./lib/auth-header");
91
88
  var auth = {
92
89
  password: require("./lib/auth/password"),
93
- totp: require("./lib/auth/totp"),
90
+ totp: require("./lib/totp"),
94
91
  passkey: require("./lib/auth/passkey"),
95
92
  jwt: require("./lib/auth/jwt"),
96
93
  oauth: require("./lib/auth/oauth"),
@@ -187,7 +184,6 @@ module.exports = {
187
184
  storage: storage,
188
185
  objectStore: objectStore,
189
186
  retry: retry,
190
- objectStoreRetry: objectStoreRetry,
191
187
  queue: queue,
192
188
  logStream: logStream,
193
189
  redact: redact,
@@ -195,6 +191,7 @@ module.exports = {
195
191
  middleware: middleware,
196
192
  atomicFile: atomicFile,
197
193
  parsers: parsers,
194
+ safeEnv: parsers.env,
198
195
  cluster: cluster,
199
196
  frameworkSchema: frameworkSchema,
200
197
  clusterStorage: clusterStorage,
package/lib/api-key.js CHANGED
@@ -68,10 +68,7 @@ var { ApiKeyError } = require("./framework-error");
68
68
 
69
69
  var observability = lazyRequire(function () { return require("./observability"); });
70
70
 
71
- function _emitEvent(name, value, labels) {
72
- try { observability().event(name, value, labels || {}); }
73
- catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
74
- }
71
+ function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
75
72
 
76
73
  var _err = ApiKeyError.factory;
77
74
 
@@ -110,8 +107,8 @@ var DEFAULT_ROTATE_GRACE_MS = C.TIME.days(7);
110
107
  // `auditSuccess: false`. Failures stay on regardless.
111
108
  var DEFAULTS = Object.freeze({
112
109
  prefix: "bk",
113
- idBytes: 8, // 16 hex chars
114
- secretBytes: 16, // 32 hex chars
110
+ idBytes: C.BYTES.bytes(8), // 16 hex chars
111
+ secretBytes: C.BYTES.bytes(16), // 32 hex chars
115
112
  trackLastUsedAt: true, // visibility on dormant / leaked keys
116
113
  auditFailures: true, // failure events are actionable signals
117
114
  auditSuccess: true, // compliance trail — opt out at extreme volume
@@ -143,30 +140,15 @@ function _validateIdentifier(name, value) {
143
140
  }
144
141
 
145
142
  function _validateCreateOpts(opts) {
146
- if (!opts || typeof opts !== "object") {
147
- throw _err("BAD_OPT", "apiKey.create: opts must be an object");
148
- }
143
+ validateOpts.requireObject(opts, "apiKey.create", ApiKeyError);
149
144
  _validateIdentifier("apiKey.create: namespace", opts.namespace);
150
145
  if (opts.prefix !== undefined) _validateIdentifier("apiKey.create: prefix", opts.prefix);
151
- if (opts.idBytes !== undefined && !_isPositiveInt(opts.idBytes)) {
152
- throw _err("BAD_OPT", "apiKey.create: idBytes must be a positive integer");
153
- }
154
- if (opts.secretBytes !== undefined && !_isPositiveInt(opts.secretBytes)) {
155
- throw _err("BAD_OPT", "apiKey.create: secretBytes must be a positive integer");
156
- }
157
- if (opts.trackLastUsedAt !== undefined && typeof opts.trackLastUsedAt !== "boolean") {
158
- throw _err("BAD_OPT", "apiKey.create: trackLastUsedAt must be a boolean");
159
- }
160
- if (opts.auditFailures !== undefined && typeof opts.auditFailures !== "boolean") {
161
- throw _err("BAD_OPT", "apiKey.create: auditFailures must be a boolean");
162
- }
163
- if (opts.auditSuccess !== undefined && typeof opts.auditSuccess !== "boolean") {
164
- throw _err("BAD_OPT", "apiKey.create: auditSuccess must be a boolean");
165
- }
166
- if (opts.purgeAfterMs !== undefined &&
167
- (typeof opts.purgeAfterMs !== "number" || !isFinite(opts.purgeAfterMs) || opts.purgeAfterMs < 0)) {
168
- throw _err("BAD_OPT", "apiKey.create: purgeAfterMs must be a non-negative finite number");
169
- }
146
+ validateOpts.optionalPositiveInt(opts.idBytes, "apiKey.create: idBytes", ApiKeyError);
147
+ validateOpts.optionalPositiveInt(opts.secretBytes, "apiKey.create: secretBytes", ApiKeyError);
148
+ validateOpts.optionalBoolean(opts.trackLastUsedAt, "apiKey.create: trackLastUsedAt", ApiKeyError);
149
+ validateOpts.optionalBoolean(opts.auditFailures, "apiKey.create: auditFailures", ApiKeyError);
150
+ validateOpts.optionalBoolean(opts.auditSuccess, "apiKey.create: auditSuccess", ApiKeyError);
151
+ validateOpts.optionalFiniteNonNegative(opts.purgeAfterMs, "apiKey.create: purgeAfterMs", ApiKeyError);
170
152
  if (opts.hashAlgo !== undefined) {
171
153
  if (typeof opts.hashAlgo !== "string" ||
172
154
  (opts.hashAlgo !== "shake256" && opts.hashAlgo !== "argon2id")) {
@@ -174,23 +156,13 @@ function _validateCreateOpts(opts) {
174
156
  JSON.stringify(opts.hashAlgo));
175
157
  }
176
158
  }
177
- if (opts.audit !== undefined && opts.audit !== null) {
178
- if (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function") {
179
- throw _err("BAD_OPT", "apiKey.create: audit must be a b.audit-shaped object (safeEmit fn)");
180
- }
181
- }
182
- if (opts.clock !== undefined && typeof opts.clock !== "function") {
183
- throw _err("BAD_OPT", "apiKey.create: clock must be a function or undefined");
184
- }
159
+ validateOpts.auditShape(opts.audit, "apiKey.create", ApiKeyError);
160
+ validateOpts.optionalFunction(opts.clock, "apiKey.create: clock", ApiKeyError);
185
161
  }
186
162
 
187
163
  function _validateIssueOpts(opts) {
188
- if (!opts || typeof opts !== "object") {
189
- throw _err("BAD_OPT", "apiKey.issue: opts must be an object");
190
- }
191
- if (typeof opts.ownerId !== "string" || opts.ownerId.length === 0) {
192
- throw _err("MISSING_OWNER", "apiKey.issue: ownerId must be a non-empty string");
193
- }
164
+ validateOpts.requireObject(opts, "apiKey.issue", ApiKeyError);
165
+ validateOpts.requireNonEmptyString(opts.ownerId, "apiKey.issue: ownerId", ApiKeyError, "MISSING_OWNER");
194
166
  if (opts.scopes !== undefined) {
195
167
  if (!Array.isArray(opts.scopes)) {
196
168
  throw _err("BAD_SCOPES", "apiKey.issue: scopes must be an array of strings");
@@ -257,23 +229,20 @@ function create(opts) {
257
229
  "purgeAfterMs", "hashAlgo", "audit", "clock",
258
230
  ], "apiKey");
259
231
  _validateCreateOpts(opts);
260
- var prefix = opts.prefix || DEFAULTS.prefix;
232
+ var cfg = validateOpts.applyDefaults(opts, DEFAULTS);
233
+ var prefix = cfg.prefix;
261
234
  var namespace = opts.namespace;
262
- var idBytes = opts.idBytes || DEFAULTS.idBytes;
263
- var secretBytes = opts.secretBytes || DEFAULTS.secretBytes;
264
- var trackLastUsedAt = (opts.trackLastUsedAt === undefined) ? DEFAULTS.trackLastUsedAt : opts.trackLastUsedAt;
265
- var auditFailures = (opts.auditFailures === undefined) ? DEFAULTS.auditFailures : opts.auditFailures;
266
- var auditSuccess = (opts.auditSuccess === undefined) ? DEFAULTS.auditSuccess : opts.auditSuccess;
267
- var purgeAfterMs = (opts.purgeAfterMs === undefined) ? DEFAULTS.purgeAfterMs : opts.purgeAfterMs;
268
- var hashAlgo = opts.hashAlgo || DEFAULTS.hashAlgo;
235
+ var idBytes = cfg.idBytes;
236
+ var secretBytes = cfg.secretBytes;
237
+ var trackLastUsedAt = cfg.trackLastUsedAt;
238
+ var auditFailures = cfg.auditFailures;
239
+ var auditSuccess = cfg.auditSuccess;
240
+ var purgeAfterMs = cfg.purgeAfterMs;
241
+ var hashAlgo = cfg.hashAlgo;
269
242
  var audit = opts.audit || null;
270
243
  var clock = opts.clock || function () { return Date.now(); };
271
244
 
272
- function _emit(action, info) {
273
- if (!audit) return;
274
- try { audit.safeEmit(Object.assign({ action: action }, info)); }
275
- catch (_e) { /* audit best-effort */ }
276
- }
245
+ var _emit = validateOpts.makeAuditEmitter(audit);
277
246
 
278
247
  // Build the audit actor by extracting the 5 W's from the supplied
279
248
  // request (WHO/WHERE/HOW), then layering caller-supplied context
@@ -50,9 +50,12 @@
50
50
  */
51
51
 
52
52
  var fs = require("fs");
53
+ var nb = require("./numeric-bounds");
53
54
  var safeJson = require("./safe-json");
54
55
  var { FrameworkError } = require("./framework-error");
55
56
 
57
+ var DEFAULT_MAX_DEPTH = 0x08;
58
+
56
59
  class ApiSnapshotError extends FrameworkError {
57
60
  constructor(code, message) {
58
61
  super(message, code);
@@ -115,7 +118,7 @@ function capture(target, opts) {
115
118
  throw new ApiSnapshotError("api-snapshot/bad-target",
116
119
  "capture: target must be a module's exports object");
117
120
  }
118
- var maxDepth = typeof opts.maxDepth === "number" && opts.maxDepth > 0 ? opts.maxDepth : 8;
121
+ var maxDepth = nb.isPositiveFiniteInt(opts.maxDepth) ? opts.maxDepth : DEFAULT_MAX_DEPTH;
119
122
  var skipUnderscore = opts.skipUnderscore !== false;
120
123
  var snapshot = _walkNode(target, 0, maxDepth, new Set(), skipUnderscore);
121
124
  if (snapshot.type !== "object") {
@@ -81,17 +81,23 @@
81
81
  */
82
82
 
83
83
  var safeAsync = require("./safe-async");
84
+ var nb = require("./numeric-bounds");
85
+ var tracing = null;
86
+ try { tracing = require("./tracing"); } catch (_e) { /* tracing optional */ }
84
87
  var { defineClass } = require("./framework-error");
85
88
  var { boot } = require("./log");
89
+ var C = require("./constants");
86
90
 
87
91
  var AppShutdownError = defineClass("AppShutdownError", { alwaysPermanent: true });
88
92
  var log = boot("app-shutdown");
89
93
 
90
- var DEFAULT_GRACE_MS = 30000;
94
+ var DEFAULT_GRACE_MS = C.TIME.seconds(30);
91
95
 
92
96
  function create(opts) {
93
97
  opts = opts || {};
94
- var graceMs = typeof opts.graceMs === "number" && opts.graceMs > 0 ? opts.graceMs : DEFAULT_GRACE_MS;
98
+ nb.requirePositiveFiniteIntIfPresent(opts.graceMs,
99
+ "app-shutdown.create: opts.graceMs", AppShutdownError, "app-shutdown/bad-grace-ms");
100
+ var graceMs = opts.graceMs !== undefined ? opts.graceMs : DEFAULT_GRACE_MS;
95
101
  var phases = Array.isArray(opts.phases) ? opts.phases.slice() : [];
96
102
  var installSignalHandlers = !!opts.installSignalHandlers;
97
103
  for (var i = 0; i < phases.length; i++) {
@@ -189,8 +195,6 @@ function create(opts) {
189
195
  draining = true;
190
196
  startTime = Date.now();
191
197
  shutdownPromise = (async function () {
192
- var tracing = null;
193
- try { tracing = require("./tracing"); } catch (_e) { /* tracing optional */ }
194
198
  var phaseResults = [];
195
199
  var remainingPhases = phases.length;
196
200
  for (var i = 0; i < phases.length; i++) {
@@ -295,7 +299,7 @@ function standardPhases(components) {
295
299
  phases.push({
296
300
  name: "mark-draining",
297
301
  run: function () { components.health.markShuttingDown(); },
298
- timeoutMs: 1000,
302
+ timeoutMs: C.TIME.seconds(1),
299
303
  });
300
304
  }
301
305
 
@@ -308,29 +312,29 @@ function standardPhases(components) {
308
312
  phases.push({
309
313
  name: "scheduler",
310
314
  run: function () { return components.scheduler.stop(); },
311
- timeoutMs: 5000,
315
+ timeoutMs: C.TIME.seconds(5),
312
316
  });
313
317
  }
314
318
 
315
319
  if (components.jobs && typeof components.jobs.shutdown === "function") {
316
320
  phases.push({
317
321
  name: "jobs",
318
- run: function () { return components.jobs.shutdown({ timeoutMs: 5000 }); },
319
- timeoutMs: 8000,
322
+ run: function () { return components.jobs.shutdown({ timeoutMs: C.TIME.seconds(5) }); },
323
+ timeoutMs: C.TIME.seconds(8),
320
324
  });
321
325
  } else if (components.queue && typeof components.queue.shutdown === "function") {
322
326
  phases.push({
323
327
  name: "queue",
324
- run: function () { return components.queue.shutdown({ timeoutMs: 5000 }); },
325
- timeoutMs: 8000,
328
+ run: function () { return components.queue.shutdown({ timeoutMs: C.TIME.seconds(5) }); },
329
+ timeoutMs: C.TIME.seconds(8),
326
330
  });
327
331
  }
328
332
 
329
333
  if (components.router && typeof components.router.closeWebSockets === "function") {
330
334
  phases.push({
331
335
  name: "websockets",
332
- run: function () { return components.router.closeWebSockets({ timeoutMs: 3000 }); },
333
- timeoutMs: 5000,
336
+ run: function () { return components.router.closeWebSockets({ timeoutMs: C.TIME.seconds(3) }); },
337
+ timeoutMs: C.TIME.seconds(5),
334
338
  });
335
339
  }
336
340
 
@@ -342,7 +346,7 @@ function standardPhases(components) {
342
346
  components.server.close(function () { resolve(); });
343
347
  });
344
348
  },
345
- timeoutMs: 10000,
349
+ timeoutMs: C.TIME.seconds(10),
346
350
  });
347
351
  }
348
352
 
@@ -350,7 +354,7 @@ function standardPhases(components) {
350
354
  phases.push({
351
355
  name: "cluster",
352
356
  run: function () { return components.cluster.shutdown(); },
353
- timeoutMs: 5000,
357
+ timeoutMs: C.TIME.seconds(5),
354
358
  });
355
359
  }
356
360
 
@@ -358,7 +362,7 @@ function standardPhases(components) {
358
362
  phases.push({
359
363
  name: "db",
360
364
  run: function () { components.db.close(); },
361
- timeoutMs: 5000,
365
+ timeoutMs: C.TIME.seconds(5),
362
366
  });
363
367
  }
364
368
 
@@ -366,7 +370,7 @@ function standardPhases(components) {
366
370
  phases.push({
367
371
  name: "external-db",
368
372
  run: function () { return components.externalDb.shutdown(); },
369
- timeoutMs: 5000,
373
+ timeoutMs: C.TIME.seconds(5),
370
374
  });
371
375
  }
372
376
 
package/lib/app.js CHANGED
@@ -92,6 +92,7 @@
92
92
  var fs = require("fs");
93
93
  var path = require("path");
94
94
  var appShutdown = require("./app-shutdown");
95
+ var C = require("./constants");
95
96
  var cluster = require("./cluster");
96
97
  var db = require("./db");
97
98
  var externalDb = require("./external-db");
@@ -269,7 +270,7 @@ async function createApp(opts) {
269
270
  orchestrator.addPhase({
270
271
  name: "beforeStop",
271
272
  run: shutdownOpts.beforeStop,
272
- timeoutMs: shutdownOpts.beforeStopTimeoutMs || 5000,
273
+ timeoutMs: shutdownOpts.beforeStopTimeoutMs || C.TIME.seconds(5),
273
274
  });
274
275
  }
275
276
 
@@ -278,14 +279,14 @@ async function createApp(opts) {
278
279
  orchestrator.addPhase({
279
280
  name: "drain-in-flight",
280
281
  run: orchestrator.waitInFlight,
281
- timeoutMs: shutdownOpts.drainTimeoutMs || 10000,
282
+ timeoutMs: shutdownOpts.drainTimeoutMs || C.TIME.seconds(10),
282
283
  });
283
284
 
284
285
  if (typeof shutdownOpts.afterDrain === "function") {
285
286
  orchestrator.addPhase({
286
287
  name: "afterDrain",
287
288
  run: shutdownOpts.afterDrain,
288
- timeoutMs: shutdownOpts.afterDrainTimeoutMs || 5000,
289
+ timeoutMs: shutdownOpts.afterDrainTimeoutMs || C.TIME.seconds(5),
289
290
  });
290
291
  }
291
292
 
@@ -314,7 +315,7 @@ async function createApp(opts) {
314
315
  name: "http-server",
315
316
  run: function () {
316
317
  return new Promise(function (resolve) {
317
- if (!server) return resolve();
318
+ if (!server) { resolve(); return; }
318
319
  server.close(function () {
319
320
  server = null; // address() now returns null
320
321
  listenPort = null;
@@ -323,7 +324,7 @@ async function createApp(opts) {
323
324
  });
324
325
  });
325
326
  },
326
- timeoutMs: 10000,
327
+ timeoutMs: C.TIME.seconds(10),
327
328
  };
328
329
  // Find the index right after the "websockets" phase if present;
329
330
  // otherwise prepend before "cluster" (or just push to end if
package/lib/archive.js CHANGED
@@ -36,26 +36,29 @@
36
36
  var zlib = require("node:zlib");
37
37
  var fs = require("node:fs");
38
38
  var nodeCrypto = require("node:crypto");
39
+ var C = require("./constants");
39
40
  var { defineClass } = require("./framework-error");
40
41
 
41
42
  var ArchiveError = defineClass("ArchiveError", { alwaysPermanent: true });
42
43
 
43
- // ZIP signatures
44
+ // ZIP signatures (APPNOTE 4.3 — file-format-fixed magic dwords)
44
45
  var SIG_LFH = 0x04034b50; // local file header
45
46
  var SIG_CFH = 0x02014b50; // central directory file header
46
47
  var SIG_EOCD = 0x06054b50; // end of central directory
47
48
 
48
- // Compression methods
49
- var METHOD_STORE = 0;
50
- var METHOD_DEFLATE = 8;
49
+ // Compression methods (APPNOTE 4.4.5 — protocol-fixed method IDs)
50
+ var METHOD_STORE_ID = 0;
51
+ var METHOD_DEFLATE_ID = 8;
51
52
 
52
53
  // CRC-32 — IEEE 802.3 polynomial. node:crypto has no native CRC32, so
53
54
  // we vendor the standard table-driven implementation.
55
+ var CRC32_TABLE_LEN = 256; // entries (one per byte value)
56
+ var CRC32_BIT_ITER = 8; // bits per byte during table build
54
57
  var CRC32_TABLE = (function () {
55
- var t = new Uint32Array(256);
56
- for (var i = 0; i < 256; i++) {
58
+ var t = new Uint32Array(CRC32_TABLE_LEN);
59
+ for (var i = 0; i < CRC32_TABLE_LEN; i++) {
57
60
  var c = i;
58
- for (var j = 0; j < 8; j++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
61
+ for (var j = 0; j < CRC32_BIT_ITER; j++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
59
62
  t[i] = c >>> 0;
60
63
  }
61
64
  return t;
@@ -107,18 +110,18 @@ function zip() {
107
110
  "addFile: content must be a Buffer or string, got " + typeof content);
108
111
 
109
112
  opts = opts || {};
110
- var method = opts.method === "store" ? METHOD_STORE : METHOD_DEFLATE;
113
+ var method = opts.method === "store" ? METHOD_STORE_ID : METHOD_DEFLATE_ID;
111
114
  var mtime = opts.mtime instanceof Date ? opts.mtime : new Date();
112
115
 
113
116
  var crc = _crc32(bodyBuf);
114
117
  var stored = bodyBuf;
115
- if (method === METHOD_DEFLATE) {
118
+ if (method === METHOD_DEFLATE_ID) {
116
119
  stored = zlib.deflateRawSync(bodyBuf);
117
120
  // If deflate didn't shrink it (small/already-compressed inputs),
118
121
  // fall back to STORE to save the operator a few bytes.
119
122
  if (stored.length >= bodyBuf.length) {
120
123
  stored = bodyBuf;
121
- method = METHOD_STORE;
124
+ method = METHOD_STORE_ID;
122
125
  }
123
126
  }
124
127
 
@@ -135,42 +138,47 @@ function zip() {
135
138
  function _buildLocalFileHeader(entry) {
136
139
  var nameBuf = Buffer.from(entry.name, "utf8");
137
140
  var dt = _msdosDateTime(entry.mtime);
138
- var hdr = Buffer.alloc(30);
139
- hdr.writeUInt32LE(SIG_LFH, 0);
140
- hdr.writeUInt16LE(20, 4); // version needed
141
- hdr.writeUInt16LE(0x0800, 6); // flags: bit 11 = UTF-8 name
142
- hdr.writeUInt16LE(entry.method, 8);
143
- hdr.writeUInt16LE(dt.time, 10);
144
- hdr.writeUInt16LE(dt.date, 12);
145
- hdr.writeUInt32LE(entry.crc, 14);
146
- hdr.writeUInt32LE(entry.stored.length, 18);
147
- hdr.writeUInt32LE(entry.uncompressedSize, 22);
148
- hdr.writeUInt16LE(nameBuf.length, 26);
149
- hdr.writeUInt16LE(0, 28); // extra field length
141
+ // APPNOTE 4.3.7 — local file header. Offsets are byte positions
142
+ // within the 30-byte fixed header; each route through C.BYTES.bytes
143
+ // so the framework's byte-math discipline applies even to format-
144
+ // fixed offsets.
145
+ var hdr = Buffer.alloc(C.BYTES.bytes(30));
146
+ hdr.writeUInt32LE(SIG_LFH, C.BYTES.bytes(0));
147
+ hdr.writeUInt16LE(20, C.BYTES.bytes(4)); // version needed
148
+ hdr.writeUInt16LE(0x0800, C.BYTES.bytes(6)); // flags: bit 11 = UTF-8 name
149
+ hdr.writeUInt16LE(entry.method, C.BYTES.bytes(0x08));
150
+ hdr.writeUInt16LE(dt.time, C.BYTES.bytes(10));
151
+ hdr.writeUInt16LE(dt.date, C.BYTES.bytes(12));
152
+ hdr.writeUInt32LE(entry.crc, C.BYTES.bytes(14));
153
+ hdr.writeUInt32LE(entry.stored.length, C.BYTES.bytes(18));
154
+ hdr.writeUInt32LE(entry.uncompressedSize, C.BYTES.bytes(22));
155
+ hdr.writeUInt16LE(nameBuf.length, C.BYTES.bytes(26));
156
+ hdr.writeUInt16LE(0, C.BYTES.bytes(28)); // extra field length
150
157
  return Buffer.concat([hdr, nameBuf]);
151
158
  }
152
159
 
153
160
  function _buildCentralDirectoryEntry(entry, lfhOffset) {
154
161
  var nameBuf = Buffer.from(entry.name, "utf8");
155
162
  var dt = _msdosDateTime(entry.mtime);
156
- var hdr = Buffer.alloc(46);
157
- hdr.writeUInt32LE(SIG_CFH, 0);
158
- hdr.writeUInt16LE(0x033f, 4); // version made by (UNIX | 6.3)
159
- hdr.writeUInt16LE(20, 6); // version needed
160
- hdr.writeUInt16LE(0x0800, 8); // flags: bit 11 = UTF-8
161
- hdr.writeUInt16LE(entry.method, 10);
162
- hdr.writeUInt16LE(dt.time, 12);
163
- hdr.writeUInt16LE(dt.date, 14);
164
- hdr.writeUInt32LE(entry.crc, 16);
165
- hdr.writeUInt32LE(entry.stored.length, 20);
166
- hdr.writeUInt32LE(entry.uncompressedSize, 24);
167
- hdr.writeUInt16LE(nameBuf.length, 28);
168
- hdr.writeUInt16LE(0, 30); // extra field length
169
- hdr.writeUInt16LE(0, 32); // file comment length
170
- hdr.writeUInt16LE(0, 34); // disk number start
171
- hdr.writeUInt16LE(0, 36); // internal file attributes
172
- hdr.writeUInt32LE(0, 38); // external file attributes
173
- hdr.writeUInt32LE(lfhOffset, 42);
163
+ // APPNOTE 4.3.12 — central directory file header (46-byte fixed prefix).
164
+ var hdr = Buffer.alloc(C.BYTES.bytes(46));
165
+ hdr.writeUInt32LE(SIG_CFH, C.BYTES.bytes(0));
166
+ hdr.writeUInt16LE(0x033f, C.BYTES.bytes(4)); // version made by (UNIX | 6.3)
167
+ hdr.writeUInt16LE(20, C.BYTES.bytes(6)); // version needed
168
+ hdr.writeUInt16LE(0x0800, C.BYTES.bytes(0x08)); // flags: bit 11 = UTF-8
169
+ hdr.writeUInt16LE(entry.method, C.BYTES.bytes(10));
170
+ hdr.writeUInt16LE(dt.time, C.BYTES.bytes(12));
171
+ hdr.writeUInt16LE(dt.date, C.BYTES.bytes(14));
172
+ hdr.writeUInt32LE(entry.crc, C.BYTES.bytes(0x10));
173
+ hdr.writeUInt32LE(entry.stored.length, C.BYTES.bytes(20));
174
+ hdr.writeUInt32LE(entry.uncompressedSize, C.BYTES.bytes(0x18));
175
+ hdr.writeUInt16LE(nameBuf.length, C.BYTES.bytes(28));
176
+ hdr.writeUInt16LE(0, C.BYTES.bytes(30)); // extra field length
177
+ hdr.writeUInt16LE(0, C.BYTES.bytes(0x20)); // file comment length
178
+ hdr.writeUInt16LE(0, C.BYTES.bytes(34)); // disk number start
179
+ hdr.writeUInt16LE(0, C.BYTES.bytes(36)); // internal file attributes
180
+ hdr.writeUInt32LE(0, C.BYTES.bytes(38)); // external file attributes
181
+ hdr.writeUInt32LE(lfhOffset, C.BYTES.bytes(42));
174
182
  return Buffer.concat([hdr, nameBuf]);
175
183
  }
176
184
 
@@ -196,16 +204,16 @@ function zip() {
196
204
  pieces.push(cdh);
197
205
  cdSize += cdh.length;
198
206
  }
199
- // End of Central Directory
200
- var eocd = Buffer.alloc(22);
201
- eocd.writeUInt32LE(SIG_EOCD, 0);
202
- eocd.writeUInt16LE(0, 4); // disk number
203
- eocd.writeUInt16LE(0, 6); // disk where CD starts
204
- eocd.writeUInt16LE(entries.length, 8); // entries on this disk
205
- eocd.writeUInt16LE(entries.length, 10); // total entries
206
- eocd.writeUInt32LE(cdSize, 12); // size of central directory
207
- eocd.writeUInt32LE(cdStart, 16); // offset of central directory
208
- eocd.writeUInt16LE(0, 20); // comment length
207
+ // APPNOTE 4.3.16 — end of central directory record (22-byte fixed).
208
+ var eocd = Buffer.alloc(C.BYTES.bytes(22));
209
+ eocd.writeUInt32LE(SIG_EOCD, C.BYTES.bytes(0));
210
+ eocd.writeUInt16LE(0, C.BYTES.bytes(4)); // disk number
211
+ eocd.writeUInt16LE(0, C.BYTES.bytes(6)); // disk where CD starts
212
+ eocd.writeUInt16LE(entries.length, C.BYTES.bytes(0x08)); // entries on this disk
213
+ eocd.writeUInt16LE(entries.length, C.BYTES.bytes(10)); // total entries
214
+ eocd.writeUInt32LE(cdSize, C.BYTES.bytes(12)); // size of central directory
215
+ eocd.writeUInt32LE(cdStart, C.BYTES.bytes(0x10)); // offset of central directory
216
+ eocd.writeUInt16LE(0, C.BYTES.bytes(20)); // comment length
209
217
  pieces.push(eocd);
210
218
  return Buffer.concat(pieces);
211
219
  }
@@ -36,12 +36,15 @@ var path = require("path");
36
36
  var { generateToken, sha3Hash } = require("./crypto");
37
37
  var safeJson = require("./safe-json");
38
38
  var C = require("./constants");
39
+ var { boot } = require("./log");
39
40
  var safeBuffer = require("./safe-buffer");
40
41
  var numericBounds = require("./numeric-bounds");
41
42
  var safeAsync = require("./safe-async");
42
43
  var retry = require("./retry");
43
44
  var { FrameworkError } = require("./framework-error");
44
45
 
46
+ var log = boot("atomic-file");
47
+
45
48
  var DEFAULTS = {
46
49
  maxBytes: C.BYTES.mib(64), // 64 MiB ceiling on read
47
50
  retryAttempts: 5,
@@ -203,7 +206,7 @@ function writeSync(filepath, data, opts) {
203
206
  var dir = path.dirname(filepath);
204
207
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
205
208
 
206
- var tmpPath = filepath + ".tmp-" + generateToken(8);
209
+ var tmpPath = filepath + ".tmp-" + generateToken(C.BYTES.bytes(8));
207
210
  var renamed = false;
208
211
  try {
209
212
  var fd = fs.openSync(tmpPath, "w", opts.fileMode);
@@ -279,7 +282,7 @@ async function write(filepath, data, opts) {
279
282
  return new Promise(function (resolve, reject) {
280
283
  var dir = path.dirname(filepath);
281
284
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
282
- var tmpPath = filepath + ".tmp-" + generateToken(8);
285
+ var tmpPath = filepath + ".tmp-" + generateToken(C.BYTES.bytes(8));
283
286
  var renamed = false;
284
287
  try {
285
288
  var fd = fs.openSync(tmpPath, "w", opts.fileMode);
@@ -424,7 +427,8 @@ async function lock(filepath, fn, opts) {
424
427
  try {
425
428
  var stat = fs.statSync(lockPath);
426
429
  if (Date.now() - stat.mtimeMs > C.TIME.minutes(5)) {
427
- try { fs.unlinkSync(lockPath); } catch (_e) {}
430
+ try { fs.unlinkSync(lockPath); }
431
+ catch (uerr) { log.debug("stale-lock unlink failed", { path: lockPath, error: uerr.message }); }
428
432
  continue;
429
433
  }
430
434
  } catch (_e) { /* stat raced with another process — keep waiting */ }
@@ -448,8 +452,10 @@ async function lock(filepath, fn, opts) {
448
452
  try {
449
453
  return await fn();
450
454
  } finally {
451
- try { fs.closeSync(fd); } catch (_e) {}
452
- try { fs.unlinkSync(lockPath); } catch (_e) {}
455
+ try { fs.closeSync(fd); }
456
+ catch (cerr) { log.debug("lock fd close failed", { error: cerr.message }); }
457
+ try { fs.unlinkSync(lockPath); }
458
+ catch (uerr) { log.debug("lock release unlink failed", { path: lockPath, error: uerr.message }); }
453
459
  }
454
460
  }
455
461