@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,178 @@
1
+ "use strict";
2
+ /**
3
+ * Minimal SNTP client for boot-time clock-drift verification.
4
+ *
5
+ * Why: the audit chain's monotonicCounter orders events deterministically
6
+ * even if the wall clock jumps, but recordedAt is the human-readable
7
+ * timestamp auditors will rely on. A clock that's silently off by hours
8
+ * (container with no RTC sync, NTP daemon stopped) makes the audit trail
9
+ * misleading.
10
+ *
11
+ * What this does:
12
+ * - Sends a single SNTPv4 query to a configured server (default
13
+ * pool.ntp.org) over UDP port 123.
14
+ * - Computes drift = (server's transmit timestamp) - (local clock).
15
+ * - Returns the drift in milliseconds.
16
+ *
17
+ * What this does NOT do:
18
+ * - Continuous synchronization (use the OS NTP daemon for that).
19
+ * - Authenticated NTP (NTS, autokey).
20
+ * - Querying multiple servers and taking median (single-shot only).
21
+ *
22
+ * The framework's policy in db.init():
23
+ * - drift |x| < 5min → log info, continue
24
+ * - drift |x| in [5min,1hr) → log warning, continue
25
+ * - drift |x| >= 1hr → log fatal, exit (BLAMEJS_NTP_STRICT=1) or warn
26
+ * - NTP unreachable → log warning, continue (network may not allow UDP/123)
27
+ */
28
+ var dgram = require("dgram");
29
+ var C = require("./constants");
30
+
31
+ // NTP epoch: 1900-01-01. Unix epoch: 1970-01-01. Offset: 70 years incl. 17
32
+ // leap days = 2,208,988,800 seconds.
33
+ var NTP_TO_UNIX_OFFSET_SECONDS = 2208988800;
34
+
35
+ var DEFAULT_SERVERS = ["pool.ntp.org", "time.cloudflare.com"];
36
+ var DEFAULT_PORT = 123;
37
+ var DEFAULT_TIMEOUT_MS = 3000;
38
+
39
+ /**
40
+ * Query an NTP server once. Resolves with { driftMs, serverTimeMs } or
41
+ * rejects with { code, message } where code is one of:
42
+ * 'ntp/timeout' — server didn't reply within timeoutMs
43
+ * 'ntp/refused' — DNS/connection error
44
+ * 'ntp/bad-reply' — packet structure wrong
45
+ */
46
+ function querySingle(server, opts) {
47
+ opts = opts || {};
48
+ var port = opts.port || DEFAULT_PORT;
49
+ var timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
50
+
51
+ return new Promise(function (resolve, reject) {
52
+ var socket = dgram.createSocket("udp4");
53
+ var settled = false;
54
+
55
+ function done(err, result) {
56
+ if (settled) return;
57
+ settled = true;
58
+ try { socket.close(); } catch (_e) { /* ignored */ }
59
+ if (err) reject(err); else resolve(result);
60
+ }
61
+
62
+ var timer = setTimeout(function () {
63
+ done({ code: "ntp/timeout", message: "no reply from " + server + " within " + timeoutMs + "ms" });
64
+ }, timeoutMs);
65
+ timer.unref();
66
+
67
+ // SNTPv4 client request: 48-byte buffer, byte 0 = 0b00_100_011 = 0x23
68
+ // LI=0 (no warning), VN=4, Mode=3 (client). Other bytes zero.
69
+ var req = Buffer.alloc(48);
70
+ req[0] = 0x23;
71
+ var sendTimeMs = Date.now();
72
+
73
+ socket.on("error", function (e) {
74
+ clearTimeout(timer);
75
+ done({ code: "ntp/refused", message: server + ": " + e.message });
76
+ });
77
+
78
+ socket.on("message", function (msg) {
79
+ clearTimeout(timer);
80
+ var receiveTimeMs = Date.now();
81
+ if (!Buffer.isBuffer(msg) || msg.length < 48) {
82
+ return done({ code: "ntp/bad-reply", message: "reply too short (" + (msg && msg.length) + " bytes)" });
83
+ }
84
+ // Bytes 40-47 = Transmit Timestamp (NTP epoch seconds.fraction)
85
+ var ntpSeconds = msg.readUInt32BE(40);
86
+ var ntpFraction = msg.readUInt32BE(44);
87
+ var serverUnixSeconds = ntpSeconds - NTP_TO_UNIX_OFFSET_SECONDS;
88
+ var fracMs = Math.round(ntpFraction / 0x100000000 * 1000);
89
+ var serverTimeMs = serverUnixSeconds * 1000 + fracMs;
90
+
91
+ // Round-trip-corrected drift: assume the server's reply transmit
92
+ // timestamp is approximately at the midpoint of our send/receive.
93
+ var midpointMs = sendTimeMs + (receiveTimeMs - sendTimeMs) / 2;
94
+ var driftMs = serverTimeMs - midpointMs;
95
+
96
+ done(null, { driftMs: driftMs, serverTimeMs: serverTimeMs, server: server });
97
+ });
98
+
99
+ socket.send(req, 0, req.length, port, server, function (err) {
100
+ if (err) {
101
+ clearTimeout(timer);
102
+ done({ code: "ntp/refused", message: "send to " + server + ": " + err.message });
103
+ }
104
+ });
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Try each server in turn; return the first successful drift measurement.
110
+ * Resolves null if all servers fail (caller decides whether that's fatal).
111
+ */
112
+ async function checkDrift(opts) {
113
+ opts = opts || {};
114
+ var servers = opts.servers || DEFAULT_SERVERS;
115
+ var lastError = null;
116
+ for (var i = 0; i < servers.length; i++) {
117
+ try {
118
+ return await querySingle(servers[i], opts);
119
+ } catch (e) {
120
+ lastError = e;
121
+ }
122
+ }
123
+ return { driftMs: null, error: lastError };
124
+ }
125
+
126
+ /**
127
+ * Boot-time check that integrates with the framework's logging policy.
128
+ * Returns a result object with { ok, driftMs, severity, message }.
129
+ * Caller (db.init) decides whether to exit.
130
+ */
131
+ async function bootCheck(opts) {
132
+ opts = opts || {};
133
+ var result = await checkDrift(opts);
134
+ if (result.driftMs === null) {
135
+ return {
136
+ ok: true, // unreachable NTP isn't a hard failure
137
+ severity: "warning",
138
+ driftMs: null,
139
+ message: "NTP unreachable: " + (result.error && result.error.message) +
140
+ " (continuing — set BLAMEJS_NTP_STRICT=1 to fail closed)",
141
+ };
142
+ }
143
+ var absMs = Math.abs(result.driftMs);
144
+ var driftStr = (result.driftMs >= 0 ? "+" : "") + result.driftMs + "ms";
145
+ if (absMs >= C.TIME.hours(1)) {
146
+ return {
147
+ ok: false,
148
+ severity: "fatal",
149
+ driftMs: result.driftMs,
150
+ server: result.server,
151
+ message: "clock drift " + driftStr + " from " + result.server + " (>= 1 hour) — refuse to boot",
152
+ };
153
+ }
154
+ if (absMs >= C.TIME.minutes(5)) {
155
+ return {
156
+ ok: true,
157
+ severity: "warning",
158
+ driftMs: result.driftMs,
159
+ server: result.server,
160
+ message: "clock drift " + driftStr + " from " + result.server + " (>= 5 minutes) — investigate",
161
+ };
162
+ }
163
+ return {
164
+ ok: true,
165
+ severity: "info",
166
+ driftMs: result.driftMs,
167
+ server: result.server,
168
+ message: "clock drift " + driftStr + " from " + result.server,
169
+ };
170
+ }
171
+
172
+ module.exports = {
173
+ querySingle: querySingle,
174
+ checkDrift: checkDrift,
175
+ bootCheck: bootCheck,
176
+ DEFAULT_SERVERS: DEFAULT_SERVERS,
177
+ NTP_TO_UNIX_OFFSET_SECONDS: NTP_TO_UNIX_OFFSET_SECONDS,
178
+ };
@@ -0,0 +1,467 @@
1
+ "use strict";
2
+ /**
3
+ * Azure Blob Storage protocol adapter — Shared Key auth (account-level).
4
+ *
5
+ * Auth: HMAC-SHA256 over a canonicalized string-to-sign (different format
6
+ * from AWS SigV4 — Azure has its own signing scheme). Signature is
7
+ * base64-encoded; Authorization header: "SharedKey <account>:<sig>".
8
+ *
9
+ * Endpoint: https://<account>.blob.core.windows.net (override via
10
+ * config.endpoint for Azurite emulator / Azure Stack / private endpoints).
11
+ *
12
+ * Config:
13
+ * {
14
+ * accountName: 'mystorage' // required
15
+ * accountKey: '<base64 storage key>' // required (REST shared key)
16
+ * container: 'my-container' // required
17
+ * endpoint: 'https://...' // optional override
18
+ * apiVersion: '2024-08-04' // x-ms-version header
19
+ * timeoutMs: C.TIME.seconds(30)
20
+ * }
21
+ *
22
+ * Reference:
23
+ * https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
24
+ * https://learn.microsoft.com/en-us/rest/api/storageservices/blob-service-rest-api
25
+ *
26
+ * Scope notes:
27
+ * - Auth: Shared Key only. SAS tokens (Shared Access Signatures) and
28
+ * Azure AD OAuth2 are not implemented; this covers the common
29
+ * server-to-storage case with rotated keys.
30
+ * - PutBlock + PutBlockList (multipart for >256MB blobs) is not
31
+ * implemented; uploads above that ceiling will fail at the API.
32
+ */
33
+ var nodeCrypto = require("crypto");
34
+ var { URL } = require("url");
35
+ var { Readable } = require("stream");
36
+ var safeXml = require("../parsers/safe-xml");
37
+ var C = require("../constants");
38
+ var { ObjectStoreError } = require("../framework-error");
39
+ var httpClient = require("../http-client");
40
+ var safeUrl = require("../safe-url");
41
+
42
+ // Azure Blob list responses are commonly multi-thousand-key paginated
43
+ // payloads — well above the parser's default 1 MiB / 10K element ceilings.
44
+ var LIST_PARSE_OPTS = {
45
+ maxBytes: C.BYTES.mib(8),
46
+ maxElements: 50000,
47
+ };
48
+
49
+ function _arrayify(value) {
50
+ if (value == null) return [];
51
+ return Array.isArray(value) ? value : [value];
52
+ }
53
+
54
+ var DEFAULT_API_VERSION = "2024-08-04";
55
+
56
+ // Service SAS expiry bounds. Azure doesn't enforce a hard max, but
57
+ // matching the SigV4 / V4 7-day cap keeps semantics uniform across
58
+ // backends and reflects best practice (long-lived SAS tokens are
59
+ // effectively credentials).
60
+ var SAS_DEFAULT_EXPIRES_SECONDS = 15 * 60;
61
+ var SAS_MAX_EXPIRES_SECONDS = 7 * 24 * 60 * 60;
62
+ var SAS_MIN_EXPIRES_SECONDS = 1;
63
+
64
+ var _err = ObjectStoreError.factory;
65
+
66
+ function _httpRequest(method, urlObj, headers, body, opts) {
67
+ return httpClient.request({
68
+ method: method,
69
+ url: urlObj,
70
+ headers: headers,
71
+ body: body,
72
+ idleTimeoutMs: opts && opts.timeoutMs,
73
+ maxResponseBytes: opts && opts.maxResponseBytes,
74
+ errorClass: ObjectStoreError,
75
+ allowedProtocols: opts && opts.allowedProtocols,
76
+ });
77
+ }
78
+
79
+ // ---- Shared Key signing ----
80
+ //
81
+ // StringToSign for Blob (Shared Key):
82
+ // VERB + "\n" +
83
+ // Content-Encoding + "\n" +
84
+ // Content-Language + "\n" +
85
+ // Content-Length + "\n" +
86
+ // Content-MD5 + "\n" +
87
+ // Content-Type + "\n" +
88
+ // Date + "\n" +
89
+ // If-Modified-Since + "\n" +
90
+ // If-Match + "\n" +
91
+ // If-None-Match + "\n" +
92
+ // If-Unmodified-Since + "\n" +
93
+ // Range + "\n" +
94
+ // CanonicalizedHeaders +
95
+ // CanonicalizedResource
96
+ //
97
+ // CanonicalizedHeaders: x-ms-* headers, lowercased, sorted, "key:value\n"
98
+ // CanonicalizedResource: "/<account>/<container>/<blob>" + sorted query
99
+ // params each on their own line as "param:value\n"
100
+
101
+ function buildStringToSign(opts) {
102
+ var headers = opts.headers || {};
103
+ var url = opts.url instanceof URL ? opts.url : new URL(opts.url);
104
+
105
+ var canonicalHeaders = (function () {
106
+ var pairs = [];
107
+ Object.keys(headers).forEach(function (k) {
108
+ var lk = k.toLowerCase();
109
+ if (lk.indexOf("x-ms-") === 0) {
110
+ pairs.push([lk, String(headers[k]).trim().replace(/\s+/g, " ")]);
111
+ }
112
+ });
113
+ pairs.sort(function (a, b) { return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; });
114
+ return pairs.map(function (p) { return p[0] + ":" + p[1]; }).join("\n");
115
+ })();
116
+
117
+ var canonicalResource = (function () {
118
+ // /<account>/<rest of path>
119
+ // Plus sorted query params, each "name:value\n"
120
+ var resourcePath = "/" + opts.accountName + url.pathname;
121
+ var paramPairs = [];
122
+ url.searchParams.forEach(function (v, k) {
123
+ paramPairs.push([k.toLowerCase(), v]);
124
+ });
125
+ paramPairs.sort(function (a, b) {
126
+ if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
127
+ return a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0;
128
+ });
129
+ // Group by name; multiple values comma-separated per Azure rules
130
+ var grouped = {};
131
+ paramPairs.forEach(function (p) {
132
+ if (!grouped[p[0]]) grouped[p[0]] = [];
133
+ grouped[p[0]].push(p[1]);
134
+ });
135
+ var queryLines = Object.keys(grouped).sort().map(function (name) {
136
+ return "\n" + name + ":" + grouped[name].join(",");
137
+ }).join("");
138
+ return resourcePath + queryLines;
139
+ })();
140
+
141
+ return [
142
+ opts.method.toUpperCase(),
143
+ headers["Content-Encoding"] || "",
144
+ headers["Content-Language"] || "",
145
+ headers["Content-Length"] && headers["Content-Length"] !== "0"
146
+ ? headers["Content-Length"] : "", // "0" → empty (Azure quirk)
147
+ headers["Content-MD5"] || "",
148
+ headers["Content-Type"] || "",
149
+ "", // Date line — empty when x-ms-date is set
150
+ headers["If-Modified-Since"] || "",
151
+ headers["If-Match"] || "",
152
+ headers["If-None-Match"] || "",
153
+ headers["If-Unmodified-Since"] || "",
154
+ headers["Range"] || "",
155
+ canonicalHeaders,
156
+ canonicalResource,
157
+ ].join("\n");
158
+ }
159
+
160
+ function signRequest(opts) {
161
+ var headers = Object.assign({}, opts.headers || {});
162
+ if (!headers["x-ms-version"]) headers["x-ms-version"] = opts.apiVersion || DEFAULT_API_VERSION;
163
+ if (!headers["x-ms-date"]) headers["x-ms-date"] = new Date().toUTCString();
164
+
165
+ var url = opts.url instanceof URL ? opts.url : new URL(opts.url);
166
+ if (!headers["host"]) headers["host"] = url.host;
167
+
168
+ var sts = buildStringToSign({
169
+ method: opts.method,
170
+ url: url,
171
+ headers: headers,
172
+ accountName: opts.accountName,
173
+ });
174
+ var keyBytes = Buffer.from(opts.accountKey, "base64");
175
+ var signature = nodeCrypto.createHmac("sha256", keyBytes).update(sts, "utf8").digest("base64");
176
+ headers["Authorization"] = "SharedKey " + opts.accountName + ":" + signature;
177
+
178
+ return { headers: headers, stringToSign: sts, signature: signature };
179
+ }
180
+
181
+ // ---- Public adapter factory ----
182
+
183
+ function create(config) {
184
+ if (!config) throw new Error("azure-blob protocol requires config");
185
+ if (!config.accountName) throw new Error("azure-blob: accountName is required");
186
+ if (!config.accountKey) throw new Error("azure-blob: accountKey is required");
187
+ if (!config.container) throw new Error("azure-blob: container is required");
188
+
189
+ var endpoint = config.endpoint || ("https://" + config.accountName + ".blob.core.windows.net");
190
+ if (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1);
191
+ var apiVersion = config.apiVersion || DEFAULT_API_VERSION;
192
+ var timeoutMs = config.timeoutMs;
193
+ // HTTPS-only by default — real Azure is always HTTPS. Operators with
194
+ // an Azurite emulator endpoint opt in via config.allowedProtocols.
195
+ var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
196
+ safeUrl.parse(endpoint, { allowedProtocols: allowedProtocols, errorClass: ObjectStoreError });
197
+ var reqOpts = { timeoutMs: timeoutMs, allowedProtocols: allowedProtocols };
198
+
199
+ function _blobUrl(key, params) {
200
+ var u = new URL(endpoint + "/" + config.container + "/" + key);
201
+ if (params) {
202
+ Object.keys(params).forEach(function (k) { u.searchParams.set(k, params[k]); });
203
+ }
204
+ return u;
205
+ }
206
+
207
+ function _containerUrl(params) {
208
+ var u = new URL(endpoint + "/" + config.container);
209
+ if (params) {
210
+ Object.keys(params).forEach(function (k) { u.searchParams.set(k, params[k]); });
211
+ }
212
+ return u;
213
+ }
214
+
215
+ function _signed(method, url, headers) {
216
+ var s = signRequest({
217
+ method: method,
218
+ url: url,
219
+ headers: headers || {},
220
+ accountName: config.accountName,
221
+ accountKey: config.accountKey,
222
+ apiVersion: apiVersion,
223
+ });
224
+ return s.headers;
225
+ }
226
+
227
+ function put(key, body, opts) {
228
+ var url = _blobUrl(key);
229
+ var buf = Buffer.isBuffer(body) ? body : Buffer.from(typeof body === "string" ? body : "", "utf8");
230
+ var contentType = (opts && opts.contentType) || "application/octet-stream";
231
+ var headers = _signed("PUT", url, {
232
+ "Content-Type": contentType,
233
+ "Content-Length": String(buf.length),
234
+ "x-ms-blob-type": "BlockBlob",
235
+ });
236
+ return _httpRequest("PUT", url, headers, buf, reqOpts).then(function (res) {
237
+ return { size: buf.length, etag: res.headers.etag };
238
+ });
239
+ }
240
+
241
+ function get(key) {
242
+ var url = _blobUrl(key);
243
+ var headers = _signed("GET", url, {});
244
+ return _httpRequest("GET", url, headers, null, reqOpts).then(function (res) {
245
+ return res.body;
246
+ });
247
+ }
248
+
249
+ function getStream(key) { return Readable.from(get(key)); }
250
+
251
+ function head(key) {
252
+ var url = _blobUrl(key);
253
+ var headers = _signed("HEAD", url, {});
254
+ return _httpRequest("HEAD", url, headers, null, reqOpts).then(function (res) {
255
+ return {
256
+ size: res.headers["content-length"] ? parseInt(res.headers["content-length"], 10) : null,
257
+ etag: res.headers.etag,
258
+ lastModified: res.headers["last-modified"] ? Date.parse(res.headers["last-modified"]) : null,
259
+ };
260
+ });
261
+ }
262
+
263
+ function deleteKey(key) {
264
+ var url = _blobUrl(key);
265
+ var headers = _signed("DELETE", url, {});
266
+ return _httpRequest("DELETE", url, headers, null, reqOpts).then(
267
+ function () { return true; },
268
+ function (e) { if (e.statusCode === 404) return false; throw e; }
269
+ );
270
+ }
271
+
272
+ function list(prefix, opts) {
273
+ opts = opts || {};
274
+ var params = { restype: "container", comp: "list" };
275
+ if (prefix) params.prefix = prefix;
276
+ if (opts.maxResults) params.maxresults = String(opts.maxResults);
277
+ if (opts.continuationToken) params.marker = opts.continuationToken;
278
+ var url = _containerUrl(params);
279
+ var headers = _signed("GET", url, {});
280
+ return _httpRequest("GET", url, headers, null, reqOpts).then(function (res) {
281
+ var doc = safeXml.parse(res.body, LIST_PARSE_OPTS);
282
+ var result = doc.EnumerationResults || {};
283
+ var blobsContainer = result.Blobs || {};
284
+ var blobs = _arrayify(blobsContainer.Blob);
285
+ var items = blobs.map(function (b) {
286
+ var props = b.Properties || {};
287
+ var size = props["Content-Length"];
288
+ var lm = props["Last-Modified"];
289
+ return {
290
+ key: b.Name,
291
+ size: size != null ? parseInt(size, 10) : null,
292
+ lastModified: lm ? Date.parse(lm) : null,
293
+ };
294
+ }).filter(function (it) { return it.key; });
295
+ // <NextMarker/> (self-closing) parses to "" — falsy. A real token
296
+ // is a non-empty string, which is truthy.
297
+ var marker = (typeof result.NextMarker === "string") ? result.NextMarker : "";
298
+ return {
299
+ items: items,
300
+ truncated: marker.length > 0,
301
+ continuationToken: marker.length > 0 ? marker : null,
302
+ };
303
+ });
304
+ }
305
+
306
+ // Service SAS (Shared Access Signature) generator for blob endpoints.
307
+ // The string-to-sign layout is fixed by API version — see Azure docs
308
+ // "Create a service SAS". We sign with the account key (HMAC-SHA256
309
+ // over the canonical string) and emit the token as URL query params.
310
+ function _buildSasToken(permissions, opts) {
311
+ var expiresIn = opts.expiresIn != null ? opts.expiresIn : SAS_DEFAULT_EXPIRES_SECONDS;
312
+ if (typeof expiresIn !== "number" ||
313
+ expiresIn < SAS_MIN_EXPIRES_SECONDS ||
314
+ expiresIn > SAS_MAX_EXPIRES_SECONDS) {
315
+ throw _err("INVALID_EXPIRES",
316
+ "presigned URL: expiresIn must be a number of seconds between " +
317
+ SAS_MIN_EXPIRES_SECONDS + " and " + SAS_MAX_EXPIRES_SECONDS +
318
+ " (7 days)", true);
319
+ }
320
+ var nowDate = opts.date || new Date();
321
+ var expiry = new Date(nowDate.getTime() + C.TIME.seconds(expiresIn));
322
+ // Azure accepts ISO 8601 with second precision; strip ms.
323
+ var signedExpiry = expiry.toISOString().replace(/\.\d{3}Z$/, "Z");
324
+ var signedStart = ""; // omitted = SAS valid immediately
325
+ var signedVersion = apiVersion;
326
+ var signedResource = "b"; // blob
327
+ var signedProtocol = "https";
328
+ var canonicalizedResource = "/blob/" + config.accountName + "/" +
329
+ config.container + "/" + opts.key;
330
+ var signedContentType = opts.contentType || "";
331
+
332
+ // String-to-sign layout for Service SAS (blob), API version 2018-11-09+:
333
+ // signedPermissions \n signedStart \n signedExpiry \n
334
+ // canonicalizedResource \n signedIdentifier \n signedIP \n
335
+ // signedProtocol \n signedVersion \n signedResource \n
336
+ // signedSnapshotTime \n signedEncryptionScope \n
337
+ // rscc \n rscd \n rsce \n rscl \n rsct
338
+ var stringToSign = [
339
+ permissions,
340
+ signedStart,
341
+ signedExpiry,
342
+ canonicalizedResource,
343
+ "", // signedIdentifier
344
+ "", // signedIP
345
+ signedProtocol,
346
+ signedVersion,
347
+ signedResource,
348
+ "", // signedSnapshotTime
349
+ "", // signedEncryptionScope
350
+ "", // rscc — Cache-Control
351
+ "", // rscd — Content-Disposition
352
+ "", // rsce — Content-Encoding
353
+ "", // rscl — Content-Language
354
+ signedContentType, // rsct — Content-Type (signed when supplied)
355
+ ].join("\n");
356
+
357
+ var keyBuf = Buffer.from(config.accountKey, "base64");
358
+ var signature = nodeCrypto.createHmac("sha256", keyBuf)
359
+ .update(stringToSign, "utf8").digest("base64");
360
+
361
+ var sas = new URLSearchParams();
362
+ sas.set("sv", signedVersion);
363
+ sas.set("sr", signedResource);
364
+ sas.set("sp", permissions);
365
+ sas.set("se", signedExpiry);
366
+ sas.set("spr", signedProtocol);
367
+ if (signedContentType) sas.set("rsct", signedContentType);
368
+ sas.set("sig", signature);
369
+
370
+ return { sas: sas.toString(), expiresAt: expiry.getTime() };
371
+ }
372
+
373
+ function _presign(method, permissions, opts) {
374
+ opts = opts || {};
375
+ if (!opts.key || typeof opts.key !== "string") {
376
+ throw _err("INVALID_KEY", "presigned URL: key is required", true);
377
+ }
378
+ if (opts.key.indexOf("\0") !== -1) {
379
+ throw _err("INVALID_KEY", "null byte in key", true);
380
+ }
381
+
382
+ var token = _buildSasToken(permissions, opts);
383
+ var url = new URL(endpoint + "/" + config.container + "/" + opts.key + "?" + token.sas);
384
+
385
+ var clientHeaders = {};
386
+ if (opts.contentType) clientHeaders["Content-Type"] = opts.contentType;
387
+ if (method === "PUT") clientHeaders["x-ms-blob-type"] = "BlockBlob";
388
+
389
+ return {
390
+ url: url.toString(),
391
+ method: method,
392
+ headers: clientHeaders,
393
+ expiresAt: token.expiresAt,
394
+ };
395
+ }
396
+
397
+ function presignedUploadUrl(opts) { return _presign("PUT", "cw", opts); }
398
+ function presignedDownloadUrl(opts) { return _presign("GET", "r", opts); }
399
+
400
+ // Azure SAS doesn't support an equivalent of S3 / GCS POST policy
401
+ // with a content-length-range constraint. The SAS spec carries
402
+ // permissions / start / expiry / IP / protocol / resource-content-
403
+ // headers but no body-size cap — Azure validates the upload at the
404
+ // service level only via the operator's optional Block Blob size
405
+ // limit applied at storage-account or container scope, not via the
406
+ // SAS token itself.
407
+ //
408
+ // Rather than throw NOT_SUPPORTED, this returns the same SAS PUT
409
+ // shape as presignedUploadUrl with an `enforcement: 'client-only'`
410
+ // marker so operators making cross-vendor decisions know the
411
+ // body-size guard is advisory on Azure and must be enforced on the
412
+ // client side or via a server-side post-upload check.
413
+ //
414
+ // For strict server-side body-size enforcement on Azure, the
415
+ // canonical pattern is: SAS-authorize the upload, then have the
416
+ // operator's app issue a HEAD on the resulting blob and delete +
417
+ // 4xx the requester if Content-Length > opts.maxBytes.
418
+ function presignedUploadPolicy(opts) {
419
+ opts = opts || {};
420
+ if (typeof opts.maxBytes !== "number" || !Number.isFinite(opts.maxBytes) ||
421
+ opts.maxBytes <= 0) {
422
+ throw _err("INVALID_MAX_BYTES",
423
+ "presignedUploadPolicy: maxBytes (positive number of bytes) is required " +
424
+ "(advisory on Azure — see docstring for server-side enforcement)", true);
425
+ }
426
+ var underlying = _presign("PUT", "cw", opts);
427
+ return {
428
+ url: underlying.url,
429
+ method: "PUT",
430
+ // SAS uploads are PUT, not multipart POST — there are no form
431
+ // fields. Operators uploading attach the body directly to the
432
+ // returned URL with the headers in `headers`.
433
+ fields: null,
434
+ headers: underlying.headers,
435
+ expiresAt: underlying.expiresAt,
436
+ maxBytes: opts.maxBytes,
437
+ enforcement: "client-only",
438
+ enforcementNote:
439
+ "Azure SAS does not natively cap upload size. Operators needing " +
440
+ "strict size enforcement must HEAD the blob post-upload and reject " +
441
+ "if Content-Length exceeds maxBytes.",
442
+ };
443
+ }
444
+
445
+ return {
446
+ protocol: "azure-blob",
447
+ endpoint: endpoint,
448
+ container: config.container,
449
+ accountName: config.accountName,
450
+ put: put,
451
+ get: get,
452
+ getStream: getStream,
453
+ head: head,
454
+ delete: deleteKey,
455
+ list: list,
456
+ presignedUploadUrl: presignedUploadUrl,
457
+ presignedDownloadUrl: presignedDownloadUrl,
458
+ presignedUploadPolicy: presignedUploadPolicy,
459
+ };
460
+ }
461
+
462
+ module.exports = {
463
+ create: create,
464
+ signRequest: signRequest,
465
+ buildStringToSign: buildStringToSign,
466
+ DEFAULT_API_VERSION: DEFAULT_API_VERSION,
467
+ };