@blamejs/core 0.11.24 → 0.11.25
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 +2 -0
- package/index.js +5 -0
- package/lib/auth/bot-challenge.js +573 -0
- package/lib/framework-error.js +6 -0
- package/lib/fsm.js +469 -0
- package/lib/guard-mail-query.js +14 -0
- package/lib/mail-agent.js +24 -10
- package/lib/mail-store-fts.js +394 -0
- package/lib/mail-store.js +142 -4
- package/lib/money.js +699 -0
- package/lib/webhook.js +229 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/webhook.js
CHANGED
|
@@ -735,6 +735,232 @@ function _writeError(res, status, code, message) {
|
|
|
735
735
|
res.end(JSON.stringify({ error: code, message: message }));
|
|
736
736
|
}
|
|
737
737
|
|
|
738
|
+
// ---- Stripe-compatible inbound HMAC-SHA-256 verifier (v0.11.25) ----
|
|
739
|
+
//
|
|
740
|
+
// Stripe (+ Stripe-shaped: Paddle, Shopify) ships inbound webhooks with
|
|
741
|
+
// `Stripe-Signature: t=<unix>,v1=<hex>[,v0=<hex>...]`. The signed payload
|
|
742
|
+
// is the literal string `<t>.<rawBody>` and the MAC is HMAC-SHA-256 keyed
|
|
743
|
+
// by the operator's `whsec_...` secret bytes (the `whsec_` prefix IS the
|
|
744
|
+
// key — Stripe's spec preserves it; do NOT strip).
|
|
745
|
+
//
|
|
746
|
+
// Spec: https://docs.stripe.com/webhooks/signature
|
|
747
|
+
// Paddle: https://developer.paddle.com/webhooks/signature-verification
|
|
748
|
+
// Shopify: https://shopify.dev/docs/apps/webhooks/configuration/https
|
|
749
|
+
//
|
|
750
|
+
// RFC 2104 (HMAC) + RFC 6234 (SHA-256). Constant-time compare via
|
|
751
|
+
// b.crypto.timingSafeEqual — never `===`.
|
|
752
|
+
|
|
753
|
+
var STRIPE_HMAC_ALG = "hmac-sha256-stripe";
|
|
754
|
+
var STRIPE_SIG_MAX_HEX = 256; // allow:raw-byte-literal — hex-char anti-DoS cap, not bytes
|
|
755
|
+
var STRIPE_DEFAULT_TOLERANCE_MS = C.TIME.minutes(5); // RFC 3161-ish 5 minute window default
|
|
756
|
+
var STRIPE_MIN_TOLERANCE_MS = C.TIME.seconds(30); // refuse below 30s
|
|
757
|
+
|
|
758
|
+
function _hmacSha256Hex(keyBytes, dataString) {
|
|
759
|
+
return nodeCrypto.createHmac("sha256", keyBytes).update(dataString, "utf8").digest("hex");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function _parseStripeSignatureHeader(header) {
|
|
763
|
+
if (typeof header !== "string" || header.length === 0) {
|
|
764
|
+
throw new WebhookError("webhook/bad-stripe-header",
|
|
765
|
+
"verify: Stripe-Signature header must be a non-empty string");
|
|
766
|
+
}
|
|
767
|
+
if (header.length > 4096) { // allow:raw-byte-literal — anti-DoS header cap
|
|
768
|
+
throw new WebhookError("webhook/bad-stripe-header",
|
|
769
|
+
"verify: Stripe-Signature header exceeds 4096 bytes");
|
|
770
|
+
}
|
|
771
|
+
var parts = header.split(",");
|
|
772
|
+
var ts = null;
|
|
773
|
+
var v1 = [];
|
|
774
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
775
|
+
var p = parts[i].trim();
|
|
776
|
+
var eq = p.indexOf("=");
|
|
777
|
+
if (eq <= 0) continue;
|
|
778
|
+
var k = p.slice(0, eq);
|
|
779
|
+
var v = p.slice(eq + 1);
|
|
780
|
+
if (k === "t") ts = v;
|
|
781
|
+
else if (k === "v1") {
|
|
782
|
+
if (v.length > STRIPE_SIG_MAX_HEX) {
|
|
783
|
+
throw new WebhookError("webhook/bad-stripe-header",
|
|
784
|
+
"verify: Stripe-Signature v1 entry exceeds " + STRIPE_SIG_MAX_HEX + " hex chars");
|
|
785
|
+
}
|
|
786
|
+
if (!/^[0-9a-f]+$/i.test(v)) continue;
|
|
787
|
+
v1.push(v.toLowerCase());
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (ts === null || !/^\d+$/.test(ts)) {
|
|
791
|
+
throw new WebhookError("webhook/bad-stripe-header",
|
|
792
|
+
"verify: Stripe-Signature missing or malformed t=<unix>");
|
|
793
|
+
}
|
|
794
|
+
if (v1.length === 0) {
|
|
795
|
+
throw new WebhookError("webhook/bad-stripe-header",
|
|
796
|
+
"verify: Stripe-Signature missing v1=<hex> entry");
|
|
797
|
+
}
|
|
798
|
+
return { timestamp: parseInt(ts, 10), v1: v1 };
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function _timingSafeHexEqual(aHex, bHex) {
|
|
802
|
+
if (typeof aHex !== "string" || typeof bHex !== "string") return false;
|
|
803
|
+
if (aHex.length !== bHex.length) return false;
|
|
804
|
+
// Route through the framework's length-tolerant timing-safe compare;
|
|
805
|
+
// bCrypto handles the hex-string shape directly without manual
|
|
806
|
+
// Buffer.from coercion.
|
|
807
|
+
return bCrypto.timingSafeEqual(aHex, bHex);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function _coerceSecretBytes(secret) {
|
|
811
|
+
if (typeof secret === "string") return Buffer.from(secret, "utf8");
|
|
812
|
+
if (Buffer.isBuffer(secret)) return Buffer.from(secret);
|
|
813
|
+
throw new WebhookError("webhook/bad-secret",
|
|
814
|
+
"verify: secret must be a non-empty string or Buffer");
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function _coerceBodyString(body) {
|
|
818
|
+
if (typeof body === "string") return body;
|
|
819
|
+
if (Buffer.isBuffer(body)) return body.toString("utf8");
|
|
820
|
+
throw new WebhookError("webhook/bad-body",
|
|
821
|
+
"verify: body must be a Buffer or string");
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* @primitive b.webhook.verify
|
|
826
|
+
* @signature b.webhook.verify(input)
|
|
827
|
+
* @since 0.11.25
|
|
828
|
+
* @status stable
|
|
829
|
+
*
|
|
830
|
+
* Stripe-spec inbound webhook signature verifier. Validates the
|
|
831
|
+
* `Stripe-Signature: t=<unix>,v1=<hex>[,v1=<hex>...]` header against an
|
|
832
|
+
* HMAC-SHA-256 over the literal `<t>.<rawBody>` string using the
|
|
833
|
+
* operator's `whsec_...` secret bytes verbatim (the prefix IS the key).
|
|
834
|
+
* Refuses signatures older than the tolerance window (default 5 min,
|
|
835
|
+
* minimum 30 s). When `nonceStore` is supplied the verifier records
|
|
836
|
+
* the accepted v1 signature so a replay within the tolerance window
|
|
837
|
+
* is refused. Constant-time compare via `b.crypto.timingSafeEqual`.
|
|
838
|
+
*
|
|
839
|
+
* @example
|
|
840
|
+
* b.webhook.verify({
|
|
841
|
+
* alg: "hmac-sha256-stripe",
|
|
842
|
+
* secret: "whsec_abc...",
|
|
843
|
+
* header: req.headers["stripe-signature"],
|
|
844
|
+
* body: rawBodyBuffer,
|
|
845
|
+
* });
|
|
846
|
+
* // → { ok: true, timestamp: 1700000000, scheme: "v1" }
|
|
847
|
+
*/
|
|
848
|
+
async function verify(input) {
|
|
849
|
+
if (!input || typeof input !== "object") {
|
|
850
|
+
throw new WebhookError("webhook/bad-opts",
|
|
851
|
+
"verify: input object required");
|
|
852
|
+
}
|
|
853
|
+
if (input.alg !== STRIPE_HMAC_ALG) {
|
|
854
|
+
throw new WebhookError("webhook/bad-alg",
|
|
855
|
+
"verify: alg must be '" + STRIPE_HMAC_ALG + "'");
|
|
856
|
+
}
|
|
857
|
+
var secretBytes = _coerceSecretBytes(input.secret);
|
|
858
|
+
if (secretBytes.length === 0) {
|
|
859
|
+
throw new WebhookError("webhook/bad-secret",
|
|
860
|
+
"verify: secret must be non-empty");
|
|
861
|
+
}
|
|
862
|
+
var bodyStr = _coerceBodyString(input.body);
|
|
863
|
+
var parsed = _parseStripeSignatureHeader(input.header);
|
|
864
|
+
var tolerance = input.toleranceMs;
|
|
865
|
+
if (tolerance === undefined) tolerance = STRIPE_DEFAULT_TOLERANCE_MS;
|
|
866
|
+
if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < STRIPE_MIN_TOLERANCE_MS) {
|
|
867
|
+
throw new WebhookError("webhook/bad-tolerance",
|
|
868
|
+
"verify: toleranceMs must be a finite number >= " + STRIPE_MIN_TOLERANCE_MS);
|
|
869
|
+
}
|
|
870
|
+
var nowMs = (input._nowMs !== undefined && typeof input._nowMs === "number")
|
|
871
|
+
? input._nowMs : Date.now();
|
|
872
|
+
var ageMs = nowMs - C.TIME.seconds(parsed.timestamp);
|
|
873
|
+
if (ageMs < 0) ageMs = -ageMs;
|
|
874
|
+
if (ageMs > tolerance) {
|
|
875
|
+
throw new WebhookError("webhook/stale-timestamp",
|
|
876
|
+
"verify: signature timestamp outside tolerance (" + ageMs + "ms > " + tolerance + "ms)");
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
var expectedHex = _hmacSha256Hex(secretBytes, parsed.timestamp + "." + bodyStr);
|
|
880
|
+
var matched = false;
|
|
881
|
+
for (var i = 0; i < parsed.v1.length; i += 1) {
|
|
882
|
+
if (_timingSafeHexEqual(parsed.v1[i], expectedHex)) {
|
|
883
|
+
matched = true;
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (!matched) {
|
|
888
|
+
throw new WebhookError("webhook/bad-signature",
|
|
889
|
+
"verify: no v1 signature matched HMAC-SHA-256 of <t>.<body>");
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Optional replay defense — the nonceStore is operator-supplied
|
|
893
|
+
// (e.g. `b.kv` or a Redis-shaped { has, set, expire }) so the
|
|
894
|
+
// primitive doesn't own retention. `has` / `set` MAY return a
|
|
895
|
+
// Promise — we always `await` them so async backends (Redis, KV,
|
|
896
|
+
// DynamoDB) work without falsely flagging a Promise as truthy.
|
|
897
|
+
if (input.nonceStore) {
|
|
898
|
+
var ns = input.nonceStore;
|
|
899
|
+
if (typeof ns.has !== "function" || typeof ns.set !== "function") {
|
|
900
|
+
throw new WebhookError("webhook/bad-nonce-store",
|
|
901
|
+
"verify: nonceStore must expose { has(key), set(key, ttlMs) }");
|
|
902
|
+
}
|
|
903
|
+
var nonceKey = "stripe:" + parsed.timestamp + ":" + expectedHex;
|
|
904
|
+
var seen = await ns.has(nonceKey);
|
|
905
|
+
if (seen) {
|
|
906
|
+
throw new WebhookError("webhook/replay",
|
|
907
|
+
"verify: signature already seen within tolerance window (replay)");
|
|
908
|
+
}
|
|
909
|
+
await ns.set(nonceKey, tolerance);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return { ok: true, timestamp: parsed.timestamp, scheme: "v1" };
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* @primitive b.webhook.sign
|
|
917
|
+
* @signature b.webhook.sign(input)
|
|
918
|
+
* @since 0.11.25
|
|
919
|
+
* @status stable
|
|
920
|
+
*
|
|
921
|
+
* Round-trip companion to `b.webhook.verify` for the
|
|
922
|
+
* `hmac-sha256-stripe` algorithm. Returns the `Stripe-Signature`
|
|
923
|
+
* header value `t=<unix>,v1=<hex>` for a given body + secret +
|
|
924
|
+
* (optional) timestamp. Operators emitting Stripe-shaped webhooks
|
|
925
|
+
* downstream — and the test surface — use this to produce the
|
|
926
|
+
* matching header.
|
|
927
|
+
*
|
|
928
|
+
* @example
|
|
929
|
+
* var header = b.webhook.sign({
|
|
930
|
+
* alg: "hmac-sha256-stripe",
|
|
931
|
+
* secret: "whsec_abc...",
|
|
932
|
+
* body: '{"id":"evt_1"}',
|
|
933
|
+
* });
|
|
934
|
+
*/
|
|
935
|
+
function sign(input) {
|
|
936
|
+
if (!input || typeof input !== "object") {
|
|
937
|
+
throw new WebhookError("webhook/bad-opts",
|
|
938
|
+
"sign: input object required");
|
|
939
|
+
}
|
|
940
|
+
if (input.alg !== STRIPE_HMAC_ALG) {
|
|
941
|
+
throw new WebhookError("webhook/bad-alg",
|
|
942
|
+
"sign: alg must be '" + STRIPE_HMAC_ALG + "'");
|
|
943
|
+
}
|
|
944
|
+
var secretBytes = _coerceSecretBytes(input.secret);
|
|
945
|
+
if (secretBytes.length === 0) {
|
|
946
|
+
throw new WebhookError("webhook/bad-secret",
|
|
947
|
+
"sign: secret must be non-empty");
|
|
948
|
+
}
|
|
949
|
+
var bodyStr = _coerceBodyString(input.body);
|
|
950
|
+
var ts;
|
|
951
|
+
if (input.timestamp !== undefined) {
|
|
952
|
+
if (typeof input.timestamp !== "number" || !isFinite(input.timestamp) || input.timestamp < 0) {
|
|
953
|
+
throw new WebhookError("webhook/bad-timestamp",
|
|
954
|
+
"sign: timestamp must be a non-negative finite number (unix seconds)");
|
|
955
|
+
}
|
|
956
|
+
ts = Math.floor(input.timestamp);
|
|
957
|
+
} else {
|
|
958
|
+
ts = Math.floor(Date.now() / 1000); // allow:raw-time-literal — unix-seconds conversion, Stripe spec uses seconds-not-ms
|
|
959
|
+
}
|
|
960
|
+
var hex = _hmacSha256Hex(secretBytes, ts + "." + bodyStr);
|
|
961
|
+
return "t=" + ts + ",v1=" + hex;
|
|
962
|
+
}
|
|
963
|
+
|
|
738
964
|
// ---- Public surface ----
|
|
739
965
|
|
|
740
966
|
module.exports = {
|
|
@@ -745,4 +971,7 @@ module.exports = {
|
|
|
745
971
|
HEADER: HEADER,
|
|
746
972
|
DEFAULTS: DEFAULTS,
|
|
747
973
|
WebhookError: WebhookError,
|
|
974
|
+
// v0.11.25 — Stripe-shaped inbound HMAC-SHA-256 verifier + signer.
|
|
975
|
+
verify: verify,
|
|
976
|
+
sign: sign,
|
|
748
977
|
};
|
package/package.json
CHANGED
package/sbom.cdx.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:6b814cb5-e99f-4bcb-a349-6b597ea780a6",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-21T05:38:05.971Z",
|
|
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.11.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.11.25",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.11.
|
|
25
|
+
"version": "0.11.25",
|
|
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.11.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.11.25",
|
|
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.11.
|
|
57
|
+
"ref": "@blamejs/core@0.11.25",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|