@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.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(src_exports, {
|
|
|
25
25
|
CONNECTOR_API_VERSION: () => CONNECTOR_API_VERSION,
|
|
26
26
|
CONNECTOR_MODES: () => CONNECTOR_MODES,
|
|
27
27
|
CREDENTIAL_SCOPES: () => CREDENTIAL_SCOPES,
|
|
28
|
+
CURSOR_READ_REASONS: () => CURSOR_READ_REASONS,
|
|
28
29
|
ConnectorAmbiguous: () => ConnectorAmbiguous,
|
|
29
30
|
ConnectorApiMismatch: () => ConnectorApiMismatch,
|
|
30
31
|
ConnectorAuthError: () => ConnectorAuthError,
|
|
@@ -36,6 +37,7 @@ __export(src_exports, {
|
|
|
36
37
|
ConnectorRequestError: () => ConnectorRequestError,
|
|
37
38
|
ConnectorTransient: () => ConnectorTransient,
|
|
38
39
|
ConnectorUnreachable: () => ConnectorUnreachable,
|
|
40
|
+
CursorReadError: () => CursorReadError,
|
|
39
41
|
DEFAULT_IDEMPOTENCY_WINDOW_SECONDS: () => DEFAULT_IDEMPOTENCY_WINDOW_SECONDS,
|
|
40
42
|
DEFAULT_RETRY: () => DEFAULT_RETRY,
|
|
41
43
|
DELIVERY_DECODE_KINDS: () => DELIVERY_DECODE_KINDS,
|
|
@@ -43,9 +45,13 @@ __export(src_exports, {
|
|
|
43
45
|
FAKE_EPOCH: () => FAKE_EPOCH,
|
|
44
46
|
FORM_FIELD_JSON_REASONS: () => FORM_FIELD_JSON_REASONS,
|
|
45
47
|
FormFieldJsonError: () => FormFieldJsonError,
|
|
48
|
+
JWT_ALGORITHMS: () => JWT_ALGORITHMS,
|
|
46
49
|
MAX_IDEMPOTENCY_KEY_LENGTH: () => MAX_IDEMPOTENCY_KEY_LENGTH,
|
|
47
50
|
NO_IDEMPOTENCY_KEY_WARNING: () => NO_IDEMPOTENCY_KEY_WARNING,
|
|
51
|
+
OIDC_JWT_SCHEME_KIND: () => OIDC_JWT_SCHEME_KIND,
|
|
48
52
|
PROBLEM_SEVERITIES: () => PROBLEM_SEVERITIES,
|
|
53
|
+
PUBSUB_PUSH_REASONS: () => PUBSUB_PUSH_REASONS,
|
|
54
|
+
PubSubPushError: () => PubSubPushError,
|
|
49
55
|
RENDERER_VERSION: () => RENDERER_VERSION,
|
|
50
56
|
REVIEW_STALE_DAYS: () => REVIEW_STALE_DAYS,
|
|
51
57
|
SANDBOX_KINDS: () => SANDBOX_KINDS,
|
|
@@ -66,6 +72,7 @@ __export(src_exports, {
|
|
|
66
72
|
constantTimeEquals: () => constantTimeEquals,
|
|
67
73
|
decodeDelivery: () => decodeDelivery,
|
|
68
74
|
decodeFormFieldJson: () => decodeFormFieldJson,
|
|
75
|
+
decodePubSubPush: () => decodePubSubPush,
|
|
69
76
|
deliver: () => deliver,
|
|
70
77
|
fakeRequest: () => fakeRequest,
|
|
71
78
|
fakeValues: () => fakeValues,
|
|
@@ -77,6 +84,8 @@ __export(src_exports, {
|
|
|
77
84
|
httpFailure: () => httpFailure,
|
|
78
85
|
idempotencyKeyFor: () => idempotencyKeyFor,
|
|
79
86
|
isEmptyPayload: () => isEmptyPayload,
|
|
87
|
+
isOidcJwtScheme: () => isOidcJwtScheme,
|
|
88
|
+
isOidcJwtSpec: () => isOidcJwtSpec,
|
|
80
89
|
isRemote: () => isRemote,
|
|
81
90
|
isSharedTokenScheme: () => isSharedTokenScheme,
|
|
82
91
|
isSharedTokenSpec: () => isSharedTokenSpec,
|
|
@@ -97,6 +106,7 @@ __export(src_exports, {
|
|
|
97
106
|
probe: () => probe,
|
|
98
107
|
providerProblems: () => providerProblems,
|
|
99
108
|
providerSandboxIsSelectable: () => providerSandboxIsSelectable,
|
|
109
|
+
readWithCursor: () => readWithCursor,
|
|
100
110
|
registerConnectionHost: () => registerConnectionHost,
|
|
101
111
|
registerTransport: () => registerTransport,
|
|
102
112
|
render: () => render,
|
|
@@ -123,6 +133,8 @@ __export(src_exports, {
|
|
|
123
133
|
utf8Length: () => utf8Length,
|
|
124
134
|
verifyDelivery: () => verifyDelivery,
|
|
125
135
|
verifyHmac: () => verifyHmac,
|
|
136
|
+
verifyJwt: () => verifyJwt,
|
|
137
|
+
verifyOidcDelivery: () => verifyOidcDelivery,
|
|
126
138
|
verifySharedToken: () => verifySharedToken,
|
|
127
139
|
withResolvedLimit: () => withResolvedLimit
|
|
128
140
|
});
|
|
@@ -293,10 +305,10 @@ var lastCallAt = /* @__PURE__ */ new Map();
|
|
|
293
305
|
async function respectRate(channel, minIntervalMs, now = () => Date.now(), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
294
306
|
if (minIntervalMs <= 0) return { waitedMs: 0 };
|
|
295
307
|
const previous = lastCallAt.get(channel);
|
|
296
|
-
const
|
|
297
|
-
const waitedMs = previous === void 0 ? 0 : Math.max(0, previous + minIntervalMs -
|
|
308
|
+
const at2 = now();
|
|
309
|
+
const waitedMs = previous === void 0 ? 0 : Math.max(0, previous + minIntervalMs - at2);
|
|
298
310
|
if (waitedMs > 0) await sleep(waitedMs);
|
|
299
|
-
lastCallAt.set(channel,
|
|
311
|
+
lastCallAt.set(channel, at2 + waitedMs);
|
|
300
312
|
return { waitedMs };
|
|
301
313
|
}
|
|
302
314
|
function resetRateState() {
|
|
@@ -794,6 +806,71 @@ function readString(config, key) {
|
|
|
794
806
|
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
795
807
|
}
|
|
796
808
|
|
|
809
|
+
// src/cursor-read.ts
|
|
810
|
+
var CURSOR_READ_REASONS = ["cursor-missing", "cursor-expired", "cursor-overflow", "bad-page"];
|
|
811
|
+
var CursorReadError = class extends Error {
|
|
812
|
+
constructor(reason, message, resyncQuery) {
|
|
813
|
+
super(message);
|
|
814
|
+
this.reason = reason;
|
|
815
|
+
this.resyncQuery = resyncQuery;
|
|
816
|
+
this.name = "CursorReadError";
|
|
817
|
+
}
|
|
818
|
+
reason;
|
|
819
|
+
resyncQuery;
|
|
820
|
+
};
|
|
821
|
+
async function readWithCursor(spec, cursor, call, options = {}) {
|
|
822
|
+
const refuse = (reason, message) => new CursorReadError(reason, message, options.resyncQuery);
|
|
823
|
+
const used = asCursor(cursor);
|
|
824
|
+
if (used === null) throw refuse("cursor-missing", `no stored ${spec.param}: the host seeds one before the first read`);
|
|
825
|
+
const collected = [];
|
|
826
|
+
let last;
|
|
827
|
+
let token = null;
|
|
828
|
+
let pages = 0;
|
|
829
|
+
for (; ; ) {
|
|
830
|
+
if (spec.page && pages === spec.page.maxPages) {
|
|
831
|
+
throw refuse("cursor-overflow", `more than ${spec.page.maxPages} pages since ${spec.param} ${used}`);
|
|
832
|
+
}
|
|
833
|
+
const params = { [spec.param]: used };
|
|
834
|
+
if (spec.page && token !== null) params[spec.page.param] = token;
|
|
835
|
+
try {
|
|
836
|
+
last = await call(params);
|
|
837
|
+
} catch (error) {
|
|
838
|
+
const status = error?.status;
|
|
839
|
+
if (spec.expiredStatus !== void 0 && status === spec.expiredStatus) {
|
|
840
|
+
throw refuse("cursor-expired", `${spec.param} ${used} is out of the provider's range (${status})`);
|
|
841
|
+
}
|
|
842
|
+
throw error;
|
|
843
|
+
}
|
|
844
|
+
pages++;
|
|
845
|
+
if (!spec.page) break;
|
|
846
|
+
const items = at(last, spec.page.collect);
|
|
847
|
+
if (items !== void 0 && !Array.isArray(items)) {
|
|
848
|
+
throw refuse("bad-page", `${spec.page.collect} is not a list`);
|
|
849
|
+
}
|
|
850
|
+
if (items) collected.push(...items);
|
|
851
|
+
token = asCursor(at(last, spec.page.from));
|
|
852
|
+
if (token === null) break;
|
|
853
|
+
}
|
|
854
|
+
const data = spec.page ? withList(last, spec.page.collect, collected) : last;
|
|
855
|
+
return { data, cursor: { used, next: asCursor(at(last, spec.advanceFrom)) } };
|
|
856
|
+
}
|
|
857
|
+
function asCursor(value) {
|
|
858
|
+
if (typeof value === "string") return value === "" ? null : value;
|
|
859
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
function at(value, path) {
|
|
863
|
+
return path.split(".").reduce(
|
|
864
|
+
(v, key) => v !== null && typeof v === "object" && !Array.isArray(v) ? v[key] : void 0,
|
|
865
|
+
value
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
function withList(data, path, list) {
|
|
869
|
+
const [head, ...rest] = path.split(".");
|
|
870
|
+
const base = data !== null && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
871
|
+
return { ...base, [head]: rest.length ? withList(base[head], rest.join("."), list) : list };
|
|
872
|
+
}
|
|
873
|
+
|
|
797
874
|
// src/drift.ts
|
|
798
875
|
function resolveRef(document, node, depth = 0) {
|
|
799
876
|
if (depth > 8 || node === null || typeof node !== "object") return node;
|
|
@@ -1362,36 +1439,118 @@ function fnv1a(value) {
|
|
|
1362
1439
|
return hash.toString(16).padStart(8, "0");
|
|
1363
1440
|
}
|
|
1364
1441
|
|
|
1442
|
+
// src/jwt.ts
|
|
1443
|
+
var JWT_ALGORITHMS = ["RS256"];
|
|
1444
|
+
var fail = (reason) => ({ ok: false, reason });
|
|
1445
|
+
async function verifyJwt(token, options) {
|
|
1446
|
+
const parts = typeof token === "string" ? token.split(".") : [];
|
|
1447
|
+
if (parts.length !== 3 || parts.some((p2, i) => p2 === "" && i < 2)) {
|
|
1448
|
+
return fail("token is not a JWS compact serialization");
|
|
1449
|
+
}
|
|
1450
|
+
const [h, p, s] = parts;
|
|
1451
|
+
const header3 = jsonSegment(h);
|
|
1452
|
+
if (!header3) return fail("token header is not base64url JSON");
|
|
1453
|
+
if (!JWT_ALGORITHMS.includes(header3.alg)) {
|
|
1454
|
+
return fail(`alg ${JSON.stringify(header3.alg)} is not accepted`);
|
|
1455
|
+
}
|
|
1456
|
+
const key = selectKey(options.keys, header3.kid);
|
|
1457
|
+
if ("reason" in key) return fail(key.reason);
|
|
1458
|
+
const signature = base64urlBytes(s);
|
|
1459
|
+
if (!signature) return fail("signature is not base64url");
|
|
1460
|
+
const subtle = globalThis.crypto?.subtle;
|
|
1461
|
+
if (!subtle) return fail("no WebCrypto in this runtime");
|
|
1462
|
+
let valid;
|
|
1463
|
+
try {
|
|
1464
|
+
const imported = await subtle.importKey(
|
|
1465
|
+
"jwk",
|
|
1466
|
+
{ kty: "RSA", n: key.jwk.n, e: key.jwk.e, alg: "RS256", ext: true },
|
|
1467
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
1468
|
+
false,
|
|
1469
|
+
["verify"]
|
|
1470
|
+
);
|
|
1471
|
+
valid = await subtle.verify("RSASSA-PKCS1-v1_5", imported, signature, new TextEncoder().encode(`${h}.${p}`));
|
|
1472
|
+
} catch {
|
|
1473
|
+
return fail("the key could not be used to verify");
|
|
1474
|
+
}
|
|
1475
|
+
if (!valid) return fail("signature did not match");
|
|
1476
|
+
const claims = jsonSegment(p);
|
|
1477
|
+
if (!claims) return fail("token payload is not base64url JSON");
|
|
1478
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
1479
|
+
const leeway = options.leeway ?? 60;
|
|
1480
|
+
if (typeof claims.exp !== "number") return fail("token has no exp");
|
|
1481
|
+
if (now > claims.exp + leeway) return fail("token has expired");
|
|
1482
|
+
if (typeof claims.nbf === "number" && now + leeway < claims.nbf) return fail("token is not valid yet");
|
|
1483
|
+
if (options.issuer !== void 0) {
|
|
1484
|
+
const issuers = typeof options.issuer === "string" ? [options.issuer] : options.issuer;
|
|
1485
|
+
if (typeof claims.iss !== "string" || !issuers.includes(claims.iss)) return fail("token issuer is not accepted");
|
|
1486
|
+
}
|
|
1487
|
+
if (options.audience !== null) {
|
|
1488
|
+
const wanted = typeof options.audience === "string" ? [options.audience] : options.audience;
|
|
1489
|
+
const offered = typeof claims.aud === "string" ? [claims.aud] : Array.isArray(claims.aud) ? claims.aud : [];
|
|
1490
|
+
if (!offered.some((a) => typeof a === "string" && wanted.includes(a))) return fail("token audience is not accepted");
|
|
1491
|
+
}
|
|
1492
|
+
return { ok: true, header: header3, claims };
|
|
1493
|
+
}
|
|
1494
|
+
function selectKey(keys, kid) {
|
|
1495
|
+
const rsa = keys.filter((k) => k.kty === "RSA" && typeof k.n === "string" && typeof k.e === "string");
|
|
1496
|
+
if (typeof kid === "string") {
|
|
1497
|
+
const match = rsa.find((k) => k.kid === kid);
|
|
1498
|
+
return match ? { jwk: match } : { reason: `no key for kid ${JSON.stringify(kid)}` };
|
|
1499
|
+
}
|
|
1500
|
+
const [only] = rsa;
|
|
1501
|
+
return rsa.length === 1 && only ? { jwk: only } : { reason: "token names no kid and the key is ambiguous" };
|
|
1502
|
+
}
|
|
1503
|
+
function jsonSegment(segment) {
|
|
1504
|
+
const bytes = base64urlBytes(segment);
|
|
1505
|
+
if (!bytes) return null;
|
|
1506
|
+
try {
|
|
1507
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
1508
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1509
|
+
} catch {
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
function base64urlBytes(segment) {
|
|
1514
|
+
if (!/^[A-Za-z0-9_-]*$/.test(segment) || segment.length % 4 === 1) return null;
|
|
1515
|
+
const standard = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
1516
|
+
const padded = standard + "=".repeat((4 - standard.length % 4) % 4);
|
|
1517
|
+
try {
|
|
1518
|
+
return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
|
|
1519
|
+
} catch {
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1365
1524
|
// src/lease.ts
|
|
1366
1525
|
var EXPIRES_AT_UNITS = ["rfc3339", "epoch-ms"];
|
|
1367
1526
|
var INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
1368
1527
|
var DEFAULT_CONTEXT = { service: "", operation: "subscription" };
|
|
1369
|
-
function instantMs(value, field,
|
|
1528
|
+
function instantMs(value, field, at2) {
|
|
1370
1529
|
if (typeof value !== "string" || !INSTANT.test(value)) {
|
|
1371
1530
|
throw new ConnectorConfigError(
|
|
1372
1531
|
`${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.`,
|
|
1373
|
-
|
|
1532
|
+
at2
|
|
1374
1533
|
);
|
|
1375
1534
|
}
|
|
1376
1535
|
const ms = Date.parse(value.replace(/(\.\d{3})\d+/, "$1"));
|
|
1377
1536
|
if (Number.isNaN(ms)) {
|
|
1378
|
-
throw new ConnectorConfigError(`${field} is not a real instant: ${JSON.stringify(value)}`,
|
|
1537
|
+
throw new ConnectorConfigError(`${field} is not a real instant: ${JSON.stringify(value)}`, at2);
|
|
1379
1538
|
}
|
|
1380
1539
|
return ms;
|
|
1381
1540
|
}
|
|
1382
|
-
function leaseFromResponse(declaration, response,
|
|
1541
|
+
function leaseFromResponse(declaration, response, at2 = DEFAULT_CONTEXT) {
|
|
1383
1542
|
const { expiresAtFrom: path, expiresAtUnit: unit } = declaration;
|
|
1384
1543
|
if (!EXPIRES_AT_UNITS.includes(unit)) {
|
|
1385
1544
|
throw new ConnectorConfigError(
|
|
1386
1545
|
`expiresAtUnit must be one of ${EXPIRES_AT_UNITS.join(" | ")}, got ${JSON.stringify(unit)}.`,
|
|
1387
|
-
|
|
1546
|
+
at2
|
|
1388
1547
|
);
|
|
1389
1548
|
}
|
|
1390
1549
|
const value = readPath(response, path);
|
|
1391
1550
|
if (value === void 0 || value === null) {
|
|
1392
1551
|
throw new ConnectorConfigError(
|
|
1393
1552
|
`${path} is not present in the response, so no lease can be built \u2014 a subscription with no known expiry is one nobody renews.`,
|
|
1394
|
-
|
|
1553
|
+
at2
|
|
1395
1554
|
);
|
|
1396
1555
|
}
|
|
1397
1556
|
let expiresAt;
|
|
@@ -1400,7 +1559,7 @@ function leaseFromResponse(declaration, response, at = DEFAULT_CONTEXT) {
|
|
|
1400
1559
|
if (!Number.isSafeInteger(ms)) {
|
|
1401
1560
|
throw new ConnectorConfigError(
|
|
1402
1561
|
`${path} must be epoch milliseconds as digits under "epoch-ms", got ${JSON.stringify(value)}.`,
|
|
1403
|
-
|
|
1562
|
+
at2
|
|
1404
1563
|
);
|
|
1405
1564
|
}
|
|
1406
1565
|
expiresAt = new Date(ms).toISOString();
|
|
@@ -1408,14 +1567,14 @@ function leaseFromResponse(declaration, response, at = DEFAULT_CONTEXT) {
|
|
|
1408
1567
|
if (typeof value !== "string" || !INSTANT.test(value)) {
|
|
1409
1568
|
throw new ConnectorConfigError(
|
|
1410
1569
|
`${path} must be an RFC 3339 instant under "rfc3339", got ${JSON.stringify(value)}.`,
|
|
1411
|
-
|
|
1570
|
+
at2
|
|
1412
1571
|
);
|
|
1413
1572
|
}
|
|
1414
|
-
expiresAt = new Date(instantMs(value, path,
|
|
1573
|
+
expiresAt = new Date(instantMs(value, path, at2)).toISOString();
|
|
1415
1574
|
}
|
|
1416
1575
|
return subscriptionLease(
|
|
1417
1576
|
{ expiresAt, renewBeforeSeconds: declaration.renewBeforeSeconds, renewOperation: declaration.renewOperation },
|
|
1418
|
-
|
|
1577
|
+
at2
|
|
1419
1578
|
);
|
|
1420
1579
|
}
|
|
1421
1580
|
function readPath(value, path) {
|
|
@@ -1426,18 +1585,18 @@ function readPath(value, path) {
|
|
|
1426
1585
|
}
|
|
1427
1586
|
return current;
|
|
1428
1587
|
}
|
|
1429
|
-
function subscriptionLease(input,
|
|
1430
|
-
instantMs(input.expiresAt, "expiresAt",
|
|
1588
|
+
function subscriptionLease(input, at2 = DEFAULT_CONTEXT) {
|
|
1589
|
+
instantMs(input.expiresAt, "expiresAt", at2);
|
|
1431
1590
|
if (!Number.isInteger(input.renewBeforeSeconds) || input.renewBeforeSeconds <= 0) {
|
|
1432
1591
|
throw new ConnectorConfigError(
|
|
1433
1592
|
`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.`,
|
|
1434
|
-
|
|
1593
|
+
at2
|
|
1435
1594
|
);
|
|
1436
1595
|
}
|
|
1437
1596
|
if (typeof input.renewOperation !== "string" || input.renewOperation.trim() === "") {
|
|
1438
1597
|
throw new ConnectorConfigError(
|
|
1439
1598
|
"renewOperation must name the operation the host calls when the lease is due; a due lease with nothing to call is one nobody renews.",
|
|
1440
|
-
|
|
1599
|
+
at2
|
|
1441
1600
|
);
|
|
1442
1601
|
}
|
|
1443
1602
|
return { expiresAt: input.expiresAt, renewBeforeSeconds: input.renewBeforeSeconds, renewOperation: input.renewOperation };
|
|
@@ -1446,10 +1605,10 @@ function leaseRenewAt(lease) {
|
|
|
1446
1605
|
return new Date(instantMs(lease.expiresAt, "expiresAt", DEFAULT_CONTEXT) - lease.renewBeforeSeconds * 1e3).toISOString();
|
|
1447
1606
|
}
|
|
1448
1607
|
function leaseState(lease, now) {
|
|
1449
|
-
const
|
|
1608
|
+
const at2 = now instanceof Date ? now.getTime() : instantMs(now, "now", DEFAULT_CONTEXT);
|
|
1450
1609
|
const expires = instantMs(lease.expiresAt, "expiresAt", DEFAULT_CONTEXT);
|
|
1451
|
-
if (
|
|
1452
|
-
if (
|
|
1610
|
+
if (at2 >= expires) return "expired";
|
|
1611
|
+
if (at2 >= expires - lease.renewBeforeSeconds * 1e3) return "due";
|
|
1453
1612
|
return "active";
|
|
1454
1613
|
}
|
|
1455
1614
|
function leaseAction(lease, now) {
|
|
@@ -1769,8 +1928,8 @@ function splitMultipart(body, boundary) {
|
|
|
1769
1928
|
if (partStart >= 0 && partStart <= body.length) children.push(body.subarray(partStart));
|
|
1770
1929
|
return children;
|
|
1771
1930
|
}
|
|
1772
|
-
function startsWith(haystack,
|
|
1773
|
-
for (let k = 0; k < needle.length; k++) if (haystack[
|
|
1931
|
+
function startsWith(haystack, at2, needle) {
|
|
1932
|
+
for (let k = 0; k < needle.length; k++) if (haystack[at2 + k] !== needle[k]) return false;
|
|
1774
1933
|
return true;
|
|
1775
1934
|
}
|
|
1776
1935
|
function parseMime(raw) {
|
|
@@ -1796,6 +1955,65 @@ function parseMime(raw) {
|
|
|
1796
1955
|
};
|
|
1797
1956
|
}
|
|
1798
1957
|
|
|
1958
|
+
// src/pubsub-push.ts
|
|
1959
|
+
var PUBSUB_PUSH_REASONS = ["not-json", "missing-message", "missing-data", "bad-encoding", "data-not-json"];
|
|
1960
|
+
var PubSubPushError = class extends Error {
|
|
1961
|
+
constructor(reason, message) {
|
|
1962
|
+
super(message);
|
|
1963
|
+
this.reason = reason;
|
|
1964
|
+
this.name = "PubSubPushError";
|
|
1965
|
+
}
|
|
1966
|
+
reason;
|
|
1967
|
+
};
|
|
1968
|
+
var UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
1969
|
+
function decodePubSubPush(raw) {
|
|
1970
|
+
let body;
|
|
1971
|
+
try {
|
|
1972
|
+
body = JSON.parse(typeof raw === "string" ? raw : UTF8.decode(raw));
|
|
1973
|
+
} catch {
|
|
1974
|
+
throw new PubSubPushError("not-json", "the push body is not JSON");
|
|
1975
|
+
}
|
|
1976
|
+
if (!isObject(body)) throw new PubSubPushError("not-json", "the push body is not a JSON object");
|
|
1977
|
+
const message = body.message;
|
|
1978
|
+
if (!isObject(message)) throw new PubSubPushError("missing-message", "the push body has no message");
|
|
1979
|
+
const data = message.data;
|
|
1980
|
+
if (typeof data !== "string" || data === "") {
|
|
1981
|
+
throw new PubSubPushError("missing-data", "the message carries no data");
|
|
1982
|
+
}
|
|
1983
|
+
let text;
|
|
1984
|
+
try {
|
|
1985
|
+
text = UTF8.decode(base64Bytes2(data));
|
|
1986
|
+
} catch {
|
|
1987
|
+
throw new PubSubPushError("bad-encoding", "the message data is not base64 of UTF-8 text");
|
|
1988
|
+
}
|
|
1989
|
+
let parsed;
|
|
1990
|
+
try {
|
|
1991
|
+
parsed = JSON.parse(text);
|
|
1992
|
+
} catch (error) {
|
|
1993
|
+
throw new PubSubPushError("data-not-json", `the message data is not JSON: ${error.message}`);
|
|
1994
|
+
}
|
|
1995
|
+
return {
|
|
1996
|
+
subscription: stringOrNull(body.subscription),
|
|
1997
|
+
messageId: stringOrNull(message.messageId) ?? stringOrNull(message.message_id),
|
|
1998
|
+
publishTime: stringOrNull(message.publishTime) ?? stringOrNull(message.publish_time),
|
|
1999
|
+
attributes: isObject(message.attributes) ? message.attributes : {},
|
|
2000
|
+
deliveryAttempt: Number.isInteger(body.deliveryAttempt) ? body.deliveryAttempt : null,
|
|
2001
|
+
data: parsed
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
function base64Bytes2(data) {
|
|
2005
|
+
const unpadded = data.replace(/={1,2}$/, "");
|
|
2006
|
+
if (!/^[A-Za-z0-9+/_-]*$/.test(unpadded) || unpadded.length % 4 === 1) throw new Error("not base64");
|
|
2007
|
+
const standard = unpadded.replace(/-/g, "+").replace(/_/g, "/");
|
|
2008
|
+
return Uint8Array.from(atob(standard + "=".repeat((4 - standard.length % 4) % 4)), (c) => c.charCodeAt(0));
|
|
2009
|
+
}
|
|
2010
|
+
function isObject(value) {
|
|
2011
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2012
|
+
}
|
|
2013
|
+
function stringOrNull(value) {
|
|
2014
|
+
return typeof value === "string" ? value : null;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
1799
2017
|
// src/form-field-json.ts
|
|
1800
2018
|
var FORM_FIELD_JSON_REASONS = ["missing-field", "duplicate-field", "bad-encoding", "not-json"];
|
|
1801
2019
|
var FormFieldJsonError = class extends Error {
|
|
@@ -1808,7 +2026,7 @@ var FormFieldJsonError = class extends Error {
|
|
|
1808
2026
|
reason;
|
|
1809
2027
|
field;
|
|
1810
2028
|
};
|
|
1811
|
-
var
|
|
2029
|
+
var UTF82 = new TextDecoder("utf-8", { fatal: true });
|
|
1812
2030
|
var HEX = /^[0-9A-Fa-f]{2}$/;
|
|
1813
2031
|
function decodeFormFieldJson(raw, field) {
|
|
1814
2032
|
const text = typeof raw === "string" ? raw : latin12(raw);
|
|
@@ -1851,14 +2069,15 @@ function component(encoded, field) {
|
|
|
1851
2069
|
}
|
|
1852
2070
|
}
|
|
1853
2071
|
try {
|
|
1854
|
-
return
|
|
2072
|
+
return UTF82.decode(new Uint8Array(bytes));
|
|
1855
2073
|
} catch {
|
|
1856
2074
|
throw new FormFieldJsonError("bad-encoding", field, "the decoded bytes are not valid UTF-8");
|
|
1857
2075
|
}
|
|
1858
2076
|
}
|
|
1859
|
-
var DELIVERY_DECODE_KINDS = ["form-field-json"];
|
|
2077
|
+
var DELIVERY_DECODE_KINDS = ["form-field-json", "pubsub-push"];
|
|
1860
2078
|
function decodeDelivery(decode, raw) {
|
|
1861
2079
|
if (decode.kind === "form-field-json") return decodeFormFieldJson(raw, decode.field);
|
|
2080
|
+
if (decode.kind === "pubsub-push") return decodePubSubPush(raw);
|
|
1862
2081
|
throw new Error(`unknown decode kind ${JSON.stringify(decode.kind)}`);
|
|
1863
2082
|
}
|
|
1864
2083
|
function latin12(bytes) {
|
|
@@ -1867,6 +2086,202 @@ function latin12(bytes) {
|
|
|
1867
2086
|
return out;
|
|
1868
2087
|
}
|
|
1869
2088
|
|
|
2089
|
+
// src/webhook.ts
|
|
2090
|
+
var SIGNED_URI_RULES = ["hubspot-v3"];
|
|
2091
|
+
var HUBSPOT_V3_DECODE = {
|
|
2092
|
+
"%3A": ":",
|
|
2093
|
+
"%2F": "/",
|
|
2094
|
+
"%3F": "?",
|
|
2095
|
+
"%40": "@",
|
|
2096
|
+
"%21": "!",
|
|
2097
|
+
"%24": "$",
|
|
2098
|
+
"%27": "'",
|
|
2099
|
+
"%28": "(",
|
|
2100
|
+
"%29": ")",
|
|
2101
|
+
"%2A": "*",
|
|
2102
|
+
"%2C": ",",
|
|
2103
|
+
"%3B": ";"
|
|
2104
|
+
};
|
|
2105
|
+
function normaliseSignedUri(url, rule) {
|
|
2106
|
+
if (rule !== "hubspot-v3") throw new Error(`unknown signed-uri rule ${JSON.stringify(rule)}`);
|
|
2107
|
+
const withoutFragment = url.split("#", 1)[0] ?? "";
|
|
2108
|
+
const q = withoutFragment.indexOf("?");
|
|
2109
|
+
if (q === -1) return withoutFragment;
|
|
2110
|
+
const query = withoutFragment.slice(q + 1).replace(/%[0-9A-Fa-f]{2}/g, (e) => HUBSPOT_V3_DECODE[e.toUpperCase()] ?? e);
|
|
2111
|
+
return `${withoutFragment.slice(0, q)}?${query}`;
|
|
2112
|
+
}
|
|
2113
|
+
function secretKeyBytes(secret, options = {}) {
|
|
2114
|
+
let material = secret;
|
|
2115
|
+
if (options.secretPrefix !== void 0) {
|
|
2116
|
+
if (!material.startsWith(options.secretPrefix)) {
|
|
2117
|
+
return { reason: `signing secret does not start with ${JSON.stringify(options.secretPrefix)}` };
|
|
2118
|
+
}
|
|
2119
|
+
material = material.slice(options.secretPrefix.length);
|
|
2120
|
+
}
|
|
2121
|
+
if ((options.secretEncoding ?? "utf8") === "base64") {
|
|
2122
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(material) || material.length % 4 !== 0) {
|
|
2123
|
+
return { reason: "signing secret is not valid base64" };
|
|
2124
|
+
}
|
|
2125
|
+
try {
|
|
2126
|
+
return { bytes: Uint8Array.from(atob(material), (c) => c.charCodeAt(0)) };
|
|
2127
|
+
} catch {
|
|
2128
|
+
return { reason: "signing secret is not valid base64" };
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
return { bytes: new TextEncoder().encode(material) };
|
|
2132
|
+
}
|
|
2133
|
+
async function verifyHmac(options) {
|
|
2134
|
+
const { raw, secret, scheme, timestamp, id } = options;
|
|
2135
|
+
const signatures = (Array.isArray(options.signature) ? options.signature : [options.signature]).filter(
|
|
2136
|
+
(s) => typeof s === "string" && s !== ""
|
|
2137
|
+
);
|
|
2138
|
+
if (!secret) {
|
|
2139
|
+
return { ok: false, reason: "no signing secret configured for this trigger" };
|
|
2140
|
+
}
|
|
2141
|
+
if (signatures.length === 0) return { ok: false, reason: "delivery carried no signature header" };
|
|
2142
|
+
if (scheme.tolerance !== void 0) {
|
|
2143
|
+
if (!timestamp) return { ok: false, reason: "delivery carried no timestamp header" };
|
|
2144
|
+
const sent = Number(timestamp);
|
|
2145
|
+
if (!Number.isFinite(sent)) return { ok: false, reason: "timestamp header is not a number" };
|
|
2146
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
2147
|
+
const sentSeconds = scheme.timestampUnit === "ms" ? sent / 1e3 : sent;
|
|
2148
|
+
if (Math.abs(now - sentSeconds) > scheme.tolerance) {
|
|
2149
|
+
return { ok: false, reason: `delivery is outside the ${scheme.tolerance}s replay window` };
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
const key = secretKeyBytes(secret, scheme);
|
|
2153
|
+
if ("reason" in key) return { ok: false, reason: key.reason };
|
|
2154
|
+
let line;
|
|
2155
|
+
if (scheme.signsRequestLine) {
|
|
2156
|
+
const { method, url } = options.request ?? {};
|
|
2157
|
+
if (!method || !url) return { ok: false, reason: "delivery carried no request method or url" };
|
|
2158
|
+
line = { method, uri: scheme.uri ? normaliseSignedUri(url, scheme.uri) : url };
|
|
2159
|
+
}
|
|
2160
|
+
const expected = await hmac(
|
|
2161
|
+
secret,
|
|
2162
|
+
scheme.payload(raw, timestamp, id, line),
|
|
2163
|
+
scheme.algorithm,
|
|
2164
|
+
scheme.encoding ?? "hex",
|
|
2165
|
+
scheme
|
|
2166
|
+
);
|
|
2167
|
+
return signatures.some((candidate) => constantTimeEquals(expected, candidate)) ? { ok: true } : { ok: false, reason: "signature did not match" };
|
|
2168
|
+
}
|
|
2169
|
+
async function hmac(secret, payload, algorithm, encoding = "hex", options = {}) {
|
|
2170
|
+
const subtle = globalThis.crypto?.subtle;
|
|
2171
|
+
if (!subtle) {
|
|
2172
|
+
throw new Error(
|
|
2173
|
+
"WebCrypto is not available in this runtime, so webhook signatures cannot be verified. Refusing to accept deliveries unverified."
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
const material = secretKeyBytes(secret, options);
|
|
2177
|
+
if ("reason" in material) throw new Error(material.reason);
|
|
2178
|
+
const encoder2 = new TextEncoder();
|
|
2179
|
+
const key = await subtle.importKey(
|
|
2180
|
+
"raw",
|
|
2181
|
+
material.bytes,
|
|
2182
|
+
{ name: "HMAC", hash: algorithm },
|
|
2183
|
+
false,
|
|
2184
|
+
["sign"]
|
|
2185
|
+
);
|
|
2186
|
+
const bytes = new Uint8Array(await subtle.sign("HMAC", key, encoder2.encode(payload)));
|
|
2187
|
+
if (encoding === "base64") {
|
|
2188
|
+
let binary = "";
|
|
2189
|
+
bytes.forEach((byte) => {
|
|
2190
|
+
binary += String.fromCharCode(byte);
|
|
2191
|
+
});
|
|
2192
|
+
return btoa(binary);
|
|
2193
|
+
}
|
|
2194
|
+
return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2195
|
+
}
|
|
2196
|
+
function constantTimeEquals(a, b) {
|
|
2197
|
+
if (a.length !== b.length) return false;
|
|
2198
|
+
let diff = 0;
|
|
2199
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2200
|
+
return diff === 0;
|
|
2201
|
+
}
|
|
2202
|
+
function isSharedTokenScheme(scheme) {
|
|
2203
|
+
return "kind" in scheme && scheme.kind === "shared-token";
|
|
2204
|
+
}
|
|
2205
|
+
function verifySharedToken(options) {
|
|
2206
|
+
const { raw, headers, secret, scheme } = options;
|
|
2207
|
+
if (!secret) {
|
|
2208
|
+
return { ok: false, reason: "no shared token configured for this trigger" };
|
|
2209
|
+
}
|
|
2210
|
+
let tokens;
|
|
2211
|
+
if (scheme.in === "header") {
|
|
2212
|
+
tokens = [header2(headers, scheme.name)];
|
|
2213
|
+
} else {
|
|
2214
|
+
let parsed;
|
|
2215
|
+
try {
|
|
2216
|
+
parsed = JSON.parse(raw);
|
|
2217
|
+
} catch {
|
|
2218
|
+
return { ok: false, reason: "delivery body is not JSON" };
|
|
2219
|
+
}
|
|
2220
|
+
tokens = readTokens(parsed, scheme.path.split("."));
|
|
2221
|
+
}
|
|
2222
|
+
const present = tokens.filter((t) => t !== void 0 && t !== null && t !== "");
|
|
2223
|
+
if (present.length === 0) return { ok: false, reason: "delivery carried no token" };
|
|
2224
|
+
let matched = present.length === tokens.length;
|
|
2225
|
+
for (const token of present) {
|
|
2226
|
+
matched = typeof token === "string" && constantTimeEquals(secret, token) && matched;
|
|
2227
|
+
}
|
|
2228
|
+
return matched ? { ok: true } : { ok: false, reason: "token did not match" };
|
|
2229
|
+
}
|
|
2230
|
+
function readTokens(value, segments) {
|
|
2231
|
+
if (segments.length === 0) return [value];
|
|
2232
|
+
const [head, ...rest] = segments;
|
|
2233
|
+
const eachElement = head.endsWith("[]");
|
|
2234
|
+
const key = eachElement ? head.slice(0, -2) : head;
|
|
2235
|
+
if (value === null || typeof value !== "object") return [void 0];
|
|
2236
|
+
const next = value[key];
|
|
2237
|
+
if (!eachElement) return readTokens(next, rest);
|
|
2238
|
+
if (!Array.isArray(next)) return [void 0];
|
|
2239
|
+
return next.flatMap((element) => readTokens(element, rest));
|
|
2240
|
+
}
|
|
2241
|
+
function handshakeResponse(handshake, query) {
|
|
2242
|
+
if (!handshake) return void 0;
|
|
2243
|
+
const raw = query[handshake.param];
|
|
2244
|
+
const token = Array.isArray(raw) ? raw[0] : raw;
|
|
2245
|
+
if (!token) return void 0;
|
|
2246
|
+
return { status: 200, contentType: handshake.contentType ?? "text/plain", body: token };
|
|
2247
|
+
}
|
|
2248
|
+
function header2(headers, name) {
|
|
2249
|
+
const wanted = name.toLowerCase();
|
|
2250
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
2251
|
+
if (key.toLowerCase() !== wanted) continue;
|
|
2252
|
+
return Array.isArray(value) ? value[0] : value;
|
|
2253
|
+
}
|
|
2254
|
+
return void 0;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// src/oidc-delivery.ts
|
|
2258
|
+
var OIDC_JWT_SCHEME_KIND = "oidc-jwt";
|
|
2259
|
+
function isOidcJwtScheme(scheme) {
|
|
2260
|
+
return typeof scheme === "object" && scheme !== null && scheme.kind === OIDC_JWT_SCHEME_KIND;
|
|
2261
|
+
}
|
|
2262
|
+
var fail2 = (reason) => ({ ok: false, reason });
|
|
2263
|
+
var BEARER = /^bearer +(\S+)$/i;
|
|
2264
|
+
async function verifyOidcDelivery(options) {
|
|
2265
|
+
const { headers, scheme, expected } = options;
|
|
2266
|
+
if (!expected || expected.keys.length === 0) return fail2("no signing keys supplied for this trigger");
|
|
2267
|
+
if (!expected.audience) return fail2("no audience configured for this trigger");
|
|
2268
|
+
if (!expected.principal) return fail2("no principal configured for this trigger");
|
|
2269
|
+
const token = BEARER.exec(header2(headers, scheme.header) ?? "")?.[1];
|
|
2270
|
+
if (!token) return fail2("delivery carried no bearer token");
|
|
2271
|
+
const verified = await verifyJwt(token, {
|
|
2272
|
+
keys: expected.keys,
|
|
2273
|
+
audience: expected.audience,
|
|
2274
|
+
issuer: scheme.issuers,
|
|
2275
|
+
...options.now === void 0 ? {} : { now: options.now }
|
|
2276
|
+
});
|
|
2277
|
+
if (!verified.ok) return fail2(verified.reason);
|
|
2278
|
+
if (verified.claims[scheme.principalClaim] !== expected.principal) return fail2("token principal is not accepted");
|
|
2279
|
+
if (scheme.verifiedClaim !== void 0 && verified.claims[scheme.verifiedClaim] !== true) {
|
|
2280
|
+
return fail2("token principal is not verified");
|
|
2281
|
+
}
|
|
2282
|
+
return { ok: true };
|
|
2283
|
+
}
|
|
2284
|
+
|
|
1870
2285
|
// src/probe.ts
|
|
1871
2286
|
async function probe(spec) {
|
|
1872
2287
|
let response;
|
|
@@ -2082,179 +2497,14 @@ var CREDENTIAL_SCOPES = ["provider", "account"];
|
|
|
2082
2497
|
var PROBLEM_SEVERITIES = ["block", "warn"];
|
|
2083
2498
|
var CANONICAL_METRICS = ["like", "share", "reply", "quote", "view"];
|
|
2084
2499
|
|
|
2085
|
-
// src/webhook.ts
|
|
2086
|
-
var SIGNED_URI_RULES = ["hubspot-v3"];
|
|
2087
|
-
var HUBSPOT_V3_DECODE = {
|
|
2088
|
-
"%3A": ":",
|
|
2089
|
-
"%2F": "/",
|
|
2090
|
-
"%3F": "?",
|
|
2091
|
-
"%40": "@",
|
|
2092
|
-
"%21": "!",
|
|
2093
|
-
"%24": "$",
|
|
2094
|
-
"%27": "'",
|
|
2095
|
-
"%28": "(",
|
|
2096
|
-
"%29": ")",
|
|
2097
|
-
"%2A": "*",
|
|
2098
|
-
"%2C": ",",
|
|
2099
|
-
"%3B": ";"
|
|
2100
|
-
};
|
|
2101
|
-
function normaliseSignedUri(url, rule) {
|
|
2102
|
-
if (rule !== "hubspot-v3") throw new Error(`unknown signed-uri rule ${JSON.stringify(rule)}`);
|
|
2103
|
-
const withoutFragment = url.split("#", 1)[0] ?? "";
|
|
2104
|
-
const q = withoutFragment.indexOf("?");
|
|
2105
|
-
if (q === -1) return withoutFragment;
|
|
2106
|
-
const query = withoutFragment.slice(q + 1).replace(/%[0-9A-Fa-f]{2}/g, (e) => HUBSPOT_V3_DECODE[e.toUpperCase()] ?? e);
|
|
2107
|
-
return `${withoutFragment.slice(0, q)}?${query}`;
|
|
2108
|
-
}
|
|
2109
|
-
function secretKeyBytes(secret, options = {}) {
|
|
2110
|
-
let material = secret;
|
|
2111
|
-
if (options.secretPrefix !== void 0) {
|
|
2112
|
-
if (!material.startsWith(options.secretPrefix)) {
|
|
2113
|
-
return { reason: `signing secret does not start with ${JSON.stringify(options.secretPrefix)}` };
|
|
2114
|
-
}
|
|
2115
|
-
material = material.slice(options.secretPrefix.length);
|
|
2116
|
-
}
|
|
2117
|
-
if ((options.secretEncoding ?? "utf8") === "base64") {
|
|
2118
|
-
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(material) || material.length % 4 !== 0) {
|
|
2119
|
-
return { reason: "signing secret is not valid base64" };
|
|
2120
|
-
}
|
|
2121
|
-
try {
|
|
2122
|
-
return { bytes: Uint8Array.from(atob(material), (c) => c.charCodeAt(0)) };
|
|
2123
|
-
} catch {
|
|
2124
|
-
return { reason: "signing secret is not valid base64" };
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
return { bytes: new TextEncoder().encode(material) };
|
|
2128
|
-
}
|
|
2129
|
-
async function verifyHmac(options) {
|
|
2130
|
-
const { raw, secret, scheme, timestamp, id } = options;
|
|
2131
|
-
const signatures = (Array.isArray(options.signature) ? options.signature : [options.signature]).filter(
|
|
2132
|
-
(s) => typeof s === "string" && s !== ""
|
|
2133
|
-
);
|
|
2134
|
-
if (!secret) {
|
|
2135
|
-
return { ok: false, reason: "no signing secret configured for this trigger" };
|
|
2136
|
-
}
|
|
2137
|
-
if (signatures.length === 0) return { ok: false, reason: "delivery carried no signature header" };
|
|
2138
|
-
if (scheme.tolerance !== void 0) {
|
|
2139
|
-
if (!timestamp) return { ok: false, reason: "delivery carried no timestamp header" };
|
|
2140
|
-
const sent = Number(timestamp);
|
|
2141
|
-
if (!Number.isFinite(sent)) return { ok: false, reason: "timestamp header is not a number" };
|
|
2142
|
-
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
2143
|
-
const sentSeconds = scheme.timestampUnit === "ms" ? sent / 1e3 : sent;
|
|
2144
|
-
if (Math.abs(now - sentSeconds) > scheme.tolerance) {
|
|
2145
|
-
return { ok: false, reason: `delivery is outside the ${scheme.tolerance}s replay window` };
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
const key = secretKeyBytes(secret, scheme);
|
|
2149
|
-
if ("reason" in key) return { ok: false, reason: key.reason };
|
|
2150
|
-
let line;
|
|
2151
|
-
if (scheme.signsRequestLine) {
|
|
2152
|
-
const { method, url } = options.request ?? {};
|
|
2153
|
-
if (!method || !url) return { ok: false, reason: "delivery carried no request method or url" };
|
|
2154
|
-
line = { method, uri: scheme.uri ? normaliseSignedUri(url, scheme.uri) : url };
|
|
2155
|
-
}
|
|
2156
|
-
const expected = await hmac(
|
|
2157
|
-
secret,
|
|
2158
|
-
scheme.payload(raw, timestamp, id, line),
|
|
2159
|
-
scheme.algorithm,
|
|
2160
|
-
scheme.encoding ?? "hex",
|
|
2161
|
-
scheme
|
|
2162
|
-
);
|
|
2163
|
-
return signatures.some((candidate) => constantTimeEquals(expected, candidate)) ? { ok: true } : { ok: false, reason: "signature did not match" };
|
|
2164
|
-
}
|
|
2165
|
-
async function hmac(secret, payload, algorithm, encoding = "hex", options = {}) {
|
|
2166
|
-
const subtle = globalThis.crypto?.subtle;
|
|
2167
|
-
if (!subtle) {
|
|
2168
|
-
throw new Error(
|
|
2169
|
-
"WebCrypto is not available in this runtime, so webhook signatures cannot be verified. Refusing to accept deliveries unverified."
|
|
2170
|
-
);
|
|
2171
|
-
}
|
|
2172
|
-
const material = secretKeyBytes(secret, options);
|
|
2173
|
-
if ("reason" in material) throw new Error(material.reason);
|
|
2174
|
-
const encoder2 = new TextEncoder();
|
|
2175
|
-
const key = await subtle.importKey(
|
|
2176
|
-
"raw",
|
|
2177
|
-
material.bytes,
|
|
2178
|
-
{ name: "HMAC", hash: algorithm },
|
|
2179
|
-
false,
|
|
2180
|
-
["sign"]
|
|
2181
|
-
);
|
|
2182
|
-
const bytes = new Uint8Array(await subtle.sign("HMAC", key, encoder2.encode(payload)));
|
|
2183
|
-
if (encoding === "base64") {
|
|
2184
|
-
let binary = "";
|
|
2185
|
-
bytes.forEach((byte) => {
|
|
2186
|
-
binary += String.fromCharCode(byte);
|
|
2187
|
-
});
|
|
2188
|
-
return btoa(binary);
|
|
2189
|
-
}
|
|
2190
|
-
return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2191
|
-
}
|
|
2192
|
-
function constantTimeEquals(a, b) {
|
|
2193
|
-
if (a.length !== b.length) return false;
|
|
2194
|
-
let diff = 0;
|
|
2195
|
-
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2196
|
-
return diff === 0;
|
|
2197
|
-
}
|
|
2198
|
-
function isSharedTokenScheme(scheme) {
|
|
2199
|
-
return "kind" in scheme && scheme.kind === "shared-token";
|
|
2200
|
-
}
|
|
2201
|
-
function verifySharedToken(options) {
|
|
2202
|
-
const { raw, headers, secret, scheme } = options;
|
|
2203
|
-
if (!secret) {
|
|
2204
|
-
return { ok: false, reason: "no shared token configured for this trigger" };
|
|
2205
|
-
}
|
|
2206
|
-
let tokens;
|
|
2207
|
-
if (scheme.in === "header") {
|
|
2208
|
-
tokens = [header2(headers, scheme.name)];
|
|
2209
|
-
} else {
|
|
2210
|
-
let parsed;
|
|
2211
|
-
try {
|
|
2212
|
-
parsed = JSON.parse(raw);
|
|
2213
|
-
} catch {
|
|
2214
|
-
return { ok: false, reason: "delivery body is not JSON" };
|
|
2215
|
-
}
|
|
2216
|
-
tokens = readTokens(parsed, scheme.path.split("."));
|
|
2217
|
-
}
|
|
2218
|
-
const present = tokens.filter((t) => t !== void 0 && t !== null && t !== "");
|
|
2219
|
-
if (present.length === 0) return { ok: false, reason: "delivery carried no token" };
|
|
2220
|
-
let matched = present.length === tokens.length;
|
|
2221
|
-
for (const token of present) {
|
|
2222
|
-
matched = typeof token === "string" && constantTimeEquals(secret, token) && matched;
|
|
2223
|
-
}
|
|
2224
|
-
return matched ? { ok: true } : { ok: false, reason: "token did not match" };
|
|
2225
|
-
}
|
|
2226
|
-
function readTokens(value, segments) {
|
|
2227
|
-
if (segments.length === 0) return [value];
|
|
2228
|
-
const [head, ...rest] = segments;
|
|
2229
|
-
const eachElement = head.endsWith("[]");
|
|
2230
|
-
const key = eachElement ? head.slice(0, -2) : head;
|
|
2231
|
-
if (value === null || typeof value !== "object") return [void 0];
|
|
2232
|
-
const next = value[key];
|
|
2233
|
-
if (!eachElement) return readTokens(next, rest);
|
|
2234
|
-
if (!Array.isArray(next)) return [void 0];
|
|
2235
|
-
return next.flatMap((element) => readTokens(element, rest));
|
|
2236
|
-
}
|
|
2237
|
-
function handshakeResponse(handshake, query) {
|
|
2238
|
-
if (!handshake) return void 0;
|
|
2239
|
-
const raw = query[handshake.param];
|
|
2240
|
-
const token = Array.isArray(raw) ? raw[0] : raw;
|
|
2241
|
-
if (!token) return void 0;
|
|
2242
|
-
return { status: 200, contentType: handshake.contentType ?? "text/plain", body: token };
|
|
2243
|
-
}
|
|
2244
|
-
function header2(headers, name) {
|
|
2245
|
-
const wanted = name.toLowerCase();
|
|
2246
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
2247
|
-
if (key.toLowerCase() !== wanted) continue;
|
|
2248
|
-
return Array.isArray(value) ? value[0] : value;
|
|
2249
|
-
}
|
|
2250
|
-
return void 0;
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
2500
|
// src/trigger.ts
|
|
2501
|
+
function isOidcJwtSpec(spec) {
|
|
2502
|
+
return isOidcJwtScheme(spec.scheme);
|
|
2503
|
+
}
|
|
2254
2504
|
function isSharedTokenSpec(spec) {
|
|
2255
2505
|
return isSharedTokenScheme(spec.scheme);
|
|
2256
2506
|
}
|
|
2257
|
-
async function verifyDelivery(trigger, delivery, secret, now) {
|
|
2507
|
+
async function verifyDelivery(trigger, delivery, secret, now, oidc) {
|
|
2258
2508
|
const spec = trigger.verification;
|
|
2259
2509
|
if (!spec) {
|
|
2260
2510
|
return {
|
|
@@ -2265,6 +2515,14 @@ async function verifyDelivery(trigger, delivery, secret, now) {
|
|
|
2265
2515
|
if (isSharedTokenSpec(spec)) {
|
|
2266
2516
|
return verifySharedToken({ raw: delivery.raw, headers: delivery.headers, secret, scheme: spec.scheme });
|
|
2267
2517
|
}
|
|
2518
|
+
if (isOidcJwtSpec(spec)) {
|
|
2519
|
+
return verifyOidcDelivery({
|
|
2520
|
+
headers: delivery.headers,
|
|
2521
|
+
scheme: spec.scheme,
|
|
2522
|
+
expected: oidc,
|
|
2523
|
+
...now === void 0 ? {} : { now }
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2268
2526
|
const kind = spec.scheme.kind ?? "hmac";
|
|
2269
2527
|
if (kind !== "hmac") {
|
|
2270
2528
|
return {
|
|
@@ -2313,6 +2571,7 @@ function hasDelivery(value) {
|
|
|
2313
2571
|
CONNECTOR_API_VERSION,
|
|
2314
2572
|
CONNECTOR_MODES,
|
|
2315
2573
|
CREDENTIAL_SCOPES,
|
|
2574
|
+
CURSOR_READ_REASONS,
|
|
2316
2575
|
ConnectorAmbiguous,
|
|
2317
2576
|
ConnectorApiMismatch,
|
|
2318
2577
|
ConnectorAuthError,
|
|
@@ -2324,6 +2583,7 @@ function hasDelivery(value) {
|
|
|
2324
2583
|
ConnectorRequestError,
|
|
2325
2584
|
ConnectorTransient,
|
|
2326
2585
|
ConnectorUnreachable,
|
|
2586
|
+
CursorReadError,
|
|
2327
2587
|
DEFAULT_IDEMPOTENCY_WINDOW_SECONDS,
|
|
2328
2588
|
DEFAULT_RETRY,
|
|
2329
2589
|
DELIVERY_DECODE_KINDS,
|
|
@@ -2331,9 +2591,13 @@ function hasDelivery(value) {
|
|
|
2331
2591
|
FAKE_EPOCH,
|
|
2332
2592
|
FORM_FIELD_JSON_REASONS,
|
|
2333
2593
|
FormFieldJsonError,
|
|
2594
|
+
JWT_ALGORITHMS,
|
|
2334
2595
|
MAX_IDEMPOTENCY_KEY_LENGTH,
|
|
2335
2596
|
NO_IDEMPOTENCY_KEY_WARNING,
|
|
2597
|
+
OIDC_JWT_SCHEME_KIND,
|
|
2336
2598
|
PROBLEM_SEVERITIES,
|
|
2599
|
+
PUBSUB_PUSH_REASONS,
|
|
2600
|
+
PubSubPushError,
|
|
2337
2601
|
RENDERER_VERSION,
|
|
2338
2602
|
REVIEW_STALE_DAYS,
|
|
2339
2603
|
SANDBOX_KINDS,
|
|
@@ -2354,6 +2618,7 @@ function hasDelivery(value) {
|
|
|
2354
2618
|
constantTimeEquals,
|
|
2355
2619
|
decodeDelivery,
|
|
2356
2620
|
decodeFormFieldJson,
|
|
2621
|
+
decodePubSubPush,
|
|
2357
2622
|
deliver,
|
|
2358
2623
|
fakeRequest,
|
|
2359
2624
|
fakeValues,
|
|
@@ -2365,6 +2630,8 @@ function hasDelivery(value) {
|
|
|
2365
2630
|
httpFailure,
|
|
2366
2631
|
idempotencyKeyFor,
|
|
2367
2632
|
isEmptyPayload,
|
|
2633
|
+
isOidcJwtScheme,
|
|
2634
|
+
isOidcJwtSpec,
|
|
2368
2635
|
isRemote,
|
|
2369
2636
|
isSharedTokenScheme,
|
|
2370
2637
|
isSharedTokenSpec,
|
|
@@ -2385,6 +2652,7 @@ function hasDelivery(value) {
|
|
|
2385
2652
|
probe,
|
|
2386
2653
|
providerProblems,
|
|
2387
2654
|
providerSandboxIsSelectable,
|
|
2655
|
+
readWithCursor,
|
|
2388
2656
|
registerConnectionHost,
|
|
2389
2657
|
registerTransport,
|
|
2390
2658
|
render,
|
|
@@ -2411,6 +2679,8 @@ function hasDelivery(value) {
|
|
|
2411
2679
|
utf8Length,
|
|
2412
2680
|
verifyDelivery,
|
|
2413
2681
|
verifyHmac,
|
|
2682
|
+
verifyJwt,
|
|
2683
|
+
verifyOidcDelivery,
|
|
2414
2684
|
verifySharedToken,
|
|
2415
2685
|
withResolvedLimit
|
|
2416
2686
|
});
|