@particle-academy/fancy-connector-core 0.12.0 → 0.14.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
@@ -163,10 +163,10 @@ var lastCallAt = /* @__PURE__ */ new Map();
163
163
  async function respectRate(channel, minIntervalMs, now = () => Date.now(), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
164
164
  if (minIntervalMs <= 0) return { waitedMs: 0 };
165
165
  const previous = lastCallAt.get(channel);
166
- const at = now();
167
- const waitedMs = previous === void 0 ? 0 : Math.max(0, previous + minIntervalMs - at);
166
+ const at2 = now();
167
+ const waitedMs = previous === void 0 ? 0 : Math.max(0, previous + minIntervalMs - at2);
168
168
  if (waitedMs > 0) await sleep(waitedMs);
169
- lastCallAt.set(channel, at + waitedMs);
169
+ lastCallAt.set(channel, at2 + waitedMs);
170
170
  return { waitedMs };
171
171
  }
172
172
  function resetRateState() {
@@ -664,6 +664,71 @@ function readString(config, key) {
664
664
  return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
665
665
  }
666
666
 
667
+ // src/cursor-read.ts
668
+ var CURSOR_READ_REASONS = ["cursor-missing", "cursor-expired", "cursor-overflow", "bad-page"];
669
+ var CursorReadError = class extends Error {
670
+ constructor(reason, message, resyncQuery) {
671
+ super(message);
672
+ this.reason = reason;
673
+ this.resyncQuery = resyncQuery;
674
+ this.name = "CursorReadError";
675
+ }
676
+ reason;
677
+ resyncQuery;
678
+ };
679
+ async function readWithCursor(spec, cursor, call, options = {}) {
680
+ const refuse = (reason, message) => new CursorReadError(reason, message, options.resyncQuery);
681
+ const used = asCursor(cursor);
682
+ if (used === null) throw refuse("cursor-missing", `no stored ${spec.param}: the host seeds one before the first read`);
683
+ const collected = [];
684
+ let last;
685
+ let token = null;
686
+ let pages = 0;
687
+ for (; ; ) {
688
+ if (spec.page && pages === spec.page.maxPages) {
689
+ throw refuse("cursor-overflow", `more than ${spec.page.maxPages} pages since ${spec.param} ${used}`);
690
+ }
691
+ const params = { [spec.param]: used };
692
+ if (spec.page && token !== null) params[spec.page.param] = token;
693
+ try {
694
+ last = await call(params);
695
+ } catch (error) {
696
+ const status = error?.status;
697
+ if (spec.expiredStatus !== void 0 && status === spec.expiredStatus) {
698
+ throw refuse("cursor-expired", `${spec.param} ${used} is out of the provider's range (${status})`);
699
+ }
700
+ throw error;
701
+ }
702
+ pages++;
703
+ if (!spec.page) break;
704
+ const items = at(last, spec.page.collect);
705
+ if (items !== void 0 && !Array.isArray(items)) {
706
+ throw refuse("bad-page", `${spec.page.collect} is not a list`);
707
+ }
708
+ if (items) collected.push(...items);
709
+ token = asCursor(at(last, spec.page.from));
710
+ if (token === null) break;
711
+ }
712
+ const data = spec.page ? withList(last, spec.page.collect, collected) : last;
713
+ return { data, cursor: { used, next: asCursor(at(last, spec.advanceFrom)) } };
714
+ }
715
+ function asCursor(value) {
716
+ if (typeof value === "string") return value === "" ? null : value;
717
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
718
+ return null;
719
+ }
720
+ function at(value, path) {
721
+ return path.split(".").reduce(
722
+ (v, key) => v !== null && typeof v === "object" && !Array.isArray(v) ? v[key] : void 0,
723
+ value
724
+ );
725
+ }
726
+ function withList(data, path, list) {
727
+ const [head, ...rest] = path.split(".");
728
+ const base = data !== null && typeof data === "object" && !Array.isArray(data) ? data : {};
729
+ return { ...base, [head]: rest.length ? withList(base[head], rest.join("."), list) : list };
730
+ }
731
+
667
732
  // src/drift.ts
668
733
  function resolveRef(document, node, depth = 0) {
669
734
  if (depth > 8 || node === null || typeof node !== "object") return node;
@@ -1232,36 +1297,118 @@ function fnv1a(value) {
1232
1297
  return hash.toString(16).padStart(8, "0");
1233
1298
  }
1234
1299
 
1300
+ // src/jwt.ts
1301
+ var JWT_ALGORITHMS = ["RS256"];
1302
+ var fail = (reason) => ({ ok: false, reason });
1303
+ async function verifyJwt(token, options) {
1304
+ const parts = typeof token === "string" ? token.split(".") : [];
1305
+ if (parts.length !== 3 || parts.some((p2, i) => p2 === "" && i < 2)) {
1306
+ return fail("token is not a JWS compact serialization");
1307
+ }
1308
+ const [h, p, s] = parts;
1309
+ const header3 = jsonSegment(h);
1310
+ if (!header3) return fail("token header is not base64url JSON");
1311
+ if (!JWT_ALGORITHMS.includes(header3.alg)) {
1312
+ return fail(`alg ${JSON.stringify(header3.alg)} is not accepted`);
1313
+ }
1314
+ const key = selectKey(options.keys, header3.kid);
1315
+ if ("reason" in key) return fail(key.reason);
1316
+ const signature = base64urlBytes(s);
1317
+ if (!signature) return fail("signature is not base64url");
1318
+ const subtle = globalThis.crypto?.subtle;
1319
+ if (!subtle) return fail("no WebCrypto in this runtime");
1320
+ let valid;
1321
+ try {
1322
+ const imported = await subtle.importKey(
1323
+ "jwk",
1324
+ { kty: "RSA", n: key.jwk.n, e: key.jwk.e, alg: "RS256", ext: true },
1325
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
1326
+ false,
1327
+ ["verify"]
1328
+ );
1329
+ valid = await subtle.verify("RSASSA-PKCS1-v1_5", imported, signature, new TextEncoder().encode(`${h}.${p}`));
1330
+ } catch {
1331
+ return fail("the key could not be used to verify");
1332
+ }
1333
+ if (!valid) return fail("signature did not match");
1334
+ const claims = jsonSegment(p);
1335
+ if (!claims) return fail("token payload is not base64url JSON");
1336
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
1337
+ const leeway = options.leeway ?? 60;
1338
+ if (typeof claims.exp !== "number") return fail("token has no exp");
1339
+ if (now > claims.exp + leeway) return fail("token has expired");
1340
+ if (typeof claims.nbf === "number" && now + leeway < claims.nbf) return fail("token is not valid yet");
1341
+ if (options.issuer !== void 0) {
1342
+ const issuers = typeof options.issuer === "string" ? [options.issuer] : options.issuer;
1343
+ if (typeof claims.iss !== "string" || !issuers.includes(claims.iss)) return fail("token issuer is not accepted");
1344
+ }
1345
+ if (options.audience !== null) {
1346
+ const wanted = typeof options.audience === "string" ? [options.audience] : options.audience;
1347
+ const offered = typeof claims.aud === "string" ? [claims.aud] : Array.isArray(claims.aud) ? claims.aud : [];
1348
+ if (!offered.some((a) => typeof a === "string" && wanted.includes(a))) return fail("token audience is not accepted");
1349
+ }
1350
+ return { ok: true, header: header3, claims };
1351
+ }
1352
+ function selectKey(keys, kid) {
1353
+ const rsa = keys.filter((k) => k.kty === "RSA" && typeof k.n === "string" && typeof k.e === "string");
1354
+ if (typeof kid === "string") {
1355
+ const match = rsa.find((k) => k.kid === kid);
1356
+ return match ? { jwk: match } : { reason: `no key for kid ${JSON.stringify(kid)}` };
1357
+ }
1358
+ const [only] = rsa;
1359
+ return rsa.length === 1 && only ? { jwk: only } : { reason: "token names no kid and the key is ambiguous" };
1360
+ }
1361
+ function jsonSegment(segment) {
1362
+ const bytes = base64urlBytes(segment);
1363
+ if (!bytes) return null;
1364
+ try {
1365
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
1366
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1367
+ } catch {
1368
+ return null;
1369
+ }
1370
+ }
1371
+ function base64urlBytes(segment) {
1372
+ if (!/^[A-Za-z0-9_-]*$/.test(segment) || segment.length % 4 === 1) return null;
1373
+ const standard = segment.replace(/-/g, "+").replace(/_/g, "/");
1374
+ const padded = standard + "=".repeat((4 - standard.length % 4) % 4);
1375
+ try {
1376
+ return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
1377
+ } catch {
1378
+ return null;
1379
+ }
1380
+ }
1381
+
1235
1382
  // src/lease.ts
1236
1383
  var EXPIRES_AT_UNITS = ["rfc3339", "epoch-ms"];
1237
1384
  var INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
1238
1385
  var DEFAULT_CONTEXT = { service: "", operation: "subscription" };
1239
- function instantMs(value, field, at) {
1386
+ function instantMs(value, field, at2) {
1240
1387
  if (typeof value !== "string" || !INSTANT.test(value)) {
1241
1388
  throw new ConnectorConfigError(
1242
1389
  `${field} must be an RFC 3339 instant such as "2026-09-22T00:00:00Z", got ${JSON.stringify(value)}. A provider's own shape \u2014 Google's epoch milliseconds, say \u2014 is converted by the connector, never guessed at here.`,
1243
- at
1390
+ at2
1244
1391
  );
1245
1392
  }
1246
1393
  const ms = Date.parse(value.replace(/(\.\d{3})\d+/, "$1"));
1247
1394
  if (Number.isNaN(ms)) {
1248
- throw new ConnectorConfigError(`${field} is not a real instant: ${JSON.stringify(value)}`, at);
1395
+ throw new ConnectorConfigError(`${field} is not a real instant: ${JSON.stringify(value)}`, at2);
1249
1396
  }
1250
1397
  return ms;
1251
1398
  }
1252
- function leaseFromResponse(declaration, response, at = DEFAULT_CONTEXT) {
1399
+ function leaseFromResponse(declaration, response, at2 = DEFAULT_CONTEXT) {
1253
1400
  const { expiresAtFrom: path, expiresAtUnit: unit } = declaration;
1254
1401
  if (!EXPIRES_AT_UNITS.includes(unit)) {
1255
1402
  throw new ConnectorConfigError(
1256
1403
  `expiresAtUnit must be one of ${EXPIRES_AT_UNITS.join(" | ")}, got ${JSON.stringify(unit)}.`,
1257
- at
1404
+ at2
1258
1405
  );
1259
1406
  }
1260
1407
  const value = readPath(response, path);
1261
1408
  if (value === void 0 || value === null) {
1262
1409
  throw new ConnectorConfigError(
1263
1410
  `${path} is not present in the response, so no lease can be built \u2014 a subscription with no known expiry is one nobody renews.`,
1264
- at
1411
+ at2
1265
1412
  );
1266
1413
  }
1267
1414
  let expiresAt;
@@ -1270,7 +1417,7 @@ function leaseFromResponse(declaration, response, at = DEFAULT_CONTEXT) {
1270
1417
  if (!Number.isSafeInteger(ms)) {
1271
1418
  throw new ConnectorConfigError(
1272
1419
  `${path} must be epoch milliseconds as digits under "epoch-ms", got ${JSON.stringify(value)}.`,
1273
- at
1420
+ at2
1274
1421
  );
1275
1422
  }
1276
1423
  expiresAt = new Date(ms).toISOString();
@@ -1278,14 +1425,14 @@ function leaseFromResponse(declaration, response, at = DEFAULT_CONTEXT) {
1278
1425
  if (typeof value !== "string" || !INSTANT.test(value)) {
1279
1426
  throw new ConnectorConfigError(
1280
1427
  `${path} must be an RFC 3339 instant under "rfc3339", got ${JSON.stringify(value)}.`,
1281
- at
1428
+ at2
1282
1429
  );
1283
1430
  }
1284
- expiresAt = new Date(instantMs(value, path, at)).toISOString();
1431
+ expiresAt = new Date(instantMs(value, path, at2)).toISOString();
1285
1432
  }
1286
1433
  return subscriptionLease(
1287
1434
  { expiresAt, renewBeforeSeconds: declaration.renewBeforeSeconds, renewOperation: declaration.renewOperation },
1288
- at
1435
+ at2
1289
1436
  );
1290
1437
  }
1291
1438
  function readPath(value, path) {
@@ -1296,18 +1443,18 @@ function readPath(value, path) {
1296
1443
  }
1297
1444
  return current;
1298
1445
  }
1299
- function subscriptionLease(input, at = DEFAULT_CONTEXT) {
1300
- instantMs(input.expiresAt, "expiresAt", at);
1446
+ function subscriptionLease(input, at2 = DEFAULT_CONTEXT) {
1447
+ instantMs(input.expiresAt, "expiresAt", at2);
1301
1448
  if (!Number.isInteger(input.renewBeforeSeconds) || input.renewBeforeSeconds <= 0) {
1302
1449
  throw new ConnectorConfigError(
1303
1450
  `renewBeforeSeconds must be a positive whole number of seconds, got ${JSON.stringify(input.renewBeforeSeconds)}. Zero would make the lease due only at the instant it expires, which is a lease nobody renews.`,
1304
- at
1451
+ at2
1305
1452
  );
1306
1453
  }
1307
1454
  if (typeof input.renewOperation !== "string" || input.renewOperation.trim() === "") {
1308
1455
  throw new ConnectorConfigError(
1309
1456
  "renewOperation must name the operation the host calls when the lease is due; a due lease with nothing to call is one nobody renews.",
1310
- at
1457
+ at2
1311
1458
  );
1312
1459
  }
1313
1460
  return { expiresAt: input.expiresAt, renewBeforeSeconds: input.renewBeforeSeconds, renewOperation: input.renewOperation };
@@ -1316,10 +1463,10 @@ function leaseRenewAt(lease) {
1316
1463
  return new Date(instantMs(lease.expiresAt, "expiresAt", DEFAULT_CONTEXT) - lease.renewBeforeSeconds * 1e3).toISOString();
1317
1464
  }
1318
1465
  function leaseState(lease, now) {
1319
- const at = now instanceof Date ? now.getTime() : instantMs(now, "now", DEFAULT_CONTEXT);
1466
+ const at2 = now instanceof Date ? now.getTime() : instantMs(now, "now", DEFAULT_CONTEXT);
1320
1467
  const expires = instantMs(lease.expiresAt, "expiresAt", DEFAULT_CONTEXT);
1321
- if (at >= expires) return "expired";
1322
- if (at >= expires - lease.renewBeforeSeconds * 1e3) return "due";
1468
+ if (at2 >= expires) return "expired";
1469
+ if (at2 >= expires - lease.renewBeforeSeconds * 1e3) return "due";
1323
1470
  return "active";
1324
1471
  }
1325
1472
  function leaseAction(lease, now) {
@@ -1639,8 +1786,8 @@ function splitMultipart(body, boundary) {
1639
1786
  if (partStart >= 0 && partStart <= body.length) children.push(body.subarray(partStart));
1640
1787
  return children;
1641
1788
  }
1642
- function startsWith(haystack, at, needle) {
1643
- for (let k = 0; k < needle.length; k++) if (haystack[at + k] !== needle[k]) return false;
1789
+ function startsWith(haystack, at2, needle) {
1790
+ for (let k = 0; k < needle.length; k++) if (haystack[at2 + k] !== needle[k]) return false;
1644
1791
  return true;
1645
1792
  }
1646
1793
  function parseMime(raw) {
@@ -1666,6 +1813,65 @@ function parseMime(raw) {
1666
1813
  };
1667
1814
  }
1668
1815
 
1816
+ // src/pubsub-push.ts
1817
+ var PUBSUB_PUSH_REASONS = ["not-json", "missing-message", "missing-data", "bad-encoding", "data-not-json"];
1818
+ var PubSubPushError = class extends Error {
1819
+ constructor(reason, message) {
1820
+ super(message);
1821
+ this.reason = reason;
1822
+ this.name = "PubSubPushError";
1823
+ }
1824
+ reason;
1825
+ };
1826
+ var UTF8 = new TextDecoder("utf-8", { fatal: true });
1827
+ function decodePubSubPush(raw) {
1828
+ let body;
1829
+ try {
1830
+ body = JSON.parse(typeof raw === "string" ? raw : UTF8.decode(raw));
1831
+ } catch {
1832
+ throw new PubSubPushError("not-json", "the push body is not JSON");
1833
+ }
1834
+ if (!isObject(body)) throw new PubSubPushError("not-json", "the push body is not a JSON object");
1835
+ const message = body.message;
1836
+ if (!isObject(message)) throw new PubSubPushError("missing-message", "the push body has no message");
1837
+ const data = message.data;
1838
+ if (typeof data !== "string" || data === "") {
1839
+ throw new PubSubPushError("missing-data", "the message carries no data");
1840
+ }
1841
+ let text;
1842
+ try {
1843
+ text = UTF8.decode(base64Bytes2(data));
1844
+ } catch {
1845
+ throw new PubSubPushError("bad-encoding", "the message data is not base64 of UTF-8 text");
1846
+ }
1847
+ let parsed;
1848
+ try {
1849
+ parsed = JSON.parse(text);
1850
+ } catch (error) {
1851
+ throw new PubSubPushError("data-not-json", `the message data is not JSON: ${error.message}`);
1852
+ }
1853
+ return {
1854
+ subscription: stringOrNull(body.subscription),
1855
+ messageId: stringOrNull(message.messageId) ?? stringOrNull(message.message_id),
1856
+ publishTime: stringOrNull(message.publishTime) ?? stringOrNull(message.publish_time),
1857
+ attributes: isObject(message.attributes) ? message.attributes : {},
1858
+ deliveryAttempt: Number.isInteger(body.deliveryAttempt) ? body.deliveryAttempt : null,
1859
+ data: parsed
1860
+ };
1861
+ }
1862
+ function base64Bytes2(data) {
1863
+ const unpadded = data.replace(/={1,2}$/, "");
1864
+ if (!/^[A-Za-z0-9+/_-]*$/.test(unpadded) || unpadded.length % 4 === 1) throw new Error("not base64");
1865
+ const standard = unpadded.replace(/-/g, "+").replace(/_/g, "/");
1866
+ return Uint8Array.from(atob(standard + "=".repeat((4 - standard.length % 4) % 4)), (c) => c.charCodeAt(0));
1867
+ }
1868
+ function isObject(value) {
1869
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1870
+ }
1871
+ function stringOrNull(value) {
1872
+ return typeof value === "string" ? value : null;
1873
+ }
1874
+
1669
1875
  // src/form-field-json.ts
1670
1876
  var FORM_FIELD_JSON_REASONS = ["missing-field", "duplicate-field", "bad-encoding", "not-json"];
1671
1877
  var FormFieldJsonError = class extends Error {
@@ -1678,7 +1884,7 @@ var FormFieldJsonError = class extends Error {
1678
1884
  reason;
1679
1885
  field;
1680
1886
  };
1681
- var UTF8 = new TextDecoder("utf-8", { fatal: true });
1887
+ var UTF82 = new TextDecoder("utf-8", { fatal: true });
1682
1888
  var HEX = /^[0-9A-Fa-f]{2}$/;
1683
1889
  function decodeFormFieldJson(raw, field) {
1684
1890
  const text = typeof raw === "string" ? raw : latin12(raw);
@@ -1721,14 +1927,15 @@ function component(encoded, field) {
1721
1927
  }
1722
1928
  }
1723
1929
  try {
1724
- return UTF8.decode(new Uint8Array(bytes));
1930
+ return UTF82.decode(new Uint8Array(bytes));
1725
1931
  } catch {
1726
1932
  throw new FormFieldJsonError("bad-encoding", field, "the decoded bytes are not valid UTF-8");
1727
1933
  }
1728
1934
  }
1729
- var DELIVERY_DECODE_KINDS = ["form-field-json"];
1935
+ var DELIVERY_DECODE_KINDS = ["form-field-json", "pubsub-push"];
1730
1936
  function decodeDelivery(decode, raw) {
1731
1937
  if (decode.kind === "form-field-json") return decodeFormFieldJson(raw, decode.field);
1938
+ if (decode.kind === "pubsub-push") return decodePubSubPush(raw);
1732
1939
  throw new Error(`unknown decode kind ${JSON.stringify(decode.kind)}`);
1733
1940
  }
1734
1941
  function latin12(bytes) {
@@ -1737,6 +1944,202 @@ function latin12(bytes) {
1737
1944
  return out;
1738
1945
  }
1739
1946
 
1947
+ // src/webhook.ts
1948
+ var SIGNED_URI_RULES = ["hubspot-v3"];
1949
+ var HUBSPOT_V3_DECODE = {
1950
+ "%3A": ":",
1951
+ "%2F": "/",
1952
+ "%3F": "?",
1953
+ "%40": "@",
1954
+ "%21": "!",
1955
+ "%24": "$",
1956
+ "%27": "'",
1957
+ "%28": "(",
1958
+ "%29": ")",
1959
+ "%2A": "*",
1960
+ "%2C": ",",
1961
+ "%3B": ";"
1962
+ };
1963
+ function normaliseSignedUri(url, rule) {
1964
+ if (rule !== "hubspot-v3") throw new Error(`unknown signed-uri rule ${JSON.stringify(rule)}`);
1965
+ const withoutFragment = url.split("#", 1)[0] ?? "";
1966
+ const q = withoutFragment.indexOf("?");
1967
+ if (q === -1) return withoutFragment;
1968
+ const query = withoutFragment.slice(q + 1).replace(/%[0-9A-Fa-f]{2}/g, (e) => HUBSPOT_V3_DECODE[e.toUpperCase()] ?? e);
1969
+ return `${withoutFragment.slice(0, q)}?${query}`;
1970
+ }
1971
+ function secretKeyBytes(secret, options = {}) {
1972
+ let material = secret;
1973
+ if (options.secretPrefix !== void 0) {
1974
+ if (!material.startsWith(options.secretPrefix)) {
1975
+ return { reason: `signing secret does not start with ${JSON.stringify(options.secretPrefix)}` };
1976
+ }
1977
+ material = material.slice(options.secretPrefix.length);
1978
+ }
1979
+ if ((options.secretEncoding ?? "utf8") === "base64") {
1980
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(material) || material.length % 4 !== 0) {
1981
+ return { reason: "signing secret is not valid base64" };
1982
+ }
1983
+ try {
1984
+ return { bytes: Uint8Array.from(atob(material), (c) => c.charCodeAt(0)) };
1985
+ } catch {
1986
+ return { reason: "signing secret is not valid base64" };
1987
+ }
1988
+ }
1989
+ return { bytes: new TextEncoder().encode(material) };
1990
+ }
1991
+ async function verifyHmac(options) {
1992
+ const { raw, secret, scheme, timestamp, id } = options;
1993
+ const signatures = (Array.isArray(options.signature) ? options.signature : [options.signature]).filter(
1994
+ (s) => typeof s === "string" && s !== ""
1995
+ );
1996
+ if (!secret) {
1997
+ return { ok: false, reason: "no signing secret configured for this trigger" };
1998
+ }
1999
+ if (signatures.length === 0) return { ok: false, reason: "delivery carried no signature header" };
2000
+ if (scheme.tolerance !== void 0) {
2001
+ if (!timestamp) return { ok: false, reason: "delivery carried no timestamp header" };
2002
+ const sent = Number(timestamp);
2003
+ if (!Number.isFinite(sent)) return { ok: false, reason: "timestamp header is not a number" };
2004
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
2005
+ const sentSeconds = scheme.timestampUnit === "ms" ? sent / 1e3 : sent;
2006
+ if (Math.abs(now - sentSeconds) > scheme.tolerance) {
2007
+ return { ok: false, reason: `delivery is outside the ${scheme.tolerance}s replay window` };
2008
+ }
2009
+ }
2010
+ const key = secretKeyBytes(secret, scheme);
2011
+ if ("reason" in key) return { ok: false, reason: key.reason };
2012
+ let line;
2013
+ if (scheme.signsRequestLine) {
2014
+ const { method, url } = options.request ?? {};
2015
+ if (!method || !url) return { ok: false, reason: "delivery carried no request method or url" };
2016
+ line = { method, uri: scheme.uri ? normaliseSignedUri(url, scheme.uri) : url };
2017
+ }
2018
+ const expected = await hmac(
2019
+ secret,
2020
+ scheme.payload(raw, timestamp, id, line),
2021
+ scheme.algorithm,
2022
+ scheme.encoding ?? "hex",
2023
+ scheme
2024
+ );
2025
+ return signatures.some((candidate) => constantTimeEquals(expected, candidate)) ? { ok: true } : { ok: false, reason: "signature did not match" };
2026
+ }
2027
+ async function hmac(secret, payload, algorithm, encoding = "hex", options = {}) {
2028
+ const subtle = globalThis.crypto?.subtle;
2029
+ if (!subtle) {
2030
+ throw new Error(
2031
+ "WebCrypto is not available in this runtime, so webhook signatures cannot be verified. Refusing to accept deliveries unverified."
2032
+ );
2033
+ }
2034
+ const material = secretKeyBytes(secret, options);
2035
+ if ("reason" in material) throw new Error(material.reason);
2036
+ const encoder2 = new TextEncoder();
2037
+ const key = await subtle.importKey(
2038
+ "raw",
2039
+ material.bytes,
2040
+ { name: "HMAC", hash: algorithm },
2041
+ false,
2042
+ ["sign"]
2043
+ );
2044
+ const bytes = new Uint8Array(await subtle.sign("HMAC", key, encoder2.encode(payload)));
2045
+ if (encoding === "base64") {
2046
+ let binary = "";
2047
+ bytes.forEach((byte) => {
2048
+ binary += String.fromCharCode(byte);
2049
+ });
2050
+ return btoa(binary);
2051
+ }
2052
+ return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
2053
+ }
2054
+ function constantTimeEquals(a, b) {
2055
+ if (a.length !== b.length) return false;
2056
+ let diff = 0;
2057
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
2058
+ return diff === 0;
2059
+ }
2060
+ function isSharedTokenScheme(scheme) {
2061
+ return "kind" in scheme && scheme.kind === "shared-token";
2062
+ }
2063
+ function verifySharedToken(options) {
2064
+ const { raw, headers, secret, scheme } = options;
2065
+ if (!secret) {
2066
+ return { ok: false, reason: "no shared token configured for this trigger" };
2067
+ }
2068
+ let tokens;
2069
+ if (scheme.in === "header") {
2070
+ tokens = [header2(headers, scheme.name)];
2071
+ } else {
2072
+ let parsed;
2073
+ try {
2074
+ parsed = JSON.parse(raw);
2075
+ } catch {
2076
+ return { ok: false, reason: "delivery body is not JSON" };
2077
+ }
2078
+ tokens = readTokens(parsed, scheme.path.split("."));
2079
+ }
2080
+ const present = tokens.filter((t) => t !== void 0 && t !== null && t !== "");
2081
+ if (present.length === 0) return { ok: false, reason: "delivery carried no token" };
2082
+ let matched = present.length === tokens.length;
2083
+ for (const token of present) {
2084
+ matched = typeof token === "string" && constantTimeEquals(secret, token) && matched;
2085
+ }
2086
+ return matched ? { ok: true } : { ok: false, reason: "token did not match" };
2087
+ }
2088
+ function readTokens(value, segments) {
2089
+ if (segments.length === 0) return [value];
2090
+ const [head, ...rest] = segments;
2091
+ const eachElement = head.endsWith("[]");
2092
+ const key = eachElement ? head.slice(0, -2) : head;
2093
+ if (value === null || typeof value !== "object") return [void 0];
2094
+ const next = value[key];
2095
+ if (!eachElement) return readTokens(next, rest);
2096
+ if (!Array.isArray(next)) return [void 0];
2097
+ return next.flatMap((element) => readTokens(element, rest));
2098
+ }
2099
+ function handshakeResponse(handshake, query) {
2100
+ if (!handshake) return void 0;
2101
+ const raw = query[handshake.param];
2102
+ const token = Array.isArray(raw) ? raw[0] : raw;
2103
+ if (!token) return void 0;
2104
+ return { status: 200, contentType: handshake.contentType ?? "text/plain", body: token };
2105
+ }
2106
+ function header2(headers, name) {
2107
+ const wanted = name.toLowerCase();
2108
+ for (const [key, value] of Object.entries(headers)) {
2109
+ if (key.toLowerCase() !== wanted) continue;
2110
+ return Array.isArray(value) ? value[0] : value;
2111
+ }
2112
+ return void 0;
2113
+ }
2114
+
2115
+ // src/oidc-delivery.ts
2116
+ var OIDC_JWT_SCHEME_KIND = "oidc-jwt";
2117
+ function isOidcJwtScheme(scheme) {
2118
+ return typeof scheme === "object" && scheme !== null && scheme.kind === OIDC_JWT_SCHEME_KIND;
2119
+ }
2120
+ var fail2 = (reason) => ({ ok: false, reason });
2121
+ var BEARER = /^bearer +(\S+)$/i;
2122
+ async function verifyOidcDelivery(options) {
2123
+ const { headers, scheme, expected } = options;
2124
+ if (!expected || expected.keys.length === 0) return fail2("no signing keys supplied for this trigger");
2125
+ if (!expected.audience) return fail2("no audience configured for this trigger");
2126
+ if (!expected.principal) return fail2("no principal configured for this trigger");
2127
+ const token = BEARER.exec(header2(headers, scheme.header) ?? "")?.[1];
2128
+ if (!token) return fail2("delivery carried no bearer token");
2129
+ const verified = await verifyJwt(token, {
2130
+ keys: expected.keys,
2131
+ audience: expected.audience,
2132
+ issuer: scheme.issuers,
2133
+ ...options.now === void 0 ? {} : { now: options.now }
2134
+ });
2135
+ if (!verified.ok) return fail2(verified.reason);
2136
+ if (verified.claims[scheme.principalClaim] !== expected.principal) return fail2("token principal is not accepted");
2137
+ if (scheme.verifiedClaim !== void 0 && verified.claims[scheme.verifiedClaim] !== true) {
2138
+ return fail2("token principal is not verified");
2139
+ }
2140
+ return { ok: true };
2141
+ }
2142
+
1740
2143
  // src/probe.ts
1741
2144
  async function probe(spec) {
1742
2145
  let response;
@@ -1952,179 +2355,14 @@ var CREDENTIAL_SCOPES = ["provider", "account"];
1952
2355
  var PROBLEM_SEVERITIES = ["block", "warn"];
1953
2356
  var CANONICAL_METRICS = ["like", "share", "reply", "quote", "view"];
1954
2357
 
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
2358
  // src/trigger.ts
2359
+ function isOidcJwtSpec(spec) {
2360
+ return isOidcJwtScheme(spec.scheme);
2361
+ }
2124
2362
  function isSharedTokenSpec(spec) {
2125
2363
  return isSharedTokenScheme(spec.scheme);
2126
2364
  }
2127
- async function verifyDelivery(trigger, delivery, secret, now) {
2365
+ async function verifyDelivery(trigger, delivery, secret, now, oidc) {
2128
2366
  const spec = trigger.verification;
2129
2367
  if (!spec) {
2130
2368
  return {
@@ -2135,6 +2373,14 @@ async function verifyDelivery(trigger, delivery, secret, now) {
2135
2373
  if (isSharedTokenSpec(spec)) {
2136
2374
  return verifySharedToken({ raw: delivery.raw, headers: delivery.headers, secret, scheme: spec.scheme });
2137
2375
  }
2376
+ if (isOidcJwtSpec(spec)) {
2377
+ return verifyOidcDelivery({
2378
+ headers: delivery.headers,
2379
+ scheme: spec.scheme,
2380
+ expected: oidc,
2381
+ ...now === void 0 ? {} : { now }
2382
+ });
2383
+ }
2138
2384
  const kind = spec.scheme.kind ?? "hmac";
2139
2385
  if (kind !== "hmac") {
2140
2386
  return {
@@ -2182,6 +2428,7 @@ export {
2182
2428
  CONNECTOR_API_VERSION,
2183
2429
  CONNECTOR_MODES,
2184
2430
  CREDENTIAL_SCOPES,
2431
+ CURSOR_READ_REASONS,
2185
2432
  ConnectorAmbiguous,
2186
2433
  ConnectorApiMismatch,
2187
2434
  ConnectorAuthError,
@@ -2193,6 +2440,7 @@ export {
2193
2440
  ConnectorRequestError,
2194
2441
  ConnectorTransient,
2195
2442
  ConnectorUnreachable,
2443
+ CursorReadError,
2196
2444
  DEFAULT_IDEMPOTENCY_WINDOW_SECONDS,
2197
2445
  DEFAULT_RETRY,
2198
2446
  DELIVERY_DECODE_KINDS,
@@ -2200,9 +2448,13 @@ export {
2200
2448
  FAKE_EPOCH,
2201
2449
  FORM_FIELD_JSON_REASONS,
2202
2450
  FormFieldJsonError,
2451
+ JWT_ALGORITHMS,
2203
2452
  MAX_IDEMPOTENCY_KEY_LENGTH,
2204
2453
  NO_IDEMPOTENCY_KEY_WARNING,
2454
+ OIDC_JWT_SCHEME_KIND,
2205
2455
  PROBLEM_SEVERITIES,
2456
+ PUBSUB_PUSH_REASONS,
2457
+ PubSubPushError,
2206
2458
  RENDERER_VERSION,
2207
2459
  REVIEW_STALE_DAYS,
2208
2460
  SANDBOX_KINDS,
@@ -2223,6 +2475,7 @@ export {
2223
2475
  constantTimeEquals,
2224
2476
  decodeDelivery,
2225
2477
  decodeFormFieldJson,
2478
+ decodePubSubPush,
2226
2479
  deliver,
2227
2480
  fakeRequest,
2228
2481
  fakeValues,
@@ -2234,6 +2487,8 @@ export {
2234
2487
  httpFailure,
2235
2488
  idempotencyKeyFor,
2236
2489
  isEmptyPayload,
2490
+ isOidcJwtScheme,
2491
+ isOidcJwtSpec,
2237
2492
  isRemote,
2238
2493
  isSharedTokenScheme,
2239
2494
  isSharedTokenSpec,
@@ -2254,6 +2509,7 @@ export {
2254
2509
  probe,
2255
2510
  providerProblems,
2256
2511
  providerSandboxIsSelectable,
2512
+ readWithCursor,
2257
2513
  registerConnectionHost,
2258
2514
  registerTransport,
2259
2515
  render,
@@ -2280,6 +2536,8 @@ export {
2280
2536
  utf8Length,
2281
2537
  verifyDelivery,
2282
2538
  verifyHmac,
2539
+ verifyJwt,
2540
+ verifyOidcDelivery,
2283
2541
  verifySharedToken,
2284
2542
  withResolvedLimit
2285
2543
  };