@blamejs/core 0.15.12 → 0.15.14
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 +4 -0
- package/index.js +2 -0
- package/lib/a2a-tasks.js +45 -29
- package/lib/acme.js +6 -5
- package/lib/agent-event-bus.js +18 -4
- package/lib/agent-idempotency.js +7 -6
- package/lib/agent-orchestrator.js +2 -5
- package/lib/agent-saga.js +3 -5
- package/lib/agent-snapshot.js +32 -2
- package/lib/agent-tenant.js +2 -5
- package/lib/ai-adverse-decision.js +2 -15
- package/lib/ai-aedt-bias-audit.js +2 -1
- package/lib/ai-capability.js +1 -6
- package/lib/ai-content-detect.js +1 -3
- package/lib/ai-dp.js +1 -5
- package/lib/ai-frontier-protocol.js +1 -1
- package/lib/ai-input.js +2 -2
- package/lib/ai-model-manifest.js +1 -1
- package/lib/ai-output.js +16 -7
- package/lib/ai-pref.js +3 -8
- package/lib/ai-quota.js +3 -14
- package/lib/api-key.js +37 -28
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +7 -1
- package/lib/archive-adapters.js +2 -4
- package/lib/archive-entry-policy.js +32 -0
- package/lib/archive-gz.js +9 -0
- package/lib/archive-read.js +14 -24
- package/lib/archive-tar-read.js +56 -24
- package/lib/archive.js +6 -12
- package/lib/arg-parser.js +7 -6
- package/lib/asn1-der.js +70 -22
- package/lib/asyncapi-traits.js +2 -6
- package/lib/atomic-file.js +312 -33
- package/lib/audit-chain.js +183 -54
- package/lib/audit-daily-review.js +36 -16
- package/lib/audit-emit.js +82 -0
- package/lib/audit-sign.js +258 -0
- package/lib/audit-tools.js +108 -23
- package/lib/audit.js +91 -2
- package/lib/auth/access-lock.js +7 -29
- package/lib/auth/bot-challenge.js +1 -1
- package/lib/auth/ciba.js +43 -4
- package/lib/auth/dpop.js +29 -36
- package/lib/auth/fido-mds3.js +14 -16
- package/lib/auth/jwt-external.js +26 -8
- package/lib/auth/jwt.js +15 -17
- package/lib/auth/lockout.js +24 -27
- package/lib/auth/oauth.js +67 -45
- package/lib/auth/oid4vci.js +55 -32
- package/lib/auth/oid4vp.js +3 -2
- package/lib/auth/openid-federation.js +6 -6
- package/lib/auth/passkey.js +4 -4
- package/lib/auth/password.js +8 -2
- package/lib/auth/saml.js +70 -40
- package/lib/auth/sd-jwt-vc-holder.js +2 -14
- package/lib/auth/sd-jwt-vc-issuer.js +2 -14
- package/lib/auth/sd-jwt-vc.js +25 -5
- package/lib/auth/status-list.js +21 -9
- package/lib/auth/step-up.js +15 -13
- package/lib/auth-bot-challenge.js +27 -39
- package/lib/backup/bundle.js +18 -20
- package/lib/backup/crypto.js +23 -8
- package/lib/backup/index.js +55 -67
- package/lib/backup/manifest.js +7 -1
- package/lib/bounded-map.js +112 -1
- package/lib/breach-deadline.js +1 -11
- package/lib/break-glass.js +41 -22
- package/lib/cache.js +7 -18
- package/lib/cbor.js +36 -12
- package/lib/cdn-cache-control.js +15 -12
- package/lib/cert.js +8 -13
- package/lib/chain-writer.js +162 -47
- package/lib/cli.js +10 -2
- package/lib/client-hints.js +7 -9
- package/lib/cloud-events.js +43 -33
- package/lib/cluster-storage.js +9 -41
- package/lib/cms-codec.js +2 -1
- package/lib/codepoint-class.js +84 -1
- package/lib/compliance-ai-act-logging.js +1 -6
- package/lib/compliance-ai-act-transparency.js +2 -4
- package/lib/compliance-eaa.js +2 -12
- package/lib/compliance-sanctions-fetcher.js +21 -28
- package/lib/compliance-sanctions.js +11 -21
- package/lib/compliance.js +3 -10
- package/lib/config-drift.js +24 -20
- package/lib/content-credentials.js +11 -8
- package/lib/content-digest.js +21 -12
- package/lib/cookies.js +15 -13
- package/lib/cose.js +39 -11
- package/lib/cra-report.js +1 -11
- package/lib/crdt.js +2 -1
- package/lib/crypto-field.js +34 -33
- package/lib/crypto.js +138 -7
- package/lib/csp.js +239 -3
- package/lib/daemon.js +46 -42
- package/lib/data-act.js +46 -13
- package/lib/db-file-lifecycle.js +14 -6
- package/lib/db-query.js +75 -49
- package/lib/db.js +66 -27
- package/lib/dbsc.js +5 -6
- package/lib/ddl-change-control.js +18 -14
- package/lib/deprecate.js +4 -5
- package/lib/did.js +6 -2
- package/lib/dora.js +43 -13
- package/lib/dr-runbook.js +1 -1
- package/lib/dsa.js +2 -1
- package/lib/dsr.js +30 -20
- package/lib/early-hints.js +19 -0
- package/lib/eat.js +5 -1
- package/lib/external-db-migrate.js +21 -22
- package/lib/external-db.js +74 -30
- package/lib/fda-21cfr11.js +42 -13
- package/lib/fdx.js +22 -18
- package/lib/file-upload.js +80 -66
- package/lib/flag-evaluation-context.js +5 -8
- package/lib/flag-providers.js +6 -2
- package/lib/forms.js +1 -1
- package/lib/framework-error.js +12 -0
- package/lib/framework-schema.js +19 -0
- package/lib/fsm.js +7 -12
- package/lib/gate-contract.js +911 -39
- package/lib/gdpr-ropa.js +22 -22
- package/lib/graphql-federation.js +18 -5
- package/lib/guard-agent-registry.js +2 -1
- package/lib/guard-all.js +3 -2
- package/lib/guard-archive.js +9 -30
- package/lib/guard-auth.js +23 -80
- package/lib/guard-cidr.js +20 -96
- package/lib/guard-csv.js +135 -196
- package/lib/guard-domain.js +23 -106
- package/lib/guard-dsn.js +17 -14
- package/lib/guard-email.js +46 -53
- package/lib/guard-envelope.js +2 -2
- package/lib/guard-event-bus-topic.js +2 -1
- package/lib/guard-filename.js +12 -60
- package/lib/guard-graphql.js +28 -75
- package/lib/guard-html-wcag.js +15 -2
- package/lib/guard-html.js +74 -128
- package/lib/guard-idempotency-key.js +2 -1
- package/lib/guard-image.js +280 -77
- package/lib/guard-imap-command.js +9 -10
- package/lib/guard-jmap.js +1 -1
- package/lib/guard-json.js +101 -109
- package/lib/guard-jsonpath.js +20 -88
- package/lib/guard-jwt.js +32 -114
- package/lib/guard-list-id.js +2 -7
- package/lib/guard-list-unsubscribe.js +2 -7
- package/lib/guard-mail-compose.js +5 -6
- package/lib/guard-mail-move.js +3 -2
- package/lib/guard-mail-query.js +5 -3
- package/lib/guard-mail-sieve.js +6 -7
- package/lib/guard-managesieve-command.js +6 -5
- package/lib/guard-markdown.js +76 -110
- package/lib/guard-message-id.js +5 -6
- package/lib/guard-mime.js +20 -99
- package/lib/guard-oauth.js +19 -73
- package/lib/guard-pdf.js +65 -72
- package/lib/guard-pop3-command.js +13 -14
- package/lib/guard-posture-chain.js +2 -1
- package/lib/guard-regex.js +24 -99
- package/lib/guard-saga-config.js +2 -1
- package/lib/guard-shell.js +22 -87
- package/lib/guard-smtp-command.js +9 -12
- package/lib/guard-sql.js +15 -13
- package/lib/guard-stream-args.js +2 -1
- package/lib/guard-svg.js +103 -149
- package/lib/guard-template.js +23 -80
- package/lib/guard-tenant-id.js +2 -1
- package/lib/guard-text.js +592 -0
- package/lib/guard-time.js +27 -95
- package/lib/guard-uuid.js +21 -93
- package/lib/guard-xml.js +76 -106
- package/lib/guard-yaml.js +24 -60
- package/lib/html-balance.js +7 -3
- package/lib/http-client-cache.js +5 -12
- package/lib/http-client-cookie-jar.js +35 -16
- package/lib/http-client.js +233 -74
- package/lib/http-message-signature.js +3 -2
- package/lib/i18n-messageformat.js +5 -7
- package/lib/i18n.js +83 -26
- package/lib/iab-tcf.js +3 -2
- package/lib/importmap-integrity.js +41 -1
- package/lib/inbox.js +8 -8
- package/lib/incident-report.js +12 -27
- package/lib/ip-utils.js +49 -6
- package/lib/jobs.js +3 -2
- package/lib/json-patch.js +1 -1
- package/lib/json-path.js +24 -3
- package/lib/jtd.js +2 -2
- package/lib/keychain.js +6 -18
- package/lib/legal-hold.js +30 -23
- package/lib/log-stream-cloudwatch.js +44 -112
- package/lib/log-stream-otlp-grpc.js +17 -14
- package/lib/log-stream-otlp.js +16 -80
- package/lib/log-stream-webhook.js +20 -92
- package/lib/log.js +2 -2
- package/lib/mail-agent.js +2 -2
- package/lib/mail-arc-sign.js +8 -3
- package/lib/mail-arf.js +4 -3
- package/lib/mail-auth.js +43 -69
- package/lib/mail-bimi.js +35 -55
- package/lib/mail-bounce.js +3 -3
- package/lib/mail-crypto-pgp.js +3 -2
- package/lib/mail-crypto-smime.js +71 -6
- package/lib/mail-dav.js +8 -35
- package/lib/mail-deploy.js +15 -14
- package/lib/mail-dkim.js +19 -38
- package/lib/mail-greylist.js +17 -30
- package/lib/mail-helo.js +4 -7
- package/lib/mail-journal.js +13 -9
- package/lib/mail-mdn.js +12 -7
- package/lib/mail-rbl.js +7 -12
- package/lib/mail-scan.js +5 -7
- package/lib/mail-send-deliver.js +33 -20
- package/lib/mail-server-imap.js +32 -87
- package/lib/mail-server-jmap.js +39 -52
- package/lib/mail-server-managesieve.js +29 -83
- package/lib/mail-server-mx.js +33 -74
- package/lib/mail-server-net.js +177 -0
- package/lib/mail-server-pop3.js +30 -83
- package/lib/mail-server-rate-limit.js +30 -6
- package/lib/mail-server-registry.js +1 -1
- package/lib/mail-server-submission.js +34 -73
- package/lib/mail-server-tls.js +98 -2
- package/lib/mail-spam-score.js +9 -12
- package/lib/mail-store-fts.js +1 -1
- package/lib/mail-store.js +3 -2
- package/lib/mail.js +28 -13
- package/lib/markup-escape.js +31 -0
- package/lib/markup-tokenizer.js +78 -0
- package/lib/mcp-tool-registry.js +26 -23
- package/lib/mcp.js +7 -5
- package/lib/mdoc.js +36 -14
- package/lib/metrics.js +46 -33
- package/lib/middleware/age-gate.js +3 -23
- package/lib/middleware/api-encrypt.js +13 -24
- package/lib/middleware/assetlinks.js +2 -7
- package/lib/middleware/bearer-auth.js +2 -1
- package/lib/middleware/body-parser.js +41 -52
- package/lib/middleware/bot-disclose.js +7 -10
- package/lib/middleware/bot-guard.js +28 -28
- package/lib/middleware/clear-site-data.js +5 -1
- package/lib/middleware/compression.js +9 -0
- package/lib/middleware/cors.js +32 -23
- package/lib/middleware/csrf-protect.js +73 -23
- package/lib/middleware/daily-byte-quota.js +12 -30
- package/lib/middleware/deny-response.js +19 -1
- package/lib/middleware/fetch-metadata.js +35 -4
- package/lib/middleware/idempotency-key.js +4 -20
- package/lib/middleware/network-allowlist.js +61 -30
- package/lib/middleware/rate-limit.js +45 -44
- package/lib/middleware/scim-server.js +5 -3
- package/lib/middleware/security-headers.js +33 -7
- package/lib/middleware/security-txt.js +2 -6
- package/lib/middleware/speculation-rules.js +6 -3
- package/lib/middleware/tus-upload.js +14 -29
- package/lib/middleware/web-app-manifest.js +2 -7
- package/lib/migrations.js +4 -13
- package/lib/mime-parse.js +34 -17
- package/lib/module-loader.js +44 -0
- package/lib/money.js +105 -0
- package/lib/mtls-ca.js +125 -41
- package/lib/network-byte-quota.js +4 -18
- package/lib/network-dane.js +8 -6
- package/lib/network-dns-resolver.js +98 -7
- package/lib/network-dns.js +9 -2
- package/lib/network-dnssec.js +18 -17
- package/lib/network-heartbeat.js +3 -2
- package/lib/network-smtp-policy.js +52 -46
- package/lib/network-tls.js +67 -47
- package/lib/network-tsig.js +2 -2
- package/lib/nis2-report.js +2 -12
- package/lib/nist-crosswalk.js +1 -1
- package/lib/nonce-store.js +81 -3
- package/lib/ntp-check.js +28 -0
- package/lib/numeric-bounds.js +31 -8
- package/lib/object-store/azure-blob-bucket-ops.js +2 -6
- package/lib/object-store/azure-blob.js +6 -47
- package/lib/object-store/gcs-bucket-ops.js +4 -2
- package/lib/object-store/gcs.js +14 -75
- package/lib/object-store/http-put.js +2 -7
- package/lib/object-store/http-request.js +157 -0
- package/lib/object-store/local.js +37 -17
- package/lib/object-store/sigv4-bucket-ops.js +2 -1
- package/lib/object-store/sigv4.js +10 -79
- package/lib/observability-otlp-exporter.js +29 -29
- package/lib/observability.js +95 -0
- package/lib/openapi-paths-builder.js +7 -3
- package/lib/otel-export.js +7 -10
- package/lib/outbox.js +54 -41
- package/lib/pagination.js +11 -15
- package/lib/parsers/safe-env.js +5 -4
- package/lib/parsers/safe-ini.js +10 -4
- package/lib/parsers/safe-toml.js +11 -9
- package/lib/parsers/safe-xml.js +3 -3
- package/lib/parsers/safe-yaml.js +28 -9
- package/lib/pick.js +63 -5
- package/lib/pipl-cn.js +69 -59
- package/lib/privacy-pass.js +8 -6
- package/lib/problem-details.js +2 -5
- package/lib/pubsub.js +4 -8
- package/lib/queue-local.js +10 -3
- package/lib/redact.js +9 -4
- package/lib/render.js +4 -2
- package/lib/request-helpers.js +334 -29
- package/lib/resource-access-lock.js +3 -3
- package/lib/restore-bundle.js +46 -18
- package/lib/restore-rollback.js +10 -4
- package/lib/restore.js +24 -22
- package/lib/retention.js +23 -9
- package/lib/router.js +17 -4
- package/lib/safe-async.js +457 -0
- package/lib/safe-buffer.js +137 -6
- package/lib/safe-decompress.js +2 -1
- package/lib/safe-dns.js +2 -1
- package/lib/safe-ical.js +14 -28
- package/lib/safe-icap.js +1 -1
- package/lib/safe-json.js +51 -8
- package/lib/safe-jsonpath.js +6 -5
- package/lib/safe-mime.js +25 -29
- package/lib/safe-redirect.js +2 -5
- package/lib/safe-schema.js +7 -6
- package/lib/safe-sieve.js +1 -1
- package/lib/safe-sql.js +126 -0
- package/lib/safe-vcard.js +13 -27
- package/lib/sandbox-worker.js +15 -2
- package/lib/sandbox.js +4 -3
- package/lib/scheduler.js +22 -13
- package/lib/sec-cyber.js +82 -37
- package/lib/seeders.js +4 -11
- package/lib/self-update-standalone-verifier.js +16 -0
- package/lib/self-update.js +92 -69
- package/lib/session-device-binding.js +112 -66
- package/lib/session.js +219 -7
- package/lib/sql.js +228 -81
- package/lib/sse.js +6 -10
- package/lib/static.js +201 -111
- package/lib/storage.js +4 -2
- package/lib/structured-fields.js +275 -16
- package/lib/template.js +7 -5
- package/lib/tenant-quota.js +53 -38
- package/lib/tsa.js +16 -17
- package/lib/validate-opts.js +154 -8
- package/lib/vault/index.js +5 -0
- package/lib/vault/passphrase-ops.js +22 -26
- package/lib/vault/passphrase-source.js +8 -3
- package/lib/vault/rotate.js +13 -18
- package/lib/vault/seal-pem-file.js +34 -49
- package/lib/vault-aad.js +3 -2
- package/lib/vc.js +3 -8
- package/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/public-suffix-list.dat +23 -10
- package/lib/vendor/public-suffix-list.data.js +5498 -5494
- package/lib/vex.js +35 -13
- package/lib/web-push-vapid.js +23 -20
- package/lib/webhook-dispatcher.js +706 -0
- package/lib/webhook.js +59 -19
- package/lib/websocket-channels.js +9 -7
- package/lib/websocket.js +8 -4
- package/lib/worm.js +3 -4
- package/lib/ws-client.js +65 -56
- package/lib/x509-chain.js +44 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/numeric-bounds.js
CHANGED
|
@@ -42,6 +42,20 @@ function shape(value) {
|
|
|
42
42
|
return (typeof value) + " " + String(value);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// _throwInt — construct + throw a code-first defineClass error. When no
|
|
46
|
+
// errorOpts is supplied the call is the historical bare `new ErrorClass(
|
|
47
|
+
// code, message)` (byte-identical for every existing caller); pass
|
|
48
|
+
// errorOpts { permanent, statusCode } to forward the framework error's
|
|
49
|
+
// non-retryable flag / HTTP status (a config opt that's wrong on retry
|
|
50
|
+
// stays wrong; a request-shaped opt carries its 4xx). Message-FIRST
|
|
51
|
+
// classes (AtomicFileError / SafeBufferError / SafeUrlError) must NOT use
|
|
52
|
+
// the require* throwers — they use the isPositiveFiniteInt predicate + their
|
|
53
|
+
// own throw (see the module header) so code/message don't swap.
|
|
54
|
+
function _throwInt(errorClass, code, message, errorOpts) {
|
|
55
|
+
if (errorOpts) throw new errorClass(code, message, errorOpts.permanent, errorOpts.statusCode);
|
|
56
|
+
throw new errorClass(code, message);
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
function isPositiveFiniteInt(value) {
|
|
46
60
|
return typeof value === "number" && Number.isFinite(value) &&
|
|
47
61
|
Number.isInteger(value) && value > 0;
|
|
@@ -52,6 +66,14 @@ function isNonNegativeFiniteInt(value) {
|
|
|
52
66
|
Number.isInteger(value) && value >= 0;
|
|
53
67
|
}
|
|
54
68
|
|
|
69
|
+
// Any-sign finite integer (no sign bound) — for callers that only require
|
|
70
|
+
// integrality (e.g. a CloudEvents 32-bit signed-integer extension, a DNS
|
|
71
|
+
// algorithm/digest-type code) and reject only non-numbers / floats / Infinity /
|
|
72
|
+
// NaN. Distinct from isNonNegativeFiniteInt (which also forbids negatives).
|
|
73
|
+
function isFiniteInt(value) {
|
|
74
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
// requirePositiveFiniteIntIfPresent / requireNonNegativeFiniteIntIfPresent —
|
|
56
78
|
// optional-shape gates that throw via the caller's framework-error class
|
|
57
79
|
// when the value is present but invalid. Replaces the per-file
|
|
@@ -64,11 +86,11 @@ function isNonNegativeFiniteInt(value) {
|
|
|
64
86
|
// nb.requirePositiveFiniteIntIfPresent(opts.graceMs,
|
|
65
87
|
// "app-shutdown.create: opts.graceMs", AppShutdownError,
|
|
66
88
|
// "app-shutdown/bad-grace-ms");
|
|
67
|
-
function requirePositiveFiniteIntIfPresent(value, label, errorClass, code) {
|
|
89
|
+
function requirePositiveFiniteIntIfPresent(value, label, errorClass, code, errorOpts) {
|
|
68
90
|
if (value === undefined) return value;
|
|
69
91
|
if (!isPositiveFiniteInt(value)) {
|
|
70
|
-
|
|
71
|
-
(label || "value") + " must be a positive finite integer; got " + shape(value));
|
|
92
|
+
_throwInt(errorClass, code,
|
|
93
|
+
(label || "value") + " must be a positive finite integer; got " + shape(value), errorOpts);
|
|
72
94
|
}
|
|
73
95
|
return value;
|
|
74
96
|
}
|
|
@@ -102,13 +124,13 @@ function _rangeSuffix(range) {
|
|
|
102
124
|
if (range.min != null) return " >= " + range.min;
|
|
103
125
|
return "";
|
|
104
126
|
}
|
|
105
|
-
function requirePositiveFiniteInt(value, label, errorClass, code, range) {
|
|
127
|
+
function requirePositiveFiniteInt(value, label, errorClass, code, range, errorOpts) {
|
|
106
128
|
var inRange = !range ||
|
|
107
129
|
((range.min == null || value >= range.min) && (range.max == null || value <= range.max));
|
|
108
130
|
if (!isPositiveFiniteInt(value) || !inRange) {
|
|
109
|
-
|
|
131
|
+
_throwInt(errorClass, code,
|
|
110
132
|
(label || "value") + " must be a positive finite integer" +
|
|
111
|
-
_rangeSuffix(range) + "; got " + shape(value));
|
|
133
|
+
_rangeSuffix(range) + "; got " + shape(value), errorOpts);
|
|
112
134
|
}
|
|
113
135
|
return value;
|
|
114
136
|
}
|
|
@@ -123,12 +145,12 @@ function requirePositiveFiniteInt(value, label, errorClass, code, range) {
|
|
|
123
145
|
// nb.requireAllPositiveFiniteIntIfPresent(opts,
|
|
124
146
|
// ["maxBytes", "maxAttrValueBytes", "maxTagDepth", "maxAttrsPerTag"],
|
|
125
147
|
// "guardHtml.validate", GuardHtmlError, "html.bad-opt");
|
|
126
|
-
function requireAllPositiveFiniteIntIfPresent(opts, names, labelPrefix, errorClass, code) {
|
|
148
|
+
function requireAllPositiveFiniteIntIfPresent(opts, names, labelPrefix, errorClass, code, errorOpts) {
|
|
127
149
|
if (!opts || !Array.isArray(names)) return;
|
|
128
150
|
for (var i = 0; i < names.length; i += 1) {
|
|
129
151
|
var n = names[i];
|
|
130
152
|
requirePositiveFiniteIntIfPresent(opts[n],
|
|
131
|
-
(labelPrefix || "") + ": " + n, errorClass, code);
|
|
153
|
+
(labelPrefix || "") + ": " + n, errorClass, code, errorOpts);
|
|
132
154
|
}
|
|
133
155
|
}
|
|
134
156
|
|
|
@@ -136,6 +158,7 @@ module.exports = {
|
|
|
136
158
|
shape: shape,
|
|
137
159
|
isPositiveFiniteInt: isPositiveFiniteInt,
|
|
138
160
|
isNonNegativeFiniteInt: isNonNegativeFiniteInt,
|
|
161
|
+
isFiniteInt: isFiniteInt,
|
|
139
162
|
requirePositiveFiniteInt: requirePositiveFiniteInt,
|
|
140
163
|
requirePositiveFiniteIntIfPresent: requirePositiveFiniteIntIfPresent,
|
|
141
164
|
requireNonNegativeFiniteIntIfPresent: requireNonNegativeFiniteIntIfPresent,
|
|
@@ -33,6 +33,7 @@ var C = require("../constants");
|
|
|
33
33
|
var httpClient = require("../http-client");
|
|
34
34
|
var requestHelpers = require("../request-helpers");
|
|
35
35
|
var safeUrl = require("../safe-url");
|
|
36
|
+
var markupEscape = require("../markup-escape").markupEscape;
|
|
36
37
|
var { ObjectStoreError } = require("../framework-error");
|
|
37
38
|
|
|
38
39
|
var _err = ObjectStoreError.factory;
|
|
@@ -122,12 +123,7 @@ function _validateCorsRule(rule, idx) {
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
function _xmlEscape(s) {
|
|
125
|
-
return
|
|
126
|
-
.replace(/&/g, "&")
|
|
127
|
-
.replace(/</g, "<")
|
|
128
|
-
.replace(/>/g, ">")
|
|
129
|
-
.replace(/"/g, """)
|
|
130
|
-
.replace(/'/g, "'");
|
|
126
|
+
return markupEscape(s, { apos: "'" });
|
|
131
127
|
}
|
|
132
128
|
|
|
133
129
|
function _buildCorsXml(rules) {
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
*/
|
|
38
38
|
var nodeCrypto = require("node:crypto");
|
|
39
39
|
var { URL } = require("node:url");
|
|
40
|
-
var { Readable } = require("node:stream");
|
|
41
40
|
var safeXml = require("../parsers/safe-xml");
|
|
42
41
|
var sharedRequest = require("./http-request");
|
|
43
42
|
var sigv4 = require("./sigv4");
|
|
@@ -93,14 +92,6 @@ function _encodeBlobKey(key) {
|
|
|
93
92
|
|
|
94
93
|
var DEFAULT_API_VERSION = "2024-08-04";
|
|
95
94
|
|
|
96
|
-
// Service SAS expiry bounds. Azure doesn't enforce a hard max, but
|
|
97
|
-
// matching the SigV4 / V4 7-day cap keeps semantics uniform across
|
|
98
|
-
// backends and reflects best practice (long-lived SAS tokens are
|
|
99
|
-
// effectively credentials).
|
|
100
|
-
var SAS_DEFAULT_EXPIRES_SECONDS = C.TIME.minutes(15) / C.TIME.seconds(1);
|
|
101
|
-
var SAS_MAX_EXPIRES_SECONDS = C.TIME.days(7) / C.TIME.seconds(1);
|
|
102
|
-
var SAS_MIN_EXPIRES_SECONDS = 1;
|
|
103
|
-
|
|
104
95
|
var _err = ObjectStoreError.factory;
|
|
105
96
|
|
|
106
97
|
var _httpRequest = sharedRequest;
|
|
@@ -307,38 +298,18 @@ function create(config) {
|
|
|
307
298
|
return getResponse(key, opts).then(function (r) { return r.body; });
|
|
308
299
|
}
|
|
309
300
|
|
|
310
|
-
function getStream(key, opts) { return
|
|
301
|
+
function getStream(key, opts) { return sharedRequest.promiseToStream(get(key, opts)); }
|
|
311
302
|
|
|
312
303
|
function getResponse(key, opts) {
|
|
313
304
|
opts = opts || {};
|
|
314
305
|
var url = _blobUrl(key);
|
|
315
|
-
var extraHeaders = {};
|
|
316
|
-
if (opts.range) {
|
|
317
|
-
extraHeaders["x-ms-range"] = "bytes=" + opts.range.start + "-" + opts.range.end;
|
|
318
|
-
}
|
|
319
|
-
if (opts.ifNoneMatch) extraHeaders["If-None-Match"] = opts.ifNoneMatch;
|
|
320
|
-
if (opts.ifMatch) extraHeaders["If-Match"] = opts.ifMatch;
|
|
321
|
-
if (opts.ifModifiedSince) extraHeaders["If-Modified-Since"] = opts.ifModifiedSince;
|
|
322
|
-
if (opts.ifUnmodifiedSince) extraHeaders["If-Unmodified-Since"] = opts.ifUnmodifiedSince;
|
|
306
|
+
var extraHeaders = sharedRequest.applyConditionalGetHeaders({}, opts, "x-ms-range");
|
|
323
307
|
var headers = _signed("GET", url, extraHeaders);
|
|
324
308
|
return _httpRequest("GET", url, headers, null, reqOpts).then(function (res) {
|
|
325
|
-
return
|
|
326
|
-
statusCode: res.statusCode,
|
|
327
|
-
body: res.body,
|
|
328
|
-
etag: res.headers && res.headers.etag,
|
|
329
|
-
lastModified: res.headers && res.headers["last-modified"]
|
|
330
|
-
? Date.parse(res.headers["last-modified"]) : null,
|
|
331
|
-
contentRange: res.headers && res.headers["content-range"] || null,
|
|
332
|
-
size: res.headers && res.headers["content-length"]
|
|
333
|
-
? parseInt(res.headers["content-length"], 10) : null,
|
|
334
|
-
contentType: res.headers && res.headers["content-type"] || null,
|
|
335
|
-
};
|
|
309
|
+
return sharedRequest.mapGetResponse(res);
|
|
336
310
|
}, function (err) {
|
|
337
311
|
if (err && err.statusCode === requestHelpers.HTTP_STATUS.NOT_MODIFIED) {
|
|
338
|
-
return
|
|
339
|
-
statusCode: requestHelpers.HTTP_STATUS.NOT_MODIFIED,
|
|
340
|
-
body: null, etag: null, lastModified: null,
|
|
341
|
-
};
|
|
312
|
+
return sharedRequest.notModifiedGetResult();
|
|
342
313
|
}
|
|
343
314
|
throw err;
|
|
344
315
|
});
|
|
@@ -348,11 +319,7 @@ function create(config) {
|
|
|
348
319
|
var url = _blobUrl(key);
|
|
349
320
|
var headers = _signed("HEAD", url, {});
|
|
350
321
|
return _httpRequest("HEAD", url, headers, null, reqOpts).then(function (res) {
|
|
351
|
-
return
|
|
352
|
-
size: res.headers["content-length"] ? parseInt(res.headers["content-length"], 10) : null,
|
|
353
|
-
etag: res.headers.etag,
|
|
354
|
-
lastModified: res.headers["last-modified"] ? Date.parse(res.headers["last-modified"]) : null,
|
|
355
|
-
};
|
|
322
|
+
return sharedRequest.mapHeadResponse(res);
|
|
356
323
|
});
|
|
357
324
|
}
|
|
358
325
|
|
|
@@ -415,15 +382,7 @@ function create(config) {
|
|
|
415
382
|
// "Create a service SAS". We sign with the account key (HMAC-SHA256
|
|
416
383
|
// over the canonical string) and emit the token as URL query params.
|
|
417
384
|
function _buildSasToken(permissions, opts) {
|
|
418
|
-
var expiresIn = opts
|
|
419
|
-
if (typeof expiresIn !== "number" ||
|
|
420
|
-
expiresIn < SAS_MIN_EXPIRES_SECONDS ||
|
|
421
|
-
expiresIn > SAS_MAX_EXPIRES_SECONDS) {
|
|
422
|
-
throw _err("INVALID_EXPIRES",
|
|
423
|
-
"presigned URL: expiresIn must be a number of seconds between " +
|
|
424
|
-
SAS_MIN_EXPIRES_SECONDS + " and " + SAS_MAX_EXPIRES_SECONDS +
|
|
425
|
-
" (7 days)", true);
|
|
426
|
-
}
|
|
385
|
+
var expiresIn = sharedRequest.resolvePresignExpires(opts, "presigned URL", "");
|
|
427
386
|
var nowDate = opts.date || new Date();
|
|
428
387
|
var expiry = new Date(nowDate.getTime() + C.TIME.seconds(expiresIn));
|
|
429
388
|
// Azure accepts ISO 8601 with second precision; strip ms.
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
* Auth: same service-account JSON / RSA-SHA256-signed JWT exchanged
|
|
24
24
|
* for an OAuth2 access token as `lib/object-store/gcs.js`.
|
|
25
25
|
*/
|
|
26
|
-
var nodeFs = require("node:fs");
|
|
27
26
|
var gcs = require("./gcs");
|
|
28
27
|
var authHeader = require("../auth-header");
|
|
29
28
|
var httpClient = require("../http-client");
|
|
30
29
|
var safeJson = require("../safe-json");
|
|
31
30
|
var safeUrl = require("../safe-url");
|
|
32
31
|
var C = require("../constants");
|
|
32
|
+
var atomicFile = require("../atomic-file");
|
|
33
33
|
var requestHelpers = require("../request-helpers");
|
|
34
34
|
var { ObjectStoreError } = require("../framework-error");
|
|
35
35
|
|
|
@@ -139,7 +139,9 @@ function create(config) {
|
|
|
139
139
|
var serviceAccount = config.serviceAccount;
|
|
140
140
|
if (!serviceAccount && config.serviceAccountFile) {
|
|
141
141
|
try {
|
|
142
|
-
|
|
142
|
+
// Cap + fd-bound read of the GCS service-account JSON (private_key). NO
|
|
143
|
+
// refuseSymlink: commonly a k8s projected-secret mount (symlink).
|
|
144
|
+
serviceAccount = safeJson.parse(atomicFile.fdSafeReadSync(config.serviceAccountFile, { maxBytes: C.BYTES.kib(64), encoding: "utf8" }));
|
|
143
145
|
} catch (e) {
|
|
144
146
|
throw _err("BAD_OPT", "gcs bucketOps: failed to read serviceAccountFile '" +
|
|
145
147
|
config.serviceAccountFile + "': " + ((e && e.message) || String(e)), true);
|
package/lib/object-store/gcs.js
CHANGED
|
@@ -22,13 +22,11 @@
|
|
|
22
22
|
* https://cloud.google.com/storage/docs/json_api/v1
|
|
23
23
|
* https://developers.google.com/identity/protocols/oauth2/service-account
|
|
24
24
|
*/
|
|
25
|
-
var nodeFs = require("node:fs");
|
|
26
25
|
var nodeCrypto = require("node:crypto");
|
|
27
|
-
var { Readable } = require("node:stream");
|
|
28
26
|
var bCrypto = require("../crypto");
|
|
29
27
|
var safeJson = require("../safe-json");
|
|
30
28
|
var C = require("../constants");
|
|
31
|
-
var
|
|
29
|
+
var atomicFile = require("../atomic-file");
|
|
32
30
|
var requestHelpers = require("../request-helpers");
|
|
33
31
|
var { ObjectStoreError } = require("../framework-error");
|
|
34
32
|
var safeUrl = require("../safe-url");
|
|
@@ -60,11 +58,6 @@ var GCS_V4_ALGORITHM = "GOOG4-RSA-SHA256";
|
|
|
60
58
|
var GCS_V4_SERVICE = "storage";
|
|
61
59
|
var GCS_V4_REGION = "auto";
|
|
62
60
|
|
|
63
|
-
// Same expiry bounds as SigV4 — V4 presigning shares the spec's 7-day cap.
|
|
64
|
-
var PRESIGN_DEFAULT_EXPIRES_SECONDS = C.TIME.minutes(15) / C.TIME.seconds(1);
|
|
65
|
-
var PRESIGN_MAX_EXPIRES_SECONDS = C.TIME.days(7) / C.TIME.seconds(1);
|
|
66
|
-
var PRESIGN_MIN_EXPIRES_SECONDS = 1;
|
|
67
|
-
|
|
68
61
|
var SERVICE_ACCOUNT_SCHEMA = {
|
|
69
62
|
type: "object",
|
|
70
63
|
required: ["client_email", "private_key"],
|
|
@@ -121,7 +114,10 @@ function create(config) {
|
|
|
121
114
|
var serviceAccount = config.serviceAccount;
|
|
122
115
|
if (!serviceAccount && config.serviceAccountFile) {
|
|
123
116
|
try {
|
|
124
|
-
|
|
117
|
+
// Cap + fd-bound read of the GCS service-account JSON (contains the
|
|
118
|
+
// private_key). NO refuseSymlink: the SA file is commonly a k8s
|
|
119
|
+
// projected-secret mount (symlink). 64 KiB bounds the uncapped read.
|
|
120
|
+
serviceAccount = safeJson.parse(atomicFile.fdSafeReadSync(config.serviceAccountFile, { maxBytes: C.BYTES.kib(64), encoding: "utf8" }), { schema: SERVICE_ACCOUNT_SCHEMA });
|
|
125
121
|
} catch (e) {
|
|
126
122
|
throw new Error("gcs: failed to read serviceAccountFile '" + config.serviceAccountFile + "': " + e.message);
|
|
127
123
|
}
|
|
@@ -230,7 +226,7 @@ function create(config) {
|
|
|
230
226
|
}
|
|
231
227
|
|
|
232
228
|
function getStream(key, opts) {
|
|
233
|
-
return
|
|
229
|
+
return sharedRequest.promiseToStream(get(key, opts));
|
|
234
230
|
}
|
|
235
231
|
|
|
236
232
|
async function getResponse(key, opts) {
|
|
@@ -238,32 +234,13 @@ function create(config) {
|
|
|
238
234
|
var token = await _ensureToken();
|
|
239
235
|
var url = _objectUrl(key, { alt: "media" });
|
|
240
236
|
var headers = authHeader.bearer(token);
|
|
241
|
-
|
|
242
|
-
headers["Range"] = "bytes=" + opts.range.start + "-" + opts.range.end;
|
|
243
|
-
}
|
|
244
|
-
if (opts.ifNoneMatch) headers["If-None-Match"] = opts.ifNoneMatch;
|
|
245
|
-
if (opts.ifMatch) headers["If-Match"] = opts.ifMatch;
|
|
246
|
-
if (opts.ifModifiedSince) headers["If-Modified-Since"] = opts.ifModifiedSince;
|
|
247
|
-
if (opts.ifUnmodifiedSince) headers["If-Unmodified-Since"] = opts.ifUnmodifiedSince;
|
|
237
|
+
sharedRequest.applyConditionalGetHeaders(headers, opts, "Range");
|
|
248
238
|
try {
|
|
249
239
|
var res = await _httpRequest("GET", url, headers, null, reqOpts);
|
|
250
|
-
return
|
|
251
|
-
statusCode: res.statusCode,
|
|
252
|
-
body: res.body,
|
|
253
|
-
etag: res.headers && res.headers.etag,
|
|
254
|
-
lastModified: res.headers && res.headers["last-modified"]
|
|
255
|
-
? Date.parse(res.headers["last-modified"]) : null,
|
|
256
|
-
contentRange: res.headers && res.headers["content-range"] || null,
|
|
257
|
-
size: res.headers && res.headers["content-length"]
|
|
258
|
-
? parseInt(res.headers["content-length"], 10) : null,
|
|
259
|
-
contentType: res.headers && res.headers["content-type"] || null,
|
|
260
|
-
};
|
|
240
|
+
return sharedRequest.mapGetResponse(res);
|
|
261
241
|
} catch (err) {
|
|
262
242
|
if (err && err.statusCode === requestHelpers.HTTP_STATUS.NOT_MODIFIED) {
|
|
263
|
-
return
|
|
264
|
-
statusCode: requestHelpers.HTTP_STATUS.NOT_MODIFIED,
|
|
265
|
-
body: null, etag: null, lastModified: null,
|
|
266
|
-
};
|
|
243
|
+
return sharedRequest.notModifiedGetResult();
|
|
267
244
|
}
|
|
268
245
|
throw err;
|
|
269
246
|
}
|
|
@@ -326,21 +303,8 @@ function create(config) {
|
|
|
326
303
|
|
|
327
304
|
function _v4Presign(method, opts) {
|
|
328
305
|
opts = opts || {};
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
if (opts.key.indexOf("\0") !== -1) {
|
|
333
|
-
throw _err("INVALID_KEY", "null byte in key", true);
|
|
334
|
-
}
|
|
335
|
-
var expiresIn = opts.expiresIn != null ? opts.expiresIn : PRESIGN_DEFAULT_EXPIRES_SECONDS;
|
|
336
|
-
if (typeof expiresIn !== "number" ||
|
|
337
|
-
expiresIn < PRESIGN_MIN_EXPIRES_SECONDS ||
|
|
338
|
-
expiresIn > PRESIGN_MAX_EXPIRES_SECONDS) {
|
|
339
|
-
throw _err("INVALID_EXPIRES",
|
|
340
|
-
"presigned URL: expiresIn must be a number of seconds between " +
|
|
341
|
-
PRESIGN_MIN_EXPIRES_SECONDS + " and " + PRESIGN_MAX_EXPIRES_SECONDS +
|
|
342
|
-
" (7 days, V4 hard cap)", true);
|
|
343
|
-
}
|
|
306
|
+
sharedRequest.requirePresignKey(opts, "presigned URL");
|
|
307
|
+
var expiresIn = sharedRequest.resolvePresignExpires(opts, "presigned URL", "V4");
|
|
344
308
|
|
|
345
309
|
// V4 presigned URLs target the XML API path-style endpoint
|
|
346
310
|
// (storage.googleapis.com/{bucket}/{key}), not the JSON API used
|
|
@@ -418,34 +382,9 @@ function create(config) {
|
|
|
418
382
|
// declared range get a 403 from the bucket itself.
|
|
419
383
|
function presignedUploadPolicy(opts) {
|
|
420
384
|
opts = opts || {};
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (opts.key.indexOf("\0") !== -1) {
|
|
425
|
-
throw _err("INVALID_KEY", "null byte in key", true);
|
|
426
|
-
}
|
|
427
|
-
if (typeof opts.maxBytes !== "number" || !Number.isFinite(opts.maxBytes) ||
|
|
428
|
-
opts.maxBytes <= 0) {
|
|
429
|
-
throw _err("INVALID_MAX_BYTES",
|
|
430
|
-
"presignedUploadPolicy: maxBytes (positive number of bytes) is required — " +
|
|
431
|
-
"POST-form policy enforces body size via the content-length-range condition; " +
|
|
432
|
-
"use presignedUploadUrl if size enforcement is not needed", true);
|
|
433
|
-
}
|
|
434
|
-
if (opts.minBytes !== undefined && !numericBounds.isNonNegativeFiniteInt(opts.minBytes)) {
|
|
435
|
-
throw _err("INVALID_MIN_BYTES",
|
|
436
|
-
"presignedUploadPolicy: minBytes must be a non-negative finite integer; got " +
|
|
437
|
-
numericBounds.shape(opts.minBytes), true);
|
|
438
|
-
}
|
|
439
|
-
var minBytes = opts.minBytes !== undefined ? opts.minBytes : 0;
|
|
440
|
-
var expiresIn = opts.expiresIn != null ? opts.expiresIn : PRESIGN_DEFAULT_EXPIRES_SECONDS;
|
|
441
|
-
if (typeof expiresIn !== "number" ||
|
|
442
|
-
expiresIn < PRESIGN_MIN_EXPIRES_SECONDS ||
|
|
443
|
-
expiresIn > PRESIGN_MAX_EXPIRES_SECONDS) {
|
|
444
|
-
throw _err("INVALID_EXPIRES",
|
|
445
|
-
"presignedUploadPolicy: expiresIn must be a number of seconds between " +
|
|
446
|
-
PRESIGN_MIN_EXPIRES_SECONDS + " and " + PRESIGN_MAX_EXPIRES_SECONDS +
|
|
447
|
-
" (7 days, V4 hard cap)", true);
|
|
448
|
-
}
|
|
385
|
+
sharedRequest.requirePresignKey(opts, "presignedUploadPolicy");
|
|
386
|
+
var minBytes = sharedRequest.resolvePresignUploadMinBytes(opts);
|
|
387
|
+
var expiresIn = sharedRequest.resolvePresignExpires(opts, "presignedUploadPolicy", "V4");
|
|
449
388
|
|
|
450
389
|
var date = opts.date || new Date();
|
|
451
390
|
var amzDate = sigv4.formatAmzDate(date);
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* Errors are surfaced as object-store errors with statusCode set so the
|
|
17
17
|
* retry layer can classify retryable vs permanent.
|
|
18
18
|
*/
|
|
19
|
-
var { Readable } = require("node:stream");
|
|
20
19
|
var { ObjectStoreError } = require("../framework-error");
|
|
21
20
|
var safeUrl = require("../safe-url");
|
|
22
21
|
var sharedRequest = require("./http-request");
|
|
@@ -96,17 +95,13 @@ function create(config) {
|
|
|
96
95
|
// https that doesn't buffer; return a Readable wrapping the buffered
|
|
97
96
|
// body. Backends that support chunked transfer (e.g. SigV4) implement
|
|
98
97
|
// a real streaming getStream in their own adapter.
|
|
99
|
-
return
|
|
98
|
+
return sharedRequest.promiseToStream(get(key));
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
function head(key) {
|
|
103
102
|
var url = _keyToUrl(baseUrl, key);
|
|
104
103
|
return _request("HEAD", url, null, headers, reqOpts).then(function (res) {
|
|
105
|
-
return
|
|
106
|
-
size: res.headers["content-length"] ? parseInt(res.headers["content-length"], 10) : null,
|
|
107
|
-
etag: res.headers.etag,
|
|
108
|
-
lastModified: res.headers["last-modified"] ? Date.parse(res.headers["last-modified"]) : null,
|
|
109
|
-
};
|
|
104
|
+
return sharedRequest.mapHeadResponse(res);
|
|
110
105
|
});
|
|
111
106
|
}
|
|
112
107
|
|
|
@@ -16,9 +16,19 @@
|
|
|
16
16
|
* different class pass `errorClass: SomeError` in opts.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
var { Readable } = require("node:stream");
|
|
19
20
|
var httpClient = require("../http-client");
|
|
21
|
+
var C = require("../constants");
|
|
22
|
+
var numericBounds = require("../numeric-bounds");
|
|
23
|
+
var requestHelpers = require("../request-helpers");
|
|
20
24
|
var { ObjectStoreError } = require("../framework-error");
|
|
21
25
|
|
|
26
|
+
// Same code-first error factory the object-store backends use; the bare
|
|
27
|
+
// cloud-provider-style codes (INVALID_KEY / INVALID_EXPIRES / ...) are the
|
|
28
|
+
// existing object-store convention, swept to namespace/kebab-case for the
|
|
29
|
+
// whole family in the v1.0 error-code pass.
|
|
30
|
+
var _err = ObjectStoreError.factory;
|
|
31
|
+
|
|
22
32
|
function request(method, url, headers, body, opts) {
|
|
23
33
|
opts = opts || {};
|
|
24
34
|
var req = {
|
|
@@ -35,4 +45,151 @@ function request(method, url, headers, body, opts) {
|
|
|
35
45
|
return httpClient.request(req);
|
|
36
46
|
}
|
|
37
47
|
|
|
48
|
+
// ---- Shared presign opts validation (gcs / sigv4) ----
|
|
49
|
+
// Every presigning entry point (presigned GET/PUT URLs and the POST-form
|
|
50
|
+
// upload policy) opened with the same key + expiry bounds checks, varying
|
|
51
|
+
// only in the message prefix and the signature-version label in the
|
|
52
|
+
// hard-cap text. Centralized so the bounds are enforced identically and a
|
|
53
|
+
// future change (a new cap, a different default) is one edit.
|
|
54
|
+
var PRESIGN_DEFAULT_EXPIRES_SECONDS = C.TIME.minutes(15) / C.TIME.seconds(1); // 15 minutes
|
|
55
|
+
var PRESIGN_MAX_EXPIRES_SECONDS = C.TIME.days(7) / C.TIME.seconds(1); // 7 days
|
|
56
|
+
var PRESIGN_MIN_EXPIRES_SECONDS = 1;
|
|
57
|
+
|
|
58
|
+
function requirePresignKey(opts, msgPrefix) {
|
|
59
|
+
if (!opts.key || typeof opts.key !== "string") {
|
|
60
|
+
throw _err("INVALID_KEY", msgPrefix + ": key is required", true);
|
|
61
|
+
}
|
|
62
|
+
if (opts.key.indexOf("\0") !== -1) {
|
|
63
|
+
throw _err("INVALID_KEY", "null byte in key", true);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// hardCapLabel names the signing version in the cap text ("V4" / "SigV4"
|
|
68
|
+
// → "(7 days, V4 hard cap)"); an empty / omitted label yields the plain
|
|
69
|
+
// "(7 days)" tail used by the Azure SAS path, whose cap is the same 7 days
|
|
70
|
+
// but is not a SigV4 family limit.
|
|
71
|
+
function resolvePresignExpires(opts, msgPrefix, hardCapLabel) {
|
|
72
|
+
var expiresIn = opts.expiresIn != null ? opts.expiresIn : PRESIGN_DEFAULT_EXPIRES_SECONDS;
|
|
73
|
+
if (typeof expiresIn !== "number" ||
|
|
74
|
+
expiresIn < PRESIGN_MIN_EXPIRES_SECONDS ||
|
|
75
|
+
expiresIn > PRESIGN_MAX_EXPIRES_SECONDS) {
|
|
76
|
+
var capTail = hardCapLabel ? " (7 days, " + hardCapLabel + " hard cap)" : " (7 days)";
|
|
77
|
+
throw _err("INVALID_EXPIRES",
|
|
78
|
+
msgPrefix + ": expiresIn must be a number of seconds between " +
|
|
79
|
+
PRESIGN_MIN_EXPIRES_SECONDS + " and " + PRESIGN_MAX_EXPIRES_SECONDS +
|
|
80
|
+
capTail, true);
|
|
81
|
+
}
|
|
82
|
+
return expiresIn;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// POST-form upload policy size bounds — returns the resolved minBytes
|
|
86
|
+
// (defaulting to 0). maxBytes is validated here but consumed by the
|
|
87
|
+
// caller (it becomes the content-length-range upper bound).
|
|
88
|
+
function resolvePresignUploadMinBytes(opts) {
|
|
89
|
+
if (typeof opts.maxBytes !== "number" || !Number.isFinite(opts.maxBytes) ||
|
|
90
|
+
opts.maxBytes <= 0) {
|
|
91
|
+
throw _err("INVALID_MAX_BYTES",
|
|
92
|
+
"presignedUploadPolicy: maxBytes (positive number of bytes) is required — " +
|
|
93
|
+
"POST-form policy enforces body size via the content-length-range condition; " +
|
|
94
|
+
"use presignedUploadUrl if size enforcement is not needed", true);
|
|
95
|
+
}
|
|
96
|
+
if (opts.minBytes !== undefined && !numericBounds.isNonNegativeFiniteInt(opts.minBytes)) {
|
|
97
|
+
throw _err("INVALID_MIN_BYTES",
|
|
98
|
+
"presignedUploadPolicy: minBytes must be a non-negative finite integer; got " +
|
|
99
|
+
numericBounds.shape(opts.minBytes), true);
|
|
100
|
+
}
|
|
101
|
+
return opts.minBytes !== undefined ? opts.minBytes : 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- Shared conditional-GET request + response mapping (gcs / sigv4 / azure) ----
|
|
105
|
+
// Every backend's getResponse() set the same RFC 7232/7233 conditional
|
|
106
|
+
// headers (Range + If-Match family), mapped the HTTP response into the same
|
|
107
|
+
// { statusCode, body, etag, lastModified, contentRange, size, contentType }
|
|
108
|
+
// shape, and turned a 304 into the same short-circuit result. Only the Range
|
|
109
|
+
// header NAME differs (S3/GCS "Range" vs Azure "x-ms-range"), so it is a
|
|
110
|
+
// parameter; the rest is identical.
|
|
111
|
+
function applyConditionalGetHeaders(target, opts, rangeHeaderName) {
|
|
112
|
+
if (opts.range) {
|
|
113
|
+
// The documented + shipped contract is an array [start, end] (see
|
|
114
|
+
// b.archive.adapters.objectStore and b.backup). Reading .start/.end off an
|
|
115
|
+
// array yields undefined → "bytes=undefined-undefined", which every store
|
|
116
|
+
// IGNORES — silently returning the FULL object instead of the byte range
|
|
117
|
+
// (over-fetch + wrong data for a partial read). Accept the array form (and
|
|
118
|
+
// {start,end} for compatibility) and validate, so a malformed range fails
|
|
119
|
+
// loudly rather than emitting a garbage header.
|
|
120
|
+
var r = opts.range;
|
|
121
|
+
var start = Array.isArray(r) ? r[0] : r.start;
|
|
122
|
+
var end = Array.isArray(r) ? r[1] : r.end;
|
|
123
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start) {
|
|
124
|
+
throw _err("INVALID_RANGE",
|
|
125
|
+
"range must be [start, end] (or { start, end }) with 0 <= start <= end, got " +
|
|
126
|
+
JSON.stringify(r), true);
|
|
127
|
+
}
|
|
128
|
+
target[rangeHeaderName] = "bytes=" + start + "-" + end;
|
|
129
|
+
}
|
|
130
|
+
if (opts.ifNoneMatch) target["If-None-Match"] = opts.ifNoneMatch;
|
|
131
|
+
if (opts.ifMatch) target["If-Match"] = opts.ifMatch;
|
|
132
|
+
if (opts.ifModifiedSince) target["If-Modified-Since"] = opts.ifModifiedSince;
|
|
133
|
+
if (opts.ifUnmodifiedSince) target["If-Unmodified-Since"] = opts.ifUnmodifiedSince;
|
|
134
|
+
return target;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function mapGetResponse(res) {
|
|
138
|
+
return {
|
|
139
|
+
statusCode: res.statusCode,
|
|
140
|
+
body: res.body,
|
|
141
|
+
etag: res.headers && res.headers.etag,
|
|
142
|
+
lastModified: res.headers && res.headers["last-modified"]
|
|
143
|
+
? Date.parse(res.headers["last-modified"]) : null,
|
|
144
|
+
contentRange: res.headers && res.headers["content-range"] || null,
|
|
145
|
+
size: res.headers && res.headers["content-length"]
|
|
146
|
+
? parseInt(res.headers["content-length"], 10) : null,
|
|
147
|
+
contentType: res.headers && res.headers["content-type"] || null,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// HEAD response projection — { size, etag, lastModified } from the response
|
|
152
|
+
// headers. Shared by the header-driven backends (azure / sigv4 / http-put);
|
|
153
|
+
// gcs.head is NOT a member (it reads a JSON metadata body, not headers).
|
|
154
|
+
function mapHeadResponse(res) {
|
|
155
|
+
return {
|
|
156
|
+
size: res.headers["content-length"] ? parseInt(res.headers["content-length"], 10) : null,
|
|
157
|
+
etag: res.headers.etag,
|
|
158
|
+
lastModified: res.headers["last-modified"] ? Date.parse(res.headers["last-modified"]) : null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The structured result a conditional GET returns when the server answers
|
|
163
|
+
// 304 Not Modified (httpClient surfaces it as a non-2xx error each backend
|
|
164
|
+
// catches and maps here).
|
|
165
|
+
function notModifiedGetResult() {
|
|
166
|
+
return {
|
|
167
|
+
statusCode: requestHelpers.HTTP_STATUS.NOT_MODIFIED,
|
|
168
|
+
body: null, etag: null, lastModified: null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Wrap a Promise<Buffer|string> as a Readable WITHOUT awaiting it first, so a
|
|
173
|
+
// backend's getStream() stays synchronous (the dispatcher hands the returned
|
|
174
|
+
// Readable straight to the consumer). A bare `Readable.from(promise)` throws
|
|
175
|
+
// ERR_INVALID_ARG_TYPE — Readable.from needs an (async-)iterable, not a Promise
|
|
176
|
+
// — which broke getStream on every remote backend. The async generator defers
|
|
177
|
+
// the await to the first read; a rejection surfaces as the stream's 'error'
|
|
178
|
+
// event, matching the dispatcher's "the Readable surfaces its own errors"
|
|
179
|
+
// contract.
|
|
180
|
+
function promiseToStream(promise) {
|
|
181
|
+
return Readable.from((async function* () { yield await promise; })());
|
|
182
|
+
}
|
|
183
|
+
|
|
38
184
|
module.exports = request;
|
|
185
|
+
module.exports.applyConditionalGetHeaders = applyConditionalGetHeaders;
|
|
186
|
+
module.exports.promiseToStream = promiseToStream;
|
|
187
|
+
module.exports.mapGetResponse = mapGetResponse;
|
|
188
|
+
module.exports.mapHeadResponse = mapHeadResponse;
|
|
189
|
+
module.exports.notModifiedGetResult = notModifiedGetResult;
|
|
190
|
+
module.exports.PRESIGN_DEFAULT_EXPIRES_SECONDS = PRESIGN_DEFAULT_EXPIRES_SECONDS;
|
|
191
|
+
module.exports.PRESIGN_MAX_EXPIRES_SECONDS = PRESIGN_MAX_EXPIRES_SECONDS;
|
|
192
|
+
module.exports.PRESIGN_MIN_EXPIRES_SECONDS = PRESIGN_MIN_EXPIRES_SECONDS;
|
|
193
|
+
module.exports.requirePresignKey = requirePresignKey;
|
|
194
|
+
module.exports.resolvePresignExpires = resolvePresignExpires;
|
|
195
|
+
module.exports.resolvePresignUploadMinBytes = resolvePresignUploadMinBytes;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
var nodeFs = require("node:fs");
|
|
15
15
|
var nodePath = require("node:path");
|
|
16
16
|
var atomicFile = require("../atomic-file");
|
|
17
|
+
var C = require("../constants");
|
|
17
18
|
var cluster = require("../cluster");
|
|
18
19
|
var { ObjectStoreError } = require("../framework-error");
|
|
19
20
|
|
|
@@ -54,16 +55,14 @@ function create(config) {
|
|
|
54
55
|
return Promise.resolve({ size: body.length });
|
|
55
56
|
}
|
|
56
57
|
if (body && typeof body.pipe === "function") {
|
|
57
|
-
// Streaming put —
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
body.on("error", reject);
|
|
66
|
-
});
|
|
58
|
+
// Streaming put — stage into a no-follow exclusive temp + atomic rename
|
|
59
|
+
// (a bare createWriteStream(full) follows a symlink planted at `full` and
|
|
60
|
+
// leaves a half-written object there if the source aborts). maxBytes is
|
|
61
|
+
// raised to the object-store ceiling; default 64 MiB is too small here.
|
|
62
|
+
return atomicFile.writeStream(full, body, {
|
|
63
|
+
fileMode: 0o600,
|
|
64
|
+
maxBytes: C.BYTES.gib(64),
|
|
65
|
+
}).then(function (r) { return { size: r.bytesWritten }; });
|
|
67
66
|
}
|
|
68
67
|
if (typeof body === "string") {
|
|
69
68
|
var buf = Buffer.from(body, "utf8");
|
|
@@ -75,18 +74,39 @@ function create(config) {
|
|
|
75
74
|
|
|
76
75
|
function get(key) {
|
|
77
76
|
var full = _resolveSafe(rootDir, key);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
// One capped fd-bound read (no existsSync check-then-read TOCTOU): get()
|
|
78
|
+
// buffers the whole object, so an uncapped read of a multi-GiB object is an
|
|
79
|
+
// OOM lever — cap it (large reads should use getStream). refuseSymlink stays
|
|
80
|
+
// OFF: operators may legitimately symlink into the store, and _resolveSafe
|
|
81
|
+
// already confines the key to rootDir.
|
|
82
|
+
try {
|
|
83
|
+
return Promise.resolve(atomicFile.fdSafeReadSync(full, {
|
|
84
|
+
maxBytes: C.BYTES.mib(64),
|
|
85
|
+
errorFor: function (kind, detail) {
|
|
86
|
+
if (kind === "enoent") return _err("NOT_FOUND", "key not found: " + key, true);
|
|
87
|
+
if (kind === "too-large") {
|
|
88
|
+
return _err("OBJECT_TOO_LARGE", "object " + key + " exceeds the buffered-get read cap (" +
|
|
89
|
+
detail.size + " > " + detail.max + " bytes) — use getStream()", true);
|
|
90
|
+
}
|
|
91
|
+
return _err("READ_FAILED", "failed to read " + key, true);
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
} catch (e) { return Promise.reject(e); }
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
function getStream(key) {
|
|
85
98
|
var full = _resolveSafe(rootDir, key);
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
// Open once and stream from that fd, mapping ENOENT to NOT_FOUND, so the
|
|
100
|
+
// existsSync→createReadStream check-then-read window is collapsed. Plain
|
|
101
|
+
// O_RDONLY (follows symlinks): operators may symlink into the store, and the
|
|
102
|
+
// key is already confined by _resolveSafe.
|
|
103
|
+
var fd;
|
|
104
|
+
try { fd = nodeFs.openSync(full, "r"); }
|
|
105
|
+
catch (e) {
|
|
106
|
+
if (e && e.code === "ENOENT") throw _err("NOT_FOUND", "key not found: " + key, true);
|
|
107
|
+
throw e;
|
|
88
108
|
}
|
|
89
|
-
return nodeFs.createReadStream(full);
|
|
109
|
+
return nodeFs.createReadStream(full, { fd: fd });
|
|
90
110
|
}
|
|
91
111
|
|
|
92
112
|
function head(key) {
|