@particle-academy/fancy-connector-core 0.12.0 → 0.13.0
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 +42 -0
- package/dist/index.cjs +371 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +259 -97
- package/dist/index.d.ts +259 -97
- package/dist/index.js +362 -172
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -43,9 +43,13 @@ __export(src_exports, {
|
|
|
43
43
|
FAKE_EPOCH: () => FAKE_EPOCH,
|
|
44
44
|
FORM_FIELD_JSON_REASONS: () => FORM_FIELD_JSON_REASONS,
|
|
45
45
|
FormFieldJsonError: () => FormFieldJsonError,
|
|
46
|
+
JWT_ALGORITHMS: () => JWT_ALGORITHMS,
|
|
46
47
|
MAX_IDEMPOTENCY_KEY_LENGTH: () => MAX_IDEMPOTENCY_KEY_LENGTH,
|
|
47
48
|
NO_IDEMPOTENCY_KEY_WARNING: () => NO_IDEMPOTENCY_KEY_WARNING,
|
|
49
|
+
OIDC_JWT_SCHEME_KIND: () => OIDC_JWT_SCHEME_KIND,
|
|
48
50
|
PROBLEM_SEVERITIES: () => PROBLEM_SEVERITIES,
|
|
51
|
+
PUBSUB_PUSH_REASONS: () => PUBSUB_PUSH_REASONS,
|
|
52
|
+
PubSubPushError: () => PubSubPushError,
|
|
49
53
|
RENDERER_VERSION: () => RENDERER_VERSION,
|
|
50
54
|
REVIEW_STALE_DAYS: () => REVIEW_STALE_DAYS,
|
|
51
55
|
SANDBOX_KINDS: () => SANDBOX_KINDS,
|
|
@@ -66,6 +70,7 @@ __export(src_exports, {
|
|
|
66
70
|
constantTimeEquals: () => constantTimeEquals,
|
|
67
71
|
decodeDelivery: () => decodeDelivery,
|
|
68
72
|
decodeFormFieldJson: () => decodeFormFieldJson,
|
|
73
|
+
decodePubSubPush: () => decodePubSubPush,
|
|
69
74
|
deliver: () => deliver,
|
|
70
75
|
fakeRequest: () => fakeRequest,
|
|
71
76
|
fakeValues: () => fakeValues,
|
|
@@ -77,6 +82,8 @@ __export(src_exports, {
|
|
|
77
82
|
httpFailure: () => httpFailure,
|
|
78
83
|
idempotencyKeyFor: () => idempotencyKeyFor,
|
|
79
84
|
isEmptyPayload: () => isEmptyPayload,
|
|
85
|
+
isOidcJwtScheme: () => isOidcJwtScheme,
|
|
86
|
+
isOidcJwtSpec: () => isOidcJwtSpec,
|
|
80
87
|
isRemote: () => isRemote,
|
|
81
88
|
isSharedTokenScheme: () => isSharedTokenScheme,
|
|
82
89
|
isSharedTokenSpec: () => isSharedTokenSpec,
|
|
@@ -123,6 +130,8 @@ __export(src_exports, {
|
|
|
123
130
|
utf8Length: () => utf8Length,
|
|
124
131
|
verifyDelivery: () => verifyDelivery,
|
|
125
132
|
verifyHmac: () => verifyHmac,
|
|
133
|
+
verifyJwt: () => verifyJwt,
|
|
134
|
+
verifyOidcDelivery: () => verifyOidcDelivery,
|
|
126
135
|
verifySharedToken: () => verifySharedToken,
|
|
127
136
|
withResolvedLimit: () => withResolvedLimit
|
|
128
137
|
});
|
|
@@ -1362,6 +1371,88 @@ function fnv1a(value) {
|
|
|
1362
1371
|
return hash.toString(16).padStart(8, "0");
|
|
1363
1372
|
}
|
|
1364
1373
|
|
|
1374
|
+
// src/jwt.ts
|
|
1375
|
+
var JWT_ALGORITHMS = ["RS256"];
|
|
1376
|
+
var fail = (reason) => ({ ok: false, reason });
|
|
1377
|
+
async function verifyJwt(token, options) {
|
|
1378
|
+
const parts = typeof token === "string" ? token.split(".") : [];
|
|
1379
|
+
if (parts.length !== 3 || parts.some((p2, i) => p2 === "" && i < 2)) {
|
|
1380
|
+
return fail("token is not a JWS compact serialization");
|
|
1381
|
+
}
|
|
1382
|
+
const [h, p, s] = parts;
|
|
1383
|
+
const header3 = jsonSegment(h);
|
|
1384
|
+
if (!header3) return fail("token header is not base64url JSON");
|
|
1385
|
+
if (!JWT_ALGORITHMS.includes(header3.alg)) {
|
|
1386
|
+
return fail(`alg ${JSON.stringify(header3.alg)} is not accepted`);
|
|
1387
|
+
}
|
|
1388
|
+
const key = selectKey(options.keys, header3.kid);
|
|
1389
|
+
if ("reason" in key) return fail(key.reason);
|
|
1390
|
+
const signature = base64urlBytes(s);
|
|
1391
|
+
if (!signature) return fail("signature is not base64url");
|
|
1392
|
+
const subtle = globalThis.crypto?.subtle;
|
|
1393
|
+
if (!subtle) return fail("no WebCrypto in this runtime");
|
|
1394
|
+
let valid;
|
|
1395
|
+
try {
|
|
1396
|
+
const imported = await subtle.importKey(
|
|
1397
|
+
"jwk",
|
|
1398
|
+
{ kty: "RSA", n: key.jwk.n, e: key.jwk.e, alg: "RS256", ext: true },
|
|
1399
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
1400
|
+
false,
|
|
1401
|
+
["verify"]
|
|
1402
|
+
);
|
|
1403
|
+
valid = await subtle.verify("RSASSA-PKCS1-v1_5", imported, signature, new TextEncoder().encode(`${h}.${p}`));
|
|
1404
|
+
} catch {
|
|
1405
|
+
return fail("the key could not be used to verify");
|
|
1406
|
+
}
|
|
1407
|
+
if (!valid) return fail("signature did not match");
|
|
1408
|
+
const claims = jsonSegment(p);
|
|
1409
|
+
if (!claims) return fail("token payload is not base64url JSON");
|
|
1410
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
1411
|
+
const leeway = options.leeway ?? 60;
|
|
1412
|
+
if (typeof claims.exp !== "number") return fail("token has no exp");
|
|
1413
|
+
if (now > claims.exp + leeway) return fail("token has expired");
|
|
1414
|
+
if (typeof claims.nbf === "number" && now + leeway < claims.nbf) return fail("token is not valid yet");
|
|
1415
|
+
if (options.issuer !== void 0) {
|
|
1416
|
+
const issuers = typeof options.issuer === "string" ? [options.issuer] : options.issuer;
|
|
1417
|
+
if (typeof claims.iss !== "string" || !issuers.includes(claims.iss)) return fail("token issuer is not accepted");
|
|
1418
|
+
}
|
|
1419
|
+
if (options.audience !== null) {
|
|
1420
|
+
const wanted = typeof options.audience === "string" ? [options.audience] : options.audience;
|
|
1421
|
+
const offered = typeof claims.aud === "string" ? [claims.aud] : Array.isArray(claims.aud) ? claims.aud : [];
|
|
1422
|
+
if (!offered.some((a) => typeof a === "string" && wanted.includes(a))) return fail("token audience is not accepted");
|
|
1423
|
+
}
|
|
1424
|
+
return { ok: true, header: header3, claims };
|
|
1425
|
+
}
|
|
1426
|
+
function selectKey(keys, kid) {
|
|
1427
|
+
const rsa = keys.filter((k) => k.kty === "RSA" && typeof k.n === "string" && typeof k.e === "string");
|
|
1428
|
+
if (typeof kid === "string") {
|
|
1429
|
+
const match = rsa.find((k) => k.kid === kid);
|
|
1430
|
+
return match ? { jwk: match } : { reason: `no key for kid ${JSON.stringify(kid)}` };
|
|
1431
|
+
}
|
|
1432
|
+
const [only] = rsa;
|
|
1433
|
+
return rsa.length === 1 && only ? { jwk: only } : { reason: "token names no kid and the key is ambiguous" };
|
|
1434
|
+
}
|
|
1435
|
+
function jsonSegment(segment) {
|
|
1436
|
+
const bytes = base64urlBytes(segment);
|
|
1437
|
+
if (!bytes) return null;
|
|
1438
|
+
try {
|
|
1439
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
1440
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1441
|
+
} catch {
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
function base64urlBytes(segment) {
|
|
1446
|
+
if (!/^[A-Za-z0-9_-]*$/.test(segment) || segment.length % 4 === 1) return null;
|
|
1447
|
+
const standard = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
1448
|
+
const padded = standard + "=".repeat((4 - standard.length % 4) % 4);
|
|
1449
|
+
try {
|
|
1450
|
+
return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
|
|
1451
|
+
} catch {
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1365
1456
|
// src/lease.ts
|
|
1366
1457
|
var EXPIRES_AT_UNITS = ["rfc3339", "epoch-ms"];
|
|
1367
1458
|
var INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
@@ -1796,6 +1887,65 @@ function parseMime(raw) {
|
|
|
1796
1887
|
};
|
|
1797
1888
|
}
|
|
1798
1889
|
|
|
1890
|
+
// src/pubsub-push.ts
|
|
1891
|
+
var PUBSUB_PUSH_REASONS = ["not-json", "missing-message", "missing-data", "bad-encoding", "data-not-json"];
|
|
1892
|
+
var PubSubPushError = class extends Error {
|
|
1893
|
+
constructor(reason, message) {
|
|
1894
|
+
super(message);
|
|
1895
|
+
this.reason = reason;
|
|
1896
|
+
this.name = "PubSubPushError";
|
|
1897
|
+
}
|
|
1898
|
+
reason;
|
|
1899
|
+
};
|
|
1900
|
+
var UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
1901
|
+
function decodePubSubPush(raw) {
|
|
1902
|
+
let body;
|
|
1903
|
+
try {
|
|
1904
|
+
body = JSON.parse(typeof raw === "string" ? raw : UTF8.decode(raw));
|
|
1905
|
+
} catch {
|
|
1906
|
+
throw new PubSubPushError("not-json", "the push body is not JSON");
|
|
1907
|
+
}
|
|
1908
|
+
if (!isObject(body)) throw new PubSubPushError("not-json", "the push body is not a JSON object");
|
|
1909
|
+
const message = body.message;
|
|
1910
|
+
if (!isObject(message)) throw new PubSubPushError("missing-message", "the push body has no message");
|
|
1911
|
+
const data = message.data;
|
|
1912
|
+
if (typeof data !== "string" || data === "") {
|
|
1913
|
+
throw new PubSubPushError("missing-data", "the message carries no data");
|
|
1914
|
+
}
|
|
1915
|
+
let text;
|
|
1916
|
+
try {
|
|
1917
|
+
text = UTF8.decode(base64Bytes2(data));
|
|
1918
|
+
} catch {
|
|
1919
|
+
throw new PubSubPushError("bad-encoding", "the message data is not base64 of UTF-8 text");
|
|
1920
|
+
}
|
|
1921
|
+
let parsed;
|
|
1922
|
+
try {
|
|
1923
|
+
parsed = JSON.parse(text);
|
|
1924
|
+
} catch (error) {
|
|
1925
|
+
throw new PubSubPushError("data-not-json", `the message data is not JSON: ${error.message}`);
|
|
1926
|
+
}
|
|
1927
|
+
return {
|
|
1928
|
+
subscription: stringOrNull(body.subscription),
|
|
1929
|
+
messageId: stringOrNull(message.messageId) ?? stringOrNull(message.message_id),
|
|
1930
|
+
publishTime: stringOrNull(message.publishTime) ?? stringOrNull(message.publish_time),
|
|
1931
|
+
attributes: isObject(message.attributes) ? message.attributes : {},
|
|
1932
|
+
deliveryAttempt: Number.isInteger(body.deliveryAttempt) ? body.deliveryAttempt : null,
|
|
1933
|
+
data: parsed
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
function base64Bytes2(data) {
|
|
1937
|
+
const unpadded = data.replace(/={1,2}$/, "");
|
|
1938
|
+
if (!/^[A-Za-z0-9+/_-]*$/.test(unpadded) || unpadded.length % 4 === 1) throw new Error("not base64");
|
|
1939
|
+
const standard = unpadded.replace(/-/g, "+").replace(/_/g, "/");
|
|
1940
|
+
return Uint8Array.from(atob(standard + "=".repeat((4 - standard.length % 4) % 4)), (c) => c.charCodeAt(0));
|
|
1941
|
+
}
|
|
1942
|
+
function isObject(value) {
|
|
1943
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1944
|
+
}
|
|
1945
|
+
function stringOrNull(value) {
|
|
1946
|
+
return typeof value === "string" ? value : null;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1799
1949
|
// src/form-field-json.ts
|
|
1800
1950
|
var FORM_FIELD_JSON_REASONS = ["missing-field", "duplicate-field", "bad-encoding", "not-json"];
|
|
1801
1951
|
var FormFieldJsonError = class extends Error {
|
|
@@ -1808,7 +1958,7 @@ var FormFieldJsonError = class extends Error {
|
|
|
1808
1958
|
reason;
|
|
1809
1959
|
field;
|
|
1810
1960
|
};
|
|
1811
|
-
var
|
|
1961
|
+
var UTF82 = new TextDecoder("utf-8", { fatal: true });
|
|
1812
1962
|
var HEX = /^[0-9A-Fa-f]{2}$/;
|
|
1813
1963
|
function decodeFormFieldJson(raw, field) {
|
|
1814
1964
|
const text = typeof raw === "string" ? raw : latin12(raw);
|
|
@@ -1851,14 +2001,15 @@ function component(encoded, field) {
|
|
|
1851
2001
|
}
|
|
1852
2002
|
}
|
|
1853
2003
|
try {
|
|
1854
|
-
return
|
|
2004
|
+
return UTF82.decode(new Uint8Array(bytes));
|
|
1855
2005
|
} catch {
|
|
1856
2006
|
throw new FormFieldJsonError("bad-encoding", field, "the decoded bytes are not valid UTF-8");
|
|
1857
2007
|
}
|
|
1858
2008
|
}
|
|
1859
|
-
var DELIVERY_DECODE_KINDS = ["form-field-json"];
|
|
2009
|
+
var DELIVERY_DECODE_KINDS = ["form-field-json", "pubsub-push"];
|
|
1860
2010
|
function decodeDelivery(decode, raw) {
|
|
1861
2011
|
if (decode.kind === "form-field-json") return decodeFormFieldJson(raw, decode.field);
|
|
2012
|
+
if (decode.kind === "pubsub-push") return decodePubSubPush(raw);
|
|
1862
2013
|
throw new Error(`unknown decode kind ${JSON.stringify(decode.kind)}`);
|
|
1863
2014
|
}
|
|
1864
2015
|
function latin12(bytes) {
|
|
@@ -1867,6 +2018,202 @@ function latin12(bytes) {
|
|
|
1867
2018
|
return out;
|
|
1868
2019
|
}
|
|
1869
2020
|
|
|
2021
|
+
// src/webhook.ts
|
|
2022
|
+
var SIGNED_URI_RULES = ["hubspot-v3"];
|
|
2023
|
+
var HUBSPOT_V3_DECODE = {
|
|
2024
|
+
"%3A": ":",
|
|
2025
|
+
"%2F": "/",
|
|
2026
|
+
"%3F": "?",
|
|
2027
|
+
"%40": "@",
|
|
2028
|
+
"%21": "!",
|
|
2029
|
+
"%24": "$",
|
|
2030
|
+
"%27": "'",
|
|
2031
|
+
"%28": "(",
|
|
2032
|
+
"%29": ")",
|
|
2033
|
+
"%2A": "*",
|
|
2034
|
+
"%2C": ",",
|
|
2035
|
+
"%3B": ";"
|
|
2036
|
+
};
|
|
2037
|
+
function normaliseSignedUri(url, rule) {
|
|
2038
|
+
if (rule !== "hubspot-v3") throw new Error(`unknown signed-uri rule ${JSON.stringify(rule)}`);
|
|
2039
|
+
const withoutFragment = url.split("#", 1)[0] ?? "";
|
|
2040
|
+
const q = withoutFragment.indexOf("?");
|
|
2041
|
+
if (q === -1) return withoutFragment;
|
|
2042
|
+
const query = withoutFragment.slice(q + 1).replace(/%[0-9A-Fa-f]{2}/g, (e) => HUBSPOT_V3_DECODE[e.toUpperCase()] ?? e);
|
|
2043
|
+
return `${withoutFragment.slice(0, q)}?${query}`;
|
|
2044
|
+
}
|
|
2045
|
+
function secretKeyBytes(secret, options = {}) {
|
|
2046
|
+
let material = secret;
|
|
2047
|
+
if (options.secretPrefix !== void 0) {
|
|
2048
|
+
if (!material.startsWith(options.secretPrefix)) {
|
|
2049
|
+
return { reason: `signing secret does not start with ${JSON.stringify(options.secretPrefix)}` };
|
|
2050
|
+
}
|
|
2051
|
+
material = material.slice(options.secretPrefix.length);
|
|
2052
|
+
}
|
|
2053
|
+
if ((options.secretEncoding ?? "utf8") === "base64") {
|
|
2054
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(material) || material.length % 4 !== 0) {
|
|
2055
|
+
return { reason: "signing secret is not valid base64" };
|
|
2056
|
+
}
|
|
2057
|
+
try {
|
|
2058
|
+
return { bytes: Uint8Array.from(atob(material), (c) => c.charCodeAt(0)) };
|
|
2059
|
+
} catch {
|
|
2060
|
+
return { reason: "signing secret is not valid base64" };
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
return { bytes: new TextEncoder().encode(material) };
|
|
2064
|
+
}
|
|
2065
|
+
async function verifyHmac(options) {
|
|
2066
|
+
const { raw, secret, scheme, timestamp, id } = options;
|
|
2067
|
+
const signatures = (Array.isArray(options.signature) ? options.signature : [options.signature]).filter(
|
|
2068
|
+
(s) => typeof s === "string" && s !== ""
|
|
2069
|
+
);
|
|
2070
|
+
if (!secret) {
|
|
2071
|
+
return { ok: false, reason: "no signing secret configured for this trigger" };
|
|
2072
|
+
}
|
|
2073
|
+
if (signatures.length === 0) return { ok: false, reason: "delivery carried no signature header" };
|
|
2074
|
+
if (scheme.tolerance !== void 0) {
|
|
2075
|
+
if (!timestamp) return { ok: false, reason: "delivery carried no timestamp header" };
|
|
2076
|
+
const sent = Number(timestamp);
|
|
2077
|
+
if (!Number.isFinite(sent)) return { ok: false, reason: "timestamp header is not a number" };
|
|
2078
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
2079
|
+
const sentSeconds = scheme.timestampUnit === "ms" ? sent / 1e3 : sent;
|
|
2080
|
+
if (Math.abs(now - sentSeconds) > scheme.tolerance) {
|
|
2081
|
+
return { ok: false, reason: `delivery is outside the ${scheme.tolerance}s replay window` };
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
const key = secretKeyBytes(secret, scheme);
|
|
2085
|
+
if ("reason" in key) return { ok: false, reason: key.reason };
|
|
2086
|
+
let line;
|
|
2087
|
+
if (scheme.signsRequestLine) {
|
|
2088
|
+
const { method, url } = options.request ?? {};
|
|
2089
|
+
if (!method || !url) return { ok: false, reason: "delivery carried no request method or url" };
|
|
2090
|
+
line = { method, uri: scheme.uri ? normaliseSignedUri(url, scheme.uri) : url };
|
|
2091
|
+
}
|
|
2092
|
+
const expected = await hmac(
|
|
2093
|
+
secret,
|
|
2094
|
+
scheme.payload(raw, timestamp, id, line),
|
|
2095
|
+
scheme.algorithm,
|
|
2096
|
+
scheme.encoding ?? "hex",
|
|
2097
|
+
scheme
|
|
2098
|
+
);
|
|
2099
|
+
return signatures.some((candidate) => constantTimeEquals(expected, candidate)) ? { ok: true } : { ok: false, reason: "signature did not match" };
|
|
2100
|
+
}
|
|
2101
|
+
async function hmac(secret, payload, algorithm, encoding = "hex", options = {}) {
|
|
2102
|
+
const subtle = globalThis.crypto?.subtle;
|
|
2103
|
+
if (!subtle) {
|
|
2104
|
+
throw new Error(
|
|
2105
|
+
"WebCrypto is not available in this runtime, so webhook signatures cannot be verified. Refusing to accept deliveries unverified."
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
const material = secretKeyBytes(secret, options);
|
|
2109
|
+
if ("reason" in material) throw new Error(material.reason);
|
|
2110
|
+
const encoder2 = new TextEncoder();
|
|
2111
|
+
const key = await subtle.importKey(
|
|
2112
|
+
"raw",
|
|
2113
|
+
material.bytes,
|
|
2114
|
+
{ name: "HMAC", hash: algorithm },
|
|
2115
|
+
false,
|
|
2116
|
+
["sign"]
|
|
2117
|
+
);
|
|
2118
|
+
const bytes = new Uint8Array(await subtle.sign("HMAC", key, encoder2.encode(payload)));
|
|
2119
|
+
if (encoding === "base64") {
|
|
2120
|
+
let binary = "";
|
|
2121
|
+
bytes.forEach((byte) => {
|
|
2122
|
+
binary += String.fromCharCode(byte);
|
|
2123
|
+
});
|
|
2124
|
+
return btoa(binary);
|
|
2125
|
+
}
|
|
2126
|
+
return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2127
|
+
}
|
|
2128
|
+
function constantTimeEquals(a, b) {
|
|
2129
|
+
if (a.length !== b.length) return false;
|
|
2130
|
+
let diff = 0;
|
|
2131
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2132
|
+
return diff === 0;
|
|
2133
|
+
}
|
|
2134
|
+
function isSharedTokenScheme(scheme) {
|
|
2135
|
+
return "kind" in scheme && scheme.kind === "shared-token";
|
|
2136
|
+
}
|
|
2137
|
+
function verifySharedToken(options) {
|
|
2138
|
+
const { raw, headers, secret, scheme } = options;
|
|
2139
|
+
if (!secret) {
|
|
2140
|
+
return { ok: false, reason: "no shared token configured for this trigger" };
|
|
2141
|
+
}
|
|
2142
|
+
let tokens;
|
|
2143
|
+
if (scheme.in === "header") {
|
|
2144
|
+
tokens = [header2(headers, scheme.name)];
|
|
2145
|
+
} else {
|
|
2146
|
+
let parsed;
|
|
2147
|
+
try {
|
|
2148
|
+
parsed = JSON.parse(raw);
|
|
2149
|
+
} catch {
|
|
2150
|
+
return { ok: false, reason: "delivery body is not JSON" };
|
|
2151
|
+
}
|
|
2152
|
+
tokens = readTokens(parsed, scheme.path.split("."));
|
|
2153
|
+
}
|
|
2154
|
+
const present = tokens.filter((t) => t !== void 0 && t !== null && t !== "");
|
|
2155
|
+
if (present.length === 0) return { ok: false, reason: "delivery carried no token" };
|
|
2156
|
+
let matched = present.length === tokens.length;
|
|
2157
|
+
for (const token of present) {
|
|
2158
|
+
matched = typeof token === "string" && constantTimeEquals(secret, token) && matched;
|
|
2159
|
+
}
|
|
2160
|
+
return matched ? { ok: true } : { ok: false, reason: "token did not match" };
|
|
2161
|
+
}
|
|
2162
|
+
function readTokens(value, segments) {
|
|
2163
|
+
if (segments.length === 0) return [value];
|
|
2164
|
+
const [head, ...rest] = segments;
|
|
2165
|
+
const eachElement = head.endsWith("[]");
|
|
2166
|
+
const key = eachElement ? head.slice(0, -2) : head;
|
|
2167
|
+
if (value === null || typeof value !== "object") return [void 0];
|
|
2168
|
+
const next = value[key];
|
|
2169
|
+
if (!eachElement) return readTokens(next, rest);
|
|
2170
|
+
if (!Array.isArray(next)) return [void 0];
|
|
2171
|
+
return next.flatMap((element) => readTokens(element, rest));
|
|
2172
|
+
}
|
|
2173
|
+
function handshakeResponse(handshake, query) {
|
|
2174
|
+
if (!handshake) return void 0;
|
|
2175
|
+
const raw = query[handshake.param];
|
|
2176
|
+
const token = Array.isArray(raw) ? raw[0] : raw;
|
|
2177
|
+
if (!token) return void 0;
|
|
2178
|
+
return { status: 200, contentType: handshake.contentType ?? "text/plain", body: token };
|
|
2179
|
+
}
|
|
2180
|
+
function header2(headers, name) {
|
|
2181
|
+
const wanted = name.toLowerCase();
|
|
2182
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
2183
|
+
if (key.toLowerCase() !== wanted) continue;
|
|
2184
|
+
return Array.isArray(value) ? value[0] : value;
|
|
2185
|
+
}
|
|
2186
|
+
return void 0;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
// src/oidc-delivery.ts
|
|
2190
|
+
var OIDC_JWT_SCHEME_KIND = "oidc-jwt";
|
|
2191
|
+
function isOidcJwtScheme(scheme) {
|
|
2192
|
+
return typeof scheme === "object" && scheme !== null && scheme.kind === OIDC_JWT_SCHEME_KIND;
|
|
2193
|
+
}
|
|
2194
|
+
var fail2 = (reason) => ({ ok: false, reason });
|
|
2195
|
+
var BEARER = /^bearer +(\S+)$/i;
|
|
2196
|
+
async function verifyOidcDelivery(options) {
|
|
2197
|
+
const { headers, scheme, expected } = options;
|
|
2198
|
+
if (!expected || expected.keys.length === 0) return fail2("no signing keys supplied for this trigger");
|
|
2199
|
+
if (!expected.audience) return fail2("no audience configured for this trigger");
|
|
2200
|
+
if (!expected.principal) return fail2("no principal configured for this trigger");
|
|
2201
|
+
const token = BEARER.exec(header2(headers, scheme.header) ?? "")?.[1];
|
|
2202
|
+
if (!token) return fail2("delivery carried no bearer token");
|
|
2203
|
+
const verified = await verifyJwt(token, {
|
|
2204
|
+
keys: expected.keys,
|
|
2205
|
+
audience: expected.audience,
|
|
2206
|
+
issuer: scheme.issuers,
|
|
2207
|
+
...options.now === void 0 ? {} : { now: options.now }
|
|
2208
|
+
});
|
|
2209
|
+
if (!verified.ok) return fail2(verified.reason);
|
|
2210
|
+
if (verified.claims[scheme.principalClaim] !== expected.principal) return fail2("token principal is not accepted");
|
|
2211
|
+
if (scheme.verifiedClaim !== void 0 && verified.claims[scheme.verifiedClaim] !== true) {
|
|
2212
|
+
return fail2("token principal is not verified");
|
|
2213
|
+
}
|
|
2214
|
+
return { ok: true };
|
|
2215
|
+
}
|
|
2216
|
+
|
|
1870
2217
|
// src/probe.ts
|
|
1871
2218
|
async function probe(spec) {
|
|
1872
2219
|
let response;
|
|
@@ -2082,179 +2429,14 @@ var CREDENTIAL_SCOPES = ["provider", "account"];
|
|
|
2082
2429
|
var PROBLEM_SEVERITIES = ["block", "warn"];
|
|
2083
2430
|
var CANONICAL_METRICS = ["like", "share", "reply", "quote", "view"];
|
|
2084
2431
|
|
|
2085
|
-
// src/webhook.ts
|
|
2086
|
-
var SIGNED_URI_RULES = ["hubspot-v3"];
|
|
2087
|
-
var HUBSPOT_V3_DECODE = {
|
|
2088
|
-
"%3A": ":",
|
|
2089
|
-
"%2F": "/",
|
|
2090
|
-
"%3F": "?",
|
|
2091
|
-
"%40": "@",
|
|
2092
|
-
"%21": "!",
|
|
2093
|
-
"%24": "$",
|
|
2094
|
-
"%27": "'",
|
|
2095
|
-
"%28": "(",
|
|
2096
|
-
"%29": ")",
|
|
2097
|
-
"%2A": "*",
|
|
2098
|
-
"%2C": ",",
|
|
2099
|
-
"%3B": ";"
|
|
2100
|
-
};
|
|
2101
|
-
function normaliseSignedUri(url, rule) {
|
|
2102
|
-
if (rule !== "hubspot-v3") throw new Error(`unknown signed-uri rule ${JSON.stringify(rule)}`);
|
|
2103
|
-
const withoutFragment = url.split("#", 1)[0] ?? "";
|
|
2104
|
-
const q = withoutFragment.indexOf("?");
|
|
2105
|
-
if (q === -1) return withoutFragment;
|
|
2106
|
-
const query = withoutFragment.slice(q + 1).replace(/%[0-9A-Fa-f]{2}/g, (e) => HUBSPOT_V3_DECODE[e.toUpperCase()] ?? e);
|
|
2107
|
-
return `${withoutFragment.slice(0, q)}?${query}`;
|
|
2108
|
-
}
|
|
2109
|
-
function secretKeyBytes(secret, options = {}) {
|
|
2110
|
-
let material = secret;
|
|
2111
|
-
if (options.secretPrefix !== void 0) {
|
|
2112
|
-
if (!material.startsWith(options.secretPrefix)) {
|
|
2113
|
-
return { reason: `signing secret does not start with ${JSON.stringify(options.secretPrefix)}` };
|
|
2114
|
-
}
|
|
2115
|
-
material = material.slice(options.secretPrefix.length);
|
|
2116
|
-
}
|
|
2117
|
-
if ((options.secretEncoding ?? "utf8") === "base64") {
|
|
2118
|
-
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(material) || material.length % 4 !== 0) {
|
|
2119
|
-
return { reason: "signing secret is not valid base64" };
|
|
2120
|
-
}
|
|
2121
|
-
try {
|
|
2122
|
-
return { bytes: Uint8Array.from(atob(material), (c) => c.charCodeAt(0)) };
|
|
2123
|
-
} catch {
|
|
2124
|
-
return { reason: "signing secret is not valid base64" };
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
return { bytes: new TextEncoder().encode(material) };
|
|
2128
|
-
}
|
|
2129
|
-
async function verifyHmac(options) {
|
|
2130
|
-
const { raw, secret, scheme, timestamp, id } = options;
|
|
2131
|
-
const signatures = (Array.isArray(options.signature) ? options.signature : [options.signature]).filter(
|
|
2132
|
-
(s) => typeof s === "string" && s !== ""
|
|
2133
|
-
);
|
|
2134
|
-
if (!secret) {
|
|
2135
|
-
return { ok: false, reason: "no signing secret configured for this trigger" };
|
|
2136
|
-
}
|
|
2137
|
-
if (signatures.length === 0) return { ok: false, reason: "delivery carried no signature header" };
|
|
2138
|
-
if (scheme.tolerance !== void 0) {
|
|
2139
|
-
if (!timestamp) return { ok: false, reason: "delivery carried no timestamp header" };
|
|
2140
|
-
const sent = Number(timestamp);
|
|
2141
|
-
if (!Number.isFinite(sent)) return { ok: false, reason: "timestamp header is not a number" };
|
|
2142
|
-
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
2143
|
-
const sentSeconds = scheme.timestampUnit === "ms" ? sent / 1e3 : sent;
|
|
2144
|
-
if (Math.abs(now - sentSeconds) > scheme.tolerance) {
|
|
2145
|
-
return { ok: false, reason: `delivery is outside the ${scheme.tolerance}s replay window` };
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
const key = secretKeyBytes(secret, scheme);
|
|
2149
|
-
if ("reason" in key) return { ok: false, reason: key.reason };
|
|
2150
|
-
let line;
|
|
2151
|
-
if (scheme.signsRequestLine) {
|
|
2152
|
-
const { method, url } = options.request ?? {};
|
|
2153
|
-
if (!method || !url) return { ok: false, reason: "delivery carried no request method or url" };
|
|
2154
|
-
line = { method, uri: scheme.uri ? normaliseSignedUri(url, scheme.uri) : url };
|
|
2155
|
-
}
|
|
2156
|
-
const expected = await hmac(
|
|
2157
|
-
secret,
|
|
2158
|
-
scheme.payload(raw, timestamp, id, line),
|
|
2159
|
-
scheme.algorithm,
|
|
2160
|
-
scheme.encoding ?? "hex",
|
|
2161
|
-
scheme
|
|
2162
|
-
);
|
|
2163
|
-
return signatures.some((candidate) => constantTimeEquals(expected, candidate)) ? { ok: true } : { ok: false, reason: "signature did not match" };
|
|
2164
|
-
}
|
|
2165
|
-
async function hmac(secret, payload, algorithm, encoding = "hex", options = {}) {
|
|
2166
|
-
const subtle = globalThis.crypto?.subtle;
|
|
2167
|
-
if (!subtle) {
|
|
2168
|
-
throw new Error(
|
|
2169
|
-
"WebCrypto is not available in this runtime, so webhook signatures cannot be verified. Refusing to accept deliveries unverified."
|
|
2170
|
-
);
|
|
2171
|
-
}
|
|
2172
|
-
const material = secretKeyBytes(secret, options);
|
|
2173
|
-
if ("reason" in material) throw new Error(material.reason);
|
|
2174
|
-
const encoder2 = new TextEncoder();
|
|
2175
|
-
const key = await subtle.importKey(
|
|
2176
|
-
"raw",
|
|
2177
|
-
material.bytes,
|
|
2178
|
-
{ name: "HMAC", hash: algorithm },
|
|
2179
|
-
false,
|
|
2180
|
-
["sign"]
|
|
2181
|
-
);
|
|
2182
|
-
const bytes = new Uint8Array(await subtle.sign("HMAC", key, encoder2.encode(payload)));
|
|
2183
|
-
if (encoding === "base64") {
|
|
2184
|
-
let binary = "";
|
|
2185
|
-
bytes.forEach((byte) => {
|
|
2186
|
-
binary += String.fromCharCode(byte);
|
|
2187
|
-
});
|
|
2188
|
-
return btoa(binary);
|
|
2189
|
-
}
|
|
2190
|
-
return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2191
|
-
}
|
|
2192
|
-
function constantTimeEquals(a, b) {
|
|
2193
|
-
if (a.length !== b.length) return false;
|
|
2194
|
-
let diff = 0;
|
|
2195
|
-
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2196
|
-
return diff === 0;
|
|
2197
|
-
}
|
|
2198
|
-
function isSharedTokenScheme(scheme) {
|
|
2199
|
-
return "kind" in scheme && scheme.kind === "shared-token";
|
|
2200
|
-
}
|
|
2201
|
-
function verifySharedToken(options) {
|
|
2202
|
-
const { raw, headers, secret, scheme } = options;
|
|
2203
|
-
if (!secret) {
|
|
2204
|
-
return { ok: false, reason: "no shared token configured for this trigger" };
|
|
2205
|
-
}
|
|
2206
|
-
let tokens;
|
|
2207
|
-
if (scheme.in === "header") {
|
|
2208
|
-
tokens = [header2(headers, scheme.name)];
|
|
2209
|
-
} else {
|
|
2210
|
-
let parsed;
|
|
2211
|
-
try {
|
|
2212
|
-
parsed = JSON.parse(raw);
|
|
2213
|
-
} catch {
|
|
2214
|
-
return { ok: false, reason: "delivery body is not JSON" };
|
|
2215
|
-
}
|
|
2216
|
-
tokens = readTokens(parsed, scheme.path.split("."));
|
|
2217
|
-
}
|
|
2218
|
-
const present = tokens.filter((t) => t !== void 0 && t !== null && t !== "");
|
|
2219
|
-
if (present.length === 0) return { ok: false, reason: "delivery carried no token" };
|
|
2220
|
-
let matched = present.length === tokens.length;
|
|
2221
|
-
for (const token of present) {
|
|
2222
|
-
matched = typeof token === "string" && constantTimeEquals(secret, token) && matched;
|
|
2223
|
-
}
|
|
2224
|
-
return matched ? { ok: true } : { ok: false, reason: "token did not match" };
|
|
2225
|
-
}
|
|
2226
|
-
function readTokens(value, segments) {
|
|
2227
|
-
if (segments.length === 0) return [value];
|
|
2228
|
-
const [head, ...rest] = segments;
|
|
2229
|
-
const eachElement = head.endsWith("[]");
|
|
2230
|
-
const key = eachElement ? head.slice(0, -2) : head;
|
|
2231
|
-
if (value === null || typeof value !== "object") return [void 0];
|
|
2232
|
-
const next = value[key];
|
|
2233
|
-
if (!eachElement) return readTokens(next, rest);
|
|
2234
|
-
if (!Array.isArray(next)) return [void 0];
|
|
2235
|
-
return next.flatMap((element) => readTokens(element, rest));
|
|
2236
|
-
}
|
|
2237
|
-
function handshakeResponse(handshake, query) {
|
|
2238
|
-
if (!handshake) return void 0;
|
|
2239
|
-
const raw = query[handshake.param];
|
|
2240
|
-
const token = Array.isArray(raw) ? raw[0] : raw;
|
|
2241
|
-
if (!token) return void 0;
|
|
2242
|
-
return { status: 200, contentType: handshake.contentType ?? "text/plain", body: token };
|
|
2243
|
-
}
|
|
2244
|
-
function header2(headers, name) {
|
|
2245
|
-
const wanted = name.toLowerCase();
|
|
2246
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
2247
|
-
if (key.toLowerCase() !== wanted) continue;
|
|
2248
|
-
return Array.isArray(value) ? value[0] : value;
|
|
2249
|
-
}
|
|
2250
|
-
return void 0;
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
2432
|
// src/trigger.ts
|
|
2433
|
+
function isOidcJwtSpec(spec) {
|
|
2434
|
+
return isOidcJwtScheme(spec.scheme);
|
|
2435
|
+
}
|
|
2254
2436
|
function isSharedTokenSpec(spec) {
|
|
2255
2437
|
return isSharedTokenScheme(spec.scheme);
|
|
2256
2438
|
}
|
|
2257
|
-
async function verifyDelivery(trigger, delivery, secret, now) {
|
|
2439
|
+
async function verifyDelivery(trigger, delivery, secret, now, oidc) {
|
|
2258
2440
|
const spec = trigger.verification;
|
|
2259
2441
|
if (!spec) {
|
|
2260
2442
|
return {
|
|
@@ -2265,6 +2447,14 @@ async function verifyDelivery(trigger, delivery, secret, now) {
|
|
|
2265
2447
|
if (isSharedTokenSpec(spec)) {
|
|
2266
2448
|
return verifySharedToken({ raw: delivery.raw, headers: delivery.headers, secret, scheme: spec.scheme });
|
|
2267
2449
|
}
|
|
2450
|
+
if (isOidcJwtSpec(spec)) {
|
|
2451
|
+
return verifyOidcDelivery({
|
|
2452
|
+
headers: delivery.headers,
|
|
2453
|
+
scheme: spec.scheme,
|
|
2454
|
+
expected: oidc,
|
|
2455
|
+
...now === void 0 ? {} : { now }
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2268
2458
|
const kind = spec.scheme.kind ?? "hmac";
|
|
2269
2459
|
if (kind !== "hmac") {
|
|
2270
2460
|
return {
|
|
@@ -2331,9 +2521,13 @@ function hasDelivery(value) {
|
|
|
2331
2521
|
FAKE_EPOCH,
|
|
2332
2522
|
FORM_FIELD_JSON_REASONS,
|
|
2333
2523
|
FormFieldJsonError,
|
|
2524
|
+
JWT_ALGORITHMS,
|
|
2334
2525
|
MAX_IDEMPOTENCY_KEY_LENGTH,
|
|
2335
2526
|
NO_IDEMPOTENCY_KEY_WARNING,
|
|
2527
|
+
OIDC_JWT_SCHEME_KIND,
|
|
2336
2528
|
PROBLEM_SEVERITIES,
|
|
2529
|
+
PUBSUB_PUSH_REASONS,
|
|
2530
|
+
PubSubPushError,
|
|
2337
2531
|
RENDERER_VERSION,
|
|
2338
2532
|
REVIEW_STALE_DAYS,
|
|
2339
2533
|
SANDBOX_KINDS,
|
|
@@ -2354,6 +2548,7 @@ function hasDelivery(value) {
|
|
|
2354
2548
|
constantTimeEquals,
|
|
2355
2549
|
decodeDelivery,
|
|
2356
2550
|
decodeFormFieldJson,
|
|
2551
|
+
decodePubSubPush,
|
|
2357
2552
|
deliver,
|
|
2358
2553
|
fakeRequest,
|
|
2359
2554
|
fakeValues,
|
|
@@ -2365,6 +2560,8 @@ function hasDelivery(value) {
|
|
|
2365
2560
|
httpFailure,
|
|
2366
2561
|
idempotencyKeyFor,
|
|
2367
2562
|
isEmptyPayload,
|
|
2563
|
+
isOidcJwtScheme,
|
|
2564
|
+
isOidcJwtSpec,
|
|
2368
2565
|
isRemote,
|
|
2369
2566
|
isSharedTokenScheme,
|
|
2370
2567
|
isSharedTokenSpec,
|
|
@@ -2411,6 +2608,8 @@ function hasDelivery(value) {
|
|
|
2411
2608
|
utf8Length,
|
|
2412
2609
|
verifyDelivery,
|
|
2413
2610
|
verifyHmac,
|
|
2611
|
+
verifyJwt,
|
|
2612
|
+
verifyOidcDelivery,
|
|
2414
2613
|
verifySharedToken,
|
|
2415
2614
|
withResolvedLimit
|
|
2416
2615
|
});
|