@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/dist/index.js CHANGED
@@ -1232,6 +1232,88 @@ function fnv1a(value) {
1232
1232
  return hash.toString(16).padStart(8, "0");
1233
1233
  }
1234
1234
 
1235
+ // src/jwt.ts
1236
+ var JWT_ALGORITHMS = ["RS256"];
1237
+ var fail = (reason) => ({ ok: false, reason });
1238
+ async function verifyJwt(token, options) {
1239
+ const parts = typeof token === "string" ? token.split(".") : [];
1240
+ if (parts.length !== 3 || parts.some((p2, i) => p2 === "" && i < 2)) {
1241
+ return fail("token is not a JWS compact serialization");
1242
+ }
1243
+ const [h, p, s] = parts;
1244
+ const header3 = jsonSegment(h);
1245
+ if (!header3) return fail("token header is not base64url JSON");
1246
+ if (!JWT_ALGORITHMS.includes(header3.alg)) {
1247
+ return fail(`alg ${JSON.stringify(header3.alg)} is not accepted`);
1248
+ }
1249
+ const key = selectKey(options.keys, header3.kid);
1250
+ if ("reason" in key) return fail(key.reason);
1251
+ const signature = base64urlBytes(s);
1252
+ if (!signature) return fail("signature is not base64url");
1253
+ const subtle = globalThis.crypto?.subtle;
1254
+ if (!subtle) return fail("no WebCrypto in this runtime");
1255
+ let valid;
1256
+ try {
1257
+ const imported = await subtle.importKey(
1258
+ "jwk",
1259
+ { kty: "RSA", n: key.jwk.n, e: key.jwk.e, alg: "RS256", ext: true },
1260
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
1261
+ false,
1262
+ ["verify"]
1263
+ );
1264
+ valid = await subtle.verify("RSASSA-PKCS1-v1_5", imported, signature, new TextEncoder().encode(`${h}.${p}`));
1265
+ } catch {
1266
+ return fail("the key could not be used to verify");
1267
+ }
1268
+ if (!valid) return fail("signature did not match");
1269
+ const claims = jsonSegment(p);
1270
+ if (!claims) return fail("token payload is not base64url JSON");
1271
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
1272
+ const leeway = options.leeway ?? 60;
1273
+ if (typeof claims.exp !== "number") return fail("token has no exp");
1274
+ if (now > claims.exp + leeway) return fail("token has expired");
1275
+ if (typeof claims.nbf === "number" && now + leeway < claims.nbf) return fail("token is not valid yet");
1276
+ if (options.issuer !== void 0) {
1277
+ const issuers = typeof options.issuer === "string" ? [options.issuer] : options.issuer;
1278
+ if (typeof claims.iss !== "string" || !issuers.includes(claims.iss)) return fail("token issuer is not accepted");
1279
+ }
1280
+ if (options.audience !== null) {
1281
+ const wanted = typeof options.audience === "string" ? [options.audience] : options.audience;
1282
+ const offered = typeof claims.aud === "string" ? [claims.aud] : Array.isArray(claims.aud) ? claims.aud : [];
1283
+ if (!offered.some((a) => typeof a === "string" && wanted.includes(a))) return fail("token audience is not accepted");
1284
+ }
1285
+ return { ok: true, header: header3, claims };
1286
+ }
1287
+ function selectKey(keys, kid) {
1288
+ const rsa = keys.filter((k) => k.kty === "RSA" && typeof k.n === "string" && typeof k.e === "string");
1289
+ if (typeof kid === "string") {
1290
+ const match = rsa.find((k) => k.kid === kid);
1291
+ return match ? { jwk: match } : { reason: `no key for kid ${JSON.stringify(kid)}` };
1292
+ }
1293
+ const [only] = rsa;
1294
+ return rsa.length === 1 && only ? { jwk: only } : { reason: "token names no kid and the key is ambiguous" };
1295
+ }
1296
+ function jsonSegment(segment) {
1297
+ const bytes = base64urlBytes(segment);
1298
+ if (!bytes) return null;
1299
+ try {
1300
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
1301
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1302
+ } catch {
1303
+ return null;
1304
+ }
1305
+ }
1306
+ function base64urlBytes(segment) {
1307
+ if (!/^[A-Za-z0-9_-]*$/.test(segment) || segment.length % 4 === 1) return null;
1308
+ const standard = segment.replace(/-/g, "+").replace(/_/g, "/");
1309
+ const padded = standard + "=".repeat((4 - standard.length % 4) % 4);
1310
+ try {
1311
+ return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
1312
+ } catch {
1313
+ return null;
1314
+ }
1315
+ }
1316
+
1235
1317
  // src/lease.ts
1236
1318
  var EXPIRES_AT_UNITS = ["rfc3339", "epoch-ms"];
1237
1319
  var INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
@@ -1666,6 +1748,65 @@ function parseMime(raw) {
1666
1748
  };
1667
1749
  }
1668
1750
 
1751
+ // src/pubsub-push.ts
1752
+ var PUBSUB_PUSH_REASONS = ["not-json", "missing-message", "missing-data", "bad-encoding", "data-not-json"];
1753
+ var PubSubPushError = class extends Error {
1754
+ constructor(reason, message) {
1755
+ super(message);
1756
+ this.reason = reason;
1757
+ this.name = "PubSubPushError";
1758
+ }
1759
+ reason;
1760
+ };
1761
+ var UTF8 = new TextDecoder("utf-8", { fatal: true });
1762
+ function decodePubSubPush(raw) {
1763
+ let body;
1764
+ try {
1765
+ body = JSON.parse(typeof raw === "string" ? raw : UTF8.decode(raw));
1766
+ } catch {
1767
+ throw new PubSubPushError("not-json", "the push body is not JSON");
1768
+ }
1769
+ if (!isObject(body)) throw new PubSubPushError("not-json", "the push body is not a JSON object");
1770
+ const message = body.message;
1771
+ if (!isObject(message)) throw new PubSubPushError("missing-message", "the push body has no message");
1772
+ const data = message.data;
1773
+ if (typeof data !== "string" || data === "") {
1774
+ throw new PubSubPushError("missing-data", "the message carries no data");
1775
+ }
1776
+ let text;
1777
+ try {
1778
+ text = UTF8.decode(base64Bytes2(data));
1779
+ } catch {
1780
+ throw new PubSubPushError("bad-encoding", "the message data is not base64 of UTF-8 text");
1781
+ }
1782
+ let parsed;
1783
+ try {
1784
+ parsed = JSON.parse(text);
1785
+ } catch (error) {
1786
+ throw new PubSubPushError("data-not-json", `the message data is not JSON: ${error.message}`);
1787
+ }
1788
+ return {
1789
+ subscription: stringOrNull(body.subscription),
1790
+ messageId: stringOrNull(message.messageId) ?? stringOrNull(message.message_id),
1791
+ publishTime: stringOrNull(message.publishTime) ?? stringOrNull(message.publish_time),
1792
+ attributes: isObject(message.attributes) ? message.attributes : {},
1793
+ deliveryAttempt: Number.isInteger(body.deliveryAttempt) ? body.deliveryAttempt : null,
1794
+ data: parsed
1795
+ };
1796
+ }
1797
+ function base64Bytes2(data) {
1798
+ const unpadded = data.replace(/={1,2}$/, "");
1799
+ if (!/^[A-Za-z0-9+/_-]*$/.test(unpadded) || unpadded.length % 4 === 1) throw new Error("not base64");
1800
+ const standard = unpadded.replace(/-/g, "+").replace(/_/g, "/");
1801
+ return Uint8Array.from(atob(standard + "=".repeat((4 - standard.length % 4) % 4)), (c) => c.charCodeAt(0));
1802
+ }
1803
+ function isObject(value) {
1804
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1805
+ }
1806
+ function stringOrNull(value) {
1807
+ return typeof value === "string" ? value : null;
1808
+ }
1809
+
1669
1810
  // src/form-field-json.ts
1670
1811
  var FORM_FIELD_JSON_REASONS = ["missing-field", "duplicate-field", "bad-encoding", "not-json"];
1671
1812
  var FormFieldJsonError = class extends Error {
@@ -1678,7 +1819,7 @@ var FormFieldJsonError = class extends Error {
1678
1819
  reason;
1679
1820
  field;
1680
1821
  };
1681
- var UTF8 = new TextDecoder("utf-8", { fatal: true });
1822
+ var UTF82 = new TextDecoder("utf-8", { fatal: true });
1682
1823
  var HEX = /^[0-9A-Fa-f]{2}$/;
1683
1824
  function decodeFormFieldJson(raw, field) {
1684
1825
  const text = typeof raw === "string" ? raw : latin12(raw);
@@ -1721,14 +1862,15 @@ function component(encoded, field) {
1721
1862
  }
1722
1863
  }
1723
1864
  try {
1724
- return UTF8.decode(new Uint8Array(bytes));
1865
+ return UTF82.decode(new Uint8Array(bytes));
1725
1866
  } catch {
1726
1867
  throw new FormFieldJsonError("bad-encoding", field, "the decoded bytes are not valid UTF-8");
1727
1868
  }
1728
1869
  }
1729
- var DELIVERY_DECODE_KINDS = ["form-field-json"];
1870
+ var DELIVERY_DECODE_KINDS = ["form-field-json", "pubsub-push"];
1730
1871
  function decodeDelivery(decode, raw) {
1731
1872
  if (decode.kind === "form-field-json") return decodeFormFieldJson(raw, decode.field);
1873
+ if (decode.kind === "pubsub-push") return decodePubSubPush(raw);
1732
1874
  throw new Error(`unknown decode kind ${JSON.stringify(decode.kind)}`);
1733
1875
  }
1734
1876
  function latin12(bytes) {
@@ -1737,6 +1879,202 @@ function latin12(bytes) {
1737
1879
  return out;
1738
1880
  }
1739
1881
 
1882
+ // src/webhook.ts
1883
+ var SIGNED_URI_RULES = ["hubspot-v3"];
1884
+ var HUBSPOT_V3_DECODE = {
1885
+ "%3A": ":",
1886
+ "%2F": "/",
1887
+ "%3F": "?",
1888
+ "%40": "@",
1889
+ "%21": "!",
1890
+ "%24": "$",
1891
+ "%27": "'",
1892
+ "%28": "(",
1893
+ "%29": ")",
1894
+ "%2A": "*",
1895
+ "%2C": ",",
1896
+ "%3B": ";"
1897
+ };
1898
+ function normaliseSignedUri(url, rule) {
1899
+ if (rule !== "hubspot-v3") throw new Error(`unknown signed-uri rule ${JSON.stringify(rule)}`);
1900
+ const withoutFragment = url.split("#", 1)[0] ?? "";
1901
+ const q = withoutFragment.indexOf("?");
1902
+ if (q === -1) return withoutFragment;
1903
+ const query = withoutFragment.slice(q + 1).replace(/%[0-9A-Fa-f]{2}/g, (e) => HUBSPOT_V3_DECODE[e.toUpperCase()] ?? e);
1904
+ return `${withoutFragment.slice(0, q)}?${query}`;
1905
+ }
1906
+ function secretKeyBytes(secret, options = {}) {
1907
+ let material = secret;
1908
+ if (options.secretPrefix !== void 0) {
1909
+ if (!material.startsWith(options.secretPrefix)) {
1910
+ return { reason: `signing secret does not start with ${JSON.stringify(options.secretPrefix)}` };
1911
+ }
1912
+ material = material.slice(options.secretPrefix.length);
1913
+ }
1914
+ if ((options.secretEncoding ?? "utf8") === "base64") {
1915
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(material) || material.length % 4 !== 0) {
1916
+ return { reason: "signing secret is not valid base64" };
1917
+ }
1918
+ try {
1919
+ return { bytes: Uint8Array.from(atob(material), (c) => c.charCodeAt(0)) };
1920
+ } catch {
1921
+ return { reason: "signing secret is not valid base64" };
1922
+ }
1923
+ }
1924
+ return { bytes: new TextEncoder().encode(material) };
1925
+ }
1926
+ async function verifyHmac(options) {
1927
+ const { raw, secret, scheme, timestamp, id } = options;
1928
+ const signatures = (Array.isArray(options.signature) ? options.signature : [options.signature]).filter(
1929
+ (s) => typeof s === "string" && s !== ""
1930
+ );
1931
+ if (!secret) {
1932
+ return { ok: false, reason: "no signing secret configured for this trigger" };
1933
+ }
1934
+ if (signatures.length === 0) return { ok: false, reason: "delivery carried no signature header" };
1935
+ if (scheme.tolerance !== void 0) {
1936
+ if (!timestamp) return { ok: false, reason: "delivery carried no timestamp header" };
1937
+ const sent = Number(timestamp);
1938
+ if (!Number.isFinite(sent)) return { ok: false, reason: "timestamp header is not a number" };
1939
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
1940
+ const sentSeconds = scheme.timestampUnit === "ms" ? sent / 1e3 : sent;
1941
+ if (Math.abs(now - sentSeconds) > scheme.tolerance) {
1942
+ return { ok: false, reason: `delivery is outside the ${scheme.tolerance}s replay window` };
1943
+ }
1944
+ }
1945
+ const key = secretKeyBytes(secret, scheme);
1946
+ if ("reason" in key) return { ok: false, reason: key.reason };
1947
+ let line;
1948
+ if (scheme.signsRequestLine) {
1949
+ const { method, url } = options.request ?? {};
1950
+ if (!method || !url) return { ok: false, reason: "delivery carried no request method or url" };
1951
+ line = { method, uri: scheme.uri ? normaliseSignedUri(url, scheme.uri) : url };
1952
+ }
1953
+ const expected = await hmac(
1954
+ secret,
1955
+ scheme.payload(raw, timestamp, id, line),
1956
+ scheme.algorithm,
1957
+ scheme.encoding ?? "hex",
1958
+ scheme
1959
+ );
1960
+ return signatures.some((candidate) => constantTimeEquals(expected, candidate)) ? { ok: true } : { ok: false, reason: "signature did not match" };
1961
+ }
1962
+ async function hmac(secret, payload, algorithm, encoding = "hex", options = {}) {
1963
+ const subtle = globalThis.crypto?.subtle;
1964
+ if (!subtle) {
1965
+ throw new Error(
1966
+ "WebCrypto is not available in this runtime, so webhook signatures cannot be verified. Refusing to accept deliveries unverified."
1967
+ );
1968
+ }
1969
+ const material = secretKeyBytes(secret, options);
1970
+ if ("reason" in material) throw new Error(material.reason);
1971
+ const encoder2 = new TextEncoder();
1972
+ const key = await subtle.importKey(
1973
+ "raw",
1974
+ material.bytes,
1975
+ { name: "HMAC", hash: algorithm },
1976
+ false,
1977
+ ["sign"]
1978
+ );
1979
+ const bytes = new Uint8Array(await subtle.sign("HMAC", key, encoder2.encode(payload)));
1980
+ if (encoding === "base64") {
1981
+ let binary = "";
1982
+ bytes.forEach((byte) => {
1983
+ binary += String.fromCharCode(byte);
1984
+ });
1985
+ return btoa(binary);
1986
+ }
1987
+ return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1988
+ }
1989
+ function constantTimeEquals(a, b) {
1990
+ if (a.length !== b.length) return false;
1991
+ let diff = 0;
1992
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1993
+ return diff === 0;
1994
+ }
1995
+ function isSharedTokenScheme(scheme) {
1996
+ return "kind" in scheme && scheme.kind === "shared-token";
1997
+ }
1998
+ function verifySharedToken(options) {
1999
+ const { raw, headers, secret, scheme } = options;
2000
+ if (!secret) {
2001
+ return { ok: false, reason: "no shared token configured for this trigger" };
2002
+ }
2003
+ let tokens;
2004
+ if (scheme.in === "header") {
2005
+ tokens = [header2(headers, scheme.name)];
2006
+ } else {
2007
+ let parsed;
2008
+ try {
2009
+ parsed = JSON.parse(raw);
2010
+ } catch {
2011
+ return { ok: false, reason: "delivery body is not JSON" };
2012
+ }
2013
+ tokens = readTokens(parsed, scheme.path.split("."));
2014
+ }
2015
+ const present = tokens.filter((t) => t !== void 0 && t !== null && t !== "");
2016
+ if (present.length === 0) return { ok: false, reason: "delivery carried no token" };
2017
+ let matched = present.length === tokens.length;
2018
+ for (const token of present) {
2019
+ matched = typeof token === "string" && constantTimeEquals(secret, token) && matched;
2020
+ }
2021
+ return matched ? { ok: true } : { ok: false, reason: "token did not match" };
2022
+ }
2023
+ function readTokens(value, segments) {
2024
+ if (segments.length === 0) return [value];
2025
+ const [head, ...rest] = segments;
2026
+ const eachElement = head.endsWith("[]");
2027
+ const key = eachElement ? head.slice(0, -2) : head;
2028
+ if (value === null || typeof value !== "object") return [void 0];
2029
+ const next = value[key];
2030
+ if (!eachElement) return readTokens(next, rest);
2031
+ if (!Array.isArray(next)) return [void 0];
2032
+ return next.flatMap((element) => readTokens(element, rest));
2033
+ }
2034
+ function handshakeResponse(handshake, query) {
2035
+ if (!handshake) return void 0;
2036
+ const raw = query[handshake.param];
2037
+ const token = Array.isArray(raw) ? raw[0] : raw;
2038
+ if (!token) return void 0;
2039
+ return { status: 200, contentType: handshake.contentType ?? "text/plain", body: token };
2040
+ }
2041
+ function header2(headers, name) {
2042
+ const wanted = name.toLowerCase();
2043
+ for (const [key, value] of Object.entries(headers)) {
2044
+ if (key.toLowerCase() !== wanted) continue;
2045
+ return Array.isArray(value) ? value[0] : value;
2046
+ }
2047
+ return void 0;
2048
+ }
2049
+
2050
+ // src/oidc-delivery.ts
2051
+ var OIDC_JWT_SCHEME_KIND = "oidc-jwt";
2052
+ function isOidcJwtScheme(scheme) {
2053
+ return typeof scheme === "object" && scheme !== null && scheme.kind === OIDC_JWT_SCHEME_KIND;
2054
+ }
2055
+ var fail2 = (reason) => ({ ok: false, reason });
2056
+ var BEARER = /^bearer +(\S+)$/i;
2057
+ async function verifyOidcDelivery(options) {
2058
+ const { headers, scheme, expected } = options;
2059
+ if (!expected || expected.keys.length === 0) return fail2("no signing keys supplied for this trigger");
2060
+ if (!expected.audience) return fail2("no audience configured for this trigger");
2061
+ if (!expected.principal) return fail2("no principal configured for this trigger");
2062
+ const token = BEARER.exec(header2(headers, scheme.header) ?? "")?.[1];
2063
+ if (!token) return fail2("delivery carried no bearer token");
2064
+ const verified = await verifyJwt(token, {
2065
+ keys: expected.keys,
2066
+ audience: expected.audience,
2067
+ issuer: scheme.issuers,
2068
+ ...options.now === void 0 ? {} : { now: options.now }
2069
+ });
2070
+ if (!verified.ok) return fail2(verified.reason);
2071
+ if (verified.claims[scheme.principalClaim] !== expected.principal) return fail2("token principal is not accepted");
2072
+ if (scheme.verifiedClaim !== void 0 && verified.claims[scheme.verifiedClaim] !== true) {
2073
+ return fail2("token principal is not verified");
2074
+ }
2075
+ return { ok: true };
2076
+ }
2077
+
1740
2078
  // src/probe.ts
1741
2079
  async function probe(spec) {
1742
2080
  let response;
@@ -1952,179 +2290,14 @@ var CREDENTIAL_SCOPES = ["provider", "account"];
1952
2290
  var PROBLEM_SEVERITIES = ["block", "warn"];
1953
2291
  var CANONICAL_METRICS = ["like", "share", "reply", "quote", "view"];
1954
2292
 
1955
- // src/webhook.ts
1956
- var SIGNED_URI_RULES = ["hubspot-v3"];
1957
- var HUBSPOT_V3_DECODE = {
1958
- "%3A": ":",
1959
- "%2F": "/",
1960
- "%3F": "?",
1961
- "%40": "@",
1962
- "%21": "!",
1963
- "%24": "$",
1964
- "%27": "'",
1965
- "%28": "(",
1966
- "%29": ")",
1967
- "%2A": "*",
1968
- "%2C": ",",
1969
- "%3B": ";"
1970
- };
1971
- function normaliseSignedUri(url, rule) {
1972
- if (rule !== "hubspot-v3") throw new Error(`unknown signed-uri rule ${JSON.stringify(rule)}`);
1973
- const withoutFragment = url.split("#", 1)[0] ?? "";
1974
- const q = withoutFragment.indexOf("?");
1975
- if (q === -1) return withoutFragment;
1976
- const query = withoutFragment.slice(q + 1).replace(/%[0-9A-Fa-f]{2}/g, (e) => HUBSPOT_V3_DECODE[e.toUpperCase()] ?? e);
1977
- return `${withoutFragment.slice(0, q)}?${query}`;
1978
- }
1979
- function secretKeyBytes(secret, options = {}) {
1980
- let material = secret;
1981
- if (options.secretPrefix !== void 0) {
1982
- if (!material.startsWith(options.secretPrefix)) {
1983
- return { reason: `signing secret does not start with ${JSON.stringify(options.secretPrefix)}` };
1984
- }
1985
- material = material.slice(options.secretPrefix.length);
1986
- }
1987
- if ((options.secretEncoding ?? "utf8") === "base64") {
1988
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(material) || material.length % 4 !== 0) {
1989
- return { reason: "signing secret is not valid base64" };
1990
- }
1991
- try {
1992
- return { bytes: Uint8Array.from(atob(material), (c) => c.charCodeAt(0)) };
1993
- } catch {
1994
- return { reason: "signing secret is not valid base64" };
1995
- }
1996
- }
1997
- return { bytes: new TextEncoder().encode(material) };
1998
- }
1999
- async function verifyHmac(options) {
2000
- const { raw, secret, scheme, timestamp, id } = options;
2001
- const signatures = (Array.isArray(options.signature) ? options.signature : [options.signature]).filter(
2002
- (s) => typeof s === "string" && s !== ""
2003
- );
2004
- if (!secret) {
2005
- return { ok: false, reason: "no signing secret configured for this trigger" };
2006
- }
2007
- if (signatures.length === 0) return { ok: false, reason: "delivery carried no signature header" };
2008
- if (scheme.tolerance !== void 0) {
2009
- if (!timestamp) return { ok: false, reason: "delivery carried no timestamp header" };
2010
- const sent = Number(timestamp);
2011
- if (!Number.isFinite(sent)) return { ok: false, reason: "timestamp header is not a number" };
2012
- const now = options.now ?? Math.floor(Date.now() / 1e3);
2013
- const sentSeconds = scheme.timestampUnit === "ms" ? sent / 1e3 : sent;
2014
- if (Math.abs(now - sentSeconds) > scheme.tolerance) {
2015
- return { ok: false, reason: `delivery is outside the ${scheme.tolerance}s replay window` };
2016
- }
2017
- }
2018
- const key = secretKeyBytes(secret, scheme);
2019
- if ("reason" in key) return { ok: false, reason: key.reason };
2020
- let line;
2021
- if (scheme.signsRequestLine) {
2022
- const { method, url } = options.request ?? {};
2023
- if (!method || !url) return { ok: false, reason: "delivery carried no request method or url" };
2024
- line = { method, uri: scheme.uri ? normaliseSignedUri(url, scheme.uri) : url };
2025
- }
2026
- const expected = await hmac(
2027
- secret,
2028
- scheme.payload(raw, timestamp, id, line),
2029
- scheme.algorithm,
2030
- scheme.encoding ?? "hex",
2031
- scheme
2032
- );
2033
- return signatures.some((candidate) => constantTimeEquals(expected, candidate)) ? { ok: true } : { ok: false, reason: "signature did not match" };
2034
- }
2035
- async function hmac(secret, payload, algorithm, encoding = "hex", options = {}) {
2036
- const subtle = globalThis.crypto?.subtle;
2037
- if (!subtle) {
2038
- throw new Error(
2039
- "WebCrypto is not available in this runtime, so webhook signatures cannot be verified. Refusing to accept deliveries unverified."
2040
- );
2041
- }
2042
- const material = secretKeyBytes(secret, options);
2043
- if ("reason" in material) throw new Error(material.reason);
2044
- const encoder2 = new TextEncoder();
2045
- const key = await subtle.importKey(
2046
- "raw",
2047
- material.bytes,
2048
- { name: "HMAC", hash: algorithm },
2049
- false,
2050
- ["sign"]
2051
- );
2052
- const bytes = new Uint8Array(await subtle.sign("HMAC", key, encoder2.encode(payload)));
2053
- if (encoding === "base64") {
2054
- let binary = "";
2055
- bytes.forEach((byte) => {
2056
- binary += String.fromCharCode(byte);
2057
- });
2058
- return btoa(binary);
2059
- }
2060
- return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
2061
- }
2062
- function constantTimeEquals(a, b) {
2063
- if (a.length !== b.length) return false;
2064
- let diff = 0;
2065
- for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
2066
- return diff === 0;
2067
- }
2068
- function isSharedTokenScheme(scheme) {
2069
- return "kind" in scheme && scheme.kind === "shared-token";
2070
- }
2071
- function verifySharedToken(options) {
2072
- const { raw, headers, secret, scheme } = options;
2073
- if (!secret) {
2074
- return { ok: false, reason: "no shared token configured for this trigger" };
2075
- }
2076
- let tokens;
2077
- if (scheme.in === "header") {
2078
- tokens = [header2(headers, scheme.name)];
2079
- } else {
2080
- let parsed;
2081
- try {
2082
- parsed = JSON.parse(raw);
2083
- } catch {
2084
- return { ok: false, reason: "delivery body is not JSON" };
2085
- }
2086
- tokens = readTokens(parsed, scheme.path.split("."));
2087
- }
2088
- const present = tokens.filter((t) => t !== void 0 && t !== null && t !== "");
2089
- if (present.length === 0) return { ok: false, reason: "delivery carried no token" };
2090
- let matched = present.length === tokens.length;
2091
- for (const token of present) {
2092
- matched = typeof token === "string" && constantTimeEquals(secret, token) && matched;
2093
- }
2094
- return matched ? { ok: true } : { ok: false, reason: "token did not match" };
2095
- }
2096
- function readTokens(value, segments) {
2097
- if (segments.length === 0) return [value];
2098
- const [head, ...rest] = segments;
2099
- const eachElement = head.endsWith("[]");
2100
- const key = eachElement ? head.slice(0, -2) : head;
2101
- if (value === null || typeof value !== "object") return [void 0];
2102
- const next = value[key];
2103
- if (!eachElement) return readTokens(next, rest);
2104
- if (!Array.isArray(next)) return [void 0];
2105
- return next.flatMap((element) => readTokens(element, rest));
2106
- }
2107
- function handshakeResponse(handshake, query) {
2108
- if (!handshake) return void 0;
2109
- const raw = query[handshake.param];
2110
- const token = Array.isArray(raw) ? raw[0] : raw;
2111
- if (!token) return void 0;
2112
- return { status: 200, contentType: handshake.contentType ?? "text/plain", body: token };
2113
- }
2114
- function header2(headers, name) {
2115
- const wanted = name.toLowerCase();
2116
- for (const [key, value] of Object.entries(headers)) {
2117
- if (key.toLowerCase() !== wanted) continue;
2118
- return Array.isArray(value) ? value[0] : value;
2119
- }
2120
- return void 0;
2121
- }
2122
-
2123
2293
  // src/trigger.ts
2294
+ function isOidcJwtSpec(spec) {
2295
+ return isOidcJwtScheme(spec.scheme);
2296
+ }
2124
2297
  function isSharedTokenSpec(spec) {
2125
2298
  return isSharedTokenScheme(spec.scheme);
2126
2299
  }
2127
- async function verifyDelivery(trigger, delivery, secret, now) {
2300
+ async function verifyDelivery(trigger, delivery, secret, now, oidc) {
2128
2301
  const spec = trigger.verification;
2129
2302
  if (!spec) {
2130
2303
  return {
@@ -2135,6 +2308,14 @@ async function verifyDelivery(trigger, delivery, secret, now) {
2135
2308
  if (isSharedTokenSpec(spec)) {
2136
2309
  return verifySharedToken({ raw: delivery.raw, headers: delivery.headers, secret, scheme: spec.scheme });
2137
2310
  }
2311
+ if (isOidcJwtSpec(spec)) {
2312
+ return verifyOidcDelivery({
2313
+ headers: delivery.headers,
2314
+ scheme: spec.scheme,
2315
+ expected: oidc,
2316
+ ...now === void 0 ? {} : { now }
2317
+ });
2318
+ }
2138
2319
  const kind = spec.scheme.kind ?? "hmac";
2139
2320
  if (kind !== "hmac") {
2140
2321
  return {
@@ -2200,9 +2381,13 @@ export {
2200
2381
  FAKE_EPOCH,
2201
2382
  FORM_FIELD_JSON_REASONS,
2202
2383
  FormFieldJsonError,
2384
+ JWT_ALGORITHMS,
2203
2385
  MAX_IDEMPOTENCY_KEY_LENGTH,
2204
2386
  NO_IDEMPOTENCY_KEY_WARNING,
2387
+ OIDC_JWT_SCHEME_KIND,
2205
2388
  PROBLEM_SEVERITIES,
2389
+ PUBSUB_PUSH_REASONS,
2390
+ PubSubPushError,
2206
2391
  RENDERER_VERSION,
2207
2392
  REVIEW_STALE_DAYS,
2208
2393
  SANDBOX_KINDS,
@@ -2223,6 +2408,7 @@ export {
2223
2408
  constantTimeEquals,
2224
2409
  decodeDelivery,
2225
2410
  decodeFormFieldJson,
2411
+ decodePubSubPush,
2226
2412
  deliver,
2227
2413
  fakeRequest,
2228
2414
  fakeValues,
@@ -2234,6 +2420,8 @@ export {
2234
2420
  httpFailure,
2235
2421
  idempotencyKeyFor,
2236
2422
  isEmptyPayload,
2423
+ isOidcJwtScheme,
2424
+ isOidcJwtSpec,
2237
2425
  isRemote,
2238
2426
  isSharedTokenScheme,
2239
2427
  isSharedTokenSpec,
@@ -2280,6 +2468,8 @@ export {
2280
2468
  utf8Length,
2281
2469
  verifyDelivery,
2282
2470
  verifyHmac,
2471
+ verifyJwt,
2472
+ verifyOidcDelivery,
2283
2473
  verifySharedToken,
2284
2474
  withResolvedLimit
2285
2475
  };