@blamejs/core 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +230 -0
- package/LICENSE +201 -0
- package/LTS-CALENDAR.md +29 -0
- package/MIGRATING.md +7 -0
- package/NOTICE +59 -0
- package/README.md +100 -0
- package/bin/blamejs.js +13 -0
- package/index.js +253 -0
- package/lib/api-key.js +705 -0
- package/lib/api-snapshot.js +335 -0
- package/lib/app-shutdown.js +381 -0
- package/lib/app.js +364 -0
- package/lib/atomic-file.js +525 -0
- package/lib/audit-chain.js +168 -0
- package/lib/audit-sign.js +319 -0
- package/lib/audit-tools.js +682 -0
- package/lib/audit.js +753 -0
- package/lib/auth/jwt.js +280 -0
- package/lib/auth/oauth.js +691 -0
- package/lib/auth/passkey.js +185 -0
- package/lib/auth/password.js +139 -0
- package/lib/auth/totp.js +17 -0
- package/lib/auth-header.js +81 -0
- package/lib/backup/bundle.js +219 -0
- package/lib/backup/crypto.js +174 -0
- package/lib/backup/index.js +490 -0
- package/lib/backup/manifest.js +275 -0
- package/lib/bundler.js +295 -0
- package/lib/cache.js +819 -0
- package/lib/chain-writer.js +234 -0
- package/lib/cli-helpers.js +201 -0
- package/lib/cli.js +1377 -0
- package/lib/cluster-provider-db.js +245 -0
- package/lib/cluster-storage.js +166 -0
- package/lib/cluster.js +691 -0
- package/lib/consent.js +222 -0
- package/lib/constants.js +186 -0
- package/lib/cookies.js +293 -0
- package/lib/credential-hash.js +303 -0
- package/lib/crypto-field.js +159 -0
- package/lib/crypto.js +250 -0
- package/lib/db-query.js +297 -0
- package/lib/db-schema.js +250 -0
- package/lib/db.js +1054 -0
- package/lib/deprecate.js +226 -0
- package/lib/dev.js +324 -0
- package/lib/error-page.js +424 -0
- package/lib/events.js +135 -0
- package/lib/external-db.js +422 -0
- package/lib/forms.js +378 -0
- package/lib/framework-error.js +189 -0
- package/lib/framework-schema.js +604 -0
- package/lib/handlers.js +350 -0
- package/lib/html-balance.js +227 -0
- package/lib/http-client.js +615 -0
- package/lib/i18n.js +780 -0
- package/lib/jobs.js +181 -0
- package/lib/lazy-require.js +48 -0
- package/lib/log-stream-local.js +137 -0
- package/lib/log-stream-webhook.js +170 -0
- package/lib/log-stream.js +211 -0
- package/lib/log.js +355 -0
- package/lib/mail-bounce.js +507 -0
- package/lib/mail.js +701 -0
- package/lib/metrics.js +647 -0
- package/lib/middleware/api-encrypt.js +553 -0
- package/lib/middleware/attach-user.js +156 -0
- package/lib/middleware/body-parser.js +883 -0
- package/lib/middleware/bot-guard.js +148 -0
- package/lib/middleware/compression.js +436 -0
- package/lib/middleware/cors.js +236 -0
- package/lib/middleware/csp-nonce.js +332 -0
- package/lib/middleware/csrf-protect.js +275 -0
- package/lib/middleware/error-handler.js +46 -0
- package/lib/middleware/health.js +358 -0
- package/lib/middleware/index.js +52 -0
- package/lib/middleware/rate-limit.js +319 -0
- package/lib/middleware/request-id.js +53 -0
- package/lib/middleware/require-auth.js +95 -0
- package/lib/middleware/security-headers.js +91 -0
- package/lib/migrations.js +353 -0
- package/lib/mtls-ca.js +333 -0
- package/lib/mtls-engine-default.js +285 -0
- package/lib/nonce-store.js +177 -0
- package/lib/notify.js +643 -0
- package/lib/ntp-check.js +178 -0
- package/lib/object-store/azure-blob.js +467 -0
- package/lib/object-store/gcs.js +469 -0
- package/lib/object-store/http-put.js +153 -0
- package/lib/object-store/index.js +140 -0
- package/lib/object-store/local.js +163 -0
- package/lib/object-store/retry.js +15 -0
- package/lib/object-store/sigv4.js +535 -0
- package/lib/observability.js +114 -0
- package/lib/pagination.js +371 -0
- package/lib/parsers/index.js +64 -0
- package/lib/parsers/safe-csv.js +224 -0
- package/lib/parsers/safe-env.js +614 -0
- package/lib/parsers/safe-toml.js +745 -0
- package/lib/parsers/safe-xml.js +379 -0
- package/lib/parsers/safe-yaml.js +977 -0
- package/lib/permissions.js +430 -0
- package/lib/pqc-agent.js +85 -0
- package/lib/pqc-gate.js +266 -0
- package/lib/protocol-dispatcher.js +144 -0
- package/lib/queue-local.js +327 -0
- package/lib/queue.js +430 -0
- package/lib/redact.js +192 -0
- package/lib/render.js +193 -0
- package/lib/request-helpers.js +178 -0
- package/lib/restore-bundle.js +239 -0
- package/lib/restore-rollback.js +254 -0
- package/lib/restore.js +301 -0
- package/lib/retry.js +329 -0
- package/lib/router.js +437 -0
- package/lib/safe-async.js +520 -0
- package/lib/safe-buffer.js +162 -0
- package/lib/safe-json.js +532 -0
- package/lib/safe-schema.js +1176 -0
- package/lib/safe-sql.js +157 -0
- package/lib/safe-url.js +109 -0
- package/lib/scheduler.js +680 -0
- package/lib/seeders.js +622 -0
- package/lib/session.js +304 -0
- package/lib/slug.js +243 -0
- package/lib/static.js +268 -0
- package/lib/storage.js +470 -0
- package/lib/subject.js +281 -0
- package/lib/template.js +781 -0
- package/lib/testing.js +621 -0
- package/lib/totp.js +285 -0
- package/lib/tracing.js +484 -0
- package/lib/validate-opts.js +56 -0
- package/lib/vault/index.js +299 -0
- package/lib/vault/passphrase-ops.js +311 -0
- package/lib/vault/passphrase-source.js +198 -0
- package/lib/vault/rotate.js +761 -0
- package/lib/vault/wrap.js +289 -0
- package/lib/vendor/MANIFEST.json +84 -0
- package/lib/vendor/argon2/argon2.cjs +466 -0
- package/lib/vendor/argon2/argon2.d.cts +62 -0
- package/lib/vendor/argon2/package.json +1 -0
- package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
- package/lib/vendor/noble-ciphers.cjs +9 -0
- package/lib/vendor/pki.cjs +181 -0
- package/lib/vendor/simplewebauthn-server.cjs +328 -0
- package/lib/webhook.js +632 -0
- package/lib/websocket-channels.js +413 -0
- package/lib/websocket.js +833 -0
- package/package.json +39 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SigV4 protocol adapter — AWS Signature Version 4.
|
|
4
|
+
*
|
|
5
|
+
* One signing implementation covers the entire S3-API-compatible family:
|
|
6
|
+
* AWS S3, Cloudflare R2, Backblaze B2 (S3 endpoint), MinIO, Wasabi,
|
|
7
|
+
* Tigris, DigitalOcean Spaces, IDrive e2, Linode Object Storage, Storj
|
|
8
|
+
* (S3 gateway). Service identifier is always "s3" for object storage.
|
|
9
|
+
*
|
|
10
|
+
* Config:
|
|
11
|
+
* {
|
|
12
|
+
* endpoint: 'https://s3.us-west-2.amazonaws.com' // or R2/MinIO/etc.
|
|
13
|
+
* region: 'us-west-2' // required
|
|
14
|
+
* bucket: 'my-bucket' // required
|
|
15
|
+
* accessKeyId: '...' // required
|
|
16
|
+
* secretAccessKey: '...' // required
|
|
17
|
+
* sessionToken: '...' // optional (STS)
|
|
18
|
+
* pathStyle: false // virtual-hosted
|
|
19
|
+
* // by default
|
|
20
|
+
* timeoutMs: C.TIME.seconds(30)
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Reference:
|
|
24
|
+
* https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
|
|
25
|
+
*/
|
|
26
|
+
var nodeCrypto = require("crypto");
|
|
27
|
+
var { URL } = require("url");
|
|
28
|
+
var { Readable } = require("stream");
|
|
29
|
+
var safeXml = require("../parsers/safe-xml");
|
|
30
|
+
var C = require("../constants");
|
|
31
|
+
var { ObjectStoreError } = require("../framework-error");
|
|
32
|
+
var httpClient = require("../http-client");
|
|
33
|
+
var safeUrl = require("../safe-url");
|
|
34
|
+
|
|
35
|
+
// S3 list responses are commonly 1000-key paginated payloads — well above
|
|
36
|
+
// the parser's default 1 MiB / 10K element ceilings. These overrides
|
|
37
|
+
// give headroom for normal responses without uncapping the parser.
|
|
38
|
+
var LIST_PARSE_OPTS = {
|
|
39
|
+
maxBytes: C.BYTES.mib(8),
|
|
40
|
+
maxElements: 50000,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function _arrayify(value) {
|
|
44
|
+
// xml-safe maps multiple same-tag children to an array; a single child
|
|
45
|
+
// stays as the bare object; zero children means the property is absent.
|
|
46
|
+
// List traversal needs a uniform array, so normalize.
|
|
47
|
+
if (value == null) return [];
|
|
48
|
+
return Array.isArray(value) ? value : [value];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
var SERVICE = "s3";
|
|
52
|
+
var ALGORITHM = "AWS4-HMAC-SHA256";
|
|
53
|
+
|
|
54
|
+
// Presigned-URL expiry bounds. The SigV4 spec caps query-string-signed
|
|
55
|
+
// URLs at 7 days; below 1 second is effectively unsignable.
|
|
56
|
+
var PRESIGN_DEFAULT_EXPIRES_SECONDS = 15 * 60; // 15 minutes
|
|
57
|
+
var PRESIGN_MAX_EXPIRES_SECONDS = 7 * 24 * 60 * 60; // 7 days (SigV4 hard cap)
|
|
58
|
+
var PRESIGN_MIN_EXPIRES_SECONDS = 1;
|
|
59
|
+
|
|
60
|
+
var _err = ObjectStoreError.factory;
|
|
61
|
+
|
|
62
|
+
// ---- SigV4 primitives ----
|
|
63
|
+
|
|
64
|
+
function sha256Hex(buf) {
|
|
65
|
+
return nodeCrypto.createHash("sha256").update(buf).digest("hex");
|
|
66
|
+
}
|
|
67
|
+
function hmacSha256(key, data) {
|
|
68
|
+
return nodeCrypto.createHmac("sha256", key).update(data).digest();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// AWS-style URI encoding: same as RFC 3986 except path '/' may be preserved.
|
|
72
|
+
function awsUriEncode(str, encodeSlash) {
|
|
73
|
+
var out = "";
|
|
74
|
+
for (var i = 0; i < str.length; i++) {
|
|
75
|
+
var c = str.charCodeAt(i);
|
|
76
|
+
var ch = str.charAt(i);
|
|
77
|
+
if ((c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A) ||
|
|
78
|
+
(c >= 0x30 && c <= 0x39) ||
|
|
79
|
+
ch === "-" || ch === "_" || ch === "." || ch === "~") {
|
|
80
|
+
out += ch;
|
|
81
|
+
} else if (ch === "/" && !encodeSlash) {
|
|
82
|
+
out += "/";
|
|
83
|
+
} else {
|
|
84
|
+
var enc = encodeURIComponent(ch).replace(/!/g, "%21").replace(/\*/g, "%2A").replace(/'/g, "%27").replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
85
|
+
out += enc;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function canonicalQueryString(searchParams) {
|
|
92
|
+
if (!searchParams || searchParams.toString() === "") return "";
|
|
93
|
+
var pairs = [];
|
|
94
|
+
searchParams.forEach(function (v, k) { pairs.push([k, v]); });
|
|
95
|
+
pairs.sort(function (a, b) {
|
|
96
|
+
if (a[0] < b[0]) return -1;
|
|
97
|
+
if (a[0] > b[0]) return 1;
|
|
98
|
+
if (a[1] < b[1]) return -1;
|
|
99
|
+
if (a[1] > b[1]) return 1;
|
|
100
|
+
return 0;
|
|
101
|
+
});
|
|
102
|
+
return pairs.map(function (p) {
|
|
103
|
+
return awsUriEncode(p[0], true) + "=" + awsUriEncode(p[1], true);
|
|
104
|
+
}).join("&");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function canonicalHeaders(headers) {
|
|
108
|
+
var pairs = [];
|
|
109
|
+
for (var k in headers) {
|
|
110
|
+
if (headers[k] === undefined || headers[k] === null) continue;
|
|
111
|
+
var lk = k.toLowerCase();
|
|
112
|
+
var v = String(headers[k]).trim().replace(/\s+/g, " ");
|
|
113
|
+
pairs.push([lk, v]);
|
|
114
|
+
}
|
|
115
|
+
pairs.sort(function (a, b) { return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; });
|
|
116
|
+
var canon = "";
|
|
117
|
+
var signed = [];
|
|
118
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
119
|
+
canon += pairs[i][0] + ":" + pairs[i][1] + "\n";
|
|
120
|
+
signed.push(pairs[i][0]);
|
|
121
|
+
}
|
|
122
|
+
return { canonical: canon, signed: signed.join(";") };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function canonicalRequest(method, urlObj, headers, payloadHash) {
|
|
126
|
+
var canonHeaders = canonicalHeaders(headers);
|
|
127
|
+
var path = urlObj.pathname;
|
|
128
|
+
if (!path) path = "/";
|
|
129
|
+
return [
|
|
130
|
+
method.toUpperCase(),
|
|
131
|
+
awsUriEncode(path, false),
|
|
132
|
+
canonicalQueryString(urlObj.searchParams),
|
|
133
|
+
canonHeaders.canonical,
|
|
134
|
+
canonHeaders.signed,
|
|
135
|
+
payloadHash,
|
|
136
|
+
].join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function stringToSign(amzDate, credentialScope, canonicalReq) {
|
|
140
|
+
return [
|
|
141
|
+
ALGORITHM,
|
|
142
|
+
amzDate,
|
|
143
|
+
credentialScope,
|
|
144
|
+
sha256Hex(canonicalReq),
|
|
145
|
+
].join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function deriveSigningKey(secretAccessKey, dateStamp, region, service) {
|
|
149
|
+
var kDate = hmacSha256("AWS4" + secretAccessKey, dateStamp);
|
|
150
|
+
var kRegion = hmacSha256(kDate, region);
|
|
151
|
+
var kService = hmacSha256(kRegion, service);
|
|
152
|
+
return hmacSha256(kService, "aws4_request");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _formatAmzDate(d) {
|
|
156
|
+
var iso = d.toISOString().replace(/[-:]/g, "");
|
|
157
|
+
return iso.slice(0, 8) + "T" + iso.slice(9, 15) + "Z";
|
|
158
|
+
}
|
|
159
|
+
function _formatDateStamp(d) {
|
|
160
|
+
return d.toISOString().slice(0, 10).replace(/-/g, "");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function signRequest(opts) {
|
|
164
|
+
var date = opts.date || new Date();
|
|
165
|
+
var amzDate = _formatAmzDate(date);
|
|
166
|
+
var dateStamp = _formatDateStamp(date);
|
|
167
|
+
var url = opts.url instanceof URL ? opts.url : new URL(opts.url);
|
|
168
|
+
|
|
169
|
+
var headers = Object.assign({}, opts.headers || {});
|
|
170
|
+
headers["host"] = url.host;
|
|
171
|
+
headers["x-amz-date"] = amzDate;
|
|
172
|
+
if (!headers["x-amz-content-sha256"]) {
|
|
173
|
+
headers["x-amz-content-sha256"] = opts.payloadHash;
|
|
174
|
+
}
|
|
175
|
+
if (opts.sessionToken) {
|
|
176
|
+
headers["x-amz-security-token"] = opts.sessionToken;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
var canon = canonicalRequest(opts.method, url, headers, opts.payloadHash);
|
|
180
|
+
var credentialScope = dateStamp + "/" + opts.region + "/" + SERVICE + "/aws4_request";
|
|
181
|
+
var sts = stringToSign(amzDate, credentialScope, canon);
|
|
182
|
+
var signingKey = deriveSigningKey(opts.secretAccessKey, dateStamp, opts.region, SERVICE);
|
|
183
|
+
var signature = nodeCrypto.createHmac("sha256", signingKey).update(sts).digest("hex");
|
|
184
|
+
|
|
185
|
+
var canonHeaders = canonicalHeaders(headers);
|
|
186
|
+
var auth = ALGORITHM +
|
|
187
|
+
" Credential=" + opts.accessKeyId + "/" + credentialScope +
|
|
188
|
+
", SignedHeaders=" + canonHeaders.signed +
|
|
189
|
+
", Signature=" + signature;
|
|
190
|
+
headers["Authorization"] = auth;
|
|
191
|
+
|
|
192
|
+
return { headers: headers, signature: signature, canonicalRequest: canon, stringToSign: sts };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---- HTTP request helper ----
|
|
196
|
+
|
|
197
|
+
function _request(method, url, headers, body, opts) {
|
|
198
|
+
return httpClient.request({
|
|
199
|
+
method: method,
|
|
200
|
+
url: url,
|
|
201
|
+
headers: headers,
|
|
202
|
+
body: body,
|
|
203
|
+
idleTimeoutMs: opts && opts.timeoutMs,
|
|
204
|
+
maxResponseBytes: opts && opts.maxResponseBytes,
|
|
205
|
+
errorClass: ObjectStoreError,
|
|
206
|
+
allowedProtocols: opts && opts.allowedProtocols,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---- Public adapter factory ----
|
|
211
|
+
|
|
212
|
+
function create(config) {
|
|
213
|
+
if (!config) throw new Error("sigv4 protocol requires config");
|
|
214
|
+
if (!config.region) throw new Error("sigv4: region is required");
|
|
215
|
+
if (!config.bucket) throw new Error("sigv4: bucket is required");
|
|
216
|
+
if (!config.accessKeyId) throw new Error("sigv4: accessKeyId is required");
|
|
217
|
+
if (!config.secretAccessKey) throw new Error("sigv4: secretAccessKey is required");
|
|
218
|
+
|
|
219
|
+
var endpoint = config.endpoint || ("https://s3." + config.region + ".amazonaws.com");
|
|
220
|
+
if (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1);
|
|
221
|
+
var pathStyle = !!(config.pathStyle || config.forcePathStyle);
|
|
222
|
+
// HTTPS-only by default — AWS S3, R2, MinIO-over-https. Operators with
|
|
223
|
+
// an internal cleartext S3-compatible endpoint (test fixtures, local
|
|
224
|
+
// dev MinIO) opt in via config.allowedProtocols.
|
|
225
|
+
var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
226
|
+
safeUrl.parse(endpoint, {
|
|
227
|
+
allowedProtocols: allowedProtocols,
|
|
228
|
+
errorClass: ObjectStoreError,
|
|
229
|
+
});
|
|
230
|
+
var reqOpts = { timeoutMs: config.timeoutMs, allowedProtocols: allowedProtocols };
|
|
231
|
+
|
|
232
|
+
function _keyToUrl(key) {
|
|
233
|
+
if (key.indexOf("\0") !== -1) throw _err("INVALID_KEY", "null byte in key", true);
|
|
234
|
+
var encoded = key.split("/").map(function (s) { return awsUriEncode(s, true); }).join("/");
|
|
235
|
+
if (pathStyle) {
|
|
236
|
+
return new URL(endpoint + "/" + config.bucket + "/" + encoded);
|
|
237
|
+
}
|
|
238
|
+
var u = new URL(endpoint);
|
|
239
|
+
u.hostname = config.bucket + "." + u.hostname;
|
|
240
|
+
u.pathname = "/" + encoded;
|
|
241
|
+
return u;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function _bucketUrl(searchParams) {
|
|
245
|
+
var u;
|
|
246
|
+
if (pathStyle) {
|
|
247
|
+
u = new URL(endpoint + "/" + config.bucket + "/");
|
|
248
|
+
} else {
|
|
249
|
+
u = new URL(endpoint);
|
|
250
|
+
u.hostname = config.bucket + "." + u.hostname;
|
|
251
|
+
u.pathname = "/";
|
|
252
|
+
}
|
|
253
|
+
if (searchParams) {
|
|
254
|
+
Object.keys(searchParams).forEach(function (k) { u.searchParams.set(k, searchParams[k]); });
|
|
255
|
+
}
|
|
256
|
+
return u;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function _makeSigned(method, url, payloadHash, extraHeaders) {
|
|
260
|
+
var signed = signRequest({
|
|
261
|
+
method: method,
|
|
262
|
+
url: url,
|
|
263
|
+
headers: extraHeaders || {},
|
|
264
|
+
payloadHash: payloadHash,
|
|
265
|
+
region: config.region,
|
|
266
|
+
accessKeyId: config.accessKeyId,
|
|
267
|
+
secretAccessKey: config.secretAccessKey,
|
|
268
|
+
sessionToken: config.sessionToken,
|
|
269
|
+
});
|
|
270
|
+
return signed.headers;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function put(key, body, opts) {
|
|
274
|
+
var url = _keyToUrl(key);
|
|
275
|
+
var buf = Buffer.isBuffer(body) ? body : Buffer.from(typeof body === "string" ? body : "", "utf8");
|
|
276
|
+
var payloadHash = sha256Hex(buf);
|
|
277
|
+
var contentType = (opts && opts.contentType) || "application/octet-stream";
|
|
278
|
+
var headers = _makeSigned("PUT", url, payloadHash, {
|
|
279
|
+
"Content-Type": contentType,
|
|
280
|
+
"Content-Length": String(buf.length),
|
|
281
|
+
});
|
|
282
|
+
return _request("PUT", url, headers, buf, reqOpts).then(function (res) {
|
|
283
|
+
return { size: buf.length, etag: res.headers.etag };
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function get(key) {
|
|
288
|
+
var url = _keyToUrl(key);
|
|
289
|
+
var headers = _makeSigned("GET", url, sha256Hex(Buffer.alloc(0)));
|
|
290
|
+
return _request("GET", url, headers, null, reqOpts).then(function (res) {
|
|
291
|
+
return res.body;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getStream(key) {
|
|
296
|
+
return Readable.from(get(key));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function head(key) {
|
|
300
|
+
var url = _keyToUrl(key);
|
|
301
|
+
var headers = _makeSigned("HEAD", url, sha256Hex(Buffer.alloc(0)));
|
|
302
|
+
return _request("HEAD", url, headers, null, reqOpts).then(function (res) {
|
|
303
|
+
return {
|
|
304
|
+
size: res.headers["content-length"] ? parseInt(res.headers["content-length"], 10) : null,
|
|
305
|
+
etag: res.headers.etag,
|
|
306
|
+
lastModified: res.headers["last-modified"] ? Date.parse(res.headers["last-modified"]) : null,
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function deleteKey(key) {
|
|
312
|
+
var url = _keyToUrl(key);
|
|
313
|
+
var headers = _makeSigned("DELETE", url, sha256Hex(Buffer.alloc(0)));
|
|
314
|
+
return _request("DELETE", url, headers, null, reqOpts).then(
|
|
315
|
+
function () { return true; },
|
|
316
|
+
function (e) { if (e.statusCode === 404) return false; throw e; }
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function _presign(method, opts) {
|
|
321
|
+
opts = opts || {};
|
|
322
|
+
if (!opts.key || typeof opts.key !== "string") {
|
|
323
|
+
throw _err("INVALID_KEY", "presigned URL: key is required", true);
|
|
324
|
+
}
|
|
325
|
+
if (opts.key.indexOf("\0") !== -1) {
|
|
326
|
+
throw _err("INVALID_KEY", "null byte in key", true);
|
|
327
|
+
}
|
|
328
|
+
var expiresIn = opts.expiresIn != null ? opts.expiresIn : PRESIGN_DEFAULT_EXPIRES_SECONDS;
|
|
329
|
+
if (typeof expiresIn !== "number" ||
|
|
330
|
+
expiresIn < PRESIGN_MIN_EXPIRES_SECONDS ||
|
|
331
|
+
expiresIn > PRESIGN_MAX_EXPIRES_SECONDS) {
|
|
332
|
+
throw _err("INVALID_EXPIRES",
|
|
333
|
+
"presigned URL: expiresIn must be a number of seconds between " +
|
|
334
|
+
PRESIGN_MIN_EXPIRES_SECONDS + " and " + PRESIGN_MAX_EXPIRES_SECONDS +
|
|
335
|
+
" (7 days, SigV4 hard cap)", true);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
var url = _keyToUrl(opts.key);
|
|
339
|
+
var date = opts.date || new Date();
|
|
340
|
+
var amzDate = _formatAmzDate(date);
|
|
341
|
+
var dateStamp = _formatDateStamp(date);
|
|
342
|
+
var credentialScope = dateStamp + "/" + config.region + "/" + SERVICE + "/aws4_request";
|
|
343
|
+
|
|
344
|
+
// Build the headers we want to sign. host is always signed; if the
|
|
345
|
+
// caller supplies opts.contentType the value is bound into the
|
|
346
|
+
// signature so the client MUST send Content-Type matching exactly,
|
|
347
|
+
// otherwise S3 rejects with SignatureDoesNotMatch — turning the
|
|
348
|
+
// hint into real server-side enforcement.
|
|
349
|
+
var headers = { host: url.host };
|
|
350
|
+
if (opts.contentType) headers["content-type"] = opts.contentType;
|
|
351
|
+
var signedHeadersStr = Object.keys(headers).sort().join(";");
|
|
352
|
+
|
|
353
|
+
// Query-string SigV4: presigning carries auth in the URL itself.
|
|
354
|
+
// The set order doesn't matter — canonicalQueryString sorts the
|
|
355
|
+
// params before hashing.
|
|
356
|
+
url.searchParams.set("X-Amz-Algorithm", ALGORITHM);
|
|
357
|
+
url.searchParams.set("X-Amz-Credential", config.accessKeyId + "/" + credentialScope);
|
|
358
|
+
url.searchParams.set("X-Amz-Date", amzDate);
|
|
359
|
+
url.searchParams.set("X-Amz-Expires", String(expiresIn));
|
|
360
|
+
url.searchParams.set("X-Amz-SignedHeaders", signedHeadersStr);
|
|
361
|
+
if (config.sessionToken) {
|
|
362
|
+
url.searchParams.set("X-Amz-Security-Token", config.sessionToken);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Payload hash for query-string presigning is the literal string
|
|
366
|
+
// "UNSIGNED-PAYLOAD" — the body is not part of the signature.
|
|
367
|
+
var canon = canonicalRequest(method, url, headers, "UNSIGNED-PAYLOAD");
|
|
368
|
+
var sts = stringToSign(amzDate, credentialScope, canon);
|
|
369
|
+
var signingKey = deriveSigningKey(config.secretAccessKey, dateStamp, config.region, SERVICE);
|
|
370
|
+
var signature = nodeCrypto.createHmac("sha256", signingKey).update(sts).digest("hex");
|
|
371
|
+
|
|
372
|
+
url.searchParams.set("X-Amz-Signature", signature);
|
|
373
|
+
|
|
374
|
+
var clientHeaders = {};
|
|
375
|
+
if (opts.contentType) clientHeaders["Content-Type"] = opts.contentType;
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
url: url.toString(),
|
|
379
|
+
method: method,
|
|
380
|
+
headers: clientHeaders,
|
|
381
|
+
expiresAt: date.getTime() + C.TIME.seconds(expiresIn),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function presignedUploadUrl(opts) { return _presign("PUT", opts); }
|
|
386
|
+
function presignedDownloadUrl(opts) { return _presign("GET", opts); }
|
|
387
|
+
|
|
388
|
+
// POST-form policy presigning (S3-compatible). Unlike query-string
|
|
389
|
+
// PUT presigning, this lets the operator enforce body-size at the
|
|
390
|
+
// upload endpoint via the `content-length-range` policy condition.
|
|
391
|
+
// Clients upload via multipart/form-data POST to the returned URL,
|
|
392
|
+
// attaching every key in `fields` plus the file as the last field.
|
|
393
|
+
//
|
|
394
|
+
// Reference: AWS S3 "Browser-Based Uploads Using POST".
|
|
395
|
+
function presignedUploadPolicy(opts) {
|
|
396
|
+
opts = opts || {};
|
|
397
|
+
if (!opts.key || typeof opts.key !== "string") {
|
|
398
|
+
throw _err("INVALID_KEY", "presignedUploadPolicy: key is required", true);
|
|
399
|
+
}
|
|
400
|
+
if (opts.key.indexOf("\0") !== -1) {
|
|
401
|
+
throw _err("INVALID_KEY", "null byte in key", true);
|
|
402
|
+
}
|
|
403
|
+
if (typeof opts.maxBytes !== "number" || !Number.isFinite(opts.maxBytes) ||
|
|
404
|
+
opts.maxBytes <= 0) {
|
|
405
|
+
throw _err("INVALID_MAX_BYTES",
|
|
406
|
+
"presignedUploadPolicy: maxBytes (positive number of bytes) is required — " +
|
|
407
|
+
"POST-form policy enforces body size via the content-length-range condition; " +
|
|
408
|
+
"use presignedUploadUrl if size enforcement is not needed", true);
|
|
409
|
+
}
|
|
410
|
+
var minBytes = (typeof opts.minBytes === "number" && opts.minBytes >= 0) ? opts.minBytes : 0;
|
|
411
|
+
var expiresIn = opts.expiresIn != null ? opts.expiresIn : PRESIGN_DEFAULT_EXPIRES_SECONDS;
|
|
412
|
+
if (typeof expiresIn !== "number" ||
|
|
413
|
+
expiresIn < PRESIGN_MIN_EXPIRES_SECONDS ||
|
|
414
|
+
expiresIn > PRESIGN_MAX_EXPIRES_SECONDS) {
|
|
415
|
+
throw _err("INVALID_EXPIRES",
|
|
416
|
+
"presignedUploadPolicy: expiresIn must be a number of seconds between " +
|
|
417
|
+
PRESIGN_MIN_EXPIRES_SECONDS + " and " + PRESIGN_MAX_EXPIRES_SECONDS +
|
|
418
|
+
" (7 days, SigV4 hard cap)", true);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
var date = opts.date || new Date();
|
|
422
|
+
var amzDate = _formatAmzDate(date);
|
|
423
|
+
var dateStamp = _formatDateStamp(date);
|
|
424
|
+
var credentialScope = dateStamp + "/" + config.region + "/" + SERVICE + "/aws4_request";
|
|
425
|
+
var credential = config.accessKeyId + "/" + credentialScope;
|
|
426
|
+
var expirationIso = new Date(date.getTime() + C.TIME.seconds(expiresIn)).toISOString();
|
|
427
|
+
|
|
428
|
+
// Policy conditions. Each entry must MATCH a corresponding form
|
|
429
|
+
// field exactly OR be a structural constraint (content-length-range
|
|
430
|
+
// / starts-with). Order doesn't matter — S3 evaluates the set.
|
|
431
|
+
var conditions = [
|
|
432
|
+
{ "bucket": config.bucket },
|
|
433
|
+
{ "key": opts.key },
|
|
434
|
+
{ "x-amz-algorithm": ALGORITHM },
|
|
435
|
+
{ "x-amz-credential": credential },
|
|
436
|
+
{ "x-amz-date": amzDate },
|
|
437
|
+
["content-length-range", minBytes, opts.maxBytes],
|
|
438
|
+
];
|
|
439
|
+
if (config.sessionToken) {
|
|
440
|
+
conditions.push({ "x-amz-security-token": config.sessionToken });
|
|
441
|
+
}
|
|
442
|
+
if (opts.contentType) {
|
|
443
|
+
conditions.push({ "content-type": opts.contentType });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
var policy = { expiration: expirationIso, conditions: conditions };
|
|
447
|
+
var policyJson = JSON.stringify(policy);
|
|
448
|
+
var policyB64 = Buffer.from(policyJson, "utf8").toString("base64");
|
|
449
|
+
|
|
450
|
+
var signingKey = deriveSigningKey(config.secretAccessKey, dateStamp, config.region, SERVICE);
|
|
451
|
+
var signature = nodeCrypto.createHmac("sha256", signingKey).update(policyB64).digest("hex");
|
|
452
|
+
|
|
453
|
+
var fields = {
|
|
454
|
+
"key": opts.key,
|
|
455
|
+
"x-amz-algorithm": ALGORITHM,
|
|
456
|
+
"x-amz-credential": credential,
|
|
457
|
+
"x-amz-date": amzDate,
|
|
458
|
+
"policy": policyB64,
|
|
459
|
+
"x-amz-signature": signature,
|
|
460
|
+
};
|
|
461
|
+
if (config.sessionToken) fields["x-amz-security-token"] = config.sessionToken;
|
|
462
|
+
if (opts.contentType) fields["content-type"] = opts.contentType;
|
|
463
|
+
|
|
464
|
+
// The bucket-root URL — no key in the path, key is a form field.
|
|
465
|
+
var url = _bucketUrl();
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
url: url.toString(),
|
|
469
|
+
method: "POST",
|
|
470
|
+
fields: fields,
|
|
471
|
+
expiresAt: date.getTime() + C.TIME.seconds(expiresIn),
|
|
472
|
+
maxBytes: opts.maxBytes,
|
|
473
|
+
enforcement: "content-length-range", // S3-side enforced
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function list(prefix, opts) {
|
|
478
|
+
opts = opts || {};
|
|
479
|
+
var params = { "list-type": "2" };
|
|
480
|
+
if (prefix) params["prefix"] = prefix;
|
|
481
|
+
if (opts.maxResults) params["max-keys"] = String(opts.maxResults);
|
|
482
|
+
if (opts.continuationToken) params["continuation-token"] = opts.continuationToken;
|
|
483
|
+
|
|
484
|
+
var url = _bucketUrl(params);
|
|
485
|
+
var headers = _makeSigned("GET", url, sha256Hex(Buffer.alloc(0)));
|
|
486
|
+
return _request("GET", url, headers, null, reqOpts).then(function (res) {
|
|
487
|
+
var doc = safeXml.parse(res.body, LIST_PARSE_OPTS);
|
|
488
|
+
var result = doc.ListBucketResult || {};
|
|
489
|
+
var contents = _arrayify(result.Contents);
|
|
490
|
+
var items = contents.map(function (c) {
|
|
491
|
+
return {
|
|
492
|
+
key: c.Key,
|
|
493
|
+
size: c.Size != null ? parseInt(c.Size, 10) : null,
|
|
494
|
+
lastModified: c.LastModified ? Date.parse(c.LastModified) : null,
|
|
495
|
+
};
|
|
496
|
+
}).filter(function (it) { return it.key; });
|
|
497
|
+
return {
|
|
498
|
+
items: items,
|
|
499
|
+
truncated: result.IsTruncated === "true",
|
|
500
|
+
continuationToken: result.NextContinuationToken || null,
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
protocol: "sigv4",
|
|
507
|
+
endpoint: endpoint,
|
|
508
|
+
bucket: config.bucket,
|
|
509
|
+
region: config.region,
|
|
510
|
+
pathStyle: pathStyle,
|
|
511
|
+
put: put,
|
|
512
|
+
get: get,
|
|
513
|
+
getStream: getStream,
|
|
514
|
+
head: head,
|
|
515
|
+
delete: deleteKey,
|
|
516
|
+
list: list,
|
|
517
|
+
presignedUploadUrl: presignedUploadUrl,
|
|
518
|
+
presignedDownloadUrl: presignedDownloadUrl,
|
|
519
|
+
presignedUploadPolicy: presignedUploadPolicy,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
module.exports = {
|
|
524
|
+
create: create,
|
|
525
|
+
signRequest: signRequest,
|
|
526
|
+
canonicalRequest: canonicalRequest,
|
|
527
|
+
stringToSign: stringToSign,
|
|
528
|
+
deriveSigningKey: deriveSigningKey,
|
|
529
|
+
canonicalQueryString: canonicalQueryString,
|
|
530
|
+
canonicalHeaders: canonicalHeaders,
|
|
531
|
+
awsUriEncode: awsUriEncode,
|
|
532
|
+
sha256Hex: sha256Hex,
|
|
533
|
+
SERVICE: SERVICE,
|
|
534
|
+
ALGORITHM: ALGORITHM,
|
|
535
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* observability — combined metrics + tracing tap surface.
|
|
4
|
+
*
|
|
5
|
+
* Framework hot paths previously called metrics.tap + tracing.tap
|
|
6
|
+
* separately, with each module repeating the lazy-require + try/catch
|
|
7
|
+
* boilerplate. This primitive folds the two into one helper:
|
|
8
|
+
*
|
|
9
|
+
* var obs = require("./observability");
|
|
10
|
+
* return obs.tap("audit.record",
|
|
11
|
+
* { action: event.action, outcome: event.outcome },
|
|
12
|
+
* async function (span) {
|
|
13
|
+
* // ... operation body ...
|
|
14
|
+
* return result;
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* Behavior:
|
|
18
|
+
* - tracing.tap wraps fn in a span (spec 9.8). Pass-through is
|
|
19
|
+
* fn(null) when no tracing registry is active — zero overhead.
|
|
20
|
+
* - After fn settles (either branch), metrics.tap fires once with
|
|
21
|
+
* the same name + the same attrs reused as labels. Existing
|
|
22
|
+
* metrics _tapHandler dispatches still work unchanged because
|
|
23
|
+
* the labels are the same shape modules previously passed.
|
|
24
|
+
* - If fn throws (sync) or rejects (async), metrics still fire
|
|
25
|
+
* before the throw propagates. Operators get the counter bump
|
|
26
|
+
* even on the failure path — the existing pattern across audit /
|
|
27
|
+
* vault / queue did the same.
|
|
28
|
+
*
|
|
29
|
+
* Why combine: every framework module that wanted both a span AND a
|
|
30
|
+
* counter previously wrote nested tap wrappers + try/catch. Centralizing
|
|
31
|
+
* keeps the call sites readable, eliminates boot-order drift each
|
|
32
|
+
* module had to reason about, and lets us change tap semantics
|
|
33
|
+
* (e.g. add a third sink) in one place.
|
|
34
|
+
*
|
|
35
|
+
* For fire-and-forget value-noting where wrapping fn doesn't fit —
|
|
36
|
+
* incrementing a counter on a side-effect deep inside an existing
|
|
37
|
+
* function — use `event(name, value, labels)`. Same shape as the
|
|
38
|
+
* legacy metrics.tap call; routes through metrics only (no span).
|
|
39
|
+
*
|
|
40
|
+
* Public API:
|
|
41
|
+
* observability.tap(name, attrs, fn) → fn's return value
|
|
42
|
+
* observability.tap(name, fn) → fn's return value (no attrs)
|
|
43
|
+
* observability.event(name, value, labels) → undefined
|
|
44
|
+
*
|
|
45
|
+
* Tests live in test/layer-0-primitives/observability.test.js.
|
|
46
|
+
*
|
|
47
|
+
* Parameters:
|
|
48
|
+
* name: string — used as both the span name AND the metrics tap
|
|
49
|
+
* name. Convention: dotted lowercase ("audit.record", "queue.enqueue").
|
|
50
|
+
* attrs: object | null — passed verbatim to tracing.tap as span
|
|
51
|
+
* attributes AND to metrics.tap as labels. Modules previously
|
|
52
|
+
* passing two slightly-different objects to the two sinks should
|
|
53
|
+
* pass one unified shape.
|
|
54
|
+
* fn: function — sync or async. Return propagates; throws propagate
|
|
55
|
+
* after metrics fire.
|
|
56
|
+
*/
|
|
57
|
+
var lazyRequire = require("./lazy-require");
|
|
58
|
+
|
|
59
|
+
var tracing = lazyRequire(function () { return require("./tracing"); });
|
|
60
|
+
var metrics = lazyRequire(function () { return require("./metrics"); });
|
|
61
|
+
|
|
62
|
+
function _safeMetricsTap(name, value, labels) {
|
|
63
|
+
try { metrics().tap(name, value, labels); }
|
|
64
|
+
catch (_e) { /* boot-order tolerance — metrics may not be loaded */ }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function tap(name, attrs, fn) {
|
|
68
|
+
if (typeof attrs === "function") { fn = attrs; attrs = null; }
|
|
69
|
+
// Tier A validation: tap is called from many call sites and a typo
|
|
70
|
+
// in the name (e.g. variable holding undefined) silently corrupts both
|
|
71
|
+
// the span tree AND the metrics counter route, with no obvious symptom
|
|
72
|
+
// until somebody opens a dashboard. Throw at first call so the
|
|
73
|
+
// operator catches it.
|
|
74
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
75
|
+
throw new TypeError("observability.tap: name must be a non-empty string, got " +
|
|
76
|
+
(typeof name) + " " + JSON.stringify(name));
|
|
77
|
+
}
|
|
78
|
+
if (typeof fn !== "function") {
|
|
79
|
+
throw new TypeError("observability.tap: fn must be a function, got " + (typeof fn));
|
|
80
|
+
}
|
|
81
|
+
return tracing().tap(name, attrs, function (span) {
|
|
82
|
+
var ret;
|
|
83
|
+
try {
|
|
84
|
+
ret = fn(span);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
_safeMetricsTap(name, 1, attrs);
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
if (ret && typeof ret.then === "function") {
|
|
90
|
+
return ret.then(
|
|
91
|
+
function (v) { _safeMetricsTap(name, 1, attrs); return v; },
|
|
92
|
+
function (e) { _safeMetricsTap(name, 1, attrs); throw e; }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
_safeMetricsTap(name, 1, attrs);
|
|
96
|
+
return ret;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Tier B (drop-silent): event is the fire-and-forget shape called from
|
|
101
|
+
// hot paths where throwing would crash the request that triggered it.
|
|
102
|
+
// Bad input is silently dropped — operators with a misnamed event see
|
|
103
|
+
// the missing counter, not a 500. metrics.tap performs its own label-
|
|
104
|
+
// name regex validation; an invalid call surfaces in the metrics module
|
|
105
|
+
// log, not via a thrown exception.
|
|
106
|
+
function event(name, value, labels) {
|
|
107
|
+
if (typeof name !== "string" || name.length === 0) return;
|
|
108
|
+
_safeMetricsTap(name, value, labels);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
tap: tap,
|
|
113
|
+
event: event,
|
|
114
|
+
};
|