@blamejs/core 0.8.43 → 0.8.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/storage.js
CHANGED
|
@@ -1,61 +1,41 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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;
|