@particle-academy/fancy-connector-core 0.6.2 → 0.7.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 +28 -0
- package/dist/index.cjs +310 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -1
- package/dist/index.d.ts +114 -1
- package/dist/index.js +305 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@ All notable changes to `fancy-connector-core` are documented here, in
|
|
|
6
6
|
**This package is pre-1.0, so breaking changes land in MINOR releases.** The
|
|
7
7
|
version number is not a promise it can keep yet; the entries below are.
|
|
8
8
|
|
|
9
|
+
## [0.7.0] - 2026-09-15
|
|
10
|
+
|
|
11
|
+
Two additions to the connector vocabulary, both approved by the owner on
|
|
12
|
+
2026-09-14 and both meeting the two-provider bar before shipping: the
|
|
13
|
+
subscription lease is for Google Calendar `events.watch` and Microsoft Graph
|
|
14
|
+
subscriptions; the MIME transform is for inbound email, provider-agnostic,
|
|
15
|
+
with Resend first.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`SubscriptionLease`** (`src/lease.ts`, `SubscriptionLease.php` with
|
|
20
|
+
`LeaseState` and `LeaseAction`) — the duty a `subscription` trigger carries,
|
|
21
|
+
as one value: the provider's expiry as an RFC 3339 instant, the connector's
|
|
22
|
+
`renewBeforeSeconds`, and the `renewOperation` the host calls when due.
|
|
23
|
+
`leaseState` says where it is (active / due / expired — both boundaries
|
|
24
|
+
inclusive, expired wins) and `leaseAction` what the host does (none / renew /
|
|
25
|
+
resync; a missed lease is resync, never a quiet re-create). Both runtimes
|
|
26
|
+
read `fixtures/subscription-lease/cases.json`. A host runs ONE renewal
|
|
27
|
+
scheduler for every expiring trigger instead of one per connector.
|
|
28
|
+
- **`parseMime` / `Mime::parse`** — raw MIME in, `headers`, `subject`,
|
|
29
|
+
`messageId`, `text`, `html`, `parts` and `attachments` out, identical in both
|
|
30
|
+
runtimes and held to the authored corpus under `fixtures/mime/` (CRLF and
|
|
31
|
+
LF; folded headers; RFC 2047 B and Q words; quoted-printable and base64;
|
|
32
|
+
nested multipart; RFC 2231 `filename*`; utf-8, us-ascii, iso-8859-1,
|
|
33
|
+
iso-8859-15 and windows-1252 — any other charset leaves a part with no
|
|
34
|
+
text). Attachments carry their bytes as base64. **What to do:** nothing
|
|
35
|
+
unless you want them; neither touches an existing surface.
|
|
36
|
+
|
|
9
37
|
## [0.6.2] - 2026-09-14
|
|
10
38
|
|
|
11
39
|
### Fixed
|
package/dist/index.cjs
CHANGED
|
@@ -64,15 +64,19 @@ __export(src_exports, {
|
|
|
64
64
|
fakeValues: () => fakeValues,
|
|
65
65
|
getConnectionHost: () => getConnectionHost,
|
|
66
66
|
graphemes: () => graphemes,
|
|
67
|
-
header: () =>
|
|
67
|
+
header: () => header2,
|
|
68
68
|
hmac: () => hmac,
|
|
69
69
|
httpFailure: () => httpFailure,
|
|
70
70
|
idempotencyKeyFor: () => idempotencyKeyFor,
|
|
71
71
|
isEmptyPayload: () => isEmptyPayload,
|
|
72
72
|
isRemote: () => isRemote,
|
|
73
73
|
isUnconditionallyRetryable: () => isUnconditionallyRetryable,
|
|
74
|
+
leaseAction: () => leaseAction,
|
|
75
|
+
leaseRenewAt: () => leaseRenewAt,
|
|
76
|
+
leaseState: () => leaseState,
|
|
74
77
|
linkRanges: () => linkRanges,
|
|
75
78
|
measure: () => measure,
|
|
79
|
+
parseMime: () => parseMime,
|
|
76
80
|
payloadHash: () => payloadHash,
|
|
77
81
|
postChain: () => postChain,
|
|
78
82
|
probe: () => probe,
|
|
@@ -97,6 +101,7 @@ __export(src_exports, {
|
|
|
97
101
|
shouldRetry: () => shouldRetry,
|
|
98
102
|
sliceByteRange: () => sliceByteRange,
|
|
99
103
|
splitToFit: () => splitToFit,
|
|
104
|
+
subscriptionLease: () => subscriptionLease,
|
|
100
105
|
triggerEvent: () => triggerEvent,
|
|
101
106
|
unchecked: () => unchecked,
|
|
102
107
|
utf8Length: () => utf8Length,
|
|
@@ -703,21 +708,21 @@ async function prepare(service, connection, options) {
|
|
|
703
708
|
}
|
|
704
709
|
function encodeForm(form) {
|
|
705
710
|
const pairs = [];
|
|
706
|
-
const
|
|
711
|
+
const walk2 = (prefix, value) => {
|
|
707
712
|
if (value === void 0 || value === null) return;
|
|
708
713
|
if (Array.isArray(value)) {
|
|
709
|
-
value.forEach((item, index) =>
|
|
714
|
+
value.forEach((item, index) => walk2(`${prefix}[${index}]`, item));
|
|
710
715
|
return;
|
|
711
716
|
}
|
|
712
717
|
if (typeof value === "object") {
|
|
713
718
|
for (const [key, nested] of Object.entries(value)) {
|
|
714
|
-
|
|
719
|
+
walk2(prefix === "" ? key : `${prefix}[${key}]`, nested);
|
|
715
720
|
}
|
|
716
721
|
return;
|
|
717
722
|
}
|
|
718
723
|
pairs.push(`${encodeURIComponent(prefix)}=${encodeURIComponent(String(value))}`);
|
|
719
724
|
};
|
|
720
|
-
|
|
725
|
+
walk2("", form);
|
|
721
726
|
return pairs.join("&");
|
|
722
727
|
}
|
|
723
728
|
var fetchTransport = async (request) => {
|
|
@@ -1021,6 +1026,59 @@ function fnv1a(value) {
|
|
|
1021
1026
|
return hash.toString(16).padStart(8, "0");
|
|
1022
1027
|
}
|
|
1023
1028
|
|
|
1029
|
+
// src/lease.ts
|
|
1030
|
+
var INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
1031
|
+
var DEFAULT_CONTEXT = { service: "", operation: "subscription" };
|
|
1032
|
+
function instantMs(value, field, at) {
|
|
1033
|
+
if (typeof value !== "string" || !INSTANT.test(value)) {
|
|
1034
|
+
throw new ConnectorConfigError(
|
|
1035
|
+
`${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.`,
|
|
1036
|
+
at
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
const ms = Date.parse(value);
|
|
1040
|
+
if (Number.isNaN(ms)) {
|
|
1041
|
+
throw new ConnectorConfigError(`${field} is not a real instant: ${JSON.stringify(value)}`, at);
|
|
1042
|
+
}
|
|
1043
|
+
return ms;
|
|
1044
|
+
}
|
|
1045
|
+
function subscriptionLease(input, at = DEFAULT_CONTEXT) {
|
|
1046
|
+
instantMs(input.expiresAt, "expiresAt", at);
|
|
1047
|
+
if (!Number.isInteger(input.renewBeforeSeconds) || input.renewBeforeSeconds <= 0) {
|
|
1048
|
+
throw new ConnectorConfigError(
|
|
1049
|
+
`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.`,
|
|
1050
|
+
at
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
if (typeof input.renewOperation !== "string" || input.renewOperation.trim() === "") {
|
|
1054
|
+
throw new ConnectorConfigError(
|
|
1055
|
+
"renewOperation must name the operation the host calls when the lease is due; a due lease with nothing to call is one nobody renews.",
|
|
1056
|
+
at
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
return { expiresAt: input.expiresAt, renewBeforeSeconds: input.renewBeforeSeconds, renewOperation: input.renewOperation };
|
|
1060
|
+
}
|
|
1061
|
+
function leaseRenewAt(lease) {
|
|
1062
|
+
return new Date(instantMs(lease.expiresAt, "expiresAt", DEFAULT_CONTEXT) - lease.renewBeforeSeconds * 1e3).toISOString();
|
|
1063
|
+
}
|
|
1064
|
+
function leaseState(lease, now) {
|
|
1065
|
+
const at = now instanceof Date ? now.getTime() : instantMs(now, "now", DEFAULT_CONTEXT);
|
|
1066
|
+
const expires = instantMs(lease.expiresAt, "expiresAt", DEFAULT_CONTEXT);
|
|
1067
|
+
if (at >= expires) return "expired";
|
|
1068
|
+
if (at >= expires - lease.renewBeforeSeconds * 1e3) return "due";
|
|
1069
|
+
return "active";
|
|
1070
|
+
}
|
|
1071
|
+
function leaseAction(lease, now) {
|
|
1072
|
+
switch (leaseState(lease, now)) {
|
|
1073
|
+
case "active":
|
|
1074
|
+
return "none";
|
|
1075
|
+
case "due":
|
|
1076
|
+
return "renew";
|
|
1077
|
+
case "expired":
|
|
1078
|
+
return "resync";
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1024
1082
|
// src/metrics.ts
|
|
1025
1083
|
function compareShape(connectorId, declared, produced) {
|
|
1026
1084
|
const declaredKeys = new Set((declared ?? []).map((metric) => metric.key));
|
|
@@ -1115,6 +1173,245 @@ function providerSandboxIsSelectable(provider) {
|
|
|
1115
1173
|
return sandboxIsSelectable(provider.sandbox);
|
|
1116
1174
|
}
|
|
1117
1175
|
|
|
1176
|
+
// src/mime.ts
|
|
1177
|
+
var CR = 13;
|
|
1178
|
+
var LF = 10;
|
|
1179
|
+
function latin1(bytes) {
|
|
1180
|
+
let s = "";
|
|
1181
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
1182
|
+
return s;
|
|
1183
|
+
}
|
|
1184
|
+
function bytesOfLatin1(s) {
|
|
1185
|
+
const out = new Uint8Array(s.length);
|
|
1186
|
+
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 255;
|
|
1187
|
+
return out;
|
|
1188
|
+
}
|
|
1189
|
+
function splitEntity(bytes) {
|
|
1190
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1191
|
+
if (bytes[i] === LF) {
|
|
1192
|
+
if (bytes[i + 1] === LF) return [i, i + 2];
|
|
1193
|
+
if (bytes[i + 1] === CR && bytes[i + 2] === LF) return [i, i + 3];
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return [bytes.length, bytes.length];
|
|
1197
|
+
}
|
|
1198
|
+
function parseHeaders(block) {
|
|
1199
|
+
const lines = block.split(/\r?\n/);
|
|
1200
|
+
const unfolded = [];
|
|
1201
|
+
for (const line of lines) {
|
|
1202
|
+
if (line === "") continue;
|
|
1203
|
+
if ((line.startsWith(" ") || line.startsWith(" ")) && unfolded.length > 0) {
|
|
1204
|
+
unfolded[unfolded.length - 1] += " " + line.trim();
|
|
1205
|
+
} else {
|
|
1206
|
+
unfolded.push(line);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
const headers = [];
|
|
1210
|
+
for (const line of unfolded) {
|
|
1211
|
+
const colon = line.indexOf(":");
|
|
1212
|
+
if (colon <= 0) continue;
|
|
1213
|
+
headers.push({ name: line.slice(0, colon).trim().toLowerCase(), value: decodeWords(line.slice(colon + 1).trim()) });
|
|
1214
|
+
}
|
|
1215
|
+
return headers;
|
|
1216
|
+
}
|
|
1217
|
+
function header(headers, name) {
|
|
1218
|
+
return headers.find((h) => h.name === name)?.value ?? null;
|
|
1219
|
+
}
|
|
1220
|
+
var ENCODED_WORD = /=\?([^?\s]+)\?([BbQq])\?([^?\s]*)\?=/g;
|
|
1221
|
+
function decodeWords(value) {
|
|
1222
|
+
const joined = value.replace(/(\?=)[ \t\r\n]+(=\?)/g, "$1$2");
|
|
1223
|
+
return joined.replace(ENCODED_WORD, (whole, charset, encoding, text) => {
|
|
1224
|
+
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))));
|
|
1225
|
+
const decoded = decodeCharset(bytes, charset);
|
|
1226
|
+
return decoded ?? whole;
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
function parseParams(value) {
|
|
1230
|
+
const [head, ...rest] = value.split(";");
|
|
1231
|
+
const params = {};
|
|
1232
|
+
const continued = {};
|
|
1233
|
+
for (const piece of rest) {
|
|
1234
|
+
const eq = piece.indexOf("=");
|
|
1235
|
+
if (eq < 0) continue;
|
|
1236
|
+
let key = piece.slice(0, eq).trim().toLowerCase();
|
|
1237
|
+
let raw = piece.slice(eq + 1).trim();
|
|
1238
|
+
if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) raw = raw.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
1239
|
+
const segment = /^(.+?)\*(\d+)(\*)?$/.exec(key);
|
|
1240
|
+
if (segment) {
|
|
1241
|
+
const [, base, n, star] = segment;
|
|
1242
|
+
(continued[base] ??= [])[Number(n)] = star ? decode2231(raw, Number(n) === 0) : raw;
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (key.endsWith("*")) {
|
|
1246
|
+
key = key.slice(0, -1);
|
|
1247
|
+
params[key] = decode2231(raw, true);
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
params[key] ??= raw;
|
|
1251
|
+
}
|
|
1252
|
+
for (const [base, parts] of Object.entries(continued)) params[base] = parts.join("");
|
|
1253
|
+
return { main: (head ?? "").trim().toLowerCase(), params };
|
|
1254
|
+
}
|
|
1255
|
+
var pendingCharset2231 = "utf-8";
|
|
1256
|
+
function decode2231(raw, first) {
|
|
1257
|
+
let text = raw;
|
|
1258
|
+
if (first) {
|
|
1259
|
+
const m = /^([^']*)'[^']*'(.*)$/s.exec(raw);
|
|
1260
|
+
if (m) {
|
|
1261
|
+
pendingCharset2231 = m[1] || "utf-8";
|
|
1262
|
+
text = m[2];
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
const bytes = bytesOfLatin1(text.replace(/%([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16))));
|
|
1266
|
+
return decodeCharset(bytes, pendingCharset2231) ?? text;
|
|
1267
|
+
}
|
|
1268
|
+
function base64Bytes(text) {
|
|
1269
|
+
return new Uint8Array(Buffer.from(text.replace(/[^A-Za-z0-9+/=]/g, ""), "base64"));
|
|
1270
|
+
}
|
|
1271
|
+
function quotedPrintableBytes(bytes) {
|
|
1272
|
+
const out = [];
|
|
1273
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1274
|
+
const b = bytes[i];
|
|
1275
|
+
if (b !== 61) {
|
|
1276
|
+
out.push(b);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
const a = bytes[i + 1];
|
|
1280
|
+
const c = bytes[i + 2];
|
|
1281
|
+
if (a === LF) {
|
|
1282
|
+
i += 1;
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
if (a === CR && c === LF) {
|
|
1286
|
+
i += 2;
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
if (a !== void 0 && c !== void 0 && isHex(a) && isHex(c)) {
|
|
1290
|
+
out.push(parseInt(String.fromCharCode(a, c), 16));
|
|
1291
|
+
i += 2;
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
out.push(b);
|
|
1295
|
+
}
|
|
1296
|
+
return new Uint8Array(out);
|
|
1297
|
+
}
|
|
1298
|
+
function isHex(b) {
|
|
1299
|
+
return b >= 48 && b <= 57 || b >= 65 && b <= 70 || b >= 97 && b <= 102;
|
|
1300
|
+
}
|
|
1301
|
+
function transferDecode(body, encoding) {
|
|
1302
|
+
switch ((encoding ?? "7bit").trim().toLowerCase()) {
|
|
1303
|
+
case "base64":
|
|
1304
|
+
return base64Bytes(latin1(body));
|
|
1305
|
+
case "quoted-printable":
|
|
1306
|
+
return quotedPrintableBytes(body);
|
|
1307
|
+
default:
|
|
1308
|
+
return body;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
function decodeCharset(bytes, charset) {
|
|
1312
|
+
switch (charset.trim().toLowerCase()) {
|
|
1313
|
+
case "utf-8":
|
|
1314
|
+
case "utf8":
|
|
1315
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
1316
|
+
case "us-ascii":
|
|
1317
|
+
case "ascii":
|
|
1318
|
+
case "iso-8859-1":
|
|
1319
|
+
case "latin1":
|
|
1320
|
+
return Buffer.from(bytes).toString("latin1");
|
|
1321
|
+
case "windows-1252":
|
|
1322
|
+
case "cp1252":
|
|
1323
|
+
return new TextDecoder("windows-1252").decode(bytes);
|
|
1324
|
+
case "iso-8859-15":
|
|
1325
|
+
case "latin9":
|
|
1326
|
+
return new TextDecoder("iso-8859-15").decode(bytes);
|
|
1327
|
+
default:
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function walk(bytes, entityHeaders, acc) {
|
|
1332
|
+
const [headerEnd, bodyStart] = splitEntity(bytes);
|
|
1333
|
+
const headers = entityHeaders ?? parseHeaders(latin1(bytes.subarray(0, headerEnd)));
|
|
1334
|
+
const body = bytes.subarray(entityHeaders ? 0 : bodyStart);
|
|
1335
|
+
const type = parseParams(header(headers, "content-type") ?? "text/plain");
|
|
1336
|
+
if (type.main.startsWith("multipart/") && type.params.boundary) {
|
|
1337
|
+
for (const child of splitMultipart(body, type.params.boundary)) walk(child, null, acc);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const disposition = parseParams(header(headers, "content-disposition") ?? "");
|
|
1341
|
+
const decoded = transferDecode(body, header(headers, "content-transfer-encoding"));
|
|
1342
|
+
const declaredDisposition = disposition.main === "attachment" || disposition.main === "inline" ? disposition.main : null;
|
|
1343
|
+
const filename = disposition.params.filename ?? type.params.name ?? null;
|
|
1344
|
+
const contentId = header(headers, "content-id")?.replace(/^<|>$/g, "") ?? null;
|
|
1345
|
+
const isText = type.main.startsWith("text/");
|
|
1346
|
+
const isAttachment = declaredDisposition === "attachment" || !isText;
|
|
1347
|
+
const part = {
|
|
1348
|
+
index: acc.parts.length,
|
|
1349
|
+
contentType: type.main,
|
|
1350
|
+
charset: type.params.charset?.toLowerCase() ?? null,
|
|
1351
|
+
disposition: declaredDisposition,
|
|
1352
|
+
filename,
|
|
1353
|
+
contentId,
|
|
1354
|
+
size: decoded.length,
|
|
1355
|
+
headers
|
|
1356
|
+
};
|
|
1357
|
+
acc.parts.push(part);
|
|
1358
|
+
if (isAttachment) {
|
|
1359
|
+
acc.attachments.push({ ...part, content: Buffer.from(decoded).toString("base64") });
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
const text = decodeCharset(decoded, type.params.charset ?? "utf-8");
|
|
1363
|
+
if (text === null) return;
|
|
1364
|
+
if (type.main === "text/plain" && acc.text === null) acc.text = text;
|
|
1365
|
+
if (type.main === "text/html" && acc.html === null) acc.html = text;
|
|
1366
|
+
}
|
|
1367
|
+
function splitMultipart(body, boundary) {
|
|
1368
|
+
const delimiter = bytesOfLatin1(`--${boundary}`);
|
|
1369
|
+
const children = [];
|
|
1370
|
+
let partStart = -1;
|
|
1371
|
+
for (let i = 0; i <= body.length - delimiter.length; i++) {
|
|
1372
|
+
if (i !== 0 && body[i - 1] !== LF) continue;
|
|
1373
|
+
if (!startsWith(body, i, delimiter)) continue;
|
|
1374
|
+
let partEnd = i;
|
|
1375
|
+
if (partEnd > 0 && body[partEnd - 1] === LF) partEnd -= 1;
|
|
1376
|
+
if (partEnd > 0 && body[partEnd - 1] === CR) partEnd -= 1;
|
|
1377
|
+
if (partStart >= 0) children.push(body.subarray(partStart, partEnd));
|
|
1378
|
+
let j = i + delimiter.length;
|
|
1379
|
+
const closing = body[j] === 45 && body[j + 1] === 45;
|
|
1380
|
+
if (closing) return children;
|
|
1381
|
+
while (j < body.length && body[j] !== LF) j++;
|
|
1382
|
+
partStart = j + 1;
|
|
1383
|
+
i = j;
|
|
1384
|
+
}
|
|
1385
|
+
if (partStart >= 0 && partStart <= body.length) children.push(body.subarray(partStart));
|
|
1386
|
+
return children;
|
|
1387
|
+
}
|
|
1388
|
+
function startsWith(haystack, at, needle) {
|
|
1389
|
+
for (let k = 0; k < needle.length; k++) if (haystack[at + k] !== needle[k]) return false;
|
|
1390
|
+
return true;
|
|
1391
|
+
}
|
|
1392
|
+
function parseMime(raw) {
|
|
1393
|
+
const bytes = typeof raw === "string" ? new Uint8Array(Buffer.from(raw, "utf8")) : raw;
|
|
1394
|
+
const [headerEnd, bodyStart] = splitEntity(bytes);
|
|
1395
|
+
const headers = parseHeaders(latin1(bytes.subarray(0, headerEnd)));
|
|
1396
|
+
const body = bytes.subarray(bodyStart);
|
|
1397
|
+
const acc = { parts: [], attachments: [], text: null, html: null };
|
|
1398
|
+
const type = parseParams(header(headers, "content-type") ?? "text/plain");
|
|
1399
|
+
if (type.main.startsWith("multipart/") && type.params.boundary) {
|
|
1400
|
+
for (const child of splitMultipart(body, type.params.boundary)) walk(child, null, acc);
|
|
1401
|
+
} else {
|
|
1402
|
+
walk(body, headers.filter((h) => h.name.startsWith("content-")), acc);
|
|
1403
|
+
}
|
|
1404
|
+
return {
|
|
1405
|
+
headers,
|
|
1406
|
+
subject: header(headers, "subject"),
|
|
1407
|
+
messageId: header(headers, "message-id"),
|
|
1408
|
+
text: acc.text,
|
|
1409
|
+
html: acc.html,
|
|
1410
|
+
parts: acc.parts,
|
|
1411
|
+
attachments: acc.attachments
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1118
1415
|
// src/probe.ts
|
|
1119
1416
|
async function probe(spec) {
|
|
1120
1417
|
let response;
|
|
@@ -1374,7 +1671,7 @@ function constantTimeEquals(a, b) {
|
|
|
1374
1671
|
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1375
1672
|
return diff === 0;
|
|
1376
1673
|
}
|
|
1377
|
-
function
|
|
1674
|
+
function header2(headers, name) {
|
|
1378
1675
|
const wanted = name.toLowerCase();
|
|
1379
1676
|
for (const [key, value] of Object.entries(headers)) {
|
|
1380
1677
|
if (key.toLowerCase() !== wanted) continue;
|
|
@@ -1392,9 +1689,9 @@ async function verifyDelivery(trigger, delivery, secret, now) {
|
|
|
1392
1689
|
reason: `${trigger.service}.${trigger.operation} declares no signature scheme, so a delivery cannot be trusted.`
|
|
1393
1690
|
};
|
|
1394
1691
|
}
|
|
1395
|
-
const rawHeader =
|
|
1692
|
+
const rawHeader = header2(delivery.headers, spec.signatureHeader);
|
|
1396
1693
|
const parsed = spec.parse && rawHeader ? spec.parse(rawHeader) : { signature: rawHeader };
|
|
1397
|
-
const timestamp = parsed.timestamp ?? (spec.timestampHeader ?
|
|
1694
|
+
const timestamp = parsed.timestamp ?? (spec.timestampHeader ? header2(delivery.headers, spec.timestampHeader) : void 0);
|
|
1398
1695
|
return verifyHmac({
|
|
1399
1696
|
raw: delivery.raw,
|
|
1400
1697
|
signature: parsed.signature,
|
|
@@ -1475,8 +1772,12 @@ function hasDelivery(value) {
|
|
|
1475
1772
|
isEmptyPayload,
|
|
1476
1773
|
isRemote,
|
|
1477
1774
|
isUnconditionallyRetryable,
|
|
1775
|
+
leaseAction,
|
|
1776
|
+
leaseRenewAt,
|
|
1777
|
+
leaseState,
|
|
1478
1778
|
linkRanges,
|
|
1479
1779
|
measure,
|
|
1780
|
+
parseMime,
|
|
1480
1781
|
payloadHash,
|
|
1481
1782
|
postChain,
|
|
1482
1783
|
probe,
|
|
@@ -1501,6 +1802,7 @@ function hasDelivery(value) {
|
|
|
1501
1802
|
shouldRetry,
|
|
1502
1803
|
sliceByteRange,
|
|
1503
1804
|
splitToFit,
|
|
1805
|
+
subscriptionLease,
|
|
1504
1806
|
triggerEvent,
|
|
1505
1807
|
unchecked,
|
|
1506
1808
|
utf8Length,
|