@particle-academy/fancy-connector-core 0.9.0 → 0.10.1

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 CHANGED
@@ -6,6 +6,70 @@ 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.10.1] - 2026-10-10
10
+
11
+ ### Fixed
12
+
13
+ - **`linkRanges`'s trailing-punctuation trim was quadratic (CodeQL
14
+ `js/polynomial-redos`, HIGH).** `match[0].replace(/[.,;:!?)\]}'"]+$/, "")`
15
+ is unanchored at the start, so V8 retries the trailing run from every
16
+ position inside it — measured at 344ms/20k, 1.2s/40k, 5.2s/80k adversarial
17
+ characters, clean quadratic. `linkRanges(text)` runs on arbitrary
18
+ post/message text, not an operator-typed value, so this is reachable from
19
+ untrusted input, not just a slow edge case. Replaced with a backward scan
20
+ over a `Set` of trailing punctuation characters — linear by construction,
21
+ each step only decrements an index. Trim behaviour is unchanged and pinned
22
+ by a 9-case table before the mechanism changed; a growth-ratio test (4x the
23
+ input must cost well under 10x, not the ~18x the old code showed) guards
24
+ against this coming back. PHP's mirror (`Text.php`) uses the same
25
+ character class but was checked directly and does not exhibit the
26
+ blowup — PCRE is linear here, so only the TypeScript side changes. Found
27
+ and measured by Fancy; `CONNECTOR_API_VERSION` is unchanged.
28
+
29
+ ## [0.10.0] - 2026-09-16
30
+
31
+ Tynn #12 (MOIC's meeting bot): an RFC 5545 invite parser, written from
32
+ scratch in both runtimes (the owner: no third-party code) — no provider
33
+ generates it; it lives beside the other pure helpers. Additive;
34
+ `CONNECTOR_API_VERSION` stays at 1.
35
+
36
+ ### Added
37
+
38
+ - **`parseInvite` / `Ical::parse`** — raw invite text to a structured
39
+ `Invite`: METHOD (REQUEST/CANCEL), UID, SEQUENCE, SUMMARY/DESCRIPTION/
40
+ LOCATION (RFC 5545-unescaped, folded lines joined first), STATUS,
41
+ ORGANIZER and every ATTENDEE (name, email, role, partstat), and an RRULE
42
+ when the event recurs. `DTSTART`/`DTEND` always come back as UTC
43
+ instants: a `Z` suffix passes through, a `TZID` resolves through the
44
+ calendar's own `VTIMEZONE` (its DAYLIGHT/STANDARD sub-components, each
45
+ with a yearly BYMONTH+BYDAY transition rule — an unknown TZID is refused
46
+ BY NAME, never guessed), and a value with neither (RFC 5545's FLOATING
47
+ time) is treated as UTC — a decision named rather than left to look like
48
+ an oversight, because resolving one correctly needs the READER's
49
+ timezone, which nothing in the invite carries.
50
+ - **`nextOccurrence` / `Ical::nextOccurrence`** — the next occurrence at or
51
+ after a given instant, for a recurring or a one-off invite alike; `null`
52
+ once the series (or the single occurrence) has nothing left to offer.
53
+ RRULE support is intentionally narrow: `FREQ` daily/weekly/monthly/yearly
54
+ with `INTERVAL`, `COUNT` and `UNTIL`, and `BYDAY` for WEEKLY only — no
55
+ `BYMONTHDAY`, `BYSETPOS`, `BYWEEKNO`, `BYYEARDAY`, `WKST`, `EXDATE` or
56
+ `RDATE`. A recurring invite using one of those still parses (its RRULE
57
+ fields are read, never dropped); `nextOccurrence` is what does not
58
+ attempt to walk it.
59
+ - **`joinTargets` / `Ical::joinTargets`** — the ALLOW-LISTED places a bot
60
+ can actually join, in the order they appear in the text: Google Meet,
61
+ Zoom (with the meeting id and passcode read out of the URL itself),
62
+ Microsoft Teams, and a dial-in phone number with its own passcode line.
63
+ Anything else — Webex, a generic link, an unrecognised dial-in shape —
64
+ is not returned: a shape this package does not know is a shape a bot
65
+ should not guess at.
66
+ - **`fixtures/ical/`** — six authored cases (the floor, a VTIMEZONE
67
+ resolution, a weekly recurrence with four `nextOccurrence` boundary
68
+ queries, a CANCEL sharing its REQUEST's uid at a higher sequence, an
69
+ allow-listed join-target extraction from a folded/escaped DESCRIPTION,
70
+ and the negative case: a non-allow-listed link ignored). Both runtimes
71
+ read it; `README.md` names the decision each case pins.
72
+
9
73
  ## [0.9.0] - 2026-09-15
10
74
 
11
75
  The verification half of Tynn #11 (Resend inbound email, signed by Svix),
package/dist/index.cjs CHANGED
@@ -75,12 +75,15 @@ __export(src_exports, {
75
75
  isSharedTokenScheme: () => isSharedTokenScheme,
76
76
  isSharedTokenSpec: () => isSharedTokenSpec,
77
77
  isUnconditionallyRetryable: () => isUnconditionallyRetryable,
78
+ joinTargets: () => joinTargets,
78
79
  leaseAction: () => leaseAction,
79
80
  leaseFromResponse: () => leaseFromResponse,
80
81
  leaseRenewAt: () => leaseRenewAt,
81
82
  leaseState: () => leaseState,
82
83
  linkRanges: () => linkRanges,
83
84
  measure: () => measure,
85
+ nextOccurrence: () => nextOccurrence,
86
+ parseInvite: () => parseInvite,
84
87
  parseMime: () => parseMime,
85
88
  payloadHash: () => payloadHash,
86
89
  postChain: () => postChain,
@@ -980,6 +983,325 @@ function unchecked(contract) {
980
983
  };
981
984
  }
982
985
 
986
+ // src/ical.ts
987
+ function unfold(text) {
988
+ return text.replace(/\r\n|\r|\n/g, "\n").replace(/\n[ \t]/g, "");
989
+ }
990
+ function unescapeText(value) {
991
+ return value.replace(/\\[nN]/g, "\n").replace(/\\,/g, ",").replace(/\\;/g, ";").replace(/\\\\/g, "\\");
992
+ }
993
+ function parseLine(line) {
994
+ const colon = findUnquoted(line, ":");
995
+ const head = colon === -1 ? line : line.slice(0, colon);
996
+ const value = colon === -1 ? "" : line.slice(colon + 1);
997
+ const [name = "", ...paramParts] = head.split(";");
998
+ const params = {};
999
+ for (const part of paramParts) {
1000
+ const eq = part.indexOf("=");
1001
+ if (eq === -1) continue;
1002
+ const key = part.slice(0, eq).toUpperCase();
1003
+ let raw = part.slice(eq + 1);
1004
+ if (raw.startsWith('"') && raw.endsWith('"')) raw = raw.slice(1, -1);
1005
+ params[key] = raw;
1006
+ }
1007
+ return { name: name.toUpperCase(), params, value };
1008
+ }
1009
+ function findUnquoted(line, char) {
1010
+ let quoted = false;
1011
+ for (let i = 0; i < line.length; i++) {
1012
+ if (line[i] === '"') quoted = !quoted;
1013
+ else if (line[i] === char && !quoted) return i;
1014
+ }
1015
+ return -1;
1016
+ }
1017
+ function blockLines(lines, name) {
1018
+ const blocks = [];
1019
+ let depth = 0;
1020
+ let current = null;
1021
+ for (const raw of lines) {
1022
+ const upper = raw.toUpperCase();
1023
+ if (upper === `BEGIN:${name}`) {
1024
+ if (depth === 0) current = [];
1025
+ depth++;
1026
+ continue;
1027
+ }
1028
+ if (upper === `END:${name}`) {
1029
+ depth--;
1030
+ if (depth === 0 && current !== null) {
1031
+ blocks.push(current);
1032
+ current = null;
1033
+ }
1034
+ continue;
1035
+ }
1036
+ if (current !== null) current.push(raw);
1037
+ }
1038
+ return blocks;
1039
+ }
1040
+ var WEEKDAY_CODES = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
1041
+ function parseByDayOrdinal(token) {
1042
+ const match = /^(-?\d+)([A-Z]{2})$/.exec(token);
1043
+ if (!match) throw new Error(`ical: VTIMEZONE rule has an unreadable BYDAY "${token}"`);
1044
+ const [, ordinalText, weekdayText] = match;
1045
+ const weekday = WEEKDAY_CODES.indexOf(weekdayText ?? "");
1046
+ if (weekday === -1) throw new Error(`ical: VTIMEZONE rule names an unknown weekday "${weekdayText}"`);
1047
+ return { ordinal: Number(ordinalText), weekday };
1048
+ }
1049
+ function parseTzTransition(lines) {
1050
+ const props = lines.map(parseLine);
1051
+ const get = (name) => props.find((p) => p.name === name)?.value;
1052
+ const offsetFrom = get("TZOFFSETFROM");
1053
+ const offsetTo = get("TZOFFSETTO");
1054
+ const rrule = get("RRULE");
1055
+ if (!offsetFrom || !offsetTo || !rrule) {
1056
+ throw new Error("ical: a VTIMEZONE sub-component needs TZOFFSETFROM, TZOFFSETTO and an RRULE");
1057
+ }
1058
+ const parts = Object.fromEntries(rrule.split(";").map((p) => p.split("=")));
1059
+ const month = Number(parts.BYMONTH);
1060
+ const { ordinal, weekday } = parseByDayOrdinal(parts.BYDAY ?? "");
1061
+ if (!month || Number.isNaN(ordinal)) {
1062
+ throw new Error(`ical: this package only reads a yearly BYMONTH+BYDAY transition rule, got "${rrule}"`);
1063
+ }
1064
+ return { offsetFrom, offsetTo, month, ordinal, weekday };
1065
+ }
1066
+ function offsetMinutes(offset) {
1067
+ const match = /^([+-])(\d{2}):?(\d{2})/.exec(offset);
1068
+ if (!match) throw new Error(`ical: unreadable UTC offset "${offset}"`);
1069
+ const sign = match[1] === "-" ? -1 : 1;
1070
+ return sign * (Number(match[2]) * 60 + Number(match[3]));
1071
+ }
1072
+ function nthWeekdayOfMonth(year, month1, weekday, ordinal) {
1073
+ if (ordinal > 0) {
1074
+ const first = Date.UTC(year, month1 - 1, 1);
1075
+ const firstWeekday = new Date(first).getUTCDay();
1076
+ const offset2 = (weekday - firstWeekday + 7) % 7;
1077
+ return 1 + offset2 + (ordinal - 1) * 7;
1078
+ }
1079
+ const lastDay = new Date(Date.UTC(year, month1, 0)).getUTCDate();
1080
+ const last = Date.UTC(year, month1 - 1, lastDay);
1081
+ const lastWeekday = new Date(last).getUTCDay();
1082
+ const offset = (lastWeekday - weekday + 7) % 7;
1083
+ return lastDay - offset - (-ordinal - 1) * 7;
1084
+ }
1085
+ function parseTimezones(lines) {
1086
+ const zones = /* @__PURE__ */ new Map();
1087
+ for (const block of blockLines(lines, "VTIMEZONE")) {
1088
+ const tzid = block.map(parseLine).find((p) => p.name === "TZID")?.value;
1089
+ if (!tzid) continue;
1090
+ const standardBlocks = blockLines(block, "STANDARD");
1091
+ const daylightBlocks = blockLines(block, "DAYLIGHT");
1092
+ const standardBlock = standardBlocks[0];
1093
+ if (!standardBlock) {
1094
+ throw new Error(`ical: VTIMEZONE "${tzid}" has no STANDARD component`);
1095
+ }
1096
+ zones.set(tzid, {
1097
+ standard: parseTzTransition(standardBlock),
1098
+ daylight: daylightBlocks[0] ? parseTzTransition(daylightBlocks[0]) : null
1099
+ });
1100
+ }
1101
+ return zones;
1102
+ }
1103
+ function resolveTz(zone, y, mo, d, h, mi, s) {
1104
+ const naiveUtcMs = Date.UTC(y, mo - 1, d, h, mi, s);
1105
+ if (!zone.daylight) {
1106
+ return new Date(naiveUtcMs - offsetMinutes(zone.standard.offsetTo) * 6e4).toISOString();
1107
+ }
1108
+ const springDay = nthWeekdayOfMonth(y, zone.daylight.month, zone.daylight.weekday, zone.daylight.ordinal);
1109
+ const fallDay = nthWeekdayOfMonth(y, zone.standard.month, zone.standard.weekday, zone.standard.ordinal);
1110
+ const spring = Date.UTC(y, zone.daylight.month - 1, springDay);
1111
+ const fall = Date.UTC(y, zone.standard.month - 1, fallDay);
1112
+ const inDaylight = naiveUtcMs >= spring && naiveUtcMs < fall;
1113
+ const offset = inDaylight ? zone.daylight.offsetTo : zone.standard.offsetTo;
1114
+ return new Date(naiveUtcMs - offsetMinutes(offset) * 6e4).toISOString();
1115
+ }
1116
+ function parseDateTimeParts(value) {
1117
+ const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(value);
1118
+ if (!match) throw new Error(`ical: unreadable date-time "${value}"`);
1119
+ return {
1120
+ y: Number(match[1]),
1121
+ mo: Number(match[2]),
1122
+ d: Number(match[3]),
1123
+ h: Number(match[4]),
1124
+ mi: Number(match[5]),
1125
+ s: Number(match[6]),
1126
+ utc: Boolean(match[7])
1127
+ };
1128
+ }
1129
+ function resolveInstant(prop, zones) {
1130
+ const parts = parseDateTimeParts(prop.value);
1131
+ if (parts.utc) return new Date(Date.UTC(parts.y, parts.mo - 1, parts.d, parts.h, parts.mi, parts.s)).toISOString();
1132
+ const tzid = prop.params.TZID;
1133
+ if (!tzid) {
1134
+ return new Date(Date.UTC(parts.y, parts.mo - 1, parts.d, parts.h, parts.mi, parts.s)).toISOString();
1135
+ }
1136
+ const zone = zones.get(tzid);
1137
+ if (!zone) {
1138
+ throw new Error(`ical: DTSTART/DTEND names TZID "${tzid}", which this calendar's VTIMEZONE does not define`);
1139
+ }
1140
+ return resolveTz(zone, parts.y, parts.mo, parts.d, parts.h, parts.mi, parts.s);
1141
+ }
1142
+ function emailOf(value) {
1143
+ return value.replace(/^mailto:/i, "");
1144
+ }
1145
+ function parseAttendee(prop) {
1146
+ return {
1147
+ name: prop.params.CN ?? null,
1148
+ email: emailOf(prop.value),
1149
+ role: prop.params.ROLE ?? null,
1150
+ partstat: prop.params.PARTSTAT ?? null
1151
+ };
1152
+ }
1153
+ var FREQS = ["DAILY", "WEEKLY", "MONTHLY", "YEARLY"];
1154
+ function parseRRule(value) {
1155
+ const parts = Object.fromEntries(
1156
+ value.split(";").map((p) => {
1157
+ const eq = p.indexOf("=");
1158
+ return [p.slice(0, eq).toUpperCase(), p.slice(eq + 1)];
1159
+ })
1160
+ );
1161
+ const freq = parts.FREQ;
1162
+ if (!FREQS.includes(freq)) {
1163
+ throw new Error(`ical: RRULE FREQ must be one of ${FREQS.join(", ")}, got "${parts.FREQ}"`);
1164
+ }
1165
+ return {
1166
+ freq,
1167
+ interval: parts.INTERVAL ? Number(parts.INTERVAL) : 1,
1168
+ count: parts.COUNT ? Number(parts.COUNT) : null,
1169
+ until: parts.UNTIL ? resolveInstant({ name: "UNTIL", params: {}, value: parts.UNTIL }, /* @__PURE__ */ new Map()) : null,
1170
+ byDay: parts.BYDAY ? parts.BYDAY.split(",") : null
1171
+ };
1172
+ }
1173
+ function parseInvite(raw) {
1174
+ const lines = unfold(raw).split("\n").filter((l) => l.length > 0);
1175
+ const zones = parseTimezones(lines);
1176
+ const eventBlocks = blockLines(lines, "VEVENT");
1177
+ const firstEvent = eventBlocks[0];
1178
+ if (!firstEvent) throw new Error("ical: no VEVENT in this calendar");
1179
+ const props = firstEvent.map(parseLine);
1180
+ const methodValue = (lines.find((l) => l.toUpperCase().startsWith("METHOD:")) ?? "").split(":")[1];
1181
+ const get = (name) => props.find((p) => p.name === name);
1182
+ const getAll = (name) => props.filter((p) => p.name === name);
1183
+ const text = (name) => {
1184
+ const p = get(name);
1185
+ return p ? unescapeText(p.value) : null;
1186
+ };
1187
+ const method = (methodValue ?? "REQUEST").trim().toUpperCase();
1188
+ if (method !== "REQUEST" && method !== "CANCEL") {
1189
+ throw new Error(`ical: METHOD must be REQUEST or CANCEL, got "${methodValue}"`);
1190
+ }
1191
+ const uidProp = get("UID");
1192
+ if (!uidProp) throw new Error("ical: VEVENT has no UID");
1193
+ const dtstartProp = get("DTSTART");
1194
+ if (!dtstartProp) throw new Error("ical: VEVENT has no DTSTART");
1195
+ const dtendProp = get("DTEND");
1196
+ const organizerProp = get("ORGANIZER");
1197
+ const rruleProp = get("RRULE");
1198
+ return {
1199
+ method,
1200
+ uid: uidProp.value,
1201
+ sequence: get("SEQUENCE") ? Number(get("SEQUENCE").value) : 0,
1202
+ summary: text("SUMMARY"),
1203
+ description: text("DESCRIPTION"),
1204
+ location: text("LOCATION"),
1205
+ status: get("STATUS")?.value ?? null,
1206
+ dtstart: resolveInstant(dtstartProp, zones),
1207
+ dtend: dtendProp ? resolveInstant(dtendProp, zones) : null,
1208
+ organizer: organizerProp ? { name: organizerProp.params.CN ?? null, email: emailOf(organizerProp.value) } : null,
1209
+ attendees: getAll("ATTENDEE").map(parseAttendee),
1210
+ rrule: rruleProp ? parseRRule(rruleProp.value) : null
1211
+ };
1212
+ }
1213
+ var DAY_MS = 864e5;
1214
+ function addCalendarUnit(ms, freq, interval) {
1215
+ const d = new Date(ms);
1216
+ if (freq === "DAILY") return ms + interval * DAY_MS;
1217
+ if (freq === "WEEKLY") return ms + interval * 7 * DAY_MS;
1218
+ const months = freq === "YEARLY" ? interval * 12 : interval;
1219
+ const day = d.getUTCDate();
1220
+ const next = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, 1, d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds()));
1221
+ const daysInNext = new Date(Date.UTC(next.getUTCFullYear(), next.getUTCMonth() + 1, 0)).getUTCDate();
1222
+ next.setUTCDate(Math.min(day, daysInNext));
1223
+ return next.getTime();
1224
+ }
1225
+ function* occurrences(rrule, dtstartMs) {
1226
+ const untilMs = rrule.until ? Date.parse(rrule.until) : null;
1227
+ let n = 0;
1228
+ const emit = (ms) => {
1229
+ if (untilMs !== null && ms > untilMs) return false;
1230
+ n++;
1231
+ return true;
1232
+ };
1233
+ if (rrule.freq === "WEEKLY" && rrule.byDay?.length) {
1234
+ const wanted = [...new Set(rrule.byDay.map((d) => WEEKDAY_CODES.indexOf(d)))].sort((a, b) => a - b);
1235
+ const dtstart = new Date(dtstartMs);
1236
+ const timeOfDay = dtstartMs - Date.UTC(dtstart.getUTCFullYear(), dtstart.getUTCMonth(), dtstart.getUTCDate());
1237
+ const startOfWeek = dtstartMs - timeOfDay - dtstart.getUTCDay() * DAY_MS;
1238
+ let weekIndex = 0;
1239
+ while (true) {
1240
+ if (weekIndex % rrule.interval === 0) {
1241
+ for (const weekday of wanted) {
1242
+ const candidate = startOfWeek + weekIndex * 7 * DAY_MS + weekday * DAY_MS + timeOfDay;
1243
+ if (candidate < dtstartMs) continue;
1244
+ if (!emit(candidate)) return;
1245
+ yield candidate;
1246
+ if (rrule.count && n >= rrule.count) return;
1247
+ }
1248
+ }
1249
+ weekIndex++;
1250
+ }
1251
+ }
1252
+ let current = dtstartMs;
1253
+ while (true) {
1254
+ if (!emit(current)) return;
1255
+ yield current;
1256
+ if (rrule.count && n >= rrule.count) return;
1257
+ current = addCalendarUnit(current, rrule.freq, rrule.interval);
1258
+ }
1259
+ }
1260
+ function nextOccurrence(invite, afterIso) {
1261
+ const afterMs = Date.parse(afterIso);
1262
+ const dtstartMs = Date.parse(invite.dtstart);
1263
+ if (!invite.rrule) {
1264
+ return dtstartMs >= afterMs ? invite.dtstart : null;
1265
+ }
1266
+ for (const ms of occurrences(invite.rrule, dtstartMs)) {
1267
+ if (ms >= afterMs) return new Date(ms).toISOString();
1268
+ }
1269
+ return null;
1270
+ }
1271
+ var ZOOM_LINK = /https:\/\/[\w.-]*zoom\.us\/j\/(\d+)(\?[^\s)]*)?/g;
1272
+ var MEET_LINK = /https:\/\/meet\.google\.com\/[a-z0-9-]+/gi;
1273
+ var TEAMS_LINK = /https:\/\/teams\.(?:microsoft|live)\.com\/l\/meetup-join\/[^\s)]+/gi;
1274
+ var DIAL_LINE = /^Dial(?:-in)?[:\s]+([+0-9][0-9\s().-]{6,}\d)/im;
1275
+ var PASSCODE_LINE = /Passcode[:\s]+([A-Za-z0-9]+)/i;
1276
+ function joinTargets(invite) {
1277
+ const text = invite.description ?? "";
1278
+ const targets = [];
1279
+ for (const match of text.matchAll(ZOOM_LINK)) {
1280
+ const query = match[2] ?? "";
1281
+ const passcode = /[?&]pwd=([^&\s]+)/.exec(query)?.[1] ?? null;
1282
+ targets.push({
1283
+ index: match.index ?? 0,
1284
+ target: { kind: "zoom", url: match[0], meetingId: match[1] ?? null, passcode, phone: null }
1285
+ });
1286
+ }
1287
+ for (const match of text.matchAll(MEET_LINK)) {
1288
+ targets.push({ index: match.index ?? 0, target: { kind: "google-meet", url: match[0], meetingId: null, passcode: null, phone: null } });
1289
+ }
1290
+ for (const match of text.matchAll(TEAMS_LINK)) {
1291
+ targets.push({ index: match.index ?? 0, target: { kind: "teams", url: match[0], meetingId: null, passcode: null, phone: null } });
1292
+ }
1293
+ const dial = DIAL_LINE.exec(text);
1294
+ const dialedPhone = dial?.[1];
1295
+ if (dial && dialedPhone !== void 0) {
1296
+ const passcode = PASSCODE_LINE.exec(text)?.[1] ?? null;
1297
+ targets.push({
1298
+ index: dial.index,
1299
+ target: { kind: "dial-in", url: null, meetingId: null, passcode, phone: dialedPhone.trim() }
1300
+ });
1301
+ }
1302
+ return targets.sort((a, b) => a.index - b.index).map((t) => t.target);
1303
+ }
1304
+
983
1305
  // src/idempotency.ts
984
1306
  var DEFAULT_IDEMPOTENCY_WINDOW_SECONDS = 24 * 60 * 60;
985
1307
  var MAX_IDEMPOTENCY_KEY_LENGTH = 255;
@@ -1544,11 +1866,17 @@ function sliceByteRange(text, range) {
1544
1866
  const bytes = encoder.encode(text);
1545
1867
  return new TextDecoder().decode(bytes.slice(range.byteStart, range.byteEnd));
1546
1868
  }
1869
+ var TRAILING_PUNCTUATION = /* @__PURE__ */ new Set([".", ",", ";", ":", "!", "?", ")", "]", "}", "'", '"']);
1870
+ function trimTrailingPunctuation(raw) {
1871
+ let end = raw.length;
1872
+ while (end > 0 && TRAILING_PUNCTUATION.has(raw[end - 1])) end -= 1;
1873
+ return raw.slice(0, end);
1874
+ }
1547
1875
  function linkRanges(text) {
1548
1876
  const found = [];
1549
1877
  for (const match of text.matchAll(/https?:\/\/[^\s]+/g)) {
1550
1878
  if (match.index === void 0) continue;
1551
- const url = match[0].replace(/[.,;:!?)\]}'"]+$/, "");
1879
+ const url = trimTrailingPunctuation(match[0]);
1552
1880
  found.push({ ...byteRangeOf(text, match.index, url), url });
1553
1881
  }
1554
1882
  return found;
@@ -1920,12 +2248,15 @@ function hasDelivery(value) {
1920
2248
  isSharedTokenScheme,
1921
2249
  isSharedTokenSpec,
1922
2250
  isUnconditionallyRetryable,
2251
+ joinTargets,
1923
2252
  leaseAction,
1924
2253
  leaseFromResponse,
1925
2254
  leaseRenewAt,
1926
2255
  leaseState,
1927
2256
  linkRanges,
1928
2257
  measure,
2258
+ nextOccurrence,
2259
+ parseInvite,
1929
2260
  parseMime,
1930
2261
  payloadHash,
1931
2262
  postChain,