@blamejs/core 0.6.37 → 0.6.58

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.
@@ -496,10 +496,17 @@ function create(config) {
496
496
  "(S3 max " + MAX_PARTS + "); increase partSizeBytes", true);
497
497
  }
498
498
 
499
- // 1. Initiate
499
+ // 1. Initiate — `?uploads` is the InitiateMultipartUpload subresource.
500
+ // Build the URL with the bare token (no trailing `=`) for consistency
501
+ // with sigv4-bucket-ops.js — strict S3 implementations route on the
502
+ // bare form; the `URLSearchParams.set("uploads", "")` idiom produces
503
+ // `?uploads=` which is accepted by AWS + MinIO for this specific
504
+ // subresource, but the framework's convention is the bare form
505
+ // everywhere. SigV4 canonicalization reads `url.searchParams` (which
506
+ // still presents `uploads=` per AWS spec) so the signature path is
507
+ // unchanged.
500
508
  var url = _keyToUrl(key);
501
- var initiateUrl = new URL(url.href);
502
- initiateUrl.searchParams.set("uploads", "");
509
+ var initiateUrl = new URL(url.href + (url.search ? "&" : "?") + "uploads");
503
510
  var initiateExtra = {
504
511
  "Content-Type": contentType,
505
512
  "Content-Length": "0",
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ /**
3
+ * protobuf-encoder — minimal proto3 wire-format encoder.
4
+ *
5
+ * Write-only — there is no decoder. The framework emits OTLP gRPC log
6
+ * records (a fixed schema) and never reads protobuf back. A decoder
7
+ * would double the surface area + maintenance burden for no use case
8
+ * the framework owns end-to-end.
9
+ *
10
+ * Wire format reference:
11
+ * https://protobuf.dev/programming-guides/encoding/
12
+ *
13
+ * Wire types used:
14
+ * 0 varint — uint32 / uint64 / int32 / int64 / bool / enum
15
+ * 1 64-bit — fixed64 / sfixed64 / double
16
+ * 2 length-delimited — string / bytes / embedded message / packed repeated
17
+ * 5 32-bit — fixed32 / sfixed32 / float
18
+ *
19
+ * Each field on the wire:
20
+ * tag = (fieldNumber << 3) | wireType (encoded as varint)
21
+ * value (per wire type)
22
+ *
23
+ * Operators reach for this primitive when constructing a protobuf
24
+ * message body for an external service that accepts proto over HTTP
25
+ * (gRPC, AWS sigv4-protobuf, GCP protobuf APIs, etc.). All buffer
26
+ * concatenation is deferred — every encoder function returns a Buffer
27
+ * the caller can splice in any order.
28
+ */
29
+
30
+ var WIRE_VARINT = 0;
31
+ var WIRE_64BIT = 1;
32
+ var WIRE_LDELIM = 2;
33
+ // WIRE_32BIT (5) is unused — fixed32 / sfixed32 / float aren't on the
34
+ // OTel logs schema. Reserved here as documentation; un-comment + add
35
+ // helpers when a future caller (metrics histograms?) needs them.
36
+ // var WIRE_32BIT = 5;
37
+
38
+ function _writeVarint(value) {
39
+ // proto3 varints are unsigned for the integer types we use here;
40
+ // negative int32/int64 would need 10-byte two's-complement encoding,
41
+ // but OTel log severity numbers / unix nanos are all non-negative.
42
+ if (typeof value === "number") {
43
+ if (value < 0) {
44
+ throw new Error("protobuf-encoder: negative varint not supported (got " + value + ")");
45
+ }
46
+ if (!Number.isFinite(value)) {
47
+ throw new Error("protobuf-encoder: non-finite varint (got " + value + ")");
48
+ }
49
+ } else if (typeof value === "bigint") {
50
+ if (value < 0n) {
51
+ throw new Error("protobuf-encoder: negative varint not supported (got " + value + ")");
52
+ }
53
+ } else {
54
+ throw new Error("protobuf-encoder: varint must be number or bigint, got " + typeof value);
55
+ }
56
+ var bytes = [];
57
+ if (typeof value === "bigint") {
58
+ var v = value;
59
+ do {
60
+ var lower = Number(v & 0x7fn);
61
+ v = v >> 7n;
62
+ if (v !== 0n) lower |= 0x80;
63
+ bytes.push(lower);
64
+ } while (v !== 0n);
65
+ } else {
66
+ // Number path — JS numbers safely hold integers up to 2^53.
67
+ var n = value;
68
+ do {
69
+ var byte = n & 0x7f;
70
+ n = Math.floor(n / 128);
71
+ if (n > 0) byte |= 0x80;
72
+ bytes.push(byte);
73
+ } while (n > 0);
74
+ }
75
+ return Buffer.from(bytes);
76
+ }
77
+
78
+ function _tag(fieldNumber, wireType) {
79
+ return _writeVarint((fieldNumber << 3) | wireType);
80
+ }
81
+
82
+ function uint32(fieldNumber, value) {
83
+ if (value === 0) return Buffer.alloc(0); // proto3 default — skip
84
+ return Buffer.concat([_tag(fieldNumber, WIRE_VARINT), _writeVarint(value)]);
85
+ }
86
+
87
+ function uint64(fieldNumber, value) {
88
+ if (value === 0 || value === 0n) return Buffer.alloc(0);
89
+ return Buffer.concat([_tag(fieldNumber, WIRE_VARINT), _writeVarint(value)]);
90
+ }
91
+
92
+ function bool(fieldNumber, value) {
93
+ if (!value) return Buffer.alloc(0); // proto3 default
94
+ return Buffer.concat([_tag(fieldNumber, WIRE_VARINT), Buffer.from([1])]);
95
+ }
96
+
97
+ function fixed64(fieldNumber, value) {
98
+ // For OTel: time_unix_nano fields are fixed64. Accept BigInt or
99
+ // Number; encode as little-endian 8 bytes.
100
+ var buf = Buffer.alloc(8);
101
+ if (typeof value === "bigint") {
102
+ buf.writeBigUInt64LE(value, 0);
103
+ } else {
104
+ if (value < 0 || !Number.isFinite(value)) {
105
+ throw new Error("protobuf-encoder: fixed64 must be non-negative finite (got " + value + ")");
106
+ }
107
+ // Number path — split into low/high 32-bit halves.
108
+ var low = value % 0x100000000;
109
+ var high = Math.floor(value / 0x100000000);
110
+ buf.writeUInt32LE(low, 0);
111
+ buf.writeUInt32LE(high, 4);
112
+ }
113
+ return Buffer.concat([_tag(fieldNumber, WIRE_64BIT), buf]);
114
+ }
115
+
116
+ function double(fieldNumber, value) {
117
+ if (value === 0) return Buffer.alloc(0); // proto3 default
118
+ var buf = Buffer.alloc(8);
119
+ buf.writeDoubleLE(value, 0);
120
+ return Buffer.concat([_tag(fieldNumber, WIRE_64BIT), buf]);
121
+ }
122
+
123
+ function string(fieldNumber, value) {
124
+ if (value === "" || value == null) return Buffer.alloc(0);
125
+ var bodyBuf = Buffer.from(String(value), "utf8");
126
+ return Buffer.concat([
127
+ _tag(fieldNumber, WIRE_LDELIM),
128
+ _writeVarint(bodyBuf.length),
129
+ bodyBuf,
130
+ ]);
131
+ }
132
+
133
+ function bytes(fieldNumber, value) {
134
+ if (!value || value.length === 0) return Buffer.alloc(0);
135
+ var buf = Buffer.isBuffer(value) ? value : Buffer.from(value);
136
+ return Buffer.concat([
137
+ _tag(fieldNumber, WIRE_LDELIM),
138
+ _writeVarint(buf.length),
139
+ buf,
140
+ ]);
141
+ }
142
+
143
+ function embeddedMessage(fieldNumber, bodyBuf) {
144
+ // bodyBuf is the already-encoded inner message body (a Buffer).
145
+ // Caller can pass an empty buffer to encode an empty message;
146
+ // proto3 default-skips messages whose every field is at default,
147
+ // but we allow the caller to choose: we always emit the tag +
148
+ // length-delimited body, which lets the operator force-include an
149
+ // empty Resource{} when needed.
150
+ return Buffer.concat([
151
+ _tag(fieldNumber, WIRE_LDELIM),
152
+ _writeVarint(bodyBuf.length),
153
+ bodyBuf,
154
+ ]);
155
+ }
156
+
157
+ // Repeated unpacked: each entry encoded with its own tag+value. This
158
+ // is the proto3 default for non-scalar repeated fields (messages,
159
+ // strings) — packed encoding only applies to scalar varint/fixed
160
+ // types. The caller passes an array and per-entry encoder.
161
+ function repeatedMessage(fieldNumber, items, perItemBodyEncoder) {
162
+ if (!items || items.length === 0) return Buffer.alloc(0);
163
+ var pieces = new Array(items.length);
164
+ for (var i = 0; i < items.length; i++) {
165
+ var inner = perItemBodyEncoder(items[i]);
166
+ pieces[i] = embeddedMessage(fieldNumber, inner);
167
+ }
168
+ return Buffer.concat(pieces);
169
+ }
170
+
171
+ module.exports = {
172
+ uint32: uint32,
173
+ uint64: uint64,
174
+ bool: bool,
175
+ fixed64: fixed64,
176
+ double: double,
177
+ string: string,
178
+ bytes: bytes,
179
+ embeddedMessage: embeddedMessage,
180
+ repeatedMessage: repeatedMessage,
181
+ // Exposed for tests — verify varint encoding directly.
182
+ _writeVarint: _writeVarint,
183
+ _tag: _tag,
184
+ };
@@ -0,0 +1,314 @@
1
+ "use strict";
2
+ /**
3
+ * AWS SQS queue adapter — backs `b.queue` with Amazon SQS so multi-replica
4
+ * apps share a managed queue without each needing to be cluster leader.
5
+ *
6
+ * Wire protocol — AWSJsonProtocol_1.0 over HTTPS, SigV4-signed via
7
+ * `lib/object-store/sigv4.js`'s service-agnostic `signRequest` helper:
8
+ *
9
+ * POST https://sqs.{region}.amazonaws.com/
10
+ * Content-Type: application/x-amz-json-1.0
11
+ * X-Amz-Target: AmazonSQS.<Action>
12
+ * Authorization: AWS4-HMAC-SHA256 ...
13
+ *
14
+ * Action → SQS API
15
+ * enqueue → SendMessage
16
+ * lease → ReceiveMessage (long-poll up to WaitTimeSeconds)
17
+ * extendLease → ChangeMessageVisibility
18
+ * complete → DeleteMessage
19
+ * fail → ChangeMessageVisibility (VisibilityTimeout=0 → re-deliver
20
+ * immediately) — DLQ routing happens server-side via the
21
+ * queue's RedrivePolicy attribute, configured at queue
22
+ * creation time outside the framework
23
+ * size → GetQueueAttributes(ApproximateNumberOfMessages)
24
+ * purge → PurgeQueue
25
+ *
26
+ * Queue-name → URL: SQS queues live at
27
+ * https://sqs.{region}.amazonaws.com/{accountId}/{queueName}
28
+ * Operators pass `accountId` + `region` and the adapter constructs URLs
29
+ * from the framework's logical queue names. Operators with cross-account
30
+ * queues / FIFO queues / VPCE endpoints pass an explicit
31
+ * `queueUrlByName(name) → url` resolver instead.
32
+ *
33
+ * What this adapter does NOT support (operator wiring required):
34
+ *
35
+ * - DLQ inspection (dlqList / dlqRetry / dlqSize) — SQS DLQs are
36
+ * separate queues; operators using one wire it as a second framework
37
+ * backend and inspect via that backend's `lease()`.
38
+ * - Flow / cron / parent-child dependencies — SQS has no native flow
39
+ * primitives; those stay on queue-local or queue-redis.
40
+ * - sweepExpired — SQS handles visibility-timeout expiry server-side.
41
+ *
42
+ * Sealing: payloads pass through `cryptoField.sealRow("_blamejs_jobs",
43
+ * row)` before SendMessage so the SQS message body is a sealed envelope
44
+ * (operator's vault-key + framework crypto stack), same posture as the
45
+ * local + redis backends.
46
+ */
47
+ var sigv4 = require("./object-store/sigv4");
48
+ var httpClient = require("./http-client");
49
+ var cryptoField = require("./crypto-field");
50
+ var safeJson = require("./safe-json");
51
+ var safeUrl = require("./safe-url");
52
+ var { generateToken } = require("./crypto");
53
+ var { QueueError } = require("./framework-error");
54
+
55
+ var _err = QueueError.factory;
56
+
57
+ var DEFAULT_VISIBILITY_TIMEOUT_SEC = 30;
58
+ var DEFAULT_WAIT_TIME_SEC = 0; // SQS supports up to 20s
59
+ var DEFAULT_MAX_MESSAGES_PER_LEASE = 10; // SQS hard cap
60
+
61
+ function _resolveEndpoint(opts) {
62
+ if (opts.endpoint) return opts.endpoint.replace(/\/+$/, "") + "/";
63
+ return "https://sqs." + opts.region + ".amazonaws.com/";
64
+ }
65
+
66
+ function _payloadHash(buf) {
67
+ // Empty body's payload hash is the SHA256 of the empty string —
68
+ // sigv4 requires the actual hash, not "UNSIGNED-PAYLOAD".
69
+ var nodeCrypto = require("node:crypto");
70
+ return nodeCrypto.createHash("sha256").update(buf || Buffer.alloc(0)).digest("hex");
71
+ }
72
+
73
+ function create(opts) {
74
+ opts = opts || {};
75
+ if (typeof opts.region !== "string" || opts.region.length === 0) {
76
+ throw _err("INVALID_CONFIG", "queue-sqs: opts.region is required", true);
77
+ }
78
+ if (typeof opts.accessKeyId !== "string" || opts.accessKeyId.length === 0) {
79
+ throw _err("INVALID_CONFIG", "queue-sqs: opts.accessKeyId is required", true);
80
+ }
81
+ if (typeof opts.secretAccessKey !== "string" || opts.secretAccessKey.length === 0) {
82
+ throw _err("INVALID_CONFIG", "queue-sqs: opts.secretAccessKey is required", true);
83
+ }
84
+ if (!opts.queueUrlByName && (!opts.accountId ||
85
+ (typeof opts.accountId !== "string" && typeof opts.accountId !== "number"))) {
86
+ throw _err("INVALID_CONFIG",
87
+ "queue-sqs: opts.accountId is required (12-digit AWS account ID) " +
88
+ "or pass opts.queueUrlByName(name) → url for cross-account / VPCE queues", true);
89
+ }
90
+
91
+ var region = opts.region;
92
+ var endpoint = _resolveEndpoint(opts);
93
+ var endpointUrl = new URL(endpoint);
94
+ var accessKeyId = opts.accessKeyId;
95
+ var secretAccessKey = opts.secretAccessKey;
96
+ var sessionToken = opts.sessionToken || null;
97
+ var accountId = opts.accountId ? String(opts.accountId) : null;
98
+ var timeoutMs = opts.timeoutMs;
99
+ var allowedProtocols = opts.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
100
+ var allowInternal = opts.allowInternal != null ? opts.allowInternal : null;
101
+ var visibilityTimeoutSec = Number(opts.visibilityTimeoutSec) || DEFAULT_VISIBILITY_TIMEOUT_SEC;
102
+ var waitTimeSec = Number(opts.waitTimeSec) || DEFAULT_WAIT_TIME_SEC;
103
+
104
+ var queueUrlResolver = typeof opts.queueUrlByName === "function"
105
+ ? opts.queueUrlByName
106
+ : function (name) {
107
+ return endpoint + accountId + "/" + name;
108
+ };
109
+
110
+ function _post(action, body) {
111
+ var bodyBuf = Buffer.from(JSON.stringify(body || {}), "utf8");
112
+ var headers = {
113
+ "Content-Type": "application/x-amz-json-1.0",
114
+ "X-Amz-Target": "AmazonSQS." + action,
115
+ "Content-Length": String(bodyBuf.length),
116
+ };
117
+ var signed = sigv4.signRequest({
118
+ method: "POST",
119
+ url: endpointUrl,
120
+ headers: headers,
121
+ payloadHash: _payloadHash(bodyBuf),
122
+ region: region,
123
+ service: "sqs",
124
+ accessKeyId: accessKeyId,
125
+ secretAccessKey: secretAccessKey,
126
+ sessionToken: sessionToken,
127
+ });
128
+ var reqOpts = {
129
+ method: "POST",
130
+ url: endpointUrl,
131
+ headers: signed.headers,
132
+ body: bodyBuf,
133
+ idleTimeoutMs: timeoutMs,
134
+ allowedProtocols: allowedProtocols,
135
+ errorClass: QueueError,
136
+ };
137
+ if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
138
+ return httpClient.request(reqOpts).then(function (res) {
139
+ var text = Buffer.isBuffer(res.body) ? res.body.toString("utf8")
140
+ : (res.body || "").toString();
141
+ if (text.length === 0) return null;
142
+ try { return safeJson.parse(text); }
143
+ catch (_e) {
144
+ throw _err("BAD_RESPONSE", "queue-sqs: " + action +
145
+ " returned non-JSON body: " + text.slice(0, 500));
146
+ }
147
+ });
148
+ }
149
+
150
+ // ---- enqueue ----
151
+ async function enqueue(queueName, payload, enqueueOpts) {
152
+ enqueueOpts = enqueueOpts || {};
153
+ var queueUrl = queueUrlResolver(queueName);
154
+ var jobId = generateToken(16);
155
+ var sealed = cryptoField.sealRow("_blamejs_jobs", {
156
+ _id: jobId,
157
+ queueName: queueName,
158
+ payload: JSON.stringify(payload == null ? null : payload),
159
+ enqueuedAt: Date.now(),
160
+ attempts: 0,
161
+ });
162
+ // SQS message body: serialize the sealed row as JSON. Receive
163
+ // replays the same shape — sealed.payload stays sealed in transit.
164
+ var bodyJson = JSON.stringify(sealed);
165
+ var sqsBody = {
166
+ QueueUrl: queueUrl,
167
+ MessageBody: bodyJson,
168
+ };
169
+ var delaySeconds = enqueueOpts.delaySeconds;
170
+ if (typeof delaySeconds === "number" && delaySeconds > 0) {
171
+ // SQS hard cap is 900s.
172
+ sqsBody.DelaySeconds = Math.min(900, Math.floor(delaySeconds));
173
+ }
174
+ var rv = await _post("SendMessage", sqsBody);
175
+ return rv && (rv.MessageId || jobId);
176
+ }
177
+
178
+ // ---- lease ----
179
+ async function lease(queueName, leaseOpts) {
180
+ leaseOpts = leaseOpts || {};
181
+ var queueUrl = queueUrlResolver(queueName);
182
+ var maxMessages = Math.min(
183
+ DEFAULT_MAX_MESSAGES_PER_LEASE,
184
+ Math.max(1, Number(leaseOpts.maxRows) || 1)
185
+ );
186
+ var visTimeout = Number(leaseOpts.visibilityTimeoutSec) || visibilityTimeoutSec;
187
+ var waitSec = Number(leaseOpts.waitTimeSec) ||
188
+ (waitTimeSec > 0 ? waitTimeSec : DEFAULT_WAIT_TIME_SEC);
189
+ var rv = await _post("ReceiveMessage", {
190
+ QueueUrl: queueUrl,
191
+ MaxNumberOfMessages: maxMessages,
192
+ VisibilityTimeout: visTimeout,
193
+ WaitTimeSeconds: waitSec,
194
+ });
195
+ var messages = (rv && rv.Messages) || [];
196
+ var out = [];
197
+ for (var i = 0; i < messages.length; i++) {
198
+ var m = messages[i];
199
+ var sealed;
200
+ try { sealed = safeJson.parse(m.Body); }
201
+ catch (_e) { continue; }
202
+ var unsealed = cryptoField.unsealRow("_blamejs_jobs", sealed);
203
+ var payload;
204
+ try {
205
+ payload = unsealed.payload != null
206
+ ? JSON.parse(unsealed.payload) : null;
207
+ } catch (_e) { payload = unsealed.payload; }
208
+ out.push({
209
+ jobId: unsealed._id,
210
+ queueName: unsealed.queueName || queueName,
211
+ payload: payload,
212
+ attempts: Number(unsealed.attempts) || 0,
213
+ enqueuedAt: Number(unsealed.enqueuedAt) || null,
214
+ leaseExpiresAt: Date.now() + (visTimeout * 1000),
215
+ // SQS-specific: receipt handle is the only way to delete /
216
+ // change visibility / extend — surface it for the framework
217
+ // wrapper.
218
+ receiptHandle: m.ReceiptHandle,
219
+ sqsMessageId: m.MessageId,
220
+ });
221
+ }
222
+ return out;
223
+ }
224
+
225
+ // ---- extendLease ----
226
+ async function extendLease(queueName, jobId, extendOpts) {
227
+ extendOpts = extendOpts || {};
228
+ if (!extendOpts.receiptHandle) {
229
+ throw _err("MISSING_RECEIPT",
230
+ "queue-sqs: extendLease requires opts.receiptHandle (returned by lease())", true);
231
+ }
232
+ var queueUrl = queueUrlResolver(queueName);
233
+ var visTimeout = Number(extendOpts.visibilityTimeoutSec) || visibilityTimeoutSec;
234
+ await _post("ChangeMessageVisibility", {
235
+ QueueUrl: queueUrl,
236
+ ReceiptHandle: extendOpts.receiptHandle,
237
+ VisibilityTimeout: visTimeout,
238
+ });
239
+ return true;
240
+ }
241
+
242
+ // ---- complete ----
243
+ async function complete(queueName, jobId, completeOpts) {
244
+ completeOpts = completeOpts || {};
245
+ if (!completeOpts.receiptHandle) {
246
+ throw _err("MISSING_RECEIPT",
247
+ "queue-sqs: complete requires opts.receiptHandle", true);
248
+ }
249
+ var queueUrl = queueUrlResolver(queueName);
250
+ await _post("DeleteMessage", {
251
+ QueueUrl: queueUrl,
252
+ ReceiptHandle: completeOpts.receiptHandle,
253
+ });
254
+ return true;
255
+ }
256
+
257
+ // ---- fail (request re-delivery; SQS server-side DLQ routing decides
258
+ // if it goes back to the main queue or to the DLQ) ----
259
+ async function fail(queueName, jobId, failOpts) {
260
+ failOpts = failOpts || {};
261
+ if (!failOpts.receiptHandle) {
262
+ throw _err("MISSING_RECEIPT",
263
+ "queue-sqs: fail requires opts.receiptHandle", true);
264
+ }
265
+ var queueUrl = queueUrlResolver(queueName);
266
+ // VisibilityTimeout=0 → message becomes visible to other consumers
267
+ // immediately. SQS's RedrivePolicy on the queue (configured at
268
+ // queue creation) tracks ApproximateReceiveCount and routes to
269
+ // the DLQ once maxReceiveCount is exceeded.
270
+ await _post("ChangeMessageVisibility", {
271
+ QueueUrl: queueUrl,
272
+ ReceiptHandle: failOpts.receiptHandle,
273
+ VisibilityTimeout: 0,
274
+ });
275
+ return true;
276
+ }
277
+
278
+ // ---- size (visible messages only — in-flight and delayed are
279
+ // reported separately by GetQueueAttributes) ----
280
+ async function size(queueName) {
281
+ var queueUrl = queueUrlResolver(queueName);
282
+ var rv = await _post("GetQueueAttributes", {
283
+ QueueUrl: queueUrl,
284
+ AttributeNames: ["ApproximateNumberOfMessages"],
285
+ });
286
+ var attrs = (rv && rv.Attributes) || {};
287
+ return Number(attrs.ApproximateNumberOfMessages) || 0;
288
+ }
289
+
290
+ // ---- purge (60s server-side rate-limited; SQS rejects a second
291
+ // purge within the cooldown window) ----
292
+ async function purge(queueName) {
293
+ var queueUrl = queueUrlResolver(queueName);
294
+ await _post("PurgeQueue", { QueueUrl: queueUrl });
295
+ return 0; // SQS doesn't return a count
296
+ }
297
+
298
+ return {
299
+ enqueue: enqueue,
300
+ lease: lease,
301
+ extendLease: extendLease,
302
+ complete: complete,
303
+ fail: fail,
304
+ size: size,
305
+ purge: purge,
306
+ // Test hook — exposes the signed-request builder for assertion-
307
+ // only verification of wire shape without standing up a mock SQS
308
+ // server.
309
+ _post: _post,
310
+ _queueUrl: queueUrlResolver,
311
+ };
312
+ }
313
+
314
+ module.exports = { create: create };
package/lib/queue.js CHANGED
@@ -39,6 +39,7 @@ var observability = require("./observability");
39
39
  var protocolDispatcher = require("./protocol-dispatcher");
40
40
  var localProto = require("./queue-local");
41
41
  var redisProto = require("./queue-redis");
42
+ var sqsProto = require("./queue-sqs");
42
43
  var retryHelper = require("./retry");
43
44
  var safeAsync = require("./safe-async");
44
45
  var { QueueError } = require("./framework-error");
@@ -46,9 +47,8 @@ var { QueueError } = require("./framework-error");
46
47
  var dispatcher = protocolDispatcher.create({
47
48
  name: "queue",
48
49
  errorClass: QueueError,
49
- protocols: { "local": localProto, "redis": redisProto },
50
+ protocols: { "local": localProto, "redis": redisProto, "sqs": sqsProto },
50
51
  deferred: {
51
- "sqs": { description: "AWS SQS (and S3-compatible queue endpoints) via SigV4" },
52
52
  "amqp": { description: "AMQP 0-9-1 (RabbitMQ etc.)" },
53
53
  "nats": { description: "NATS JetStream" },
54
54
  },
@@ -112,9 +112,22 @@ function toBuffer(data, opts) {
112
112
 
113
113
  function boundedChunkCollector(opts) {
114
114
  opts = opts || {};
115
- var maxBytes = (typeof opts.maxBytes === "number" && opts.maxBytes > 0) ? opts.maxBytes : null;
115
+ // maxBytes must be a positive finite integer. Accepting `Infinity`
116
+ // would defeat the entire point of the bounded collector (a hostile
117
+ // 10-GB upstream would accumulate fully); accepting `3.5` would set
118
+ // a non-sensical fractional cap that confuses downstream `total +
119
+ // chunk.length > maxBytes` arithmetic. NaN, negative, zero, non-
120
+ // numbers all reject with the same `buffer/bad-arg` so operators
121
+ // see one consistent error at boot from a typo or misconfiguration.
122
+ var maxBytes = (typeof opts.maxBytes === "number" &&
123
+ Number.isFinite(opts.maxBytes) &&
124
+ Number.isInteger(opts.maxBytes) &&
125
+ opts.maxBytes > 0) ? opts.maxBytes : null;
116
126
  if (maxBytes === null) {
117
- throw new SafeBufferError("boundedChunkCollector requires maxBytes", "buffer/bad-arg");
127
+ throw new SafeBufferError(
128
+ "boundedChunkCollector requires maxBytes (positive finite integer); got " +
129
+ JSON.stringify(opts.maxBytes),
130
+ "buffer/bad-arg");
118
131
  }
119
132
  var errClass = opts.errorClass;
120
133
  var sizeCode = opts.sizeCode || "buffer/too-large";
package/lib/totp.js CHANGED
@@ -203,9 +203,17 @@ function verify(secret, code, opts) {
203
203
  var nowMs = (opts && opts.now) || Date.now();
204
204
  var currentStep = Math.floor(nowMs / 1000 / resolved.stepSeconds);
205
205
  var lastUsedStep = (opts && typeof opts.lastUsedStep === "number") ? opts.lastUsedStep : null;
206
- // Pad incoming to the configured digit count so timingSafeEqual gets
207
- // equal-length buffers regardless of how the caller stringified.
208
- var userCode = String(code).padStart(resolved.digits, "0");
206
+ // Strip the whitespace + common separators that every authenticator UI
207
+ // and clipboard paste introduces ("123 456", "123-456", "123.456").
208
+ // RFC 6238 / NIST 800-63B don't mandate normalization, but Google
209
+ // Authenticator, Authy, Duo, and every other consumer-facing TOTP
210
+ // implementation strips these before comparison; not doing so is a
211
+ // silent operator footgun where users mash a code from their phone
212
+ // into a login form and the verifier rejects it because of one space.
213
+ // After stripping, padStart pads the configured digit count so
214
+ // timingSafeEqual gets equal-length buffers regardless of how the
215
+ // caller stringified.
216
+ var userCode = String(code).replace(/[\s.\-_]/g, "").padStart(resolved.digits, "0");
209
217
  var userBuf = Buffer.from(userCode);
210
218
 
211
219
  for (var d = -resolved.driftSteps; d <= resolved.driftSteps; d++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.6.37",
3
+ "version": "0.6.58",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:a13cf9c3-cc15-4fc1-bff7-03071923ae8d",
5
+ "serialNumber": "urn:uuid:246b27c1-6c5e-4ffc-9840-0275a3ed556a",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-02T21:50:09.461Z",
8
+ "timestamp": "2026-05-03T13:19:55.918Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.6.37",
22
+ "bom-ref": "@blamejs/core@0.6.58",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.6.37",
25
+ "version": "0.6.58",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.6.37",
29
+ "purl": "pkg:npm/%40blamejs/core@0.6.58",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.6.37",
57
+ "ref": "@blamejs/core@0.6.58",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]