@blamejs/core 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. package/package.json +39 -0
@@ -0,0 +1,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
+ };