@particle-academy/fancy-connector-core 0.6.2 → 0.8.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 +91 -0
- package/dist/index.cjs +422 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +225 -2
- package/dist/index.d.ts +225 -2
- package/dist/index.js +411 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -595,21 +595,21 @@ async function prepare(service, connection, options) {
|
|
|
595
595
|
}
|
|
596
596
|
function encodeForm(form) {
|
|
597
597
|
const pairs = [];
|
|
598
|
-
const
|
|
598
|
+
const walk2 = (prefix, value) => {
|
|
599
599
|
if (value === void 0 || value === null) return;
|
|
600
600
|
if (Array.isArray(value)) {
|
|
601
|
-
value.forEach((item, index) =>
|
|
601
|
+
value.forEach((item, index) => walk2(`${prefix}[${index}]`, item));
|
|
602
602
|
return;
|
|
603
603
|
}
|
|
604
604
|
if (typeof value === "object") {
|
|
605
605
|
for (const [key, nested] of Object.entries(value)) {
|
|
606
|
-
|
|
606
|
+
walk2(prefix === "" ? key : `${prefix}[${key}]`, nested);
|
|
607
607
|
}
|
|
608
608
|
return;
|
|
609
609
|
}
|
|
610
610
|
pairs.push(`${encodeURIComponent(prefix)}=${encodeURIComponent(String(value))}`);
|
|
611
611
|
};
|
|
612
|
-
|
|
612
|
+
walk2("", form);
|
|
613
613
|
return pairs.join("&");
|
|
614
614
|
}
|
|
615
615
|
var fetchTransport = async (request) => {
|
|
@@ -913,6 +913,107 @@ function fnv1a(value) {
|
|
|
913
913
|
return hash.toString(16).padStart(8, "0");
|
|
914
914
|
}
|
|
915
915
|
|
|
916
|
+
// src/lease.ts
|
|
917
|
+
var EXPIRES_AT_UNITS = ["rfc3339", "epoch-ms"];
|
|
918
|
+
var INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
919
|
+
var DEFAULT_CONTEXT = { service: "", operation: "subscription" };
|
|
920
|
+
function instantMs(value, field, at) {
|
|
921
|
+
if (typeof value !== "string" || !INSTANT.test(value)) {
|
|
922
|
+
throw new ConnectorConfigError(
|
|
923
|
+
`${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.`,
|
|
924
|
+
at
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
const ms = Date.parse(value.replace(/(\.\d{3})\d+/, "$1"));
|
|
928
|
+
if (Number.isNaN(ms)) {
|
|
929
|
+
throw new ConnectorConfigError(`${field} is not a real instant: ${JSON.stringify(value)}`, at);
|
|
930
|
+
}
|
|
931
|
+
return ms;
|
|
932
|
+
}
|
|
933
|
+
function leaseFromResponse(declaration, response, at = DEFAULT_CONTEXT) {
|
|
934
|
+
const { expiresAtFrom: path, expiresAtUnit: unit } = declaration;
|
|
935
|
+
if (!EXPIRES_AT_UNITS.includes(unit)) {
|
|
936
|
+
throw new ConnectorConfigError(
|
|
937
|
+
`expiresAtUnit must be one of ${EXPIRES_AT_UNITS.join(" | ")}, got ${JSON.stringify(unit)}.`,
|
|
938
|
+
at
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
const value = readPath(response, path);
|
|
942
|
+
if (value === void 0 || value === null) {
|
|
943
|
+
throw new ConnectorConfigError(
|
|
944
|
+
`${path} is not present in the response, so no lease can be built \u2014 a subscription with no known expiry is one nobody renews.`,
|
|
945
|
+
at
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
let expiresAt;
|
|
949
|
+
if (unit === "epoch-ms") {
|
|
950
|
+
const ms = typeof value === "number" ? value : typeof value === "string" && /^\d+$/.test(value) ? Number(value) : Number.NaN;
|
|
951
|
+
if (!Number.isSafeInteger(ms)) {
|
|
952
|
+
throw new ConnectorConfigError(
|
|
953
|
+
`${path} must be epoch milliseconds as digits under "epoch-ms", got ${JSON.stringify(value)}.`,
|
|
954
|
+
at
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
expiresAt = new Date(ms).toISOString();
|
|
958
|
+
} else {
|
|
959
|
+
if (typeof value !== "string" || !INSTANT.test(value)) {
|
|
960
|
+
throw new ConnectorConfigError(
|
|
961
|
+
`${path} must be an RFC 3339 instant under "rfc3339", got ${JSON.stringify(value)}.`,
|
|
962
|
+
at
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
expiresAt = new Date(instantMs(value, path, at)).toISOString();
|
|
966
|
+
}
|
|
967
|
+
return subscriptionLease(
|
|
968
|
+
{ expiresAt, renewBeforeSeconds: declaration.renewBeforeSeconds, renewOperation: declaration.renewOperation },
|
|
969
|
+
at
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
function readPath(value, path) {
|
|
973
|
+
let current = value;
|
|
974
|
+
for (const segment of path.split(".")) {
|
|
975
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
976
|
+
current = current[segment];
|
|
977
|
+
}
|
|
978
|
+
return current;
|
|
979
|
+
}
|
|
980
|
+
function subscriptionLease(input, at = DEFAULT_CONTEXT) {
|
|
981
|
+
instantMs(input.expiresAt, "expiresAt", at);
|
|
982
|
+
if (!Number.isInteger(input.renewBeforeSeconds) || input.renewBeforeSeconds <= 0) {
|
|
983
|
+
throw new ConnectorConfigError(
|
|
984
|
+
`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.`,
|
|
985
|
+
at
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
if (typeof input.renewOperation !== "string" || input.renewOperation.trim() === "") {
|
|
989
|
+
throw new ConnectorConfigError(
|
|
990
|
+
"renewOperation must name the operation the host calls when the lease is due; a due lease with nothing to call is one nobody renews.",
|
|
991
|
+
at
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
return { expiresAt: input.expiresAt, renewBeforeSeconds: input.renewBeforeSeconds, renewOperation: input.renewOperation };
|
|
995
|
+
}
|
|
996
|
+
function leaseRenewAt(lease) {
|
|
997
|
+
return new Date(instantMs(lease.expiresAt, "expiresAt", DEFAULT_CONTEXT) - lease.renewBeforeSeconds * 1e3).toISOString();
|
|
998
|
+
}
|
|
999
|
+
function leaseState(lease, now) {
|
|
1000
|
+
const at = now instanceof Date ? now.getTime() : instantMs(now, "now", DEFAULT_CONTEXT);
|
|
1001
|
+
const expires = instantMs(lease.expiresAt, "expiresAt", DEFAULT_CONTEXT);
|
|
1002
|
+
if (at >= expires) return "expired";
|
|
1003
|
+
if (at >= expires - lease.renewBeforeSeconds * 1e3) return "due";
|
|
1004
|
+
return "active";
|
|
1005
|
+
}
|
|
1006
|
+
function leaseAction(lease, now) {
|
|
1007
|
+
switch (leaseState(lease, now)) {
|
|
1008
|
+
case "active":
|
|
1009
|
+
return "none";
|
|
1010
|
+
case "due":
|
|
1011
|
+
return "renew";
|
|
1012
|
+
case "expired":
|
|
1013
|
+
return "resync";
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
916
1017
|
// src/metrics.ts
|
|
917
1018
|
function compareShape(connectorId, declared, produced) {
|
|
918
1019
|
const declaredKeys = new Set((declared ?? []).map((metric) => metric.key));
|
|
@@ -1007,6 +1108,245 @@ function providerSandboxIsSelectable(provider) {
|
|
|
1007
1108
|
return sandboxIsSelectable(provider.sandbox);
|
|
1008
1109
|
}
|
|
1009
1110
|
|
|
1111
|
+
// src/mime.ts
|
|
1112
|
+
var CR = 13;
|
|
1113
|
+
var LF = 10;
|
|
1114
|
+
function latin1(bytes) {
|
|
1115
|
+
let s = "";
|
|
1116
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
1117
|
+
return s;
|
|
1118
|
+
}
|
|
1119
|
+
function bytesOfLatin1(s) {
|
|
1120
|
+
const out = new Uint8Array(s.length);
|
|
1121
|
+
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 255;
|
|
1122
|
+
return out;
|
|
1123
|
+
}
|
|
1124
|
+
function splitEntity(bytes) {
|
|
1125
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1126
|
+
if (bytes[i] === LF) {
|
|
1127
|
+
if (bytes[i + 1] === LF) return [i, i + 2];
|
|
1128
|
+
if (bytes[i + 1] === CR && bytes[i + 2] === LF) return [i, i + 3];
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return [bytes.length, bytes.length];
|
|
1132
|
+
}
|
|
1133
|
+
function parseHeaders(block) {
|
|
1134
|
+
const lines = block.split(/\r?\n/);
|
|
1135
|
+
const unfolded = [];
|
|
1136
|
+
for (const line of lines) {
|
|
1137
|
+
if (line === "") continue;
|
|
1138
|
+
if ((line.startsWith(" ") || line.startsWith(" ")) && unfolded.length > 0) {
|
|
1139
|
+
unfolded[unfolded.length - 1] += " " + line.trim();
|
|
1140
|
+
} else {
|
|
1141
|
+
unfolded.push(line);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const headers = [];
|
|
1145
|
+
for (const line of unfolded) {
|
|
1146
|
+
const colon = line.indexOf(":");
|
|
1147
|
+
if (colon <= 0) continue;
|
|
1148
|
+
headers.push({ name: line.slice(0, colon).trim().toLowerCase(), value: decodeWords(line.slice(colon + 1).trim()) });
|
|
1149
|
+
}
|
|
1150
|
+
return headers;
|
|
1151
|
+
}
|
|
1152
|
+
function header(headers, name) {
|
|
1153
|
+
return headers.find((h) => h.name === name)?.value ?? null;
|
|
1154
|
+
}
|
|
1155
|
+
var ENCODED_WORD = /=\?([^?\s]+)\?([BbQq])\?([^?\s]*)\?=/g;
|
|
1156
|
+
function decodeWords(value) {
|
|
1157
|
+
const joined = value.replace(/(\?=)[ \t\r\n]+(=\?)/g, "$1$2");
|
|
1158
|
+
return joined.replace(ENCODED_WORD, (whole, charset, encoding, text) => {
|
|
1159
|
+
const bytes = encoding.toUpperCase() === "B" ? base64Bytes(text) : bytesOfLatin1(text.replace(/_/g, " ").replace(/=([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16))));
|
|
1160
|
+
const decoded = decodeCharset(bytes, charset);
|
|
1161
|
+
return decoded ?? whole;
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
function parseParams(value) {
|
|
1165
|
+
const [head, ...rest] = value.split(";");
|
|
1166
|
+
const params = {};
|
|
1167
|
+
const continued = {};
|
|
1168
|
+
for (const piece of rest) {
|
|
1169
|
+
const eq = piece.indexOf("=");
|
|
1170
|
+
if (eq < 0) continue;
|
|
1171
|
+
let key = piece.slice(0, eq).trim().toLowerCase();
|
|
1172
|
+
let raw = piece.slice(eq + 1).trim();
|
|
1173
|
+
if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) raw = raw.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
1174
|
+
const segment = /^(.+?)\*(\d+)(\*)?$/.exec(key);
|
|
1175
|
+
if (segment) {
|
|
1176
|
+
const [, base, n, star] = segment;
|
|
1177
|
+
(continued[base] ??= [])[Number(n)] = star ? decode2231(raw, Number(n) === 0) : raw;
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
if (key.endsWith("*")) {
|
|
1181
|
+
key = key.slice(0, -1);
|
|
1182
|
+
params[key] = decode2231(raw, true);
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
params[key] ??= raw;
|
|
1186
|
+
}
|
|
1187
|
+
for (const [base, parts] of Object.entries(continued)) params[base] = parts.join("");
|
|
1188
|
+
return { main: (head ?? "").trim().toLowerCase(), params };
|
|
1189
|
+
}
|
|
1190
|
+
var pendingCharset2231 = "utf-8";
|
|
1191
|
+
function decode2231(raw, first) {
|
|
1192
|
+
let text = raw;
|
|
1193
|
+
if (first) {
|
|
1194
|
+
const m = /^([^']*)'[^']*'(.*)$/s.exec(raw);
|
|
1195
|
+
if (m) {
|
|
1196
|
+
pendingCharset2231 = m[1] || "utf-8";
|
|
1197
|
+
text = m[2];
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
const bytes = bytesOfLatin1(text.replace(/%([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16))));
|
|
1201
|
+
return decodeCharset(bytes, pendingCharset2231) ?? text;
|
|
1202
|
+
}
|
|
1203
|
+
function base64Bytes(text) {
|
|
1204
|
+
return new Uint8Array(Buffer.from(text.replace(/[^A-Za-z0-9+/=]/g, ""), "base64"));
|
|
1205
|
+
}
|
|
1206
|
+
function quotedPrintableBytes(bytes) {
|
|
1207
|
+
const out = [];
|
|
1208
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1209
|
+
const b = bytes[i];
|
|
1210
|
+
if (b !== 61) {
|
|
1211
|
+
out.push(b);
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
const a = bytes[i + 1];
|
|
1215
|
+
const c = bytes[i + 2];
|
|
1216
|
+
if (a === LF) {
|
|
1217
|
+
i += 1;
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
if (a === CR && c === LF) {
|
|
1221
|
+
i += 2;
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
if (a !== void 0 && c !== void 0 && isHex(a) && isHex(c)) {
|
|
1225
|
+
out.push(parseInt(String.fromCharCode(a, c), 16));
|
|
1226
|
+
i += 2;
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
out.push(b);
|
|
1230
|
+
}
|
|
1231
|
+
return new Uint8Array(out);
|
|
1232
|
+
}
|
|
1233
|
+
function isHex(b) {
|
|
1234
|
+
return b >= 48 && b <= 57 || b >= 65 && b <= 70 || b >= 97 && b <= 102;
|
|
1235
|
+
}
|
|
1236
|
+
function transferDecode(body, encoding) {
|
|
1237
|
+
switch ((encoding ?? "7bit").trim().toLowerCase()) {
|
|
1238
|
+
case "base64":
|
|
1239
|
+
return base64Bytes(latin1(body));
|
|
1240
|
+
case "quoted-printable":
|
|
1241
|
+
return quotedPrintableBytes(body);
|
|
1242
|
+
default:
|
|
1243
|
+
return body;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
function decodeCharset(bytes, charset) {
|
|
1247
|
+
switch (charset.trim().toLowerCase()) {
|
|
1248
|
+
case "utf-8":
|
|
1249
|
+
case "utf8":
|
|
1250
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
1251
|
+
case "us-ascii":
|
|
1252
|
+
case "ascii":
|
|
1253
|
+
case "iso-8859-1":
|
|
1254
|
+
case "latin1":
|
|
1255
|
+
return Buffer.from(bytes).toString("latin1");
|
|
1256
|
+
case "windows-1252":
|
|
1257
|
+
case "cp1252":
|
|
1258
|
+
return new TextDecoder("windows-1252").decode(bytes);
|
|
1259
|
+
case "iso-8859-15":
|
|
1260
|
+
case "latin9":
|
|
1261
|
+
return new TextDecoder("iso-8859-15").decode(bytes);
|
|
1262
|
+
default:
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function walk(bytes, entityHeaders, acc) {
|
|
1267
|
+
const [headerEnd, bodyStart] = splitEntity(bytes);
|
|
1268
|
+
const headers = entityHeaders ?? parseHeaders(latin1(bytes.subarray(0, headerEnd)));
|
|
1269
|
+
const body = bytes.subarray(entityHeaders ? 0 : bodyStart);
|
|
1270
|
+
const type = parseParams(header(headers, "content-type") ?? "text/plain");
|
|
1271
|
+
if (type.main.startsWith("multipart/") && type.params.boundary) {
|
|
1272
|
+
for (const child of splitMultipart(body, type.params.boundary)) walk(child, null, acc);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const disposition = parseParams(header(headers, "content-disposition") ?? "");
|
|
1276
|
+
const decoded = transferDecode(body, header(headers, "content-transfer-encoding"));
|
|
1277
|
+
const declaredDisposition = disposition.main === "attachment" || disposition.main === "inline" ? disposition.main : null;
|
|
1278
|
+
const filename = disposition.params.filename ?? type.params.name ?? null;
|
|
1279
|
+
const contentId = header(headers, "content-id")?.replace(/^<|>$/g, "") ?? null;
|
|
1280
|
+
const isText = type.main.startsWith("text/");
|
|
1281
|
+
const isAttachment = declaredDisposition === "attachment" || !isText;
|
|
1282
|
+
const part = {
|
|
1283
|
+
index: acc.parts.length,
|
|
1284
|
+
contentType: type.main,
|
|
1285
|
+
charset: type.params.charset?.toLowerCase() ?? null,
|
|
1286
|
+
disposition: declaredDisposition,
|
|
1287
|
+
filename,
|
|
1288
|
+
contentId,
|
|
1289
|
+
size: decoded.length,
|
|
1290
|
+
headers
|
|
1291
|
+
};
|
|
1292
|
+
acc.parts.push(part);
|
|
1293
|
+
if (isAttachment) {
|
|
1294
|
+
acc.attachments.push({ ...part, content: Buffer.from(decoded).toString("base64") });
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
const text = decodeCharset(decoded, type.params.charset ?? "utf-8");
|
|
1298
|
+
if (text === null) return;
|
|
1299
|
+
if (type.main === "text/plain" && acc.text === null) acc.text = text;
|
|
1300
|
+
if (type.main === "text/html" && acc.html === null) acc.html = text;
|
|
1301
|
+
}
|
|
1302
|
+
function splitMultipart(body, boundary) {
|
|
1303
|
+
const delimiter = bytesOfLatin1(`--${boundary}`);
|
|
1304
|
+
const children = [];
|
|
1305
|
+
let partStart = -1;
|
|
1306
|
+
for (let i = 0; i <= body.length - delimiter.length; i++) {
|
|
1307
|
+
if (i !== 0 && body[i - 1] !== LF) continue;
|
|
1308
|
+
if (!startsWith(body, i, delimiter)) continue;
|
|
1309
|
+
let partEnd = i;
|
|
1310
|
+
if (partEnd > 0 && body[partEnd - 1] === LF) partEnd -= 1;
|
|
1311
|
+
if (partEnd > 0 && body[partEnd - 1] === CR) partEnd -= 1;
|
|
1312
|
+
if (partStart >= 0) children.push(body.subarray(partStart, partEnd));
|
|
1313
|
+
let j = i + delimiter.length;
|
|
1314
|
+
const closing = body[j] === 45 && body[j + 1] === 45;
|
|
1315
|
+
if (closing) return children;
|
|
1316
|
+
while (j < body.length && body[j] !== LF) j++;
|
|
1317
|
+
partStart = j + 1;
|
|
1318
|
+
i = j;
|
|
1319
|
+
}
|
|
1320
|
+
if (partStart >= 0 && partStart <= body.length) children.push(body.subarray(partStart));
|
|
1321
|
+
return children;
|
|
1322
|
+
}
|
|
1323
|
+
function startsWith(haystack, at, needle) {
|
|
1324
|
+
for (let k = 0; k < needle.length; k++) if (haystack[at + k] !== needle[k]) return false;
|
|
1325
|
+
return true;
|
|
1326
|
+
}
|
|
1327
|
+
function parseMime(raw) {
|
|
1328
|
+
const bytes = typeof raw === "string" ? new Uint8Array(Buffer.from(raw, "utf8")) : raw;
|
|
1329
|
+
const [headerEnd, bodyStart] = splitEntity(bytes);
|
|
1330
|
+
const headers = parseHeaders(latin1(bytes.subarray(0, headerEnd)));
|
|
1331
|
+
const body = bytes.subarray(bodyStart);
|
|
1332
|
+
const acc = { parts: [], attachments: [], text: null, html: null };
|
|
1333
|
+
const type = parseParams(header(headers, "content-type") ?? "text/plain");
|
|
1334
|
+
if (type.main.startsWith("multipart/") && type.params.boundary) {
|
|
1335
|
+
for (const child of splitMultipart(body, type.params.boundary)) walk(child, null, acc);
|
|
1336
|
+
} else {
|
|
1337
|
+
walk(body, headers.filter((h) => h.name.startsWith("content-")), acc);
|
|
1338
|
+
}
|
|
1339
|
+
return {
|
|
1340
|
+
headers,
|
|
1341
|
+
subject: header(headers, "subject"),
|
|
1342
|
+
messageId: header(headers, "message-id"),
|
|
1343
|
+
text: acc.text,
|
|
1344
|
+
html: acc.html,
|
|
1345
|
+
parts: acc.parts,
|
|
1346
|
+
attachments: acc.attachments
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1010
1350
|
// src/probe.ts
|
|
1011
1351
|
async function probe(spec) {
|
|
1012
1352
|
let response;
|
|
@@ -1266,7 +1606,53 @@ function constantTimeEquals(a, b) {
|
|
|
1266
1606
|
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1267
1607
|
return diff === 0;
|
|
1268
1608
|
}
|
|
1269
|
-
function
|
|
1609
|
+
function isSharedTokenScheme(scheme) {
|
|
1610
|
+
return "kind" in scheme && scheme.kind === "shared-token";
|
|
1611
|
+
}
|
|
1612
|
+
function verifySharedToken(options) {
|
|
1613
|
+
const { raw, headers, secret, scheme } = options;
|
|
1614
|
+
if (!secret) {
|
|
1615
|
+
return { ok: false, reason: "no shared token configured for this trigger" };
|
|
1616
|
+
}
|
|
1617
|
+
let tokens;
|
|
1618
|
+
if (scheme.in === "header") {
|
|
1619
|
+
tokens = [header2(headers, scheme.name)];
|
|
1620
|
+
} else {
|
|
1621
|
+
let parsed;
|
|
1622
|
+
try {
|
|
1623
|
+
parsed = JSON.parse(raw);
|
|
1624
|
+
} catch {
|
|
1625
|
+
return { ok: false, reason: "delivery body is not JSON" };
|
|
1626
|
+
}
|
|
1627
|
+
tokens = readTokens(parsed, scheme.path.split("."));
|
|
1628
|
+
}
|
|
1629
|
+
const present = tokens.filter((t) => t !== void 0 && t !== null && t !== "");
|
|
1630
|
+
if (present.length === 0) return { ok: false, reason: "delivery carried no token" };
|
|
1631
|
+
let matched = present.length === tokens.length;
|
|
1632
|
+
for (const token of present) {
|
|
1633
|
+
matched = typeof token === "string" && constantTimeEquals(secret, token) && matched;
|
|
1634
|
+
}
|
|
1635
|
+
return matched ? { ok: true } : { ok: false, reason: "token did not match" };
|
|
1636
|
+
}
|
|
1637
|
+
function readTokens(value, segments) {
|
|
1638
|
+
if (segments.length === 0) return [value];
|
|
1639
|
+
const [head, ...rest] = segments;
|
|
1640
|
+
const eachElement = head.endsWith("[]");
|
|
1641
|
+
const key = eachElement ? head.slice(0, -2) : head;
|
|
1642
|
+
if (value === null || typeof value !== "object") return [void 0];
|
|
1643
|
+
const next = value[key];
|
|
1644
|
+
if (!eachElement) return readTokens(next, rest);
|
|
1645
|
+
if (!Array.isArray(next)) return [void 0];
|
|
1646
|
+
return next.flatMap((element) => readTokens(element, rest));
|
|
1647
|
+
}
|
|
1648
|
+
function handshakeResponse(handshake, query) {
|
|
1649
|
+
if (!handshake) return void 0;
|
|
1650
|
+
const raw = query[handshake.param];
|
|
1651
|
+
const token = Array.isArray(raw) ? raw[0] : raw;
|
|
1652
|
+
if (!token) return void 0;
|
|
1653
|
+
return { status: 200, contentType: handshake.contentType ?? "text/plain", body: token };
|
|
1654
|
+
}
|
|
1655
|
+
function header2(headers, name) {
|
|
1270
1656
|
const wanted = name.toLowerCase();
|
|
1271
1657
|
for (const [key, value] of Object.entries(headers)) {
|
|
1272
1658
|
if (key.toLowerCase() !== wanted) continue;
|
|
@@ -1276,6 +1662,9 @@ function header(headers, name) {
|
|
|
1276
1662
|
}
|
|
1277
1663
|
|
|
1278
1664
|
// src/trigger.ts
|
|
1665
|
+
function isSharedTokenSpec(spec) {
|
|
1666
|
+
return isSharedTokenScheme(spec.scheme);
|
|
1667
|
+
}
|
|
1279
1668
|
async function verifyDelivery(trigger, delivery, secret, now) {
|
|
1280
1669
|
const spec = trigger.verification;
|
|
1281
1670
|
if (!spec) {
|
|
@@ -1284,9 +1673,12 @@ async function verifyDelivery(trigger, delivery, secret, now) {
|
|
|
1284
1673
|
reason: `${trigger.service}.${trigger.operation} declares no signature scheme, so a delivery cannot be trusted.`
|
|
1285
1674
|
};
|
|
1286
1675
|
}
|
|
1287
|
-
|
|
1676
|
+
if (isSharedTokenSpec(spec)) {
|
|
1677
|
+
return verifySharedToken({ raw: delivery.raw, headers: delivery.headers, secret, scheme: spec.scheme });
|
|
1678
|
+
}
|
|
1679
|
+
const rawHeader = header2(delivery.headers, spec.signatureHeader);
|
|
1288
1680
|
const parsed = spec.parse && rawHeader ? spec.parse(rawHeader) : { signature: rawHeader };
|
|
1289
|
-
const timestamp = parsed.timestamp ?? (spec.timestampHeader ?
|
|
1681
|
+
const timestamp = parsed.timestamp ?? (spec.timestampHeader ? header2(delivery.headers, spec.timestampHeader) : void 0);
|
|
1290
1682
|
return verifyHmac({
|
|
1291
1683
|
raw: delivery.raw,
|
|
1292
1684
|
signature: parsed.signature,
|
|
@@ -1333,6 +1725,7 @@ export {
|
|
|
1333
1725
|
ConnectorUnreachable,
|
|
1334
1726
|
DEFAULT_IDEMPOTENCY_WINDOW_SECONDS,
|
|
1335
1727
|
DEFAULT_RETRY,
|
|
1728
|
+
EXPIRES_AT_UNITS,
|
|
1336
1729
|
FAKE_EPOCH,
|
|
1337
1730
|
MAX_IDEMPOTENCY_KEY_LENGTH,
|
|
1338
1731
|
NO_IDEMPOTENCY_KEY_WARNING,
|
|
@@ -1359,15 +1752,23 @@ export {
|
|
|
1359
1752
|
fakeValues,
|
|
1360
1753
|
getConnectionHost,
|
|
1361
1754
|
graphemes,
|
|
1362
|
-
|
|
1755
|
+
handshakeResponse,
|
|
1756
|
+
header2 as header,
|
|
1363
1757
|
hmac,
|
|
1364
1758
|
httpFailure,
|
|
1365
1759
|
idempotencyKeyFor,
|
|
1366
1760
|
isEmptyPayload,
|
|
1367
1761
|
isRemote,
|
|
1762
|
+
isSharedTokenScheme,
|
|
1763
|
+
isSharedTokenSpec,
|
|
1368
1764
|
isUnconditionallyRetryable,
|
|
1765
|
+
leaseAction,
|
|
1766
|
+
leaseFromResponse,
|
|
1767
|
+
leaseRenewAt,
|
|
1768
|
+
leaseState,
|
|
1369
1769
|
linkRanges,
|
|
1370
1770
|
measure,
|
|
1771
|
+
parseMime,
|
|
1371
1772
|
payloadHash,
|
|
1372
1773
|
postChain,
|
|
1373
1774
|
probe,
|
|
@@ -1392,11 +1793,13 @@ export {
|
|
|
1392
1793
|
shouldRetry,
|
|
1393
1794
|
sliceByteRange,
|
|
1394
1795
|
splitToFit,
|
|
1796
|
+
subscriptionLease,
|
|
1395
1797
|
triggerEvent,
|
|
1396
1798
|
unchecked,
|
|
1397
1799
|
utf8Length,
|
|
1398
1800
|
verifyDelivery,
|
|
1399
1801
|
verifyHmac,
|
|
1802
|
+
verifySharedToken,
|
|
1400
1803
|
withResolvedLimit
|
|
1401
1804
|
};
|
|
1402
1805
|
//# sourceMappingURL=index.js.map
|