@particle-academy/fancy-connector-core 0.11.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.cjs CHANGED
@@ -43,12 +43,17 @@ __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,
56
+ SIGNED_URI_RULES: () => SIGNED_URI_RULES,
52
57
  SUPPORTED_CONNECTOR_API: () => SUPPORTED_CONNECTOR_API,
53
58
  assertConnectorApi: () => assertConnectorApi,
54
59
  byteRangeOf: () => byteRangeOf,
@@ -65,6 +70,7 @@ __export(src_exports, {
65
70
  constantTimeEquals: () => constantTimeEquals,
66
71
  decodeDelivery: () => decodeDelivery,
67
72
  decodeFormFieldJson: () => decodeFormFieldJson,
73
+ decodePubSubPush: () => decodePubSubPush,
68
74
  deliver: () => deliver,
69
75
  fakeRequest: () => fakeRequest,
70
76
  fakeValues: () => fakeValues,
@@ -76,6 +82,8 @@ __export(src_exports, {
76
82
  httpFailure: () => httpFailure,
77
83
  idempotencyKeyFor: () => idempotencyKeyFor,
78
84
  isEmptyPayload: () => isEmptyPayload,
85
+ isOidcJwtScheme: () => isOidcJwtScheme,
86
+ isOidcJwtSpec: () => isOidcJwtSpec,
79
87
  isRemote: () => isRemote,
80
88
  isSharedTokenScheme: () => isSharedTokenScheme,
81
89
  isSharedTokenSpec: () => isSharedTokenSpec,
@@ -88,6 +96,7 @@ __export(src_exports, {
88
96
  linkRanges: () => linkRanges,
89
97
  measure: () => measure,
90
98
  nextOccurrence: () => nextOccurrence,
99
+ normaliseSignedUri: () => normaliseSignedUri,
91
100
  parseInvite: () => parseInvite,
92
101
  parseMime: () => parseMime,
93
102
  payloadHash: () => payloadHash,
@@ -121,6 +130,8 @@ __export(src_exports, {
121
130
  utf8Length: () => utf8Length,
122
131
  verifyDelivery: () => verifyDelivery,
123
132
  verifyHmac: () => verifyHmac,
133
+ verifyJwt: () => verifyJwt,
134
+ verifyOidcDelivery: () => verifyOidcDelivery,
124
135
  verifySharedToken: () => verifySharedToken,
125
136
  withResolvedLimit: () => withResolvedLimit
126
137
  });
@@ -1360,6 +1371,88 @@ function fnv1a(value) {
1360
1371
  return hash.toString(16).padStart(8, "0");
1361
1372
  }
1362
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
+
1363
1456
  // src/lease.ts
1364
1457
  var EXPIRES_AT_UNITS = ["rfc3339", "epoch-ms"];
1365
1458
  var INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
@@ -1794,6 +1887,65 @@ function parseMime(raw) {
1794
1887
  };
1795
1888
  }
1796
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
+
1797
1949
  // src/form-field-json.ts
1798
1950
  var FORM_FIELD_JSON_REASONS = ["missing-field", "duplicate-field", "bad-encoding", "not-json"];
1799
1951
  var FormFieldJsonError = class extends Error {
@@ -1806,7 +1958,7 @@ var FormFieldJsonError = class extends Error {
1806
1958
  reason;
1807
1959
  field;
1808
1960
  };
1809
- var UTF8 = new TextDecoder("utf-8", { fatal: true });
1961
+ var UTF82 = new TextDecoder("utf-8", { fatal: true });
1810
1962
  var HEX = /^[0-9A-Fa-f]{2}$/;
1811
1963
  function decodeFormFieldJson(raw, field) {
1812
1964
  const text = typeof raw === "string" ? raw : latin12(raw);
@@ -1849,14 +2001,15 @@ function component(encoded, field) {
1849
2001
  }
1850
2002
  }
1851
2003
  try {
1852
- return UTF8.decode(new Uint8Array(bytes));
2004
+ return UTF82.decode(new Uint8Array(bytes));
1853
2005
  } catch {
1854
2006
  throw new FormFieldJsonError("bad-encoding", field, "the decoded bytes are not valid UTF-8");
1855
2007
  }
1856
2008
  }
1857
- var DELIVERY_DECODE_KINDS = ["form-field-json"];
2009
+ var DELIVERY_DECODE_KINDS = ["form-field-json", "pubsub-push"];
1858
2010
  function decodeDelivery(decode, raw) {
1859
2011
  if (decode.kind === "form-field-json") return decodeFormFieldJson(raw, decode.field);
2012
+ if (decode.kind === "pubsub-push") return decodePubSubPush(raw);
1860
2013
  throw new Error(`unknown decode kind ${JSON.stringify(decode.kind)}`);
1861
2014
  }
1862
2015
  function latin12(bytes) {
@@ -1865,6 +2018,202 @@ function latin12(bytes) {
1865
2018
  return out;
1866
2019
  }
1867
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
+
1868
2217
  // src/probe.ts
1869
2218
  async function probe(spec) {
1870
2219
  let response;
@@ -2080,143 +2429,14 @@ var CREDENTIAL_SCOPES = ["provider", "account"];
2080
2429
  var PROBLEM_SEVERITIES = ["block", "warn"];
2081
2430
  var CANONICAL_METRICS = ["like", "share", "reply", "quote", "view"];
2082
2431
 
2083
- // src/webhook.ts
2084
- function secretKeyBytes(secret, options = {}) {
2085
- let material = secret;
2086
- if (options.secretPrefix !== void 0) {
2087
- if (!material.startsWith(options.secretPrefix)) {
2088
- return { reason: `signing secret does not start with ${JSON.stringify(options.secretPrefix)}` };
2089
- }
2090
- material = material.slice(options.secretPrefix.length);
2091
- }
2092
- if ((options.secretEncoding ?? "utf8") === "base64") {
2093
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(material) || material.length % 4 !== 0) {
2094
- return { reason: "signing secret is not valid base64" };
2095
- }
2096
- try {
2097
- return { bytes: Uint8Array.from(atob(material), (c) => c.charCodeAt(0)) };
2098
- } catch {
2099
- return { reason: "signing secret is not valid base64" };
2100
- }
2101
- }
2102
- return { bytes: new TextEncoder().encode(material) };
2103
- }
2104
- async function verifyHmac(options) {
2105
- const { raw, secret, scheme, timestamp, id } = options;
2106
- const signatures = (Array.isArray(options.signature) ? options.signature : [options.signature]).filter(
2107
- (s) => typeof s === "string" && s !== ""
2108
- );
2109
- if (!secret) {
2110
- return { ok: false, reason: "no signing secret configured for this trigger" };
2111
- }
2112
- if (signatures.length === 0) return { ok: false, reason: "delivery carried no signature header" };
2113
- if (scheme.tolerance !== void 0) {
2114
- if (!timestamp) return { ok: false, reason: "delivery carried no timestamp header" };
2115
- const sent = Number(timestamp);
2116
- if (!Number.isFinite(sent)) return { ok: false, reason: "timestamp header is not a number" };
2117
- const now = options.now ?? Math.floor(Date.now() / 1e3);
2118
- if (Math.abs(now - sent) > scheme.tolerance) {
2119
- return { ok: false, reason: `delivery is outside the ${scheme.tolerance}s replay window` };
2120
- }
2121
- }
2122
- const key = secretKeyBytes(secret, scheme);
2123
- if ("reason" in key) return { ok: false, reason: key.reason };
2124
- const expected = await hmac(secret, scheme.payload(raw, timestamp, id), scheme.algorithm, scheme.encoding ?? "hex", scheme);
2125
- return signatures.some((candidate) => constantTimeEquals(expected, candidate)) ? { ok: true } : { ok: false, reason: "signature did not match" };
2126
- }
2127
- async function hmac(secret, payload, algorithm, encoding = "hex", options = {}) {
2128
- const subtle = globalThis.crypto?.subtle;
2129
- if (!subtle) {
2130
- throw new Error(
2131
- "WebCrypto is not available in this runtime, so webhook signatures cannot be verified. Refusing to accept deliveries unverified."
2132
- );
2133
- }
2134
- const material = secretKeyBytes(secret, options);
2135
- if ("reason" in material) throw new Error(material.reason);
2136
- const encoder2 = new TextEncoder();
2137
- const key = await subtle.importKey(
2138
- "raw",
2139
- material.bytes,
2140
- { name: "HMAC", hash: algorithm },
2141
- false,
2142
- ["sign"]
2143
- );
2144
- const bytes = new Uint8Array(await subtle.sign("HMAC", key, encoder2.encode(payload)));
2145
- if (encoding === "base64") {
2146
- let binary = "";
2147
- bytes.forEach((byte) => {
2148
- binary += String.fromCharCode(byte);
2149
- });
2150
- return btoa(binary);
2151
- }
2152
- return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
2153
- }
2154
- function constantTimeEquals(a, b) {
2155
- if (a.length !== b.length) return false;
2156
- let diff = 0;
2157
- for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
2158
- return diff === 0;
2159
- }
2160
- function isSharedTokenScheme(scheme) {
2161
- return "kind" in scheme && scheme.kind === "shared-token";
2162
- }
2163
- function verifySharedToken(options) {
2164
- const { raw, headers, secret, scheme } = options;
2165
- if (!secret) {
2166
- return { ok: false, reason: "no shared token configured for this trigger" };
2167
- }
2168
- let tokens;
2169
- if (scheme.in === "header") {
2170
- tokens = [header2(headers, scheme.name)];
2171
- } else {
2172
- let parsed;
2173
- try {
2174
- parsed = JSON.parse(raw);
2175
- } catch {
2176
- return { ok: false, reason: "delivery body is not JSON" };
2177
- }
2178
- tokens = readTokens(parsed, scheme.path.split("."));
2179
- }
2180
- const present = tokens.filter((t) => t !== void 0 && t !== null && t !== "");
2181
- if (present.length === 0) return { ok: false, reason: "delivery carried no token" };
2182
- let matched = present.length === tokens.length;
2183
- for (const token of present) {
2184
- matched = typeof token === "string" && constantTimeEquals(secret, token) && matched;
2185
- }
2186
- return matched ? { ok: true } : { ok: false, reason: "token did not match" };
2187
- }
2188
- function readTokens(value, segments) {
2189
- if (segments.length === 0) return [value];
2190
- const [head, ...rest] = segments;
2191
- const eachElement = head.endsWith("[]");
2192
- const key = eachElement ? head.slice(0, -2) : head;
2193
- if (value === null || typeof value !== "object") return [void 0];
2194
- const next = value[key];
2195
- if (!eachElement) return readTokens(next, rest);
2196
- if (!Array.isArray(next)) return [void 0];
2197
- return next.flatMap((element) => readTokens(element, rest));
2198
- }
2199
- function handshakeResponse(handshake, query) {
2200
- if (!handshake) return void 0;
2201
- const raw = query[handshake.param];
2202
- const token = Array.isArray(raw) ? raw[0] : raw;
2203
- if (!token) return void 0;
2204
- return { status: 200, contentType: handshake.contentType ?? "text/plain", body: token };
2205
- }
2206
- function header2(headers, name) {
2207
- const wanted = name.toLowerCase();
2208
- for (const [key, value] of Object.entries(headers)) {
2209
- if (key.toLowerCase() !== wanted) continue;
2210
- return Array.isArray(value) ? value[0] : value;
2211
- }
2212
- return void 0;
2213
- }
2214
-
2215
2432
  // src/trigger.ts
2433
+ function isOidcJwtSpec(spec) {
2434
+ return isOidcJwtScheme(spec.scheme);
2435
+ }
2216
2436
  function isSharedTokenSpec(spec) {
2217
2437
  return isSharedTokenScheme(spec.scheme);
2218
2438
  }
2219
- async function verifyDelivery(trigger, delivery, secret, now) {
2439
+ async function verifyDelivery(trigger, delivery, secret, now, oidc) {
2220
2440
  const spec = trigger.verification;
2221
2441
  if (!spec) {
2222
2442
  return {
@@ -2227,6 +2447,14 @@ async function verifyDelivery(trigger, delivery, secret, now) {
2227
2447
  if (isSharedTokenSpec(spec)) {
2228
2448
  return verifySharedToken({ raw: delivery.raw, headers: delivery.headers, secret, scheme: spec.scheme });
2229
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
+ }
2230
2458
  const kind = spec.scheme.kind ?? "hmac";
2231
2459
  if (kind !== "hmac") {
2232
2460
  return {
@@ -2246,6 +2474,7 @@ async function verifyDelivery(trigger, delivery, secret, now) {
2246
2474
  scheme: spec.scheme,
2247
2475
  timestamp,
2248
2476
  id,
2477
+ ...delivery.method === void 0 && delivery.url === void 0 ? {} : { request: { method: delivery.method ?? "", url: delivery.url ?? "" } },
2249
2478
  ...now === void 0 ? {} : { now }
2250
2479
  });
2251
2480
  }
@@ -2292,12 +2521,17 @@ function hasDelivery(value) {
2292
2521
  FAKE_EPOCH,
2293
2522
  FORM_FIELD_JSON_REASONS,
2294
2523
  FormFieldJsonError,
2524
+ JWT_ALGORITHMS,
2295
2525
  MAX_IDEMPOTENCY_KEY_LENGTH,
2296
2526
  NO_IDEMPOTENCY_KEY_WARNING,
2527
+ OIDC_JWT_SCHEME_KIND,
2297
2528
  PROBLEM_SEVERITIES,
2529
+ PUBSUB_PUSH_REASONS,
2530
+ PubSubPushError,
2298
2531
  RENDERER_VERSION,
2299
2532
  REVIEW_STALE_DAYS,
2300
2533
  SANDBOX_KINDS,
2534
+ SIGNED_URI_RULES,
2301
2535
  SUPPORTED_CONNECTOR_API,
2302
2536
  assertConnectorApi,
2303
2537
  byteRangeOf,
@@ -2314,6 +2548,7 @@ function hasDelivery(value) {
2314
2548
  constantTimeEquals,
2315
2549
  decodeDelivery,
2316
2550
  decodeFormFieldJson,
2551
+ decodePubSubPush,
2317
2552
  deliver,
2318
2553
  fakeRequest,
2319
2554
  fakeValues,
@@ -2325,6 +2560,8 @@ function hasDelivery(value) {
2325
2560
  httpFailure,
2326
2561
  idempotencyKeyFor,
2327
2562
  isEmptyPayload,
2563
+ isOidcJwtScheme,
2564
+ isOidcJwtSpec,
2328
2565
  isRemote,
2329
2566
  isSharedTokenScheme,
2330
2567
  isSharedTokenSpec,
@@ -2337,6 +2574,7 @@ function hasDelivery(value) {
2337
2574
  linkRanges,
2338
2575
  measure,
2339
2576
  nextOccurrence,
2577
+ normaliseSignedUri,
2340
2578
  parseInvite,
2341
2579
  parseMime,
2342
2580
  payloadHash,
@@ -2370,6 +2608,8 @@ function hasDelivery(value) {
2370
2608
  utf8Length,
2371
2609
  verifyDelivery,
2372
2610
  verifyHmac,
2611
+ verifyJwt,
2612
+ verifyOidcDelivery,
2373
2613
  verifySharedToken,
2374
2614
  withResolvedLimit
2375
2615
  });