@particle-academy/fancy-connector-core 0.8.1 → 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 +94 -0
- package/dist/index.cjs +364 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +140 -6
- package/dist/index.d.ts +140 -6
- package/dist/index.js +360 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -860,6 +860,325 @@ function unchecked(contract) {
|
|
|
860
860
|
};
|
|
861
861
|
}
|
|
862
862
|
|
|
863
|
+
// src/ical.ts
|
|
864
|
+
function unfold(text) {
|
|
865
|
+
return text.replace(/\r\n|\r|\n/g, "\n").replace(/\n[ \t]/g, "");
|
|
866
|
+
}
|
|
867
|
+
function unescapeText(value) {
|
|
868
|
+
return value.replace(/\\[nN]/g, "\n").replace(/\\,/g, ",").replace(/\\;/g, ";").replace(/\\\\/g, "\\");
|
|
869
|
+
}
|
|
870
|
+
function parseLine(line) {
|
|
871
|
+
const colon = findUnquoted(line, ":");
|
|
872
|
+
const head = colon === -1 ? line : line.slice(0, colon);
|
|
873
|
+
const value = colon === -1 ? "" : line.slice(colon + 1);
|
|
874
|
+
const [name = "", ...paramParts] = head.split(";");
|
|
875
|
+
const params = {};
|
|
876
|
+
for (const part of paramParts) {
|
|
877
|
+
const eq = part.indexOf("=");
|
|
878
|
+
if (eq === -1) continue;
|
|
879
|
+
const key = part.slice(0, eq).toUpperCase();
|
|
880
|
+
let raw = part.slice(eq + 1);
|
|
881
|
+
if (raw.startsWith('"') && raw.endsWith('"')) raw = raw.slice(1, -1);
|
|
882
|
+
params[key] = raw;
|
|
883
|
+
}
|
|
884
|
+
return { name: name.toUpperCase(), params, value };
|
|
885
|
+
}
|
|
886
|
+
function findUnquoted(line, char) {
|
|
887
|
+
let quoted = false;
|
|
888
|
+
for (let i = 0; i < line.length; i++) {
|
|
889
|
+
if (line[i] === '"') quoted = !quoted;
|
|
890
|
+
else if (line[i] === char && !quoted) return i;
|
|
891
|
+
}
|
|
892
|
+
return -1;
|
|
893
|
+
}
|
|
894
|
+
function blockLines(lines, name) {
|
|
895
|
+
const blocks = [];
|
|
896
|
+
let depth = 0;
|
|
897
|
+
let current = null;
|
|
898
|
+
for (const raw of lines) {
|
|
899
|
+
const upper = raw.toUpperCase();
|
|
900
|
+
if (upper === `BEGIN:${name}`) {
|
|
901
|
+
if (depth === 0) current = [];
|
|
902
|
+
depth++;
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (upper === `END:${name}`) {
|
|
906
|
+
depth--;
|
|
907
|
+
if (depth === 0 && current !== null) {
|
|
908
|
+
blocks.push(current);
|
|
909
|
+
current = null;
|
|
910
|
+
}
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
if (current !== null) current.push(raw);
|
|
914
|
+
}
|
|
915
|
+
return blocks;
|
|
916
|
+
}
|
|
917
|
+
var WEEKDAY_CODES = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
|
|
918
|
+
function parseByDayOrdinal(token) {
|
|
919
|
+
const match = /^(-?\d+)([A-Z]{2})$/.exec(token);
|
|
920
|
+
if (!match) throw new Error(`ical: VTIMEZONE rule has an unreadable BYDAY "${token}"`);
|
|
921
|
+
const [, ordinalText, weekdayText] = match;
|
|
922
|
+
const weekday = WEEKDAY_CODES.indexOf(weekdayText ?? "");
|
|
923
|
+
if (weekday === -1) throw new Error(`ical: VTIMEZONE rule names an unknown weekday "${weekdayText}"`);
|
|
924
|
+
return { ordinal: Number(ordinalText), weekday };
|
|
925
|
+
}
|
|
926
|
+
function parseTzTransition(lines) {
|
|
927
|
+
const props = lines.map(parseLine);
|
|
928
|
+
const get = (name) => props.find((p) => p.name === name)?.value;
|
|
929
|
+
const offsetFrom = get("TZOFFSETFROM");
|
|
930
|
+
const offsetTo = get("TZOFFSETTO");
|
|
931
|
+
const rrule = get("RRULE");
|
|
932
|
+
if (!offsetFrom || !offsetTo || !rrule) {
|
|
933
|
+
throw new Error("ical: a VTIMEZONE sub-component needs TZOFFSETFROM, TZOFFSETTO and an RRULE");
|
|
934
|
+
}
|
|
935
|
+
const parts = Object.fromEntries(rrule.split(";").map((p) => p.split("=")));
|
|
936
|
+
const month = Number(parts.BYMONTH);
|
|
937
|
+
const { ordinal, weekday } = parseByDayOrdinal(parts.BYDAY ?? "");
|
|
938
|
+
if (!month || Number.isNaN(ordinal)) {
|
|
939
|
+
throw new Error(`ical: this package only reads a yearly BYMONTH+BYDAY transition rule, got "${rrule}"`);
|
|
940
|
+
}
|
|
941
|
+
return { offsetFrom, offsetTo, month, ordinal, weekday };
|
|
942
|
+
}
|
|
943
|
+
function offsetMinutes(offset) {
|
|
944
|
+
const match = /^([+-])(\d{2}):?(\d{2})/.exec(offset);
|
|
945
|
+
if (!match) throw new Error(`ical: unreadable UTC offset "${offset}"`);
|
|
946
|
+
const sign = match[1] === "-" ? -1 : 1;
|
|
947
|
+
return sign * (Number(match[2]) * 60 + Number(match[3]));
|
|
948
|
+
}
|
|
949
|
+
function nthWeekdayOfMonth(year, month1, weekday, ordinal) {
|
|
950
|
+
if (ordinal > 0) {
|
|
951
|
+
const first = Date.UTC(year, month1 - 1, 1);
|
|
952
|
+
const firstWeekday = new Date(first).getUTCDay();
|
|
953
|
+
const offset2 = (weekday - firstWeekday + 7) % 7;
|
|
954
|
+
return 1 + offset2 + (ordinal - 1) * 7;
|
|
955
|
+
}
|
|
956
|
+
const lastDay = new Date(Date.UTC(year, month1, 0)).getUTCDate();
|
|
957
|
+
const last = Date.UTC(year, month1 - 1, lastDay);
|
|
958
|
+
const lastWeekday = new Date(last).getUTCDay();
|
|
959
|
+
const offset = (lastWeekday - weekday + 7) % 7;
|
|
960
|
+
return lastDay - offset - (-ordinal - 1) * 7;
|
|
961
|
+
}
|
|
962
|
+
function parseTimezones(lines) {
|
|
963
|
+
const zones = /* @__PURE__ */ new Map();
|
|
964
|
+
for (const block of blockLines(lines, "VTIMEZONE")) {
|
|
965
|
+
const tzid = block.map(parseLine).find((p) => p.name === "TZID")?.value;
|
|
966
|
+
if (!tzid) continue;
|
|
967
|
+
const standardBlocks = blockLines(block, "STANDARD");
|
|
968
|
+
const daylightBlocks = blockLines(block, "DAYLIGHT");
|
|
969
|
+
const standardBlock = standardBlocks[0];
|
|
970
|
+
if (!standardBlock) {
|
|
971
|
+
throw new Error(`ical: VTIMEZONE "${tzid}" has no STANDARD component`);
|
|
972
|
+
}
|
|
973
|
+
zones.set(tzid, {
|
|
974
|
+
standard: parseTzTransition(standardBlock),
|
|
975
|
+
daylight: daylightBlocks[0] ? parseTzTransition(daylightBlocks[0]) : null
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
return zones;
|
|
979
|
+
}
|
|
980
|
+
function resolveTz(zone, y, mo, d, h, mi, s) {
|
|
981
|
+
const naiveUtcMs = Date.UTC(y, mo - 1, d, h, mi, s);
|
|
982
|
+
if (!zone.daylight) {
|
|
983
|
+
return new Date(naiveUtcMs - offsetMinutes(zone.standard.offsetTo) * 6e4).toISOString();
|
|
984
|
+
}
|
|
985
|
+
const springDay = nthWeekdayOfMonth(y, zone.daylight.month, zone.daylight.weekday, zone.daylight.ordinal);
|
|
986
|
+
const fallDay = nthWeekdayOfMonth(y, zone.standard.month, zone.standard.weekday, zone.standard.ordinal);
|
|
987
|
+
const spring = Date.UTC(y, zone.daylight.month - 1, springDay);
|
|
988
|
+
const fall = Date.UTC(y, zone.standard.month - 1, fallDay);
|
|
989
|
+
const inDaylight = naiveUtcMs >= spring && naiveUtcMs < fall;
|
|
990
|
+
const offset = inDaylight ? zone.daylight.offsetTo : zone.standard.offsetTo;
|
|
991
|
+
return new Date(naiveUtcMs - offsetMinutes(offset) * 6e4).toISOString();
|
|
992
|
+
}
|
|
993
|
+
function parseDateTimeParts(value) {
|
|
994
|
+
const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(value);
|
|
995
|
+
if (!match) throw new Error(`ical: unreadable date-time "${value}"`);
|
|
996
|
+
return {
|
|
997
|
+
y: Number(match[1]),
|
|
998
|
+
mo: Number(match[2]),
|
|
999
|
+
d: Number(match[3]),
|
|
1000
|
+
h: Number(match[4]),
|
|
1001
|
+
mi: Number(match[5]),
|
|
1002
|
+
s: Number(match[6]),
|
|
1003
|
+
utc: Boolean(match[7])
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
function resolveInstant(prop, zones) {
|
|
1007
|
+
const parts = parseDateTimeParts(prop.value);
|
|
1008
|
+
if (parts.utc) return new Date(Date.UTC(parts.y, parts.mo - 1, parts.d, parts.h, parts.mi, parts.s)).toISOString();
|
|
1009
|
+
const tzid = prop.params.TZID;
|
|
1010
|
+
if (!tzid) {
|
|
1011
|
+
return new Date(Date.UTC(parts.y, parts.mo - 1, parts.d, parts.h, parts.mi, parts.s)).toISOString();
|
|
1012
|
+
}
|
|
1013
|
+
const zone = zones.get(tzid);
|
|
1014
|
+
if (!zone) {
|
|
1015
|
+
throw new Error(`ical: DTSTART/DTEND names TZID "${tzid}", which this calendar's VTIMEZONE does not define`);
|
|
1016
|
+
}
|
|
1017
|
+
return resolveTz(zone, parts.y, parts.mo, parts.d, parts.h, parts.mi, parts.s);
|
|
1018
|
+
}
|
|
1019
|
+
function emailOf(value) {
|
|
1020
|
+
return value.replace(/^mailto:/i, "");
|
|
1021
|
+
}
|
|
1022
|
+
function parseAttendee(prop) {
|
|
1023
|
+
return {
|
|
1024
|
+
name: prop.params.CN ?? null,
|
|
1025
|
+
email: emailOf(prop.value),
|
|
1026
|
+
role: prop.params.ROLE ?? null,
|
|
1027
|
+
partstat: prop.params.PARTSTAT ?? null
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
var FREQS = ["DAILY", "WEEKLY", "MONTHLY", "YEARLY"];
|
|
1031
|
+
function parseRRule(value) {
|
|
1032
|
+
const parts = Object.fromEntries(
|
|
1033
|
+
value.split(";").map((p) => {
|
|
1034
|
+
const eq = p.indexOf("=");
|
|
1035
|
+
return [p.slice(0, eq).toUpperCase(), p.slice(eq + 1)];
|
|
1036
|
+
})
|
|
1037
|
+
);
|
|
1038
|
+
const freq = parts.FREQ;
|
|
1039
|
+
if (!FREQS.includes(freq)) {
|
|
1040
|
+
throw new Error(`ical: RRULE FREQ must be one of ${FREQS.join(", ")}, got "${parts.FREQ}"`);
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
freq,
|
|
1044
|
+
interval: parts.INTERVAL ? Number(parts.INTERVAL) : 1,
|
|
1045
|
+
count: parts.COUNT ? Number(parts.COUNT) : null,
|
|
1046
|
+
until: parts.UNTIL ? resolveInstant({ name: "UNTIL", params: {}, value: parts.UNTIL }, /* @__PURE__ */ new Map()) : null,
|
|
1047
|
+
byDay: parts.BYDAY ? parts.BYDAY.split(",") : null
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
function parseInvite(raw) {
|
|
1051
|
+
const lines = unfold(raw).split("\n").filter((l) => l.length > 0);
|
|
1052
|
+
const zones = parseTimezones(lines);
|
|
1053
|
+
const eventBlocks = blockLines(lines, "VEVENT");
|
|
1054
|
+
const firstEvent = eventBlocks[0];
|
|
1055
|
+
if (!firstEvent) throw new Error("ical: no VEVENT in this calendar");
|
|
1056
|
+
const props = firstEvent.map(parseLine);
|
|
1057
|
+
const methodValue = (lines.find((l) => l.toUpperCase().startsWith("METHOD:")) ?? "").split(":")[1];
|
|
1058
|
+
const get = (name) => props.find((p) => p.name === name);
|
|
1059
|
+
const getAll = (name) => props.filter((p) => p.name === name);
|
|
1060
|
+
const text = (name) => {
|
|
1061
|
+
const p = get(name);
|
|
1062
|
+
return p ? unescapeText(p.value) : null;
|
|
1063
|
+
};
|
|
1064
|
+
const method = (methodValue ?? "REQUEST").trim().toUpperCase();
|
|
1065
|
+
if (method !== "REQUEST" && method !== "CANCEL") {
|
|
1066
|
+
throw new Error(`ical: METHOD must be REQUEST or CANCEL, got "${methodValue}"`);
|
|
1067
|
+
}
|
|
1068
|
+
const uidProp = get("UID");
|
|
1069
|
+
if (!uidProp) throw new Error("ical: VEVENT has no UID");
|
|
1070
|
+
const dtstartProp = get("DTSTART");
|
|
1071
|
+
if (!dtstartProp) throw new Error("ical: VEVENT has no DTSTART");
|
|
1072
|
+
const dtendProp = get("DTEND");
|
|
1073
|
+
const organizerProp = get("ORGANIZER");
|
|
1074
|
+
const rruleProp = get("RRULE");
|
|
1075
|
+
return {
|
|
1076
|
+
method,
|
|
1077
|
+
uid: uidProp.value,
|
|
1078
|
+
sequence: get("SEQUENCE") ? Number(get("SEQUENCE").value) : 0,
|
|
1079
|
+
summary: text("SUMMARY"),
|
|
1080
|
+
description: text("DESCRIPTION"),
|
|
1081
|
+
location: text("LOCATION"),
|
|
1082
|
+
status: get("STATUS")?.value ?? null,
|
|
1083
|
+
dtstart: resolveInstant(dtstartProp, zones),
|
|
1084
|
+
dtend: dtendProp ? resolveInstant(dtendProp, zones) : null,
|
|
1085
|
+
organizer: organizerProp ? { name: organizerProp.params.CN ?? null, email: emailOf(organizerProp.value) } : null,
|
|
1086
|
+
attendees: getAll("ATTENDEE").map(parseAttendee),
|
|
1087
|
+
rrule: rruleProp ? parseRRule(rruleProp.value) : null
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
var DAY_MS = 864e5;
|
|
1091
|
+
function addCalendarUnit(ms, freq, interval) {
|
|
1092
|
+
const d = new Date(ms);
|
|
1093
|
+
if (freq === "DAILY") return ms + interval * DAY_MS;
|
|
1094
|
+
if (freq === "WEEKLY") return ms + interval * 7 * DAY_MS;
|
|
1095
|
+
const months = freq === "YEARLY" ? interval * 12 : interval;
|
|
1096
|
+
const day = d.getUTCDate();
|
|
1097
|
+
const next = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, 1, d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds()));
|
|
1098
|
+
const daysInNext = new Date(Date.UTC(next.getUTCFullYear(), next.getUTCMonth() + 1, 0)).getUTCDate();
|
|
1099
|
+
next.setUTCDate(Math.min(day, daysInNext));
|
|
1100
|
+
return next.getTime();
|
|
1101
|
+
}
|
|
1102
|
+
function* occurrences(rrule, dtstartMs) {
|
|
1103
|
+
const untilMs = rrule.until ? Date.parse(rrule.until) : null;
|
|
1104
|
+
let n = 0;
|
|
1105
|
+
const emit = (ms) => {
|
|
1106
|
+
if (untilMs !== null && ms > untilMs) return false;
|
|
1107
|
+
n++;
|
|
1108
|
+
return true;
|
|
1109
|
+
};
|
|
1110
|
+
if (rrule.freq === "WEEKLY" && rrule.byDay?.length) {
|
|
1111
|
+
const wanted = [...new Set(rrule.byDay.map((d) => WEEKDAY_CODES.indexOf(d)))].sort((a, b) => a - b);
|
|
1112
|
+
const dtstart = new Date(dtstartMs);
|
|
1113
|
+
const timeOfDay = dtstartMs - Date.UTC(dtstart.getUTCFullYear(), dtstart.getUTCMonth(), dtstart.getUTCDate());
|
|
1114
|
+
const startOfWeek = dtstartMs - timeOfDay - dtstart.getUTCDay() * DAY_MS;
|
|
1115
|
+
let weekIndex = 0;
|
|
1116
|
+
while (true) {
|
|
1117
|
+
if (weekIndex % rrule.interval === 0) {
|
|
1118
|
+
for (const weekday of wanted) {
|
|
1119
|
+
const candidate = startOfWeek + weekIndex * 7 * DAY_MS + weekday * DAY_MS + timeOfDay;
|
|
1120
|
+
if (candidate < dtstartMs) continue;
|
|
1121
|
+
if (!emit(candidate)) return;
|
|
1122
|
+
yield candidate;
|
|
1123
|
+
if (rrule.count && n >= rrule.count) return;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
weekIndex++;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
let current = dtstartMs;
|
|
1130
|
+
while (true) {
|
|
1131
|
+
if (!emit(current)) return;
|
|
1132
|
+
yield current;
|
|
1133
|
+
if (rrule.count && n >= rrule.count) return;
|
|
1134
|
+
current = addCalendarUnit(current, rrule.freq, rrule.interval);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function nextOccurrence(invite, afterIso) {
|
|
1138
|
+
const afterMs = Date.parse(afterIso);
|
|
1139
|
+
const dtstartMs = Date.parse(invite.dtstart);
|
|
1140
|
+
if (!invite.rrule) {
|
|
1141
|
+
return dtstartMs >= afterMs ? invite.dtstart : null;
|
|
1142
|
+
}
|
|
1143
|
+
for (const ms of occurrences(invite.rrule, dtstartMs)) {
|
|
1144
|
+
if (ms >= afterMs) return new Date(ms).toISOString();
|
|
1145
|
+
}
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
var ZOOM_LINK = /https:\/\/[\w.-]*zoom\.us\/j\/(\d+)(\?[^\s)]*)?/g;
|
|
1149
|
+
var MEET_LINK = /https:\/\/meet\.google\.com\/[a-z0-9-]+/gi;
|
|
1150
|
+
var TEAMS_LINK = /https:\/\/teams\.(?:microsoft|live)\.com\/l\/meetup-join\/[^\s)]+/gi;
|
|
1151
|
+
var DIAL_LINE = /^Dial(?:-in)?[:\s]+([+0-9][0-9\s().-]{6,}\d)/im;
|
|
1152
|
+
var PASSCODE_LINE = /Passcode[:\s]+([A-Za-z0-9]+)/i;
|
|
1153
|
+
function joinTargets(invite) {
|
|
1154
|
+
const text = invite.description ?? "";
|
|
1155
|
+
const targets = [];
|
|
1156
|
+
for (const match of text.matchAll(ZOOM_LINK)) {
|
|
1157
|
+
const query = match[2] ?? "";
|
|
1158
|
+
const passcode = /[?&]pwd=([^&\s]+)/.exec(query)?.[1] ?? null;
|
|
1159
|
+
targets.push({
|
|
1160
|
+
index: match.index ?? 0,
|
|
1161
|
+
target: { kind: "zoom", url: match[0], meetingId: match[1] ?? null, passcode, phone: null }
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
for (const match of text.matchAll(MEET_LINK)) {
|
|
1165
|
+
targets.push({ index: match.index ?? 0, target: { kind: "google-meet", url: match[0], meetingId: null, passcode: null, phone: null } });
|
|
1166
|
+
}
|
|
1167
|
+
for (const match of text.matchAll(TEAMS_LINK)) {
|
|
1168
|
+
targets.push({ index: match.index ?? 0, target: { kind: "teams", url: match[0], meetingId: null, passcode: null, phone: null } });
|
|
1169
|
+
}
|
|
1170
|
+
const dial = DIAL_LINE.exec(text);
|
|
1171
|
+
const dialedPhone = dial?.[1];
|
|
1172
|
+
if (dial && dialedPhone !== void 0) {
|
|
1173
|
+
const passcode = PASSCODE_LINE.exec(text)?.[1] ?? null;
|
|
1174
|
+
targets.push({
|
|
1175
|
+
index: dial.index,
|
|
1176
|
+
target: { kind: "dial-in", url: null, meetingId: null, passcode, phone: dialedPhone.trim() }
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
return targets.sort((a, b) => a.index - b.index).map((t) => t.target);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
863
1182
|
// src/idempotency.ts
|
|
864
1183
|
var DEFAULT_IDEMPOTENCY_WINDOW_SECONDS = 24 * 60 * 60;
|
|
865
1184
|
var MAX_IDEMPOTENCY_KEY_LENGTH = 255;
|
|
@@ -1557,12 +1876,35 @@ var PROBLEM_SEVERITIES = ["block", "warn"];
|
|
|
1557
1876
|
var CANONICAL_METRICS = ["like", "share", "reply", "quote", "view"];
|
|
1558
1877
|
|
|
1559
1878
|
// src/webhook.ts
|
|
1879
|
+
function secretKeyBytes(secret, options = {}) {
|
|
1880
|
+
let material = secret;
|
|
1881
|
+
if (options.secretPrefix !== void 0) {
|
|
1882
|
+
if (!material.startsWith(options.secretPrefix)) {
|
|
1883
|
+
return { reason: `signing secret does not start with ${JSON.stringify(options.secretPrefix)}` };
|
|
1884
|
+
}
|
|
1885
|
+
material = material.slice(options.secretPrefix.length);
|
|
1886
|
+
}
|
|
1887
|
+
if ((options.secretEncoding ?? "utf8") === "base64") {
|
|
1888
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(material) || material.length % 4 !== 0) {
|
|
1889
|
+
return { reason: "signing secret is not valid base64" };
|
|
1890
|
+
}
|
|
1891
|
+
try {
|
|
1892
|
+
return { bytes: Uint8Array.from(atob(material), (c) => c.charCodeAt(0)) };
|
|
1893
|
+
} catch {
|
|
1894
|
+
return { reason: "signing secret is not valid base64" };
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
return { bytes: new TextEncoder().encode(material) };
|
|
1898
|
+
}
|
|
1560
1899
|
async function verifyHmac(options) {
|
|
1561
|
-
const { raw,
|
|
1900
|
+
const { raw, secret, scheme, timestamp, id } = options;
|
|
1901
|
+
const signatures = (Array.isArray(options.signature) ? options.signature : [options.signature]).filter(
|
|
1902
|
+
(s) => typeof s === "string" && s !== ""
|
|
1903
|
+
);
|
|
1562
1904
|
if (!secret) {
|
|
1563
1905
|
return { ok: false, reason: "no signing secret configured for this trigger" };
|
|
1564
1906
|
}
|
|
1565
|
-
if (
|
|
1907
|
+
if (signatures.length === 0) return { ok: false, reason: "delivery carried no signature header" };
|
|
1566
1908
|
if (scheme.tolerance !== void 0) {
|
|
1567
1909
|
if (!timestamp) return { ok: false, reason: "delivery carried no timestamp header" };
|
|
1568
1910
|
const sent = Number(timestamp);
|
|
@@ -1572,20 +1914,24 @@ async function verifyHmac(options) {
|
|
|
1572
1914
|
return { ok: false, reason: `delivery is outside the ${scheme.tolerance}s replay window` };
|
|
1573
1915
|
}
|
|
1574
1916
|
}
|
|
1575
|
-
const
|
|
1576
|
-
|
|
1917
|
+
const key = secretKeyBytes(secret, scheme);
|
|
1918
|
+
if ("reason" in key) return { ok: false, reason: key.reason };
|
|
1919
|
+
const expected = await hmac(secret, scheme.payload(raw, timestamp, id), scheme.algorithm, scheme.encoding ?? "hex", scheme);
|
|
1920
|
+
return signatures.some((candidate) => constantTimeEquals(expected, candidate)) ? { ok: true } : { ok: false, reason: "signature did not match" };
|
|
1577
1921
|
}
|
|
1578
|
-
async function hmac(secret, payload, algorithm, encoding = "hex") {
|
|
1922
|
+
async function hmac(secret, payload, algorithm, encoding = "hex", options = {}) {
|
|
1579
1923
|
const subtle = globalThis.crypto?.subtle;
|
|
1580
1924
|
if (!subtle) {
|
|
1581
1925
|
throw new Error(
|
|
1582
1926
|
"WebCrypto is not available in this runtime, so webhook signatures cannot be verified. Refusing to accept deliveries unverified."
|
|
1583
1927
|
);
|
|
1584
1928
|
}
|
|
1929
|
+
const material = secretKeyBytes(secret, options);
|
|
1930
|
+
if ("reason" in material) throw new Error(material.reason);
|
|
1585
1931
|
const encoder2 = new TextEncoder();
|
|
1586
1932
|
const key = await subtle.importKey(
|
|
1587
1933
|
"raw",
|
|
1588
|
-
|
|
1934
|
+
material.bytes,
|
|
1589
1935
|
{ name: "HMAC", hash: algorithm },
|
|
1590
1936
|
false,
|
|
1591
1937
|
["sign"]
|
|
@@ -1686,12 +2032,15 @@ async function verifyDelivery(trigger, delivery, secret, now) {
|
|
|
1686
2032
|
const rawHeader = header2(delivery.headers, spec.signatureHeader);
|
|
1687
2033
|
const parsed = spec.parse && rawHeader ? spec.parse(rawHeader) : { signature: rawHeader };
|
|
1688
2034
|
const timestamp = parsed.timestamp ?? (spec.timestampHeader ? header2(delivery.headers, spec.timestampHeader) : void 0);
|
|
2035
|
+
const id = spec.idHeader ? header2(delivery.headers, spec.idHeader) : void 0;
|
|
2036
|
+
if (spec.idHeader && !id) return { ok: false, reason: "delivery carried no id header" };
|
|
1689
2037
|
return verifyHmac({
|
|
1690
2038
|
raw: delivery.raw,
|
|
1691
|
-
signature: parsed.signature,
|
|
2039
|
+
signature: parsed.signatures ?? parsed.signature,
|
|
1692
2040
|
secret,
|
|
1693
2041
|
scheme: spec.scheme,
|
|
1694
2042
|
timestamp,
|
|
2043
|
+
id,
|
|
1695
2044
|
...now === void 0 ? {} : { now }
|
|
1696
2045
|
});
|
|
1697
2046
|
}
|
|
@@ -1769,12 +2118,15 @@ export {
|
|
|
1769
2118
|
isSharedTokenScheme,
|
|
1770
2119
|
isSharedTokenSpec,
|
|
1771
2120
|
isUnconditionallyRetryable,
|
|
2121
|
+
joinTargets,
|
|
1772
2122
|
leaseAction,
|
|
1773
2123
|
leaseFromResponse,
|
|
1774
2124
|
leaseRenewAt,
|
|
1775
2125
|
leaseState,
|
|
1776
2126
|
linkRanges,
|
|
1777
2127
|
measure,
|
|
2128
|
+
nextOccurrence,
|
|
2129
|
+
parseInvite,
|
|
1778
2130
|
parseMime,
|
|
1779
2131
|
payloadHash,
|
|
1780
2132
|
postChain,
|
|
@@ -1795,6 +2147,7 @@ export {
|
|
|
1795
2147
|
sandboxIsSelectable,
|
|
1796
2148
|
sandboxRefusal,
|
|
1797
2149
|
satisfiesMinimum,
|
|
2150
|
+
secretKeyBytes,
|
|
1798
2151
|
seedFrom,
|
|
1799
2152
|
shapeOf,
|
|
1800
2153
|
shouldRetry,
|