@blamejs/core 0.4.1

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 (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. package/package.json +39 -0
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+ /**
3
+ * backup-manifest — schema + validation for backup bundle manifests.
4
+ *
5
+ * Every backup bundle the framework produces carries a manifest.json
6
+ * at its root. The manifest is the single source of truth for: which
7
+ * files are in the bundle, their plaintext size + checksum, the per-
8
+ * file salt used by backup-crypto, and the wrapped vault key needed
9
+ * to interpret vault-sealed file contents post-restore.
10
+ *
11
+ * The encrypted file BYTES are stored separately under the bundle's
12
+ * files/ directory (one file per entry) — the manifest references
13
+ * them by relativePath. This keeps the manifest small enough to
14
+ * inspect / log without carrying gigabytes of base64 inline, and lets
15
+ * a restorer stream-decrypt files one at a time without loading the
16
+ * whole bundle into memory.
17
+ *
18
+ * Manifest format (v1):
19
+ *
20
+ * {
21
+ * version: 1,
22
+ * createdAt: "<ISO 8601 string>",
23
+ * framework: "blamejs",
24
+ * frameworkVersion: "0.1.84",
25
+ * vaultKeySalt: "<hex>", // salt used to wrap vault.key
26
+ * vaultKeyEnc: "<base64>", // wrapped vault.key bytes
27
+ * files: [
28
+ * {
29
+ * relativePath: "db.enc", // operator-facing path under dataDir
30
+ * encryptedPath: "files/db.enc.bin", // path within the bundle
31
+ * size: 12345, // plaintext size in bytes
32
+ * checksum: "<sha3-512 hex>", // checksum of the PLAINTEXT
33
+ * encryptedSize: 12369, // size of the encrypted blob (bytes)
34
+ * salt: "<hex>", // per-file Argon2 salt
35
+ * kind: "raw" // "raw" | "vault-sealed" | "plaintext"
36
+ * },
37
+ * ...
38
+ * ],
39
+ * metadata: { ... operator-supplied free-form ... }
40
+ * }
41
+ *
42
+ * The `kind` field hints at how the post-restore framework should
43
+ * interpret the file:
44
+ * - raw on-disk binary that the framework reads as-is
45
+ * (db.enc, db.key.enc, etc.)
46
+ * - vault-sealed the file's contents are vault-prefix wrapped
47
+ * (legacy-style sealed PEMs); restore writes them
48
+ * back as-is and the framework unseals at use
49
+ * - plaintext operator content not under vault discipline
50
+ * (config snapshots, README, etc.)
51
+ *
52
+ * The kind is informational — the bundler does NOT re-seal or unseal
53
+ * based on it; backup-crypto encrypts every file's bytes the same
54
+ * way regardless of kind. Operators / dashboards can use kind for
55
+ * post-restore validation.
56
+ *
57
+ * var bm = b.backupManifest;
58
+ *
59
+ * var manifest = bm.create({
60
+ * vaultKeySalt: "...",
61
+ * vaultKeyEnc: "<base64>",
62
+ * files: [...],
63
+ * metadata: { reason: "scheduled-daily" },
64
+ * });
65
+ * var json = bm.serialize(manifest); // canonical JSON for storage
66
+ *
67
+ * var loaded = bm.parse(json); // validates on parse, throws on error
68
+ * var v = bm.validate(loaded); // → { ok, errors } without throwing
69
+ */
70
+
71
+ var C = require("../constants");
72
+ var { FrameworkError } = require("../framework-error");
73
+
74
+ class BackupManifestError extends FrameworkError {
75
+ constructor(code, message) {
76
+ super(message, code);
77
+ this.name = "BackupManifestError";
78
+ this.permanent = true;
79
+ this.isBackupManifestError = true;
80
+ }
81
+ }
82
+
83
+ var FORMAT_VERSION = 1;
84
+ var FRAMEWORK_NAME = "blamejs";
85
+ var VALID_KINDS = { "raw": 1, "vault-sealed": 1, "plaintext": 1 };
86
+ var HEX_RE = /^[0-9a-fA-F]+$/;
87
+ var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
88
+
89
+ function _isHex(s, evenLength) {
90
+ if (typeof s !== "string" || s.length === 0) return false;
91
+ if (!HEX_RE.test(s)) return false;
92
+ if (evenLength && s.length % 2 !== 0) return false;
93
+ return true;
94
+ }
95
+ function _isBase64(s) {
96
+ return typeof s === "string" && s.length > 0 && BASE64_RE.test(s);
97
+ }
98
+ function _isIso8601(s) {
99
+ if (typeof s !== "string" || s.length === 0) return false;
100
+ var d = new Date(s);
101
+ return !isNaN(d.getTime()) && d.toISOString() === s;
102
+ }
103
+
104
+ function _validateFileEntry(f, idx, errors) {
105
+ if (!f || typeof f !== "object") {
106
+ errors.push("files[" + idx + "]: must be an object");
107
+ return;
108
+ }
109
+ if (typeof f.relativePath !== "string" || f.relativePath.length === 0) {
110
+ errors.push("files[" + idx + "].relativePath: required non-empty string");
111
+ } else if (f.relativePath.indexOf("..") !== -1 || /^[/\\]/.test(f.relativePath)) {
112
+ // No traversal — when restored, relativePath joins under dataDir
113
+ errors.push("files[" + idx + "].relativePath: must be a relative path without '..' or leading separator");
114
+ }
115
+ if (typeof f.encryptedPath !== "string" || f.encryptedPath.length === 0) {
116
+ errors.push("files[" + idx + "].encryptedPath: required non-empty string");
117
+ } else if (f.encryptedPath.indexOf("..") !== -1 || /^[/\\]/.test(f.encryptedPath)) {
118
+ errors.push("files[" + idx + "].encryptedPath: must be a relative path without '..' or leading separator");
119
+ }
120
+ if (typeof f.size !== "number" || !Number.isInteger(f.size) || f.size < 0) {
121
+ errors.push("files[" + idx + "].size: required non-negative integer");
122
+ }
123
+ if (typeof f.encryptedSize !== "number" || !Number.isInteger(f.encryptedSize) || f.encryptedSize < 0) {
124
+ errors.push("files[" + idx + "].encryptedSize: required non-negative integer");
125
+ }
126
+ if (!_isHex(f.checksum, true) || f.checksum.length !== 128) {
127
+ errors.push("files[" + idx + "].checksum: required 128-char hex string (sha3-512)");
128
+ }
129
+ if (!_isHex(f.salt, true)) {
130
+ errors.push("files[" + idx + "].salt: required hex string");
131
+ }
132
+ if (typeof f.kind !== "string" || !VALID_KINDS[f.kind]) {
133
+ errors.push("files[" + idx + "].kind: must be one of raw, vault-sealed, plaintext");
134
+ }
135
+ }
136
+
137
+ function validate(manifest) {
138
+ var errors = [];
139
+ if (!manifest || typeof manifest !== "object") {
140
+ return { ok: false, errors: ["manifest must be an object"] };
141
+ }
142
+ if (manifest.version !== FORMAT_VERSION) {
143
+ errors.push("version: required " + FORMAT_VERSION + ", got " + manifest.version);
144
+ }
145
+ if (manifest.framework !== FRAMEWORK_NAME) {
146
+ errors.push("framework: required '" + FRAMEWORK_NAME + "', got " + JSON.stringify(manifest.framework));
147
+ }
148
+ if (typeof manifest.frameworkVersion !== "string" || manifest.frameworkVersion.length === 0) {
149
+ errors.push("frameworkVersion: required non-empty string");
150
+ }
151
+ if (!_isIso8601(manifest.createdAt)) {
152
+ errors.push("createdAt: required ISO-8601 timestamp string");
153
+ }
154
+ if (!_isHex(manifest.vaultKeySalt, true)) {
155
+ errors.push("vaultKeySalt: required hex string");
156
+ }
157
+ if (!_isBase64(manifest.vaultKeyEnc)) {
158
+ errors.push("vaultKeyEnc: required base64 string");
159
+ }
160
+ if (!Array.isArray(manifest.files)) {
161
+ errors.push("files: required array");
162
+ } else {
163
+ // Detect duplicate relativePath / encryptedPath — would corrupt restore
164
+ var seenRel = Object.create(null);
165
+ var seenEnc = Object.create(null);
166
+ for (var i = 0; i < manifest.files.length; i++) {
167
+ _validateFileEntry(manifest.files[i], i, errors);
168
+ var f = manifest.files[i];
169
+ if (f && typeof f.relativePath === "string") {
170
+ if (seenRel[f.relativePath]) {
171
+ errors.push("files[" + i + "].relativePath: duplicate '" + f.relativePath + "'");
172
+ }
173
+ seenRel[f.relativePath] = true;
174
+ }
175
+ if (f && typeof f.encryptedPath === "string") {
176
+ if (seenEnc[f.encryptedPath]) {
177
+ errors.push("files[" + i + "].encryptedPath: duplicate '" + f.encryptedPath + "'");
178
+ }
179
+ seenEnc[f.encryptedPath] = true;
180
+ }
181
+ }
182
+ }
183
+ if (manifest.metadata !== undefined &&
184
+ (manifest.metadata === null || typeof manifest.metadata !== "object" || Array.isArray(manifest.metadata))) {
185
+ errors.push("metadata: must be a plain object when present");
186
+ }
187
+ return { ok: errors.length === 0, errors: errors };
188
+ }
189
+
190
+ function create(opts) {
191
+ opts = opts || {};
192
+ var manifest = {
193
+ version: FORMAT_VERSION,
194
+ framework: FRAMEWORK_NAME,
195
+ frameworkVersion: typeof opts.frameworkVersion === "string" && opts.frameworkVersion.length > 0
196
+ ? opts.frameworkVersion
197
+ : (C.version || "0.0.0"),
198
+ createdAt: opts.createdAt || new Date().toISOString(),
199
+ vaultKeySalt: opts.vaultKeySalt,
200
+ vaultKeyEnc: opts.vaultKeyEnc,
201
+ files: Array.isArray(opts.files) ? opts.files.slice() : [],
202
+ };
203
+ if (opts.metadata && typeof opts.metadata === "object" && !Array.isArray(opts.metadata)) {
204
+ manifest.metadata = Object.assign({}, opts.metadata);
205
+ }
206
+ var v = validate(manifest);
207
+ if (!v.ok) {
208
+ throw new BackupManifestError("backup-manifest/invalid",
209
+ "create: " + v.errors.join("; "));
210
+ }
211
+ return manifest;
212
+ }
213
+
214
+ function serialize(manifest) {
215
+ var v = validate(manifest);
216
+ if (!v.ok) {
217
+ throw new BackupManifestError("backup-manifest/invalid",
218
+ "serialize: " + v.errors.join("; "));
219
+ }
220
+ // Stable key ordering so the same manifest object always serializes
221
+ // to the same bytes (operators can hash the manifest as part of
222
+ // bundle-integrity logging without surprises across runs).
223
+ var canonical = {
224
+ version: manifest.version,
225
+ framework: manifest.framework,
226
+ frameworkVersion: manifest.frameworkVersion,
227
+ createdAt: manifest.createdAt,
228
+ vaultKeySalt: manifest.vaultKeySalt,
229
+ vaultKeyEnc: manifest.vaultKeyEnc,
230
+ files: manifest.files.map(function (f) {
231
+ return {
232
+ relativePath: f.relativePath,
233
+ encryptedPath: f.encryptedPath,
234
+ size: f.size,
235
+ encryptedSize: f.encryptedSize,
236
+ checksum: f.checksum,
237
+ salt: f.salt,
238
+ kind: f.kind,
239
+ };
240
+ }),
241
+ };
242
+ if (manifest.metadata) canonical.metadata = manifest.metadata;
243
+ return JSON.stringify(canonical, null, 2) + "\n";
244
+ }
245
+
246
+ function parse(jsonStr) {
247
+ if (typeof jsonStr !== "string" && !Buffer.isBuffer(jsonStr)) {
248
+ throw new BackupManifestError("backup-manifest/bad-input",
249
+ "parse: argument must be a string or Buffer");
250
+ }
251
+ var s = Buffer.isBuffer(jsonStr) ? jsonStr.toString("utf8") : jsonStr;
252
+ var obj;
253
+ try { obj = JSON.parse(s); }
254
+ catch (e) {
255
+ throw new BackupManifestError("backup-manifest/bad-json",
256
+ "parse: not valid JSON: " + ((e && e.message) || String(e)));
257
+ }
258
+ var v = validate(obj);
259
+ if (!v.ok) {
260
+ throw new BackupManifestError("backup-manifest/invalid",
261
+ "parse: " + v.errors.join("; "));
262
+ }
263
+ return obj;
264
+ }
265
+
266
+ module.exports = {
267
+ create: create,
268
+ validate: validate,
269
+ serialize: serialize,
270
+ parse: parse,
271
+ FORMAT_VERSION: FORMAT_VERSION,
272
+ FRAMEWORK_NAME: FRAMEWORK_NAME,
273
+ VALID_KINDS: VALID_KINDS,
274
+ BackupManifestError: BackupManifestError,
275
+ };
package/lib/bundler.js ADDED
@@ -0,0 +1,295 @@
1
+ "use strict";
2
+ /**
3
+ * bundler — content-hashed asset pipeline + manifest.
4
+ *
5
+ * What this primitive does today:
6
+ * - Reads each named entry from disk
7
+ * - Computes a content hash (SHA3-512, first 16 hex chars)
8
+ * - Writes the entry to outdir/<name>.<hash>.<ext> for cache-busting
9
+ * - Emits manifest.json mapping logical name → hashed filename
10
+ * - Optionally watches entries and rebuilds on change
11
+ *
12
+ * What it does NOT do today (deliberately deferred):
13
+ * - Module-graph resolution (esbuild-style multi-file bundling)
14
+ * - Tree shaking, dead-code elimination, AST transforms
15
+ * - Source maps
16
+ * - Minification (a regex-based pass mishandles comments inside
17
+ * string and regex literals; AST-based minification waits for the
18
+ * esbuild-wasm vendoring slice)
19
+ *
20
+ * Operators with multi-file ESM source either pre-concat manually and
21
+ * point bundler at the result, or use an external tool today; the
22
+ * bundler primitive's interface stays stable when full bundling lands.
23
+ *
24
+ * var bundler = b.bundler.create({
25
+ * entries: { app: "./public/js/app.js", styles: "./public/css/app.css" },
26
+ * outdir: "./public/dist",
27
+ * manifest: "manifest.json", // false to skip; default "manifest.json"
28
+ * hashLen: 16, // hex chars in filename hash (default 16)
29
+ * hash: true, // include hash in filename; false → name.ext
30
+ * cwd: process.cwd(),
31
+ * });
32
+ *
33
+ * var result = await bundler.build();
34
+ * // { outputs: [{ name, entry, path, hash, bytes, ext }], manifestPath, durationMs }
35
+ *
36
+ * bundler.watch(function (rebuild) { ... }); // rebuild on entry change
37
+ * await bundler.close(); // stop watchers
38
+ *
39
+ * Manifest format (manifest.json under outdir):
40
+ * { "app": "app.4a8c2f1d9e3b7062.js", "styles": "styles.b29f1e7c.css" }
41
+ *
42
+ * Integrates with lib/static.js: serve `outdir` as a static directory;
43
+ * lib/static.js's hashed-path detection sets long-cache headers on
44
+ * files that look hashed, and integrity() reads the manifest to
45
+ * generate Subresource Integrity attributes.
46
+ */
47
+
48
+ var path = require("path");
49
+ var fs = require("fs");
50
+ var crypto = require("./crypto");
51
+ var atomicFile = require("./atomic-file");
52
+ var { defineClass } = require("./framework-error");
53
+ var safeJson = require("./safe-json");
54
+ var validateOpts = require("./validate-opts");
55
+
56
+ var BundlerError = defineClass("BundlerError", { alwaysPermanent: true });
57
+
58
+ function _hashContent(buf, hexLen) {
59
+ // SHA3-512 → take the first hexLen hex chars. Same family as the
60
+ // framework's other content fingerprints (no SHA-256 for new code).
61
+ return crypto.sha3Hash(buf).slice(0, hexLen);
62
+ }
63
+
64
+ function _hashedName(baseName, hash, ext) {
65
+ return baseName + "." + hash + ext;
66
+ }
67
+
68
+ // outDir mode is 0o755 (world-readable) because the bundler emits
69
+ // assets a public HTTP server reads. Other framework dirs default to
70
+ // 0o700 via atomicFile.ensureDir.
71
+ function _ensureOutDir(p) {
72
+ try { atomicFile.ensureDir(p, 0o755); }
73
+ catch (e) {
74
+ if (e && e.code !== "EEXIST") {
75
+ throw new BundlerError("bundler/mkdir-failed",
76
+ "could not create outdir '" + p + "': " + ((e && e.message) || String(e)));
77
+ }
78
+ }
79
+ }
80
+
81
+ function _validateEntries(entries) {
82
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
83
+ throw new BundlerError("bundler/no-entries",
84
+ "bundler.create requires opts.entries (a { name: path } map)");
85
+ }
86
+ var names = Object.keys(entries);
87
+ if (names.length === 0) {
88
+ throw new BundlerError("bundler/no-entries",
89
+ "bundler.create: opts.entries map must have at least one entry");
90
+ }
91
+ // Reject names with path separators or '..' so operator-supplied
92
+ // logical names can't escape the outdir on write.
93
+ for (var i = 0; i < names.length; i++) {
94
+ var n = names[i];
95
+ if (typeof n !== "string" || n.length === 0 ||
96
+ /[\\/]/.test(n) || n === ".." || n === ".") {
97
+ throw new BundlerError("bundler/bad-entry-name",
98
+ "entry name '" + n + "' must be a non-empty string without path separators");
99
+ }
100
+ var p = entries[n];
101
+ if (typeof p !== "string" || p.length === 0) {
102
+ throw new BundlerError("bundler/bad-entry-path",
103
+ "entry '" + n + "' must map to a non-empty source path");
104
+ }
105
+ }
106
+ }
107
+
108
+ function create(opts) {
109
+ opts = opts || {};
110
+ validateOpts(opts, [
111
+ "entries", "outdir", "cwd",
112
+ "manifest", "hash", "hashLen", "graceMs", "log",
113
+ "_watch", "_setTimeout", "_clearTimeout",
114
+ ], "b.bundler");
115
+ _validateEntries(opts.entries);
116
+ if (typeof opts.outdir !== "string" || opts.outdir.length === 0) {
117
+ throw new BundlerError("bundler/no-outdir",
118
+ "bundler.create requires opts.outdir");
119
+ }
120
+
121
+ var entries = Object.assign({}, opts.entries);
122
+ var cwd = opts.cwd || process.cwd();
123
+ var outdir = path.isAbsolute(opts.outdir) ? opts.outdir : path.resolve(cwd, opts.outdir);
124
+ var manifestName = (opts.manifest === false || opts.manifest === null)
125
+ ? null
126
+ : (typeof opts.manifest === "string" && opts.manifest.length > 0
127
+ ? opts.manifest
128
+ : "manifest.json");
129
+ var hashOn = opts.hash !== false;
130
+ var hashLen = typeof opts.hashLen === "number" && opts.hashLen >= 4 && opts.hashLen <= 64
131
+ ? Math.floor(opts.hashLen) : 16;
132
+ var log = opts.log || null;
133
+
134
+ // Test seam: tests pass a fake watcher so we don't actually fs.watch
135
+ var watchFn = opts._watch || function (dirOrFile, wopts, listener) {
136
+ return fs.watch(dirOrFile, wopts, listener);
137
+ };
138
+ var setTimeoutFn = opts._setTimeout || setTimeout;
139
+ var clearTimeoutFn = opts._clearTimeout || clearTimeout;
140
+ var graceMs = typeof opts.graceMs === "number" && opts.graceMs >= 0 ? opts.graceMs : 100;
141
+
142
+ var watchers = [];
143
+ var debounceTimer = null;
144
+ var watching = false;
145
+
146
+ function _resolveEntry(p) {
147
+ return path.isAbsolute(p) ? p : path.resolve(cwd, p);
148
+ }
149
+
150
+ function _logVia(level, message, fields) {
151
+ if (log && typeof log[level] === "function") {
152
+ try { log[level](message, fields); } catch (_e) { /* logger best-effort */ }
153
+ return;
154
+ }
155
+ var line = "[blamejs:bundler] " + message + (fields ? " " + JSON.stringify(fields) : "");
156
+ if (level === "error" || level === "warn" || level === "fatal") console.error(line);
157
+ else console.log(line);
158
+ }
159
+
160
+ async function build() {
161
+ var t0 = Date.now();
162
+ _ensureOutDir(outdir);
163
+
164
+ var outputs = [];
165
+ var manifest = {};
166
+ var names = Object.keys(entries);
167
+
168
+ for (var i = 0; i < names.length; i++) {
169
+ var name = names[i];
170
+ var entryPath = _resolveEntry(entries[name]);
171
+ var ext = path.extname(entryPath);
172
+ var content;
173
+ try { content = fs.readFileSync(entryPath); }
174
+ catch (e) {
175
+ throw new BundlerError("bundler/read-failed",
176
+ "could not read entry '" + name + "' at " + entryPath +
177
+ ": " + ((e && e.message) || String(e)));
178
+ }
179
+ var hash = hashOn ? _hashContent(content, hashLen) : null;
180
+ var outName = hashOn ? _hashedName(name, hash, ext) : (name + ext);
181
+ var outPath = path.join(outdir, outName);
182
+ // atomic-file write so a concurrent reader (the http server
183
+ // serving outdir) never sees a partial file
184
+ atomicFile.writeSync(outPath, content, { mode: 0o644 });
185
+ outputs.push({
186
+ name: name,
187
+ entry: entryPath,
188
+ path: outPath,
189
+ hash: hash,
190
+ bytes: content.length,
191
+ ext: ext,
192
+ });
193
+ manifest[name] = outName;
194
+ }
195
+
196
+ var manifestPath = null;
197
+ if (manifestName) {
198
+ manifestPath = path.join(outdir, manifestName);
199
+ atomicFile.writeSync(
200
+ manifestPath,
201
+ safeJson.stringify(manifest, null, 2) + "\n",
202
+ { mode: 0o644 }
203
+ );
204
+ }
205
+
206
+ var result = {
207
+ outputs: outputs,
208
+ manifestPath: manifestPath,
209
+ manifest: manifest,
210
+ durationMs: Date.now() - t0,
211
+ };
212
+ _logVia("info", "build complete",
213
+ { entries: outputs.length, durationMs: result.durationMs });
214
+ return result;
215
+ }
216
+
217
+ function _scheduleRebuild(reason, callback) {
218
+ if (debounceTimer) {
219
+ try { clearTimeoutFn(debounceTimer); } catch (_e) {}
220
+ }
221
+ debounceTimer = setTimeoutFn(function () {
222
+ debounceTimer = null;
223
+ _logVia("info", "rebuilding", { reason: reason });
224
+ build().then(
225
+ function (r) { if (callback) try { callback(null, r); } catch (_e) {} },
226
+ function (e) {
227
+ _logVia("error", "rebuild failed", { error: (e && e.message) || String(e) });
228
+ if (callback) try { callback(e, null); } catch (_e) {}
229
+ }
230
+ );
231
+ }, graceMs);
232
+ if (debounceTimer && typeof debounceTimer.unref === "function") debounceTimer.unref();
233
+ }
234
+
235
+ function watch(callback) {
236
+ if (watching) return;
237
+ watching = true;
238
+ var names = Object.keys(entries);
239
+ for (var i = 0; i < names.length; i++) {
240
+ (function (name) {
241
+ var entryPath = _resolveEntry(entries[name]);
242
+ // Watch the entry's directory (single-file watches are flaky
243
+ // across editors that write-then-rename). Filter events to the
244
+ // entry's basename only.
245
+ var dir = path.dirname(entryPath);
246
+ var base = path.basename(entryPath);
247
+ var w;
248
+ try {
249
+ w = watchFn(dir, { persistent: false }, function (eventType, filename) {
250
+ if (filename && String(filename) === base) {
251
+ _scheduleRebuild(name, callback);
252
+ }
253
+ });
254
+ } catch (e) {
255
+ _logVia("warn", "could not watch " + dir,
256
+ { error: (e && e.message) || String(e) });
257
+ return;
258
+ }
259
+ if (w && typeof w.on === "function") {
260
+ w.on("error", function (err) {
261
+ _logVia("warn", "watcher error",
262
+ { dir: dir, error: (err && err.message) || String(err) });
263
+ });
264
+ }
265
+ watchers.push(w);
266
+ })(names[i]);
267
+ }
268
+ }
269
+
270
+ async function close() {
271
+ watching = false;
272
+ if (debounceTimer) {
273
+ try { clearTimeoutFn(debounceTimer); } catch (_e) {}
274
+ debounceTimer = null;
275
+ }
276
+ for (var i = 0; i < watchers.length; i++) {
277
+ try { if (watchers[i] && typeof watchers[i].close === "function") watchers[i].close(); }
278
+ catch (_e) { /* close best-effort */ }
279
+ }
280
+ watchers = [];
281
+ }
282
+
283
+ return {
284
+ build: build,
285
+ watch: watch,
286
+ close: close,
287
+ entries: entries,
288
+ outdir: outdir,
289
+ };
290
+ }
291
+
292
+ module.exports = {
293
+ create: create,
294
+ BundlerError: BundlerError,
295
+ };