@particle-academy/fancy-connector-core 0.9.0 → 0.10.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 +44 -0
- package/dist/index.cjs +325 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +87 -1
- package/dist/index.d.ts +87 -1
- package/dist/index.js +322 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,50 @@ 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.0] - 2026-09-16
|
|
10
|
+
|
|
11
|
+
Tynn #12 (MOIC's meeting bot): an RFC 5545 invite parser, written from
|
|
12
|
+
scratch in both runtimes (the owner: no third-party code) — no provider
|
|
13
|
+
generates it; it lives beside the other pure helpers. Additive;
|
|
14
|
+
`CONNECTOR_API_VERSION` stays at 1.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`parseInvite` / `Ical::parse`** — raw invite text to a structured
|
|
19
|
+
`Invite`: METHOD (REQUEST/CANCEL), UID, SEQUENCE, SUMMARY/DESCRIPTION/
|
|
20
|
+
LOCATION (RFC 5545-unescaped, folded lines joined first), STATUS,
|
|
21
|
+
ORGANIZER and every ATTENDEE (name, email, role, partstat), and an RRULE
|
|
22
|
+
when the event recurs. `DTSTART`/`DTEND` always come back as UTC
|
|
23
|
+
instants: a `Z` suffix passes through, a `TZID` resolves through the
|
|
24
|
+
calendar's own `VTIMEZONE` (its DAYLIGHT/STANDARD sub-components, each
|
|
25
|
+
with a yearly BYMONTH+BYDAY transition rule — an unknown TZID is refused
|
|
26
|
+
BY NAME, never guessed), and a value with neither (RFC 5545's FLOATING
|
|
27
|
+
time) is treated as UTC — a decision named rather than left to look like
|
|
28
|
+
an oversight, because resolving one correctly needs the READER's
|
|
29
|
+
timezone, which nothing in the invite carries.
|
|
30
|
+
- **`nextOccurrence` / `Ical::nextOccurrence`** — the next occurrence at or
|
|
31
|
+
after a given instant, for a recurring or a one-off invite alike; `null`
|
|
32
|
+
once the series (or the single occurrence) has nothing left to offer.
|
|
33
|
+
RRULE support is intentionally narrow: `FREQ` daily/weekly/monthly/yearly
|
|
34
|
+
with `INTERVAL`, `COUNT` and `UNTIL`, and `BYDAY` for WEEKLY only — no
|
|
35
|
+
`BYMONTHDAY`, `BYSETPOS`, `BYWEEKNO`, `BYYEARDAY`, `WKST`, `EXDATE` or
|
|
36
|
+
`RDATE`. A recurring invite using one of those still parses (its RRULE
|
|
37
|
+
fields are read, never dropped); `nextOccurrence` is what does not
|
|
38
|
+
attempt to walk it.
|
|
39
|
+
- **`joinTargets` / `Ical::joinTargets`** — the ALLOW-LISTED places a bot
|
|
40
|
+
can actually join, in the order they appear in the text: Google Meet,
|
|
41
|
+
Zoom (with the meeting id and passcode read out of the URL itself),
|
|
42
|
+
Microsoft Teams, and a dial-in phone number with its own passcode line.
|
|
43
|
+
Anything else — Webex, a generic link, an unrecognised dial-in shape —
|
|
44
|
+
is not returned: a shape this package does not know is a shape a bot
|
|
45
|
+
should not guess at.
|
|
46
|
+
- **`fixtures/ical/`** — six authored cases (the floor, a VTIMEZONE
|
|
47
|
+
resolution, a weekly recurrence with four `nextOccurrence` boundary
|
|
48
|
+
queries, a CANCEL sharing its REQUEST's uid at a higher sequence, an
|
|
49
|
+
allow-listed join-target extraction from a folded/escaped DESCRIPTION,
|
|
50
|
+
and the negative case: a non-allow-listed link ignored). Both runtimes
|
|
51
|
+
read it; `README.md` names the decision each case pins.
|
|
52
|
+
|
|
9
53
|
## [0.9.0] - 2026-09-15
|
|
10
54
|
|
|
11
55
|
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;
|
|
@@ -1920,12 +2242,15 @@ function hasDelivery(value) {
|
|
|
1920
2242
|
isSharedTokenScheme,
|
|
1921
2243
|
isSharedTokenSpec,
|
|
1922
2244
|
isUnconditionallyRetryable,
|
|
2245
|
+
joinTargets,
|
|
1923
2246
|
leaseAction,
|
|
1924
2247
|
leaseFromResponse,
|
|
1925
2248
|
leaseRenewAt,
|
|
1926
2249
|
leaseState,
|
|
1927
2250
|
linkRanges,
|
|
1928
2251
|
measure,
|
|
2252
|
+
nextOccurrence,
|
|
2253
|
+
parseInvite,
|
|
1929
2254
|
parseMime,
|
|
1930
2255
|
payloadHash,
|
|
1931
2256
|
postChain,
|