@blamejs/core 0.8.43 → 0.8.50

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
package/lib/storage.js CHANGED
@@ -1,61 +1,41 @@
1
1
  "use strict";
2
2
  /**
3
- * Storage abstraction — multi-backend, classification-routed, residency-
4
- * enforced file storage with per-file vault-sealed encryption.
5
- *
6
- * Two configuration shapes (both supported, internally normalized to
7
- * the multi-backend form):
8
- *
9
- * 1. Single-backend (legacy shape preserved):
10
- * storage.init({ backend: 'local', uploadDir: './data/uploads' })
11
- *
12
- * 2. Multi-backend:
13
- * storage.init({
14
- * backends: {
15
- * 'eu-private': { protocol: 'http-put', baseUrl: '...',
16
- * classifications: ['personal'], residencyTag: 'EU' },
17
- * 'us-ops': { protocol: 'local', rootDir: '/data/ops',
18
- * classifications: ['operational', 'public'],
19
- * residencyTag: 'US' },
20
- * },
21
- * defaultClassification: 'personal',
22
- * refuseUnclassified: true,
23
- * });
24
- *
25
- * Classification routing (per-call):
26
- * storage.saveFile(buf, 'invoice.pdf', { classification: 'personal' })
27
- * → routes to a backend whose `classifications` includes 'personal'.
28
- * storage.saveFile(buf, 'logo.png', { backend: 'us-ops' })
29
- * explicit backend; framework still validates the backend serves
30
- * the classification.
31
- *
32
- * Residency enforcement (boot-time):
33
- * - If db.getDataResidency() declares a region, every backend serving the
34
- * 'personal' classification must have residencyTag === region (or be
35
- * listed in dataResidency.allowedStorageRegions).
36
- * - Refuses to boot otherwise — catches operator misconfiguration where
37
- * a US-region backend was configured for personal data in an EU app.
38
- *
39
- * Audit hooks:
40
- * - Every saveFile records a 'system.storage.write' event with metadata
41
- * { backend, classification, residencyTag, sizeBytes }.
42
- * - getFile records 'system.storage.read'.
43
- * - delete records 'system.storage.delete'.
44
- *
45
- * Public API (sync entry, async ops since backends may be remote):
46
- * storage.init(opts) (sync)
47
- * storage.saveFile(buffer, key, opts?) async → { storedPath, encryptionKey, backend, classification }
48
- * storage.getFileBuffer(storedPath, sealedKey, opts?) async → Buffer
49
- * storage.getFileStream(storedPath, sealedKey, opts?) async → Readable
50
- * storage.saveRaw(buffer, key, opts?) async → { storedPath, backend }
51
- * storage.getRawBuffer(storedPath, opts?) async → Buffer
52
- * storage.deleteFile(storedPath, opts?) async → boolean
53
- * storage.exists(storedPath, opts?) async → boolean
54
- * storage.presignedUploadUrl(key, opts?) → { url, method, headers, expiresAt }
55
- * storage.presignedDownloadUrl(key, opts?) → { url, method, headers, expiresAt }
56
- * storage.presignedUploadPolicy(key, opts) → { url, method, fields, expiresAt, maxBytes, enforcement }
57
- * storage.listBackends() → [{ name, protocol, classifications, residencyTag }]
58
- * storage.getBackend(name) → backend instance (or null)
3
+ * @module b.storage
4
+ * @featured true
5
+ * @nav Data
6
+ * @title Storage
7
+ *
8
+ * @intro
9
+ * Filesystem-and-cloud-backed object storage with sealed per-file
10
+ * encryption keys, classification routing, and residency enforcement.
11
+ *
12
+ * `b.storage` sits one layer above `b.objectStore`: the lower
13
+ * primitive abstracts the byte-level adapter (local FS, sigv4-style
14
+ * S3-compatible, GCS, Azure Blob, generic HTTP-PUT); this module
15
+ * adds the framework-shaped policy on top multi-backend
16
+ * registration, per-call classification → backend dispatch,
17
+ * boot-time residency validation against `b.db.getDataResidency()`,
18
+ * per-file XChaCha20-Poly1305 encryption with the data key sealed
19
+ * into the framework's vault, and audit-chain emission for every
20
+ * read / write / delete / presign.
21
+ *
22
+ * Configuration accepts either the legacy single-backend shape
23
+ * (`{ backend, uploadDir }`) or the multi-backend shape
24
+ * (`{ backends: { name: cfg, ... }, defaultClassification,
25
+ * refuseUnclassified }`). Both normalize internally to the
26
+ * multi-backend form. `refuseUnclassified: true` forces every call
27
+ * to declare its `classification` explicitly, which is the right
28
+ * posture for apps mixing personal / operational / public data
29
+ * across different residency zones.
30
+ *
31
+ * Encrypted save/get is the default surface (`saveFile` /
32
+ * `getFileBuffer` / `getFileStream`); `saveRaw` / `getRawBuffer`
33
+ * skip the per-file encryption envelope for content that is
34
+ * already-public or already-encrypted (e.g. signed image assets,
35
+ * pre-encrypted backup bundles).
36
+ *
37
+ * @card
38
+ * Filesystem-and-cloud-backed object storage with sealed per-file encryption keys, classification routing, and residency enforcement.
59
39
  */
60
40
  var C = require("./constants");
61
41
  var { generateBytes, encryptPacked, decryptPacked } = require("./crypto");
@@ -76,6 +56,52 @@ var _err = StorageError.factory;
76
56
 
77
57
  // ---- Init ----
78
58
 
59
+ /**
60
+ * @primitive b.storage.init
61
+ * @signature b.storage.init(opts)
62
+ * @since 0.1.0
63
+ * @status stable
64
+ * @related b.storage.saveFile, b.storage.listBackends, b.objectStore.buildBackend
65
+ *
66
+ * Register one or more storage backends and lock the framework into
67
+ * the configured policy. Idempotent — a second call after the first
68
+ * succeeds is a no-op (operators rebuild via `_resetForTest` only).
69
+ * Validates classification → residency mapping at boot so a
70
+ * misconfigured deployment (US backend serving EU personal data)
71
+ * fails fast instead of leaking on first write.
72
+ *
73
+ * @opts
74
+ * backend: "local" | "sigv4" | "gcs" | "azure-blob" | "http-put", // single-backend shorthand
75
+ * uploadDir: string, // local backend root (single-backend shorthand)
76
+ * backends: object, // multi-backend map: name -> backend cfg
77
+ * defaultClassification: string, // applied when a call omits { classification }
78
+ * refuseUnclassified: boolean, // refuse calls without explicit classification
79
+ *
80
+ * @example
81
+ * // Single-backend, local FS — typical small-app shape.
82
+ * b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
83
+ *
84
+ * @example
85
+ * // Multi-backend with classification routing + residency tags.
86
+ * b.storage.init({
87
+ * backends: {
88
+ * "eu-private": {
89
+ * protocol: "local",
90
+ * rootDir: "/srv/eu/private",
91
+ * classifications: ["personal"],
92
+ * residencyTag: "EU",
93
+ * },
94
+ * "us-ops": {
95
+ * protocol: "local",
96
+ * rootDir: "/srv/us/ops",
97
+ * classifications: ["operational", "public"],
98
+ * residencyTag: "US",
99
+ * },
100
+ * },
101
+ * defaultClassification: "operational",
102
+ * refuseUnclassified: true,
103
+ * });
104
+ */
79
105
  function init(opts) {
80
106
  if (initialized) return;
81
107
  if (!opts) throw _err("INVALID_CONFIG", "storage.init() requires options", true);
@@ -244,6 +270,37 @@ function _emit(action, info) {
244
270
 
245
271
  // ---- Public API ----
246
272
 
273
+ /**
274
+ * @primitive b.storage.saveFile
275
+ * @signature b.storage.saveFile(buffer, key, opts)
276
+ * @since 0.1.0
277
+ * @status stable
278
+ * @compliance gdpr, hipaa, pci-dss, soc2
279
+ * @related b.storage.getFileBuffer, b.storage.deleteFile, b.storage.saveRaw
280
+ *
281
+ * Encrypt `buffer` under a fresh XChaCha20-Poly1305 data key, seal
282
+ * the data key into the framework vault, and write the ciphertext to
283
+ * the backend selected by `opts.classification` (or `opts.backend`
284
+ * for explicit pinning). Returns the storage path plus the sealed
285
+ * key the caller MUST persist alongside the row that references the
286
+ * blob — without it, the bytes are unrecoverable. Emits a
287
+ * `system.storage.write` audit event with `{ backend, classification,
288
+ * residencyTag, sizeBytes }`.
289
+ *
290
+ * @opts
291
+ * classification: string, // route to a backend serving this classification
292
+ * backend: string, // explicit backend by name (still validates classification serve)
293
+ *
294
+ * @example
295
+ * var buf = Buffer.from("invoice pdf bytes");
296
+ * var saved = await b.storage.saveFile(buf, "invoices/2026/001.pdf", {
297
+ * classification: "personal",
298
+ * });
299
+ * // → { storedPath: "invoices/2026/001.pdf",
300
+ * // encryptionKey: "v1:...", // sealed; persist with the row
301
+ * // backend: "eu-private",
302
+ * // classification: "personal" }
303
+ */
247
304
  async function saveFile(buffer, key, opts) {
248
305
  _requireInit();
249
306
  if (!Buffer.isBuffer(buffer)) throw _err("INVALID_BODY", "saveFile body must be a Buffer", true);
@@ -268,6 +325,31 @@ async function saveFile(buffer, key, opts) {
268
325
  };
269
326
  }
270
327
 
328
+ /**
329
+ * @primitive b.storage.getFileBuffer
330
+ * @signature b.storage.getFileBuffer(key, sealedKey, opts)
331
+ * @since 0.1.0
332
+ * @status stable
333
+ * @related b.storage.saveFile, b.storage.getFileStream
334
+ *
335
+ * Fetch the ciphertext at `key` from the routed backend, unseal the
336
+ * per-file data key via the framework vault, and return the
337
+ * decrypted plaintext as a Buffer. The AEAD tag is verified before
338
+ * any plaintext is released — a tampered ciphertext throws
339
+ * `crypto/decrypt-failed`, never returns partial bytes. Emits
340
+ * `system.storage.read` with `{ backend, key, sizeBytes }`.
341
+ *
342
+ * @opts
343
+ * classification: string, // route to a backend serving this classification
344
+ * backend: string, // explicit backend by name
345
+ *
346
+ * @example
347
+ * // Round-trip a small text payload through saveFile/getFileBuffer.
348
+ * b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
349
+ * var saved = await b.storage.saveFile(Buffer.from("hello"), "greet.txt");
350
+ * var roundTrip = await b.storage.getFileBuffer("greet.txt", saved.encryptionKey);
351
+ * roundTrip.toString("utf8"); // → "hello"
352
+ */
271
353
  async function getFileBuffer(key, sealedKey, opts) {
272
354
  _requireInit();
273
355
  opts = opts || {};
@@ -284,6 +366,33 @@ async function getFileBuffer(key, sealedKey, opts) {
284
366
  return decrypted;
285
367
  }
286
368
 
369
+ /**
370
+ * @primitive b.storage.getFileStream
371
+ * @signature b.storage.getFileStream(key, sealedKey, opts)
372
+ * @since 0.1.0
373
+ * @status stable
374
+ * @related b.storage.getFileBuffer, b.storage.saveFile
375
+ *
376
+ * Buffer-then-stream variant of `getFileBuffer` — returns a
377
+ * `stream.Readable` once the AEAD tag has verified the entire
378
+ * ciphertext. Per-file XChaCha20-Poly1305 needs the whole frame
379
+ * before it can release the first byte; chunked AEAD with
380
+ * per-chunk tags would let us stream end-to-end at the cost of
381
+ * finer-grained tampering windows, so the framework defaults to
382
+ * the safe variant.
383
+ *
384
+ * @opts
385
+ * classification: string, // route to a backend serving this classification
386
+ * backend: string, // explicit backend by name
387
+ *
388
+ * @example
389
+ * b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
390
+ * var saved = await b.storage.saveFile(Buffer.from("stream-me"), "blob.bin");
391
+ * var stream = await b.storage.getFileStream("blob.bin", saved.encryptionKey);
392
+ * var chunks = [];
393
+ * for await (var chunk of stream) chunks.push(chunk);
394
+ * Buffer.concat(chunks).toString("utf8"); // → "stream-me"
395
+ */
287
396
  async function getFileStream(key, sealedKey, opts) {
288
397
  // Buffer-then-stream: per-file XChaCha20 encryption needs the whole
289
398
  // ciphertext to verify the AEAD tag before any plaintext can be released
@@ -293,6 +402,29 @@ async function getFileStream(key, sealedKey, opts) {
293
402
  return require("stream").Readable.from(buf);
294
403
  }
295
404
 
405
+ /**
406
+ * @primitive b.storage.saveRaw
407
+ * @signature b.storage.saveRaw(buffer, key, opts)
408
+ * @since 0.1.0
409
+ * @status stable
410
+ * @related b.storage.saveFile, b.storage.getRawBuffer
411
+ *
412
+ * Write `buffer` to the routed backend as-is, skipping the per-file
413
+ * encryption envelope. Use for content that is already public
414
+ * (signed CDN assets, image thumbnails) or already encrypted
415
+ * (pre-sealed backup bundles); use `saveFile` for everything else.
416
+ * Audit metadata records `raw: true` so storage reads in the audit
417
+ * chain can be distinguished from encrypted reads.
418
+ *
419
+ * @opts
420
+ * classification: string, // route to a backend serving this classification
421
+ * backend: string, // explicit backend by name
422
+ *
423
+ * @example
424
+ * b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
425
+ * var saved = await b.storage.saveRaw(Buffer.from("public-bytes"), "logo.png");
426
+ * // → { storedPath: "logo.png", backend: "default" }
427
+ */
296
428
  async function saveRaw(buffer, key, opts) {
297
429
  _requireInit();
298
430
  if (!Buffer.isBuffer(buffer)) throw _err("INVALID_BODY", "saveRaw body must be a Buffer", true);
@@ -312,6 +444,28 @@ async function saveRaw(buffer, key, opts) {
312
444
  return { storedPath: key, backend: picked.backend.name };
313
445
  }
314
446
 
447
+ /**
448
+ * @primitive b.storage.getRawBuffer
449
+ * @signature b.storage.getRawBuffer(key, opts)
450
+ * @since 0.1.0
451
+ * @status stable
452
+ * @related b.storage.saveRaw, b.storage.getFileBuffer
453
+ *
454
+ * Fetch the raw bytes at `key` from the routed backend. No
455
+ * decryption layer is applied — the caller receives whatever was
456
+ * stored, byte-for-byte. Pair with `saveRaw`; for encrypted blobs
457
+ * use `getFileBuffer` instead so the AEAD tag is verified.
458
+ *
459
+ * @opts
460
+ * classification: string, // route to a backend serving this classification
461
+ * backend: string, // explicit backend by name
462
+ *
463
+ * @example
464
+ * b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
465
+ * await b.storage.saveRaw(Buffer.from("raw-payload"), "asset.bin");
466
+ * var bytes = await b.storage.getRawBuffer("asset.bin");
467
+ * bytes.toString("utf8"); // → "raw-payload"
468
+ */
315
469
  async function getRawBuffer(key, opts) {
316
470
  _requireInit();
317
471
  opts = opts || {};
@@ -319,6 +473,34 @@ async function getRawBuffer(key, opts) {
319
473
  return picked.backend.get(key);
320
474
  }
321
475
 
476
+ /**
477
+ * @primitive b.storage.deleteFile
478
+ * @signature b.storage.deleteFile(key, opts)
479
+ * @since 0.1.0
480
+ * @status stable
481
+ * @compliance gdpr
482
+ * @related b.storage.saveFile, b.storage.exists
483
+ *
484
+ * Remove `key` from the routed backend. Returns `true` when the
485
+ * object existed and was removed, `false` when it was already
486
+ * absent. Emits `system.storage.delete` with `{ backend, key,
487
+ * existed }` so the audit chain records GDPR right-to-erasure
488
+ * flows. The sealed encryption key the caller persisted alongside
489
+ * the row should be discarded by the caller after a successful
490
+ * delete — without the bytes, the key has no recovery value.
491
+ *
492
+ * @opts
493
+ * classification: string, // route to a backend serving this classification
494
+ * backend: string, // explicit backend by name
495
+ *
496
+ * @example
497
+ * b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
498
+ * await b.storage.saveRaw(Buffer.from("doomed"), "tmp/x.bin");
499
+ * var existed = await b.storage.deleteFile("tmp/x.bin");
500
+ * // → true
501
+ * var second = await b.storage.deleteFile("tmp/x.bin");
502
+ * // → false
503
+ */
322
504
  async function deleteFile(key, opts) {
323
505
  _requireInit();
324
506
  opts = opts || {};
@@ -334,6 +516,31 @@ async function deleteFile(key, opts) {
334
516
  return result;
335
517
  }
336
518
 
519
+ /**
520
+ * @primitive b.storage.exists
521
+ * @signature b.storage.exists(key, opts)
522
+ * @since 0.1.0
523
+ * @status stable
524
+ * @related b.storage.deleteFile, b.storage.getFileBuffer
525
+ *
526
+ * HEAD-style existence check — returns `true` when the routed
527
+ * backend reports the key present, `false` on `NOT_FOUND`. Other
528
+ * backend errors propagate so transient outages aren't swallowed
529
+ * as "doesn't exist." Cheaper than a full GET when the caller only
530
+ * needs to gate a downstream operation on presence.
531
+ *
532
+ * @opts
533
+ * classification: string, // route to a backend serving this classification
534
+ * backend: string, // explicit backend by name
535
+ *
536
+ * @example
537
+ * b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
538
+ * await b.storage.saveRaw(Buffer.from("here"), "probe.bin");
539
+ * var present = await b.storage.exists("probe.bin");
540
+ * // → true
541
+ * var missing = await b.storage.exists("nope.bin");
542
+ * // → false
543
+ */
337
544
  async function exists(key, opts) {
338
545
  _requireInit();
339
546
  opts = opts || {};
@@ -347,6 +554,25 @@ async function exists(key, opts) {
347
554
  }
348
555
  }
349
556
 
557
+ /**
558
+ * @primitive b.storage.listBackends
559
+ * @signature b.storage.listBackends()
560
+ * @since 0.1.0
561
+ * @status stable
562
+ * @related b.storage.getBackend, b.storage.init
563
+ *
564
+ * Snapshot every registered backend with `{ name, protocol,
565
+ * classifications, residencyTag, breakerState }`. The
566
+ * `breakerState` is the live circuit-breaker state from the
567
+ * underlying `b.objectStore` adapter — handy for ops dashboards
568
+ * surfacing a degraded backend before it cascades.
569
+ *
570
+ * @example
571
+ * b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
572
+ * var info = b.storage.listBackends();
573
+ * info[0].name; // → "default"
574
+ * info[0].protocol; // → "local"
575
+ */
350
576
  function listBackends() {
351
577
  _requireInit();
352
578
  var out = [];
@@ -389,23 +615,138 @@ function _presign(direction, key, opts) {
389
615
  return result;
390
616
  }
391
617
 
618
+ /**
619
+ * @primitive b.storage.presignedUploadUrl
620
+ * @signature b.storage.presignedUploadUrl(key, opts)
621
+ * @since 0.4.0
622
+ * @status stable
623
+ * @related b.storage.presignedDownloadUrl, b.storage.presignedUploadPolicy
624
+ *
625
+ * Issue a short-lived signed URL the client uses to PUT bytes
626
+ * directly to the object store, bypassing the framework process
627
+ * for the upload bytes. Backend-dependent: sigv4 / gcs / azure-blob
628
+ * support it natively; local / http-put backends throw
629
+ * `PRESIGN_NOT_SUPPORTED`. Emits `system.storage.presign` with
630
+ * `direction: "upload"`.
631
+ *
632
+ * @opts
633
+ * classification: string, // route to a backend serving this classification
634
+ * backend: string, // explicit backend by name
635
+ * expiresInSec: number, // URL lifetime; backend-defaulted when omitted
636
+ * contentType: string, // pin the upload Content-Type into the signature
637
+ *
638
+ * @example
639
+ * b.storage.init({
640
+ * backends: {
641
+ * "us-ops": {
642
+ * protocol: "sigv4",
643
+ * endpoint: "https://s3.us-east-1.amazonaws.com",
644
+ * region: "us-east-1",
645
+ * bucket: "uploads",
646
+ * accessKeyId: "AKIAEXAMPLE",
647
+ * secretAccessKey: "secret",
648
+ * classifications: ["operational"],
649
+ * residencyTag: "US",
650
+ * },
651
+ * },
652
+ * });
653
+ * var presigned = b.storage.presignedUploadUrl("incoming/x.bin", {
654
+ * backend: "us-ops",
655
+ * expiresInSec: 300,
656
+ * });
657
+ * presigned.method; // → "PUT"
658
+ */
392
659
  function presignedUploadUrl(key, opts) { return _presign("Upload", key, opts); }
660
+
661
+ /**
662
+ * @primitive b.storage.presignedDownloadUrl
663
+ * @signature b.storage.presignedDownloadUrl(key, opts)
664
+ * @since 0.4.0
665
+ * @status stable
666
+ * @related b.storage.presignedUploadUrl, b.storage.getFileBuffer
667
+ *
668
+ * Issue a short-lived signed URL the client uses to GET bytes
669
+ * directly from the object store. Same backend-support matrix as
670
+ * the upload variant. Use this only with `saveRaw` content —
671
+ * encrypted blobs (`saveFile`) need the per-file sealed key, which
672
+ * the framework does not expose to the client.
673
+ *
674
+ * @opts
675
+ * classification: string, // route to a backend serving this classification
676
+ * backend: string, // explicit backend by name
677
+ * expiresInSec: number, // URL lifetime; backend-defaulted when omitted
678
+ *
679
+ * @example
680
+ * b.storage.init({
681
+ * backends: {
682
+ * "us-ops": {
683
+ * protocol: "sigv4",
684
+ * endpoint: "https://s3.us-east-1.amazonaws.com",
685
+ * region: "us-east-1",
686
+ * bucket: "uploads",
687
+ * accessKeyId: "AKIAEXAMPLE",
688
+ * secretAccessKey: "secret",
689
+ * classifications: ["public"],
690
+ * residencyTag: "US",
691
+ * },
692
+ * },
693
+ * });
694
+ * var presigned = b.storage.presignedDownloadUrl("public/logo.png", {
695
+ * backend: "us-ops",
696
+ * expiresInSec: 60,
697
+ * });
698
+ * presigned.method; // → "GET"
699
+ */
393
700
  function presignedDownloadUrl(key, opts) { return _presign("Download", key, opts); }
394
701
 
395
- // presignedUploadPolicy — issues a POST-form policy (or vendor-equivalent
396
- // PUT) that the client uses to upload directly to the object store with
397
- // server-side body-size enforcement. Distinct from presignedUploadUrl
398
- // because that signs only the URL; this signs a full policy document
399
- // that includes a content-length-range condition.
400
- //
401
- // Vendor enforcement:
402
- // sigv4 — content-length-range condition; S3 rejects bodies outside
403
- // gcs — content-length-range condition; GCS rejects bodies outside
404
- // azure — SAS doesn't natively cap body size; returns SAS PUT URL
405
- // with enforcement: "client-only" operator must HEAD the
406
- // blob post-upload and reject if oversize
407
- // local — NOT_SUPPORTED (use saveFile directly)
408
- // http-put NOT_SUPPORTED (no signing convention)
702
+ /**
703
+ * @primitive b.storage.presignedUploadPolicy
704
+ * @signature b.storage.presignedUploadPolicy(key, opts)
705
+ * @since 0.6.0
706
+ * @status stable
707
+ * @related b.storage.presignedUploadUrl, b.fileUpload
708
+ *
709
+ * Issue a signed POST-form policy (sigv4 / gcs) or vendor-equivalent
710
+ * PUT (azure-blob) that the client uploads against, with the body-
711
+ * size cap baked into the signature so an oversize upload is
712
+ * rejected by the object store, not by the framework process. Use
713
+ * this not `presignedUploadUrl` — when the upload size matters
714
+ * and you can't trust the client. `result.enforcement` indicates
715
+ * whether the cap is server-side (`"server"`) or client-only
716
+ * (`"client-only"` — Azure SAS, where the operator must HEAD the
717
+ * blob post-upload to reject oversize). `local` and `http-put`
718
+ * backends throw `PRESIGN_NOT_SUPPORTED`.
719
+ *
720
+ * @opts
721
+ * classification: string, // route to a backend serving this classification
722
+ * backend: string, // explicit backend by name
723
+ * maxBytes: number, // body-size cap (required for size enforcement)
724
+ * expiresInSec: number, // policy lifetime; backend-defaulted when omitted
725
+ * contentType: string, // pin the upload Content-Type into the policy
726
+ *
727
+ * @example
728
+ * b.storage.init({
729
+ * backends: {
730
+ * "us-ops": {
731
+ * protocol: "sigv4",
732
+ * endpoint: "https://s3.us-east-1.amazonaws.com",
733
+ * region: "us-east-1",
734
+ * bucket: "uploads",
735
+ * accessKeyId: "AKIAEXAMPLE",
736
+ * secretAccessKey: "secret",
737
+ * classifications: ["operational"],
738
+ * residencyTag: "US",
739
+ * },
740
+ * },
741
+ * });
742
+ * var policy = b.storage.presignedUploadPolicy("user/avatar.png", {
743
+ * backend: "us-ops",
744
+ * maxBytes: 5 * 1024 * 1024, // 5 MiB cap, server-enforced
745
+ * expiresInSec: 300,
746
+ * contentType: "image/png",
747
+ * });
748
+ * policy.enforcement; // → "server"
749
+ */
409
750
  function presignedUploadPolicy(key, opts) {
410
751
  _requireInit();
411
752
  if (typeof key !== "string" || key.length === 0) {
@@ -434,6 +775,28 @@ function presignedUploadPolicy(key, opts) {
434
775
  return result;
435
776
  }
436
777
 
778
+ /**
779
+ * @primitive b.storage.getBackend
780
+ * @signature b.storage.getBackend(name)
781
+ * @since 0.6.0
782
+ * @status stable
783
+ * @related b.storage.listBackends, b.storage.init
784
+ *
785
+ * Return the named backend instance from the underlying
786
+ * `b.objectStore` adapter, or `null` when no backend with that
787
+ * name is registered. Most operator code routes through the
788
+ * dispatching primitives (`saveFile` / `getFileBuffer` / ...);
789
+ * `getBackend` is the escape hatch for adapter-specific operations
790
+ * (lifecycle policy ops, vendor-specific HEAD probes) the
791
+ * framework does not abstract.
792
+ *
793
+ * @example
794
+ * b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
795
+ * var backend = b.storage.getBackend("default");
796
+ * backend.protocol; // → "local"
797
+ * var missing = b.storage.getBackend("does-not-exist");
798
+ * // → null
799
+ */
437
800
  function getBackend(name) {
438
801
  _requireInit();
439
802
  return backends[name] || null;