@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/CHANGELOG.md +78 -0
- package/README.md +6 -1
- package/dist/index.cjs +464 -194
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +325 -97
- package/dist/index.d.ts +325 -97
- package/dist/index.js +452 -194
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
167
|
-
const waitedMs = previous === void 0 ? 0 : Math.max(0, previous + minIntervalMs -
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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)}`,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1428
|
+
at2
|
|
1282
1429
|
);
|
|
1283
1430
|
}
|
|
1284
|
-
expiresAt = new Date(instantMs(value, path,
|
|
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
|
-
|
|
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,
|
|
1300
|
-
instantMs(input.expiresAt, "expiresAt",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
1322
|
-
if (
|
|
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,
|
|
1643
|
-
for (let k = 0; k < needle.length; k++) if (haystack[
|
|
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
|
|
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
|
|
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
|
};
|