@blamejs/core 0.6.36 → 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.
- package/CHANGELOG.md +34 -0
- package/README.md +2 -2
- package/lib/audit.js +1 -0
- package/lib/bundler.js +166 -31
- package/lib/i18n-messageformat.js +398 -0
- package/lib/i18n.js +17 -0
- package/lib/log-stream-otlp-grpc.js +413 -0
- package/lib/log-stream.js +8 -0
- package/lib/mail.js +18 -3
- package/lib/mtls-ca.js +155 -0
- package/lib/mtls-engine-default.js +40 -0
- package/lib/object-store/azure-blob-bucket-ops.js +291 -0
- package/lib/object-store/gcs-bucket-ops.js +327 -0
- package/lib/object-store/index.js +65 -14
- package/lib/object-store/sigv4-bucket-ops.js +639 -39
- package/lib/object-store/sigv4.js +10 -3
- package/lib/protobuf-encoder.js +184 -0
- package/lib/queue-sqs.js +314 -0
- package/lib/queue.js +2 -2
- package/lib/safe-buffer.js +15 -2
- package/lib/totp.js +11 -3
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -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
|
+
};
|
package/lib/queue-sqs.js
ADDED
|
@@ -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
|
},
|
package/lib/safe-buffer.js
CHANGED
|
@@ -112,9 +112,22 @@ function toBuffer(data, opts) {
|
|
|
112
112
|
|
|
113
113
|
function boundedChunkCollector(opts) {
|
|
114
114
|
opts = opts || {};
|
|
115
|
-
|
|
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(
|
|
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
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
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
package/sbom.cyclonedx.json
CHANGED
|
@@ -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:
|
|
5
|
+
"serialNumber": "urn:uuid:246b27c1-6c5e-4ffc-9840-0275a3ed556a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.6.58",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.6.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.6.58",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|