@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 +64 -0
- package/dist/index.cjs +332 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +87 -13
- package/dist/index.d.ts +87 -13
- package/dist/index.js +329 -1
- package/dist/index.js.map +1 -1
- package/package.json +69 -69
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]
|
|
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,
|