@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,266 @@
1
+ "use strict";
2
+ /**
3
+ * pqc-gate — TCP-level enforcement of post-quantum key exchange.
4
+ *
5
+ * Inspects the TLS ClientHello before letting the connection reach the
6
+ * HTTPS server. Connections that don't offer a PQC hybrid group in
7
+ * supported_groups are rejected with a TLS handshake_failure alert.
8
+ *
9
+ * Architecture:
10
+ *
11
+ * net.Server (public 443) → parse ClientHello → pipe to internal HTTPS server
12
+ *
13
+ * The HTTPS server binds to a private loopback port and never directly
14
+ * sees the internet. Operators set Node's TLS group preference at the
15
+ * HTTPS server level (so accepted connections still negotiate PQC) and
16
+ * use this gate as the outermost rejection point. Both layers must be
17
+ * configured PQC-only — gate without HTTPS-side enforcement leaves a
18
+ * connection that downgrades to a non-PQ group at handshake time.
19
+ *
20
+ * var server = b.pqcGate.create({
21
+ * internalPort: 8443,
22
+ * internalHost: "127.0.0.1",
23
+ * bypass: ["127.0.0.1", "::1", "::ffff:127.0.0.1"],
24
+ * clientHelloTimeoutMs: 5000,
25
+ * maxClientHelloBytes: 16384,
26
+ * log: logInstance,
27
+ * });
28
+ * server.listen(443, "0.0.0.0");
29
+ *
30
+ * Bypass: localhost connections skip the gate so health probes and
31
+ * sidecars (which often do plain TCP or use older TLS configurations)
32
+ * still work without weakening the public-facing posture.
33
+ *
34
+ * Test seam: opts._connect / _server / _setTimeout / _clearTimeout
35
+ * default to net.createConnection / net.createServer / setTimeout /
36
+ * clearTimeout. The unit tests use fakes to drive the parser and
37
+ * connection lifecycle deterministically.
38
+ */
39
+ var net = require("node:net");
40
+ var { PQC_GROUPS } = require("./constants");
41
+ var validateOpts = require("./validate-opts");
42
+ var { boot } = require("./log");
43
+
44
+ var DEFAULT_LOG = boot("pqc-gate");
45
+ var DEFAULT_BYPASS = Object.freeze(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
46
+ var DEFAULT_CLIENTHELLO_TIMEOUT_MS = 5000;
47
+ var DEFAULT_MAX_CLIENTHELLO_BYTES = 16384;
48
+
49
+ // TLS handshake_failure alert (fatal). Sent before destroying a socket
50
+ // so a strict TLS client logs a precise reason rather than a vague
51
+ // connection-reset.
52
+ var TLS_ALERT_HANDSHAKE_FAILURE = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x28]);
53
+
54
+ // Set built once from the framework's PQC_GROUPS constant. New PQC
55
+ // groups added there flow into the gate without code changes.
56
+ var PQC_GROUP_IDS = new Set(Object.values(PQC_GROUPS));
57
+
58
+ // clientHelloHasPQC — return true iff the parsed ClientHello buffer
59
+ // includes any group listed in PQC_GROUP_IDS in its supported_groups
60
+ // extension. Returns false on malformed input or when no PQC group is
61
+ // present.
62
+ function clientHelloHasPQC(buf) {
63
+ if (!buf || buf.length < 44) return false;
64
+
65
+ // TLS record header: type(1) version(2) length(2)
66
+ if (buf[0] !== 0x16) return false; // not a handshake record
67
+
68
+ var recordLen = buf.readUInt16BE(3);
69
+ var recordEnd = Math.min(5 + recordLen, buf.length);
70
+
71
+ // Handshake header: type(1) length(3)
72
+ if (buf.length < 10) return false;
73
+ if (buf[5] !== 0x01) return false; // not ClientHello
74
+
75
+ // Body: version(2) + random(32) = 34 bytes after handshake header
76
+ var offset = 9 + 2 + 32;
77
+ if (offset + 1 > recordEnd) return false;
78
+
79
+ // Session ID: length(1) + data
80
+ var sessionIdLen = buf[offset];
81
+ offset += 1 + sessionIdLen;
82
+ if (offset + 2 > recordEnd) return false;
83
+
84
+ // Cipher suites: length(2) + data
85
+ var cipherSuitesLen = buf.readUInt16BE(offset);
86
+ offset += 2 + cipherSuitesLen;
87
+ if (offset + 1 > recordEnd) return false;
88
+
89
+ // Compression methods: length(1) + data
90
+ var compLen = buf[offset];
91
+ offset += 1 + compLen;
92
+ if (offset + 2 > recordEnd) return false;
93
+
94
+ // Extensions: total_length(2)
95
+ var extensionsLen = buf.readUInt16BE(offset);
96
+ offset += 2;
97
+ var extensionsEnd = Math.min(offset + extensionsLen, recordEnd);
98
+
99
+ while (offset + 4 <= extensionsEnd) {
100
+ var extType = buf.readUInt16BE(offset);
101
+ var extLen = buf.readUInt16BE(offset + 2);
102
+ offset += 4;
103
+
104
+ if (extType === 0x000A && extLen >= 2 && offset + extLen <= extensionsEnd) {
105
+ // supported_groups: list_length(2) + group_ids(2 each)
106
+ var listLen = buf.readUInt16BE(offset);
107
+ var groupsOffset = offset + 2;
108
+ var groupsEnd = Math.min(groupsOffset + listLen, offset + extLen);
109
+ while (groupsOffset + 2 <= groupsEnd) {
110
+ var groupId = buf.readUInt16BE(groupsOffset);
111
+ if (PQC_GROUP_IDS.has(groupId)) return true;
112
+ groupsOffset += 2;
113
+ }
114
+ return false;
115
+ }
116
+ offset += extLen;
117
+ }
118
+ return false;
119
+ }
120
+
121
+ function _isBypassed(remoteAddr, bypass) {
122
+ if (!remoteAddr) return false;
123
+ for (var i = 0; i < bypass.length; i++) {
124
+ if (bypass[i] === remoteAddr) return true;
125
+ }
126
+ return false;
127
+ }
128
+
129
+ function _logVia(log, level, msg, fields) {
130
+ if (log && typeof log[level] === "function") {
131
+ try { log[level](msg, fields); } catch (_e) { /* logger best-effort */ }
132
+ return;
133
+ }
134
+ // Fallback to the framework's per-module console channel
135
+ var line = msg + (fields ? " " + JSON.stringify(fields) : "");
136
+ if (level === "error" || level === "fatal" || level === "warn") {
137
+ DEFAULT_LOG.warn(line);
138
+ } else {
139
+ DEFAULT_LOG(line);
140
+ }
141
+ }
142
+
143
+ function create(opts) {
144
+ opts = opts || {};
145
+ validateOpts(opts, [
146
+ "internalPort", "internalHost", "bypass",
147
+ "clientHelloTimeoutMs", "maxClientHelloBytes", "log",
148
+ "_connect", "_server", "_setTimeout", "_clearTimeout",
149
+ ], "b.pqcGate");
150
+ var internalPort = opts.internalPort;
151
+ if (typeof internalPort !== "number" || internalPort < 1 || internalPort > 65535) {
152
+ throw new Error("pqc-gate: opts.internalPort must be a port number (1-65535)");
153
+ }
154
+ var internalHost = typeof opts.internalHost === "string" ? opts.internalHost : "127.0.0.1";
155
+ var bypass = Array.isArray(opts.bypass) ? opts.bypass.slice() : DEFAULT_BYPASS.slice();
156
+ var clientHelloTimeoutMs = typeof opts.clientHelloTimeoutMs === "number" && opts.clientHelloTimeoutMs > 0
157
+ ? opts.clientHelloTimeoutMs : DEFAULT_CLIENTHELLO_TIMEOUT_MS;
158
+ var maxClientHelloBytes = typeof opts.maxClientHelloBytes === "number" && opts.maxClientHelloBytes > 0
159
+ ? opts.maxClientHelloBytes : DEFAULT_MAX_CLIENTHELLO_BYTES;
160
+ var log = opts.log || null;
161
+
162
+ // Test seams
163
+ var connectFn = opts._connect || function (cOpts, cb) { return net.createConnection(cOpts, cb); };
164
+ var serverFn = opts._server || function (sOpts, cb) { return net.createServer(sOpts, cb); };
165
+ var setTimeoutFn = opts._setTimeout || setTimeout;
166
+ var clearTimeoutFn = opts._clearTimeout || clearTimeout;
167
+
168
+ function pipeToInternal(socket, prependData) {
169
+ var internal = connectFn({ port: internalPort, host: internalHost }, function () {
170
+ if (prependData) internal.write(prependData);
171
+ socket.pipe(internal);
172
+ internal.pipe(socket);
173
+ socket.resume();
174
+ });
175
+ internal.on("error", function () { socket.destroy(); });
176
+ socket.on("error", function () { internal.destroy(); });
177
+ internal.on("close", function () { socket.destroy(); });
178
+ socket.on("close", function () { internal.destroy(); });
179
+ }
180
+
181
+ function _onConnection(socket) {
182
+ var clientIp = socket.remoteAddress || "";
183
+
184
+ if (_isBypassed(clientIp, bypass)) {
185
+ pipeToInternal(socket);
186
+ return;
187
+ }
188
+
189
+ var chunks = [];
190
+ var totalLen = 0;
191
+ var resolved = false;
192
+
193
+ var timeout = setTimeoutFn(function () {
194
+ if (resolved) return;
195
+ resolved = true;
196
+ _logVia(log, "warn", "ClientHello timeout", { ip: clientIp });
197
+ try { socket.destroy(); } catch (_e) { /* socket may already be torn down */ }
198
+ }, clientHelloTimeoutMs);
199
+
200
+ socket.on("data", function onData(chunk) {
201
+ if (resolved) return;
202
+ chunks.push(chunk);
203
+ totalLen += chunk.length;
204
+
205
+ if (totalLen > maxClientHelloBytes) {
206
+ resolved = true;
207
+ try { clearTimeoutFn(timeout); } catch (_e) {}
208
+ _logVia(log, "warn", "ClientHello too large", { ip: clientIp, size: totalLen });
209
+ try { socket.destroy(); } catch (_e) {}
210
+ return;
211
+ }
212
+
213
+ // Reject non-handshake first byte immediately
214
+ if (totalLen >= 1 && chunks[0][0] !== 0x16) {
215
+ resolved = true;
216
+ try { clearTimeoutFn(timeout); } catch (_e) {}
217
+ try { socket.destroy(); } catch (_e) {}
218
+ return;
219
+ }
220
+
221
+ if (totalLen < 5) return; // need at least the record header
222
+
223
+ var buf = Buffer.concat(chunks);
224
+ var recordLen = buf.readUInt16BE(3);
225
+ var neededLen = 5 + recordLen;
226
+
227
+ if (buf.length < Math.min(neededLen, maxClientHelloBytes)) return;
228
+
229
+ resolved = true;
230
+ try { clearTimeoutFn(timeout); } catch (_e) {}
231
+ socket.removeListener("data", onData);
232
+ socket.pause();
233
+
234
+ if (clientHelloHasPQC(buf)) {
235
+ pipeToInternal(socket, buf);
236
+ } else {
237
+ _logVia(log, "warn",
238
+ "connection rejected — no PQC group in ClientHello", { ip: clientIp });
239
+ try {
240
+ socket.write(TLS_ALERT_HANDSHAKE_FAILURE, function () {
241
+ try { socket.destroy(); } catch (_e) {}
242
+ });
243
+ } catch (_e) {
244
+ try { socket.destroy(); } catch (_e2) {}
245
+ }
246
+ }
247
+ });
248
+
249
+ socket.on("error", function () {
250
+ resolved = true;
251
+ try { clearTimeoutFn(timeout); } catch (_e) {}
252
+ });
253
+
254
+ socket.resume();
255
+ }
256
+
257
+ return serverFn({ pauseOnConnect: true }, _onConnection);
258
+ }
259
+
260
+ module.exports = {
261
+ create: create,
262
+ clientHelloHasPQC: clientHelloHasPQC,
263
+ PQC_GROUP_IDS: PQC_GROUP_IDS,
264
+ TLS_ALERT_HANDSHAKE_FAILURE: TLS_ALERT_HANDSHAKE_FAILURE,
265
+ DEFAULT_BYPASS: DEFAULT_BYPASS,
266
+ };
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ /**
3
+ * protocol-dispatcher — pluggable protocol resolver with deferred-protocol
4
+ * surfacing.
5
+ *
6
+ * Three framework modules ship a "pick a backend by protocol name" surface:
7
+ * - lib/queue.js (local, deferred: redis/sqs/amqp/nats)
8
+ * - lib/log-stream.js (local + webhook, deferred: syslog/otlp/cloudwatch)
9
+ * - lib/object-store/index.js (local + http-put + sigv4 + gcs + azure-blob)
10
+ *
11
+ * Each previously copied a ~30-line dispatch block: validate config has a
12
+ * protocol, look up DEFERRED_PROTOCOLS for a clear "not yet implemented"
13
+ * error, look up PROTOCOLS for the real implementation, throw on unknown.
14
+ * This primitive owns that lookup + error shape so callers focus on
15
+ * post-resolve logic (retry+breaker wrapping, classification, etc.).
16
+ *
17
+ * Public API:
18
+ *
19
+ * var dispatcher = protocolDispatcher.create({
20
+ * name: "queue", // appears in error messages
21
+ * errorClass: QueueError, // thrown on unknown/deferred/missing
22
+ * protocols: { local: localProto }, // protocol-name → module
23
+ * deferred: { redis: { description: "Redis Streams" } },
24
+ * fallbackProtocol: "local", // hinted in deferred-error
25
+ * });
26
+ *
27
+ * var proto = dispatcher.resolve(cfg.protocol);
28
+ * // throws errorClass when:
29
+ * // - cfg.protocol is undefined / empty → MISSING_PROTOCOL
30
+ * // - cfg.protocol matches a deferred-protocol entry → PROTOCOL_NOT_IMPLEMENTED
31
+ * // - cfg.protocol is neither implemented nor deferred → UNKNOWN_PROTOCOL
32
+ * // returns the protocol module (caller invokes proto.create(cfg) etc.)
33
+ *
34
+ * dispatcher.protocols // → ["local"]
35
+ * dispatcher.deferred // → ["redis"]
36
+ *
37
+ * Why resolve-only (not build): the post-resolve flow differs per module
38
+ * (retry+breaker for queue+object-store, batched dispatch for log-stream,
39
+ * classification routing for object-store). A one-size-fits-all builder
40
+ * would either miss those concerns or expose them as a sprawling opts
41
+ * surface. Resolve-only keeps the primitive small and the post-resolve
42
+ * code where it belongs.
43
+ *
44
+ * Error surface: every throw carries the dispatcher's `name` so an
45
+ * operator with multiple dispatchers (queue + log-stream) reading the
46
+ * error message knows which one rejected the protocol.
47
+ */
48
+ var { defineClass } = require("./framework-error");
49
+
50
+ // Default error class — operators / test fixtures that don't want to
51
+ // supply their own get this generic shape with the same code namespace.
52
+ var ProtocolDispatcherError = defineClass("ProtocolDispatcherError", { withStatusCode: true });
53
+
54
+ // Tier A validation — called at create-time, throw on bad input so
55
+ // operators catch typos at app boot, not first request.
56
+ function _validateConfig(opts) {
57
+ if (!opts || typeof opts !== "object") {
58
+ throw new Error("protocolDispatcher.create: opts is required");
59
+ }
60
+ if (typeof opts.name !== "string" || opts.name.length === 0) {
61
+ throw new Error("protocolDispatcher.create: opts.name (string) is required");
62
+ }
63
+ if (!opts.protocols || typeof opts.protocols !== "object" || Array.isArray(opts.protocols)) {
64
+ throw new Error("protocolDispatcher.create: opts.protocols (object) is required");
65
+ }
66
+ // Each protocol value should look like a module with .create — operators
67
+ // wiring a typo'd reference get a clearer error than "undefined is not
68
+ // a function" when resolve() hits it.
69
+ var pkeys = Object.keys(opts.protocols);
70
+ for (var i = 0; i < pkeys.length; i++) {
71
+ var p = opts.protocols[pkeys[i]];
72
+ if (!p || typeof p !== "object" || typeof p.create !== "function") {
73
+ throw new Error("protocolDispatcher.create: opts.protocols['" + pkeys[i] +
74
+ "'] must be an object with a .create function (got " +
75
+ (p === null ? "null" : typeof p) + ")");
76
+ }
77
+ }
78
+ if (opts.deferred !== undefined && opts.deferred !== null) {
79
+ if (typeof opts.deferred !== "object" || Array.isArray(opts.deferred)) {
80
+ throw new Error("protocolDispatcher.create: opts.deferred must be an object (or omitted)");
81
+ }
82
+ }
83
+ if (opts.fallbackProtocol !== undefined && opts.fallbackProtocol !== null) {
84
+ if (typeof opts.fallbackProtocol !== "string" || opts.fallbackProtocol.length === 0) {
85
+ throw new Error("protocolDispatcher.create: opts.fallbackProtocol must be a non-empty string (or omitted)");
86
+ }
87
+ }
88
+ if (opts.errorClass !== undefined && opts.errorClass !== null) {
89
+ if (typeof opts.errorClass !== "function") {
90
+ throw new Error("protocolDispatcher.create: opts.errorClass must be a constructor (or omitted)");
91
+ }
92
+ }
93
+ }
94
+
95
+ function create(opts) {
96
+ _validateConfig(opts);
97
+ var name = opts.name;
98
+ var protocols = Object.assign({}, opts.protocols);
99
+ var deferred = Object.assign({}, opts.deferred || {});
100
+ var fallbackProtocol = opts.fallbackProtocol || null;
101
+ var ErrorClass = opts.errorClass || ProtocolDispatcherError;
102
+
103
+ function _err(code, message) {
104
+ // ErrorClass is expected to be a defineClass-built class with
105
+ // (code, message, permanent[, statusCode]) signature. Pass
106
+ // permanent=true since protocol mismatches are config errors, not
107
+ // transient failures.
108
+ return new ErrorClass(code, message, true);
109
+ }
110
+
111
+ function resolve(protocol) {
112
+ if (typeof protocol !== "string" || protocol.length === 0) {
113
+ throw _err("MISSING_PROTOCOL",
114
+ name + " backend requires { protocol }");
115
+ }
116
+ if (Object.prototype.hasOwnProperty.call(deferred, protocol)) {
117
+ var d = deferred[protocol];
118
+ var msg = name + " protocol '" + protocol + "' is not yet implemented";
119
+ if (d && d.description) msg += " (" + d.description + ")";
120
+ if (d && d.since) msg += "; deferred to " + d.since;
121
+ if (fallbackProtocol) msg += ". Use protocol: '" + fallbackProtocol + "' for now.";
122
+ throw _err("PROTOCOL_NOT_IMPLEMENTED", msg);
123
+ }
124
+ if (!Object.prototype.hasOwnProperty.call(protocols, protocol)) {
125
+ var known = Object.keys(protocols).sort().join(", ");
126
+ throw _err("UNKNOWN_PROTOCOL",
127
+ "unknown " + name + " protocol: '" + protocol +
128
+ "' (known: " + (known || "[none]") + ")");
129
+ }
130
+ return protocols[protocol];
131
+ }
132
+
133
+ return {
134
+ name: name,
135
+ resolve: resolve,
136
+ protocols: Object.keys(protocols).sort(),
137
+ deferred: Object.keys(deferred).sort(),
138
+ };
139
+ }
140
+
141
+ module.exports = {
142
+ create: create,
143
+ ProtocolDispatcherError: ProtocolDispatcherError,
144
+ };