@mulmoclaude/core 1.6.0 → 1.7.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/assets/helps/error-recovery.md +46 -0
- package/assets/helps/google-calendar-collection.md +57 -3
- package/assets/helps/google.md +3 -2
- package/dist/calendarGrid-CaS9er8i.cjs +576 -0
- package/dist/calendarGrid-CaS9er8i.cjs.map +1 -0
- package/dist/calendarGrid-CrGxeCOG.js +397 -0
- package/dist/calendarGrid-CrGxeCOG.js.map +1 -0
- package/dist/collection/index.cjs +44 -44
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.js +2 -2
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/index.cjs +4 -4
- package/dist/collection/server/index.js +3 -3
- package/dist/collection-watchers/index.cjs +5 -5
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.js +4 -4
- package/dist/{discovery--kRUWed0.cjs → discovery-DYcR83qx.cjs} +64 -53
- package/dist/discovery-DYcR83qx.cjs.map +1 -0
- package/dist/{discovery-CKR4TOX3.js → discovery-_fjsluLx.js} +43 -32
- package/dist/discovery-_fjsluLx.js.map +1 -0
- package/dist/feeds/index.cjs +4 -4
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/index.cjs +6 -6
- package/dist/feeds/server/index.js +4 -4
- package/dist/google/apiClient.d.ts +6 -0
- package/dist/google/calendar.d.ts +79 -8
- package/dist/google/calendarPushState.d.ts +17 -0
- package/dist/google/collectionPush.d.ts +64 -0
- package/dist/google/collectionSync.d.ts +10 -0
- package/dist/google/index.cjs +712 -36
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.d.ts +7 -3
- package/dist/google/index.js +683 -37
- package/dist/google/index.js.map +1 -1
- package/dist/google/pushDateTime.d.ts +10 -0
- package/dist/google/pushPlan.d.ts +73 -0
- package/dist/{ingestTypes-xX8VpQqh.cjs → ingestTypes-D1GQdG8e.cjs} +3 -3
- package/dist/{ingestTypes-xX8VpQqh.cjs.map → ingestTypes-D1GQdG8e.cjs.map} +1 -1
- package/dist/{ingestTypes-L59cIGgX.js → ingestTypes-XReG-li7.js} +2 -2
- package/dist/{ingestTypes-L59cIGgX.js.map → ingestTypes-XReG-li7.js.map} +1 -1
- package/dist/{promptSafety-CQ5Un4wi.js → promptSafety-C--op_6a.js} +3 -269
- package/dist/promptSafety-C--op_6a.js.map +1 -0
- package/dist/{promptSafety-DfZYNtjK.cjs → promptSafety-FD8cQn3e.cjs} +8 -358
- package/dist/promptSafety-FD8cQn3e.cjs.map +1 -0
- package/dist/remote-host/index.cjs +8 -0
- package/dist/remote-host/index.cjs.map +1 -1
- package/dist/remote-host/index.d.ts +21 -0
- package/dist/remote-host/index.js +8 -1
- package/dist/remote-host/index.js.map +1 -1
- package/dist/{server-D_6FLygK.js → server-BlIrtGKl.js} +4 -4
- package/dist/{server-D_6FLygK.js.map → server-BlIrtGKl.js.map} +1 -1
- package/dist/{server-gEOeV_Nv.cjs → server-l4TZ5Lbf.cjs} +21 -21
- package/dist/{server-gEOeV_Nv.cjs.map → server-l4TZ5Lbf.cjs.map} +1 -1
- package/package.json +1 -1
- package/dist/discovery--kRUWed0.cjs.map +0 -1
- package/dist/discovery-CKR4TOX3.js.map +0 -1
- package/dist/ids-BnTh3Hl0.cjs +0 -226
- package/dist/ids-BnTh3Hl0.cjs.map +0 -1
- package/dist/ids-s4GfmE93.js +0 -131
- package/dist/ids-s4GfmE93.js.map +0 -1
- package/dist/promptSafety-CQ5Un4wi.js.map +0 -1
- package/dist/promptSafety-DfZYNtjK.cjs.map +0 -1
package/dist/google/index.cjs
CHANGED
|
@@ -4,7 +4,8 @@ const require_dist = require("../dist-pWpC-b04.cjs");
|
|
|
4
4
|
const require_text = require("../text-CaAh9ZSO.cjs");
|
|
5
5
|
const require_atomic = require("../atomic-C_7YpMiM.cjs");
|
|
6
6
|
const require_utils_fetch = require("../utils/fetch.cjs");
|
|
7
|
-
const
|
|
7
|
+
const require_calendarGrid = require("../calendarGrid-CaS9er8i.cjs");
|
|
8
|
+
const require_discovery = require("../discovery-DYcR83qx.cjs");
|
|
8
9
|
let node_fs = require("node:fs");
|
|
9
10
|
let node_path = require("node:path");
|
|
10
11
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
@@ -24,7 +25,7 @@ var log = require_discovery.createForwardingLogger(() => hostSlot.peek());
|
|
|
24
25
|
//#endregion
|
|
25
26
|
//#region src/google/datetime.ts
|
|
26
27
|
var ISO_DATE_TIME_WITH_OFFSET_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:Z|[+-](\d{2}):(\d{2}))$/;
|
|
27
|
-
var FRACTIONAL_SECONDS_RE$
|
|
28
|
+
var FRACTIONAL_SECONDS_RE$2 = /\.\d+(?=Z|[+-])/;
|
|
28
29
|
var MAX_HOUR = 23;
|
|
29
30
|
var MAX_MINUTE = 59;
|
|
30
31
|
var MAX_SECOND = 59;
|
|
@@ -33,7 +34,7 @@ var isRealCalendarDate = (year, month, day) => {
|
|
|
33
34
|
return roundTrip.getUTCFullYear() === year && roundTrip.getUTCMonth() === month - 1 && roundTrip.getUTCDate() === day;
|
|
34
35
|
};
|
|
35
36
|
var isIsoDateTimeWithOffset = (value) => {
|
|
36
|
-
const match = ISO_DATE_TIME_WITH_OFFSET_RE.exec(value.replace(FRACTIONAL_SECONDS_RE$
|
|
37
|
+
const match = ISO_DATE_TIME_WITH_OFFSET_RE.exec(value.replace(FRACTIONAL_SECONDS_RE$2, ""));
|
|
37
38
|
if (!match) return false;
|
|
38
39
|
const [year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0] = match.slice(1, 7).map(Number);
|
|
39
40
|
const timeInRange = hour <= MAX_HOUR && minute <= MAX_MINUTE && second <= MAX_SECOND;
|
|
@@ -555,14 +556,15 @@ var isGoogleApiError = (value) => value instanceof GoogleApiError;
|
|
|
555
556
|
/** 403 usually means the API is not enabled for the user's Cloud project —
|
|
556
557
|
* name the API so the agent's recovery guidance can be specific. */
|
|
557
558
|
var googleApiError = (apiLabel, status, body) => {
|
|
558
|
-
return new GoogleApiError(status, `${apiLabel}: HTTP ${status}${status ===
|
|
559
|
+
return new GoogleApiError(status, `${apiLabel}: HTTP ${status}${status === 403 ? ` (is the ${apiLabel} enabled for the Cloud project?)` : ""}${body ? ` — ${require_text.truncate(body, ERROR_BODY_MAX_CHARS)}` : ""}`);
|
|
559
560
|
};
|
|
560
561
|
async function googleRequest(apiLabel, accessToken, url, init = {}) {
|
|
561
|
-
const { contentType = "application/json", expectText = false, ...rest } = init;
|
|
562
|
+
const { contentType = "application/json", expectText = false, extraHeaders, ...rest } = init;
|
|
562
563
|
const response = await require_utils_fetch.fetchWithTimeout(url, {
|
|
563
564
|
...rest,
|
|
564
565
|
timeoutMs: GOOGLE_API_TIMEOUT_MS,
|
|
565
566
|
headers: {
|
|
567
|
+
...extraHeaders,
|
|
566
568
|
Authorization: `Bearer ${accessToken}`,
|
|
567
569
|
"Content-Type": contentType
|
|
568
570
|
}
|
|
@@ -591,6 +593,16 @@ var MAX_CALENDAR_LIST_PAGES = 40;
|
|
|
591
593
|
var EVENT_SYNC_PAGE_SIZE = 2500;
|
|
592
594
|
var MAX_EVENT_SYNC_PAGES = 200;
|
|
593
595
|
var HTTP_GONE = 410;
|
|
596
|
+
var EVENT_ABSENT_STATUSES = [404, HTTP_GONE];
|
|
597
|
+
/** An `If-Match` write whose etag no longer matches — someone else changed the
|
|
598
|
+
* event since it was read. */
|
|
599
|
+
var HTTP_PRECONDITION_FAILED = 412;
|
|
600
|
+
/** An `insert` whose caller-supplied event id is already taken. */
|
|
601
|
+
var HTTP_CONFLICT = 409;
|
|
602
|
+
/** Google's status for an event that was deleted; a sync window reports it so
|
|
603
|
+
* consumers can remove their copy. Single-sourced — the pull and the push both
|
|
604
|
+
* branch on it. */
|
|
605
|
+
var CANCELLED_EVENT_STATUS = "cancelled";
|
|
594
606
|
/** Resolve a declared calendarId to the one the API and the sync-token store
|
|
595
607
|
* both address. `||` (not `??`) so an empty string also falls back instead of
|
|
596
608
|
* building a malformed `/calendars//events` URL. Single-sourced because an
|
|
@@ -598,7 +610,7 @@ var HTTP_GONE = 410;
|
|
|
598
610
|
* apart while they share a sync token silently loses events (#2184). */
|
|
599
611
|
var canonicalCalendarId = (calendarId) => calendarId || DEFAULT_CALENDAR_ID;
|
|
600
612
|
var eventsUrl = (calendarId) => `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(canonicalCalendarId(calendarId))}/events`;
|
|
601
|
-
var eventTime = (value) => {
|
|
613
|
+
var eventTime$1 = (value) => {
|
|
602
614
|
if (!isRecord(value)) return "";
|
|
603
615
|
if (typeof value.dateTime === "string") return value.dateTime;
|
|
604
616
|
if (typeof value.date === "string") return value.date;
|
|
@@ -609,8 +621,8 @@ var toEventSummary = (value) => {
|
|
|
609
621
|
return {
|
|
610
622
|
id: stringField(record, "id"),
|
|
611
623
|
summary: stringField(record, "summary"),
|
|
612
|
-
start: eventTime(record.start),
|
|
613
|
-
end: eventTime(record.end),
|
|
624
|
+
start: eventTime$1(record.start),
|
|
625
|
+
end: eventTime$1(record.end),
|
|
614
626
|
htmlLink: stringField(record, "htmlLink"),
|
|
615
627
|
status: stringField(record, "status"),
|
|
616
628
|
colorId: stringField(record, "colorId")
|
|
@@ -626,7 +638,8 @@ var toCalendarSummary = (value) => {
|
|
|
626
638
|
accessRole: stringField(record, "accessRole"),
|
|
627
639
|
backgroundColor: stringField(record, "backgroundColor"),
|
|
628
640
|
foregroundColor: stringField(record, "foregroundColor"),
|
|
629
|
-
colorId: stringField(record, "colorId")
|
|
641
|
+
colorId: stringField(record, "colorId"),
|
|
642
|
+
timeZone: stringField(record, "timeZone")
|
|
630
643
|
};
|
|
631
644
|
};
|
|
632
645
|
var toColorMap = (value) => {
|
|
@@ -642,13 +655,21 @@ var toColorMap = (value) => {
|
|
|
642
655
|
/** Kept as a named export for the existing unit tests / callers; the shared
|
|
643
656
|
* helper now carries the wording. */
|
|
644
657
|
var calendarApiError = (status, body) => googleApiError(CALENDAR_API_LABEL, status, body);
|
|
658
|
+
/** Normalise either spelling of a span into the pair the API body carries. */
|
|
659
|
+
var resolveEventSpan = (span) => "start" in span ? {
|
|
660
|
+
start: span.start,
|
|
661
|
+
end: span.end
|
|
662
|
+
} : {
|
|
663
|
+
start: { dateTime: span.startDateTime },
|
|
664
|
+
end: { dateTime: span.endDateTime }
|
|
665
|
+
};
|
|
645
666
|
async function createCalendarEvent(accessToken, input) {
|
|
646
667
|
const body = {
|
|
647
668
|
summary: input.summary,
|
|
648
669
|
description: input.description,
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
...input.
|
|
670
|
+
...resolveEventSpan(input),
|
|
671
|
+
...input.colorId ? { colorId: input.colorId } : {},
|
|
672
|
+
...input.eventId ? { id: input.eventId } : {}
|
|
652
673
|
};
|
|
653
674
|
return toEventSummary(await googleRequest(CALENDAR_API_LABEL, accessToken, eventsUrl(input.calendarId), {
|
|
654
675
|
method: "POST",
|
|
@@ -661,11 +682,17 @@ var eventUrl = (calendarId, eventId) => `${eventsUrl(calendarId)}/${encodeURICom
|
|
|
661
682
|
* `undefined` means "leave as is" and `""` means "clear it", so the two cannot
|
|
662
683
|
* be collapsed: dropping `description: ""` would silently ignore a request to
|
|
663
684
|
* empty the body text. Pure so the distinction is testable without network. */
|
|
685
|
+
/** One end of the patch: the structured value if given, else the flat one
|
|
686
|
+
* wrapped, else absent (= leave it alone). */
|
|
687
|
+
var eventEnd = (key, structured, flat) => {
|
|
688
|
+
if (structured !== void 0) return { [key]: structured };
|
|
689
|
+
return flat !== void 0 ? { [key]: { dateTime: flat } } : {};
|
|
690
|
+
};
|
|
664
691
|
var buildEventPatch = (input) => ({
|
|
665
692
|
...input.summary !== void 0 ? { summary: input.summary } : {},
|
|
666
693
|
...input.description !== void 0 ? { description: input.description } : {},
|
|
667
|
-
...input.
|
|
668
|
-
...input.
|
|
694
|
+
...eventEnd("start", input.start, input.startDateTime),
|
|
695
|
+
...eventEnd("end", input.end, input.endDateTime),
|
|
669
696
|
...input.colorId ? { colorId: input.colorId } : {}
|
|
670
697
|
});
|
|
671
698
|
/** Edit an existing event in place. PATCH, not PUT — a PUT would need the whole
|
|
@@ -674,9 +701,28 @@ var buildEventPatch = (input) => ({
|
|
|
674
701
|
async function updateCalendarEvent(accessToken, input) {
|
|
675
702
|
return toEventSummary(await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId), {
|
|
676
703
|
method: "PATCH",
|
|
677
|
-
body: JSON.stringify(buildEventPatch(input))
|
|
704
|
+
body: JSON.stringify(buildEventPatch(input)),
|
|
705
|
+
...input.ifMatch ? { extraHeaders: { "If-Match": input.ifMatch } } : {}
|
|
678
706
|
}));
|
|
679
707
|
}
|
|
708
|
+
/** Read ONE event, or null when it is gone (404 / 410).
|
|
709
|
+
*
|
|
710
|
+
* A push needs Google's current value per changed record to tell a local-only
|
|
711
|
+
* edit from a both-sides conflict. Deliberately not `syncCalendarEvents`: that
|
|
712
|
+
* consumes the calendar's sync token, and the token's window is served once —
|
|
713
|
+
* spending it here would make the next pull miss everything in it. */
|
|
714
|
+
async function getCalendarEvent(accessToken, input) {
|
|
715
|
+
try {
|
|
716
|
+
const fetched = await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId));
|
|
717
|
+
return {
|
|
718
|
+
event: toEventSummary(fetched),
|
|
719
|
+
etag: stringField(asRecord(fetched), "etag")
|
|
720
|
+
};
|
|
721
|
+
} catch (error) {
|
|
722
|
+
if (isGoogleApiError(error) && EVENT_ABSENT_STATUSES.includes(error.status)) return null;
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
680
726
|
/** Remove an event. Google answers 204 with no body, so there is nothing to
|
|
681
727
|
* return; a second delete of the same id answers 410, which surfaces as a
|
|
682
728
|
* GoogleApiError rather than being swallowed. */
|
|
@@ -756,6 +802,17 @@ async function collectCalendarPages(fetchPage, maxPages = MAX_CALENDAR_LIST_PAGE
|
|
|
756
802
|
/** The calendars the user has added/subscribed to (primary + secondary +
|
|
757
803
|
* shared), each with its id, name and colour, following pagination. Needs the
|
|
758
804
|
* calendar-list read scope (GOOGLE_CALENDARLIST_SCOPE). */
|
|
805
|
+
/** One calendar's own resource, addressed by id rather than looked up in the
|
|
806
|
+
* user's list.
|
|
807
|
+
*
|
|
808
|
+
* `calendarList` only holds calendars the user has ADDED; a calendar shared
|
|
809
|
+
* with them can be readable and writable by id without appearing there. This is
|
|
810
|
+
* how a push learns such a calendar's `timeZone`. Note the resource carries no
|
|
811
|
+
* `accessRole` — that is a calendarList property — so reachability here says
|
|
812
|
+
* nothing about writability. */
|
|
813
|
+
async function getCalendar(accessToken, calendarId) {
|
|
814
|
+
return toCalendarSummary(await googleRequest(CALENDAR_API_LABEL, accessToken, `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(canonicalCalendarId(calendarId))}`));
|
|
815
|
+
}
|
|
759
816
|
async function listCalendars(accessToken) {
|
|
760
817
|
return collectCalendarPages(async (pageToken) => {
|
|
761
818
|
const params = new URLSearchParams({ maxResults: String(CALENDAR_LIST_PAGE_SIZE) });
|
|
@@ -784,25 +841,25 @@ function calendarSyncStatePath(workspaceRoot) {
|
|
|
784
841
|
return node_path.default.join(workspaceRoot ?? require_discovery.getWorkspaceRoot(), "data", "calendar", ".sync-state.json");
|
|
785
842
|
}
|
|
786
843
|
var calendarKey = canonicalCalendarId;
|
|
787
|
-
async function readState(workspaceRoot) {
|
|
844
|
+
async function readState$1(workspaceRoot) {
|
|
788
845
|
const stored = await readJsonOrNull(calendarSyncStatePath(workspaceRoot));
|
|
789
846
|
return stored?.tokens ? stored : { tokens: {} };
|
|
790
847
|
}
|
|
791
|
-
var writeQueue = { tail: Promise.resolve() };
|
|
792
|
-
async function updateState(mutate, workspaceRoot) {
|
|
793
|
-
const run = writeQueue.tail.then(async () => {
|
|
794
|
-
const state = await readState(workspaceRoot);
|
|
848
|
+
var writeQueue$1 = { tail: Promise.resolve() };
|
|
849
|
+
async function updateState$1(mutate, workspaceRoot) {
|
|
850
|
+
const run = writeQueue$1.tail.then(async () => {
|
|
851
|
+
const state = await readState$1(workspaceRoot);
|
|
795
852
|
await writeJsonAtomicWithMode(calendarSyncStatePath(workspaceRoot), { tokens: mutate(state.tokens) }, SYNC_STATE_MODE);
|
|
796
853
|
});
|
|
797
|
-
writeQueue.tail = run.catch(() => void 0);
|
|
854
|
+
writeQueue$1.tail = run.catch(() => void 0);
|
|
798
855
|
return await run;
|
|
799
856
|
}
|
|
800
857
|
async function loadCalendarSyncToken(calendarId, workspaceRoot) {
|
|
801
|
-
return (await readState(workspaceRoot)).tokens[calendarKey(calendarId)] ?? null;
|
|
858
|
+
return (await readState$1(workspaceRoot)).tokens[calendarKey(calendarId)] ?? null;
|
|
802
859
|
}
|
|
803
860
|
async function saveCalendarSyncToken(calendarId, syncToken, workspaceRoot) {
|
|
804
861
|
const stored = calendarKey(calendarId);
|
|
805
|
-
await updateState((tokens) => ({
|
|
862
|
+
await updateState$1((tokens) => ({
|
|
806
863
|
...tokens,
|
|
807
864
|
[stored]: syncToken
|
|
808
865
|
}), workspaceRoot);
|
|
@@ -811,7 +868,60 @@ async function saveCalendarSyncToken(calendarId, syncToken, workspaceRoot) {
|
|
|
811
868
|
* starts a clean full sync. */
|
|
812
869
|
async function clearCalendarSyncToken(calendarId, workspaceRoot) {
|
|
813
870
|
const dropped = calendarKey(calendarId);
|
|
814
|
-
await updateState((tokens) => Object.fromEntries(Object.entries(tokens).filter(([key]) => key !== dropped)), workspaceRoot);
|
|
871
|
+
await updateState$1((tokens) => Object.fromEntries(Object.entries(tokens).filter(([key]) => key !== dropped)), workspaceRoot);
|
|
872
|
+
}
|
|
873
|
+
//#endregion
|
|
874
|
+
//#region src/google/calendarPushState.ts
|
|
875
|
+
var PUSH_STATE_MODE = 384;
|
|
876
|
+
/** `<workspace>/data/calendar/.push-state.json` */
|
|
877
|
+
function calendarPushStatePath(workspaceRoot) {
|
|
878
|
+
return node_path.default.join(workspaceRoot ?? require_discovery.getWorkspaceRoot(), "data", "calendar", ".push-state.json");
|
|
879
|
+
}
|
|
880
|
+
var toShadowEvent = (event) => ({
|
|
881
|
+
summary: event.summary,
|
|
882
|
+
start: event.start,
|
|
883
|
+
end: event.end,
|
|
884
|
+
colorId: event.colorId
|
|
885
|
+
});
|
|
886
|
+
async function readState(workspaceRoot) {
|
|
887
|
+
const stored = await readJsonOrNull(calendarPushStatePath(workspaceRoot));
|
|
888
|
+
return stored?.events ? stored : { events: {} };
|
|
889
|
+
}
|
|
890
|
+
var writeQueue = { tail: Promise.resolve() };
|
|
891
|
+
async function updateState(mutate, workspaceRoot) {
|
|
892
|
+
const run = writeQueue.tail.then(async () => {
|
|
893
|
+
const state = await readState(workspaceRoot);
|
|
894
|
+
await writeJsonAtomicWithMode(calendarPushStatePath(workspaceRoot), { events: mutate(state.events) }, PUSH_STATE_MODE);
|
|
895
|
+
});
|
|
896
|
+
writeQueue.tail = run.catch(() => void 0);
|
|
897
|
+
return await run;
|
|
898
|
+
}
|
|
899
|
+
async function loadCalendarShadow(calendarId, workspaceRoot) {
|
|
900
|
+
return (await readState(workspaceRoot)).events[canonicalCalendarId(calendarId)] ?? {};
|
|
901
|
+
}
|
|
902
|
+
/** Apply a batch to one calendar's baseline. `null` removes an entry (the event
|
|
903
|
+
* was cancelled), so a cancel-then-recreate cannot resume from a stale one.
|
|
904
|
+
* Merged rather than replaced: a sync window describes only what changed. */
|
|
905
|
+
function mergeShadow(current, updates) {
|
|
906
|
+
const removed = new Set(Object.entries(updates).filter(([, entry]) => entry === null).map(([eventId]) => eventId));
|
|
907
|
+
const kept = Object.entries(current).filter(([eventId]) => !removed.has(eventId));
|
|
908
|
+
const written = Object.entries(updates).filter((entry) => entry[1] !== null);
|
|
909
|
+
return Object.fromEntries([...kept, ...written]);
|
|
910
|
+
}
|
|
911
|
+
async function saveCalendarShadow(calendarId, updates, workspaceRoot) {
|
|
912
|
+
if (Object.keys(updates).length === 0) return;
|
|
913
|
+
const key = canonicalCalendarId(calendarId);
|
|
914
|
+
await updateState((events) => ({
|
|
915
|
+
...events,
|
|
916
|
+
[key]: mergeShadow(events[key] ?? {}, updates)
|
|
917
|
+
}), workspaceRoot);
|
|
918
|
+
}
|
|
919
|
+
/** Drop one calendar's whole baseline. Called wherever the sync token is
|
|
920
|
+
* cleared: a baseline outliving its token would let a push diff against events
|
|
921
|
+
* the workspace no longer holds. */
|
|
922
|
+
async function clearCalendarShadow(calendarId, workspaceRoot) {
|
|
923
|
+
const dropped = canonicalCalendarId(calendarId);
|
|
924
|
+
await updateState((events) => Object.fromEntries(Object.entries(events).filter(([key]) => key !== dropped)), workspaceRoot);
|
|
815
925
|
}
|
|
816
926
|
//#endregion
|
|
817
927
|
//#region src/google/collectionDateTime.ts
|
|
@@ -819,9 +929,9 @@ async function clearCalendarSyncToken(calendarId, workspaceRoot) {
|
|
|
819
929
|
var ALL_DAY_CLOCK = "00:00";
|
|
820
930
|
var DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
821
931
|
/** RFC3339 zone designator: `Z`, `+09:00`, `-0500`. */
|
|
822
|
-
var ZONE_SUFFIX_RE = /(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
932
|
+
var ZONE_SUFFIX_RE$1 = /(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
823
933
|
/** Fractional seconds have no place in the collection datetime shape. */
|
|
824
|
-
var FRACTIONAL_SECONDS_RE = /\.\d+$/;
|
|
934
|
+
var FRACTIONAL_SECONDS_RE$1 = /\.\d+$/;
|
|
825
935
|
/** Normalise one Google Calendar time value for storage in a `datetime` field.
|
|
826
936
|
* Anything that isn't a recognisable Google shape (empty string, non-string,
|
|
827
937
|
* free text) is returned untouched — the record lint should report it rather
|
|
@@ -831,13 +941,12 @@ function toCollectionDateTime(value) {
|
|
|
831
941
|
const trimmed = value.trim();
|
|
832
942
|
if (DATE_ONLY_RE.test(trimmed)) return `${trimmed}T${ALL_DAY_CLOCK}`;
|
|
833
943
|
if (!trimmed.includes("T")) return value;
|
|
834
|
-
return trimmed.replace(ZONE_SUFFIX_RE, "").replace(FRACTIONAL_SECONDS_RE, "");
|
|
944
|
+
return trimmed.replace(ZONE_SUFFIX_RE$1, "").replace(FRACTIONAL_SECONDS_RE$1, "");
|
|
835
945
|
}
|
|
836
946
|
//#endregion
|
|
837
947
|
//#region src/google/collectionSync.ts
|
|
838
948
|
var GOOGLE_CALENDAR_SYNC_TASK_ID = "system:google-calendar-sync";
|
|
839
949
|
var DEFAULT_SYNC_INTERVAL_MS = 3600 * 1e3;
|
|
840
|
-
var CANCELLED_STATUS = "cancelled";
|
|
841
950
|
var DATETIME_FIELD_TYPE = "datetime";
|
|
842
951
|
/** Own-property lookup, mirroring what the record lint reads: a spec reachable
|
|
843
952
|
* only through the prototype chain is not a DECLARED field, so it must not
|
|
@@ -893,7 +1002,7 @@ async function applyEvent(collection, event, workspaceRoot) {
|
|
|
893
1002
|
kind: "unwritable",
|
|
894
1003
|
message: `collection '${collection.slug}' is read-only`
|
|
895
1004
|
};
|
|
896
|
-
if (event.status ===
|
|
1005
|
+
if (event.status === "cancelled") {
|
|
897
1006
|
const deleted = await store.delete(event.id);
|
|
898
1007
|
return classifyDelete(event.id, deleted.kind);
|
|
899
1008
|
}
|
|
@@ -909,6 +1018,7 @@ async function applyEvent(collection, event, workspaceRoot) {
|
|
|
909
1018
|
}
|
|
910
1019
|
async function restartFullSync(accessToken, calendarId, workspaceRoot) {
|
|
911
1020
|
await clearCalendarSyncToken(calendarId, workspaceRoot);
|
|
1021
|
+
await clearCalendarShadow(calendarId, workspaceRoot);
|
|
912
1022
|
return await syncCalendarEvents(accessToken, { calendarId });
|
|
913
1023
|
}
|
|
914
1024
|
/** Serialise `run` against whatever is already running for `key`.
|
|
@@ -946,8 +1056,16 @@ var calendarLocks = /* @__PURE__ */ new Map();
|
|
|
946
1056
|
* writes are upserts by event id — but it is a wasted full walk. Queued, the
|
|
947
1057
|
* second pass resumes from the token the first just stored and fetches only
|
|
948
1058
|
* what is genuinely new. */
|
|
1059
|
+
/** Serialise anything that touches ONE calendar's sync state.
|
|
1060
|
+
*
|
|
1061
|
+
* Shared with the push path (#2598), not only the pull doors: a push that
|
|
1062
|
+
* overtakes an in-flight pull would record a baseline for events the pull is
|
|
1063
|
+
* still writing, so the next push would diff against a future it never saw. */
|
|
1064
|
+
async function withCalendarLock(calendarId, run) {
|
|
1065
|
+
return await withKeyedLock(calendarLocks, canonicalCalendarId(calendarId), run);
|
|
1066
|
+
}
|
|
949
1067
|
async function syncCalendarGroup(calendarId, collections, workspaceRoot) {
|
|
950
|
-
return await
|
|
1068
|
+
return await withCalendarLock(calendarId, () => syncCalendarGroupNow(calendarId, collections, workspaceRoot));
|
|
951
1069
|
}
|
|
952
1070
|
async function syncCalendarGroupNow(calendarId, collections, workspaceRoot) {
|
|
953
1071
|
const accessToken = await getGoogleAccessToken();
|
|
@@ -971,9 +1089,15 @@ async function syncCalendarGroupNow(calendarId, collections, workspaceRoot) {
|
|
|
971
1089
|
});
|
|
972
1090
|
return results;
|
|
973
1091
|
}
|
|
1092
|
+
await saveCalendarShadow(calendarId, shadowUpdates(result.events), workspaceRoot);
|
|
974
1093
|
if (result.nextSyncToken) await advanceToken(calendarId, result.nextSyncToken, collections, workspaceRoot);
|
|
975
1094
|
return results;
|
|
976
1095
|
}
|
|
1096
|
+
/** The baseline this window establishes: what Google now says per event, and
|
|
1097
|
+
* `null` for a cancelled one so a recreate cannot resume from a dead baseline. */
|
|
1098
|
+
function shadowUpdates(events) {
|
|
1099
|
+
return Object.fromEntries(events.map((event) => [event.id, event.status === "cancelled" ? null : toShadowEvent(event)]));
|
|
1100
|
+
}
|
|
977
1101
|
/** Save the window's token unless every collection that consumed it was deleted
|
|
978
1102
|
* while the sync was in flight.
|
|
979
1103
|
*
|
|
@@ -1065,6 +1189,7 @@ async function releaseOrphanedCalendarToken(deleted, workspaceRoot) {
|
|
|
1065
1189
|
const orphaned = orphanedCalendarId(deleted, (await require_discovery.discoverCollections({ workspaceRoot })).map((collection) => collection.schema));
|
|
1066
1190
|
if (orphaned === null) return null;
|
|
1067
1191
|
await clearCalendarSyncToken(orphaned, workspaceRoot);
|
|
1192
|
+
await clearCalendarShadow(orphaned, workspaceRoot);
|
|
1068
1193
|
log.info("google", "cleared the sync token of a calendar no collection reads any more", { calendarId: orphaned });
|
|
1069
1194
|
return orphaned;
|
|
1070
1195
|
} catch (error) {
|
|
@@ -1174,6 +1299,522 @@ function googleCalendarSyncTaskDef(opts) {
|
|
|
1174
1299
|
};
|
|
1175
1300
|
}
|
|
1176
1301
|
//#endregion
|
|
1302
|
+
//#region src/google/pushDateTime.ts
|
|
1303
|
+
/** RFC3339 zone designator: `Z`, `+09:00`, `-0500`. */
|
|
1304
|
+
var ZONE_SUFFIX_RE = /(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
1305
|
+
var FRACTIONAL_SECONDS_RE = /\.\d+$/;
|
|
1306
|
+
var TWO_DIGITS_RE = /^\d{2}$/;
|
|
1307
|
+
var CLOCK_SEPARATOR = ":";
|
|
1308
|
+
var DATE_TIME_SEPARATOR = "T";
|
|
1309
|
+
var MIN_CLOCK_SEGMENTS = 2;
|
|
1310
|
+
var MAX_CLOCK_SEGMENTS = 3;
|
|
1311
|
+
var WHOLE_MINUTE_SECONDS = "00";
|
|
1312
|
+
var MAX_SECONDS = 59;
|
|
1313
|
+
/** Whether `value` is a real calendar day (`2026-02-30` is not).
|
|
1314
|
+
*
|
|
1315
|
+
* Delegated to the parser the record lint, the calendar grid and the day view
|
|
1316
|
+
* already share, so the push agrees with the rest of the app on what a valid
|
|
1317
|
+
* stored date IS — and rejects an impossible one here instead of turning it
|
|
1318
|
+
* into an opaque Google 400. */
|
|
1319
|
+
var isCalendarDate = (value) => require_calendarGrid.parseIsoDate(value) !== null;
|
|
1320
|
+
/** A value Google already accepts verbatim, as the event time to send, or null.
|
|
1321
|
+
*
|
|
1322
|
+
* Reached when the schema maps `start` onto a `string` field: `projectValue`
|
|
1323
|
+
* normalises only `datetime` fields, so that column holds Google's own text,
|
|
1324
|
+
* zone and all. The civil part is still validated — matching a trailing `Z` is
|
|
1325
|
+
* not evidence that the rest is a date-time. */
|
|
1326
|
+
function rawGoogleTime(value) {
|
|
1327
|
+
if (isCalendarDate(value)) return { date: value };
|
|
1328
|
+
const zone = value.match(ZONE_SUFFIX_RE)?.[0];
|
|
1329
|
+
if (zone === void 0) return null;
|
|
1330
|
+
return require_calendarGrid.parseIsoDateTime(value.slice(0, value.length - zone.length).replace(FRACTIONAL_SECONDS_RE, "")) === null ? null : { dateTime: value };
|
|
1331
|
+
}
|
|
1332
|
+
/** Google's own all-day `end` is EXCLUSIVE — a single day on the 12th ends on
|
|
1333
|
+
* the 13th. The pull stores that as `2026-05-13T00:00` and this takes the date
|
|
1334
|
+
* part straight back, so the exclusive end round-trips untouched. Looks
|
|
1335
|
+
* off-by-one; adjusting it would shorten every all-day event by a day. */
|
|
1336
|
+
function allDayFrom(datePart) {
|
|
1337
|
+
return { date: datePart };
|
|
1338
|
+
}
|
|
1339
|
+
/** Split `HH:MM[:SS]` into RFC3339's always-three segments, or null.
|
|
1340
|
+
*
|
|
1341
|
+
* Split rather than one regex: expressing the optional seconds as `(?::(\d{2}))?`
|
|
1342
|
+
* nests a quantifier inside a quantified group, which the ReDoS lint rejects. */
|
|
1343
|
+
function clockSegments(clockPart) {
|
|
1344
|
+
const segments = clockPart.split(CLOCK_SEPARATOR);
|
|
1345
|
+
if (segments.length < MIN_CLOCK_SEGMENTS || segments.length > MAX_CLOCK_SEGMENTS) return null;
|
|
1346
|
+
if (!segments.every((segment) => TWO_DIGITS_RE.test(segment))) return null;
|
|
1347
|
+
const [hours, minutes, seconds] = segments;
|
|
1348
|
+
if (seconds !== void 0 && Number(seconds) > MAX_SECONDS) return null;
|
|
1349
|
+
return [
|
|
1350
|
+
hours,
|
|
1351
|
+
minutes,
|
|
1352
|
+
seconds ?? WHOLE_MINUTE_SECONDS
|
|
1353
|
+
].join(CLOCK_SEPARATOR);
|
|
1354
|
+
}
|
|
1355
|
+
/** `YYYY-MM-DDTHH:MM[:SS]` → its date and its RFC3339 clock, or null.
|
|
1356
|
+
*
|
|
1357
|
+
* Validity comes from the shared strict parser, so an out-of-range clock or an
|
|
1358
|
+
* impossible day is refused here rather than sent to Google. */
|
|
1359
|
+
function parseStoredDateTime(value) {
|
|
1360
|
+
if (require_calendarGrid.parseIsoDateTime(value) === null) return null;
|
|
1361
|
+
const [datePart, clockPart, ...extra] = value.split(DATE_TIME_SEPARATOR);
|
|
1362
|
+
if (datePart === void 0 || clockPart === void 0 || extra.length > 0) return null;
|
|
1363
|
+
const clock = clockSegments(clockPart);
|
|
1364
|
+
return clock === null ? null : {
|
|
1365
|
+
datePart,
|
|
1366
|
+
clock
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
/** The zone designator to re-attach, or null when `previous` carries none. */
|
|
1370
|
+
function zoneSuffixOf(previous) {
|
|
1371
|
+
if (previous === void 0) return null;
|
|
1372
|
+
return previous.match(ZONE_SUFFIX_RE)?.[0] ?? null;
|
|
1373
|
+
}
|
|
1374
|
+
/** Build the event time to send for one field.
|
|
1375
|
+
*
|
|
1376
|
+
* `previous` is the value Google last reported for this event, absent for a
|
|
1377
|
+
* locally-created record. Returns null when `local` is neither a stored
|
|
1378
|
+
* datetime nor a raw Google value — the caller reports that record rather than
|
|
1379
|
+
* letting this invent a time Google would silently accept. */
|
|
1380
|
+
function toGoogleEventTime(local, previous, calendarTimeZone) {
|
|
1381
|
+
if (typeof local !== "string") return null;
|
|
1382
|
+
const trimmed = local.trim();
|
|
1383
|
+
if (trimmed.length === 0) return null;
|
|
1384
|
+
const raw = rawGoogleTime(trimmed);
|
|
1385
|
+
if (raw !== null) return raw;
|
|
1386
|
+
const parts = parseStoredDateTime(trimmed);
|
|
1387
|
+
if (parts === null) return null;
|
|
1388
|
+
if (previous !== void 0 && isCalendarDate(previous)) return allDayFrom(parts.datePart);
|
|
1389
|
+
const zone = zoneSuffixOf(previous);
|
|
1390
|
+
const dateTime = `${parts.datePart}${DATE_TIME_SEPARATOR}${parts.clock}`;
|
|
1391
|
+
return zone === null ? {
|
|
1392
|
+
dateTime,
|
|
1393
|
+
timeZone: calendarTimeZone
|
|
1394
|
+
} : { dateTime: `${dateTime}${zone}` };
|
|
1395
|
+
}
|
|
1396
|
+
//#endregion
|
|
1397
|
+
//#region src/google/pushPlan.ts
|
|
1398
|
+
/** The event fields Google lets a caller write. `htmlLink` and `status` are
|
|
1399
|
+
* read-only, so a record column mapped to either is ignored here rather than
|
|
1400
|
+
* rejected — the mapping was authored for the pull, and a push has no business
|
|
1401
|
+
* invalidating it. */
|
|
1402
|
+
var PUSHABLE_SOURCE_FIELDS = [
|
|
1403
|
+
"summary",
|
|
1404
|
+
"start",
|
|
1405
|
+
"end",
|
|
1406
|
+
"colorId"
|
|
1407
|
+
];
|
|
1408
|
+
var isPushableSource = (source) => PUSHABLE_SOURCE_FIELDS.includes(source);
|
|
1409
|
+
/** The subset of a schema's `map` a push can act on: collection field → event field. */
|
|
1410
|
+
function pushableMap(map) {
|
|
1411
|
+
return Object.fromEntries(Object.entries(map).filter((entry) => isPushableSource(entry[1])));
|
|
1412
|
+
}
|
|
1413
|
+
/** Event field → the record column feeding it. The schema authors the map the
|
|
1414
|
+
* other way round (for the pull); a push looks up by event field. Two columns
|
|
1415
|
+
* mapped to one event field is already ambiguous for the pull — last wins. */
|
|
1416
|
+
function bySourceField(map) {
|
|
1417
|
+
return Object.fromEntries(Object.entries(map).map(([field, source]) => [source, field]));
|
|
1418
|
+
}
|
|
1419
|
+
/** Google accepts a caller-chosen event id of 5-1024 base32hex characters. A
|
|
1420
|
+
* record id outside that (a semantic id like `team-standup`) cannot be used,
|
|
1421
|
+
* and v1 reports it instead of re-keying the record — see the plan. */
|
|
1422
|
+
var CLIENT_EVENT_ID_RE = /^[0-9a-v]{5,1024}$/;
|
|
1423
|
+
var isClientSettableEventId = (eventId) => CLIENT_EVENT_ID_RE.test(eventId);
|
|
1424
|
+
/** A field's value as comparable/sendable text.
|
|
1425
|
+
*
|
|
1426
|
+
* Absent, null and `""` all collapse to `""`: Google reports an inherited colour
|
|
1427
|
+
* as `""` while the record file may omit the key entirely, and treating those as
|
|
1428
|
+
* different would report a local edit on every single push.
|
|
1429
|
+
*
|
|
1430
|
+
* A non-primitive is JSON-encoded rather than left to `String`, whose
|
|
1431
|
+
* `[object Object]` would make two DIFFERENT objects compare equal — a
|
|
1432
|
+
* silently-skipped edit. */
|
|
1433
|
+
var fieldText = (value) => {
|
|
1434
|
+
if (value === void 0 || value === null) return "";
|
|
1435
|
+
if (typeof value === "string") return value;
|
|
1436
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
1437
|
+
return JSON.stringify(value) ?? "";
|
|
1438
|
+
};
|
|
1439
|
+
/** A stored datetime with no seconds — what the collection's own date-time input
|
|
1440
|
+
* produces. Google's values always carry seconds, so the pull writes them and a
|
|
1441
|
+
* hand-typed value does not. */
|
|
1442
|
+
var SECONDLESS_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/;
|
|
1443
|
+
var WHOLE_MINUTE = ":00";
|
|
1444
|
+
/** Field text with `…T09:30` and `…T09:30:00` collapsed to one spelling.
|
|
1445
|
+
*
|
|
1446
|
+
* Without this, a record the user typed a time into never stops looking edited:
|
|
1447
|
+
* the push writes it, Google answers with seconds, the baseline projection keeps
|
|
1448
|
+
* them, and the seconds-less record then differs from its own baseline on every
|
|
1449
|
+
* later push — re-PATCHing the same event on every click, forever.
|
|
1450
|
+
*
|
|
1451
|
+
* A value that carries a zone offset (a `start` mapped onto a `string` field) is
|
|
1452
|
+
* left alone: it is Google's own text on both sides already. */
|
|
1453
|
+
var comparableText = (value) => {
|
|
1454
|
+
const text = fieldText(value);
|
|
1455
|
+
return SECONDLESS_DATETIME_RE.test(text) ? `${text}${WHOLE_MINUTE}` : text;
|
|
1456
|
+
};
|
|
1457
|
+
/** A baseline shaped as the event it came from, so the comparison can run
|
|
1458
|
+
* through the very projection that wrote the record. `htmlLink`/`status` are
|
|
1459
|
+
* never pushable, so their absence from the baseline cannot matter. */
|
|
1460
|
+
var asEventSummary = (eventId, shadow) => ({
|
|
1461
|
+
id: eventId,
|
|
1462
|
+
htmlLink: "",
|
|
1463
|
+
status: "",
|
|
1464
|
+
...shadow
|
|
1465
|
+
});
|
|
1466
|
+
/** What the record WOULD hold if nobody had edited it since the baseline.
|
|
1467
|
+
*
|
|
1468
|
+
* Deliberately `toCollectionRecord` — the same function the pull writes with —
|
|
1469
|
+
* so "unchanged" here means exactly "the pull would produce this". A private
|
|
1470
|
+
* re-implementation would drift from it and report phantom edits. */
|
|
1471
|
+
function baselineRecord(eventId, shadow, map, primaryKey, fields) {
|
|
1472
|
+
return toCollectionRecord(asEventSummary(eventId, shadow), map, primaryKey, fields);
|
|
1473
|
+
}
|
|
1474
|
+
/** The mapped fields whose local value no longer matches the baseline. */
|
|
1475
|
+
function locallyChangedFields(record, baseline, map) {
|
|
1476
|
+
return Object.entries(map).filter(([field]) => comparableText(record[field]) !== comparableText(baseline[field])).map(([, source]) => source);
|
|
1477
|
+
}
|
|
1478
|
+
/** The fields Google changed since the baseline, restricted to the ones the
|
|
1479
|
+
* local edit touches.
|
|
1480
|
+
*
|
|
1481
|
+
* Field-level on purpose: Google moving an event the user retitled locally is
|
|
1482
|
+
* not a conflict, because the patch only carries the title. Comparing whole
|
|
1483
|
+
* events would refuse those pushes for no reason. */
|
|
1484
|
+
function conflictingFields(shadow, current, changed) {
|
|
1485
|
+
return changed.filter((source) => comparableText(shadow[source]) !== comparableText(current[source]));
|
|
1486
|
+
}
|
|
1487
|
+
/** Classify one record against its baseline, before Google is consulted.
|
|
1488
|
+
*
|
|
1489
|
+
* No baseline means the record never came from a sync, so it is local-only and
|
|
1490
|
+
* has to be created. Google is queried ONLY for a `changed` record, which is
|
|
1491
|
+
* what keeps a push over a large collection cheap. */
|
|
1492
|
+
function planRecord(eventId, record, shadow, map, primaryKey, fields) {
|
|
1493
|
+
if (shadow === void 0) return { kind: "create" };
|
|
1494
|
+
const changed = locallyChangedFields(record, baselineRecord(eventId, shadow, map, primaryKey, fields), map);
|
|
1495
|
+
return changed.length === 0 ? { kind: "unchanged" } : {
|
|
1496
|
+
kind: "changed",
|
|
1497
|
+
fields: changed
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
/** Whether an event that already holds this record's id may be adopted as its
|
|
1501
|
+
* baseline.
|
|
1502
|
+
*
|
|
1503
|
+
* A 409 from `insert` usually means a previous push created the event but its
|
|
1504
|
+
* baseline never landed. It can ALSO mean an unrelated event happens to hold
|
|
1505
|
+
* this id, and adopting that would bind the record to a stranger's event — so
|
|
1506
|
+
* adoption needs evidence the two are the same logical event, not just the same
|
|
1507
|
+
* id (Codex review).
|
|
1508
|
+
*
|
|
1509
|
+
* The evidence is that the remote event ALREADY equals what this record would
|
|
1510
|
+
* write. Adoption then records a baseline and writes nothing, so it cannot
|
|
1511
|
+
* modify whatever that event is. Anything else is refused. */
|
|
1512
|
+
function mayAdoptExisting(eventId, record, existing, map, primaryKey, fields) {
|
|
1513
|
+
return locallyChangedFields(record, baselineRecord(eventId, existing, map, primaryKey, fields), map).length === 0;
|
|
1514
|
+
}
|
|
1515
|
+
/** Record ids the baseline knows but the collection no longer holds — deleted
|
|
1516
|
+
* locally. v1 reports the count and pushes nothing: `calendarDeleteEvent`
|
|
1517
|
+
* removes the event for every attendee and cannot be undone. */
|
|
1518
|
+
function locallyDeletedIds(shadow, presentIds) {
|
|
1519
|
+
const present = new Set(presentIds);
|
|
1520
|
+
return Object.keys(shadow).filter((eventId) => !present.has(eventId));
|
|
1521
|
+
}
|
|
1522
|
+
//#endregion
|
|
1523
|
+
//#region src/google/collectionPush.ts
|
|
1524
|
+
/** Access roles that may write events. `reader` / `freeBusyReader` cannot, and a
|
|
1525
|
+
* push under them fails per event with an opaque 403 — checked once up front so
|
|
1526
|
+
* the user is told the real reason. */
|
|
1527
|
+
var WRITABLE_ACCESS_ROLES = ["owner", "writer"];
|
|
1528
|
+
/** Whether to refuse the whole push before touching a single event.
|
|
1529
|
+
*
|
|
1530
|
+
* Only on POSITIVE evidence of a non-writable role. An unlisted calendar is
|
|
1531
|
+
* unknown and must fall through: hard-denying it would block the feature
|
|
1532
|
+
* outright for a calendar the user can in fact write to. Those attempts surface
|
|
1533
|
+
* Google's own 403 per record if the write really is not allowed. */
|
|
1534
|
+
var isDeniedAccessRole = (accessRole) => accessRole !== null && !WRITABLE_ACCESS_ROLES.includes(accessRole);
|
|
1535
|
+
/** Resolve the calendar a schema names, for its writability and its timezone.
|
|
1536
|
+
*
|
|
1537
|
+
* The list is consulted first because only it reports `accessRole`, which is
|
|
1538
|
+
* what lets a read-only calendar be refused with the real reason instead of an
|
|
1539
|
+
* opaque per-event 403. `"primary"` is matched on the `primary` flag, not the
|
|
1540
|
+
* id — the primary calendar's own id is the account's email address, which never
|
|
1541
|
+
* equals the literal the schema declares.
|
|
1542
|
+
*
|
|
1543
|
+
* A calendar absent from the list is NOT denied: it is fetched by id for its
|
|
1544
|
+
* timezone and its role left unknown. A 404 there means the calendar really is
|
|
1545
|
+
* unreachable, and propagates as a `failed` outcome. */
|
|
1546
|
+
async function liveCalendarMeta(accessToken, calendarId) {
|
|
1547
|
+
const key = canonicalCalendarId(calendarId);
|
|
1548
|
+
const calendars = await listCalendars(accessToken);
|
|
1549
|
+
const listed = key === "primary" ? calendars.find((calendar) => calendar.primary) : calendars.find((calendar) => calendar.id === key);
|
|
1550
|
+
if (listed) return {
|
|
1551
|
+
accessRole: listed.accessRole,
|
|
1552
|
+
timeZone: listed.timeZone
|
|
1553
|
+
};
|
|
1554
|
+
const direct = await getCalendar(accessToken, calendarId);
|
|
1555
|
+
log.info("google", "calendar is not in the user's list — pushing without an up-front role check", { calendarId: key });
|
|
1556
|
+
return {
|
|
1557
|
+
accessRole: null,
|
|
1558
|
+
timeZone: direct.timeZone
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
async function findCalendarCollection(slug, workspaceRoot) {
|
|
1562
|
+
return (await require_discovery.discoverCollections({ workspaceRoot })).find((collection) => collection.slug === slug && collection.schema.googleCalendar) ?? null;
|
|
1563
|
+
}
|
|
1564
|
+
var liveDeps = {
|
|
1565
|
+
findCollection: findCalendarCollection,
|
|
1566
|
+
isLinked: async () => Boolean((await loadGoogleTokens())?.refresh_token),
|
|
1567
|
+
accessToken: getGoogleAccessToken,
|
|
1568
|
+
calendarMeta: liveCalendarMeta
|
|
1569
|
+
};
|
|
1570
|
+
var localValue = (ctx, record, source) => {
|
|
1571
|
+
const field = ctx.bySource[source];
|
|
1572
|
+
return field === void 0 ? void 0 : record[field];
|
|
1573
|
+
};
|
|
1574
|
+
/** One end of the span, rebuilt from the stored clock plus whatever the baseline
|
|
1575
|
+
* remembers about the original (zone, all-day). */
|
|
1576
|
+
function eventTime(ctx, record, source, previous) {
|
|
1577
|
+
const value = localValue(ctx, record, source);
|
|
1578
|
+
if (value === void 0) return {
|
|
1579
|
+
ok: false,
|
|
1580
|
+
reason: `no \`${source}\` field is mapped`
|
|
1581
|
+
};
|
|
1582
|
+
const time = toGoogleEventTime(value, previous, ctx.timeZone);
|
|
1583
|
+
if (time === null) return {
|
|
1584
|
+
ok: false,
|
|
1585
|
+
reason: `\`${source}\` is not a date-time Google accepts (got ${JSON.stringify(value)})`
|
|
1586
|
+
};
|
|
1587
|
+
if ("dateTime" in time && time.timeZone === "") return {
|
|
1588
|
+
ok: false,
|
|
1589
|
+
reason: `the calendar reports no timezone, so \`${source}\` cannot be sent without one`
|
|
1590
|
+
};
|
|
1591
|
+
return {
|
|
1592
|
+
ok: true,
|
|
1593
|
+
time
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
async function createFromRecord(ctx, eventId, record) {
|
|
1597
|
+
if (!isClientSettableEventId(eventId)) return {
|
|
1598
|
+
kind: "skipped",
|
|
1599
|
+
message: `${eventId}: the record id cannot be used as a Google event id (needs 5-1024 characters from 0-9a-v)`
|
|
1600
|
+
};
|
|
1601
|
+
const start = eventTime(ctx, record, "start", void 0);
|
|
1602
|
+
const end = eventTime(ctx, record, "end", void 0);
|
|
1603
|
+
if (!start.ok) return {
|
|
1604
|
+
kind: "skipped",
|
|
1605
|
+
message: `${eventId}: ${start.reason}`
|
|
1606
|
+
};
|
|
1607
|
+
if (!end.ok) return {
|
|
1608
|
+
kind: "skipped",
|
|
1609
|
+
message: `${eventId}: ${end.reason}`
|
|
1610
|
+
};
|
|
1611
|
+
const colorId = fieldText(localValue(ctx, record, "colorId"));
|
|
1612
|
+
return {
|
|
1613
|
+
kind: "created",
|
|
1614
|
+
event: await createCalendarEvent(ctx.accessToken, {
|
|
1615
|
+
eventId,
|
|
1616
|
+
calendarId: ctx.calendarId,
|
|
1617
|
+
summary: fieldText(localValue(ctx, record, "summary")),
|
|
1618
|
+
start: start.time,
|
|
1619
|
+
end: end.time,
|
|
1620
|
+
...colorId ? { colorId } : {}
|
|
1621
|
+
})
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
/** Only the fields the local edit touched — PATCH semantics, so an untouched
|
|
1625
|
+
* attendee list / recurrence rule is left alone. */
|
|
1626
|
+
function buildPatch(ctx, record, changed, shadow) {
|
|
1627
|
+
const resolved = ["start", "end"].filter((source) => changed.includes(source)).map((source) => ({
|
|
1628
|
+
source,
|
|
1629
|
+
result: eventTime(ctx, record, source, shadow[source])
|
|
1630
|
+
}));
|
|
1631
|
+
const [failure] = resolved.flatMap((entry) => entry.result.ok ? [] : [entry.result.reason]);
|
|
1632
|
+
if (failure !== void 0) return {
|
|
1633
|
+
ok: false,
|
|
1634
|
+
reason: failure
|
|
1635
|
+
};
|
|
1636
|
+
const colorId = fieldText(localValue(ctx, record, "colorId"));
|
|
1637
|
+
if (changed.includes("colorId") && colorId === "") return {
|
|
1638
|
+
ok: false,
|
|
1639
|
+
reason: "Google cannot clear an event colour once set"
|
|
1640
|
+
};
|
|
1641
|
+
return {
|
|
1642
|
+
ok: true,
|
|
1643
|
+
patch: {
|
|
1644
|
+
...changed.includes("summary") ? { summary: fieldText(localValue(ctx, record, "summary")) } : {},
|
|
1645
|
+
...changed.includes("colorId") ? { colorId } : {},
|
|
1646
|
+
...Object.fromEntries(resolved.flatMap((entry) => entry.result.ok ? [[entry.source, entry.result.time]] : []))
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
async function updateFromRecord(ctx, eventId, record, changed, shadow) {
|
|
1651
|
+
const fetched = await getCalendarEvent(ctx.accessToken, {
|
|
1652
|
+
calendarId: ctx.calendarId,
|
|
1653
|
+
eventId
|
|
1654
|
+
});
|
|
1655
|
+
if (fetched === null || fetched.event.status === "cancelled") return {
|
|
1656
|
+
kind: "skipped",
|
|
1657
|
+
message: `${eventId}: the event no longer exists in Google — sync first, then re-create it`
|
|
1658
|
+
};
|
|
1659
|
+
if (conflictingFields(shadow, toShadowEvent(fetched.event), changed).length > 0) return { kind: "conflict" };
|
|
1660
|
+
const built = buildPatch(ctx, record, changed, shadow);
|
|
1661
|
+
if (!built.ok) return {
|
|
1662
|
+
kind: "skipped",
|
|
1663
|
+
message: `${eventId}: ${built.reason}`
|
|
1664
|
+
};
|
|
1665
|
+
try {
|
|
1666
|
+
return {
|
|
1667
|
+
kind: "updated",
|
|
1668
|
+
event: await updateCalendarEvent(ctx.accessToken, {
|
|
1669
|
+
eventId,
|
|
1670
|
+
calendarId: ctx.calendarId,
|
|
1671
|
+
ifMatch: fetched.etag,
|
|
1672
|
+
...built.patch
|
|
1673
|
+
})
|
|
1674
|
+
};
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
if (isGoogleApiError(error) && error.status === 412) return { kind: "conflict" };
|
|
1677
|
+
throw error;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
/** Create, recovering from the one 409 that is provably safe to recover from.
|
|
1681
|
+
*
|
|
1682
|
+
* A 409 means the id is taken, which is USUALLY a previous push whose baseline
|
|
1683
|
+
* never landed. It can also be an unrelated event that happens to hold this id,
|
|
1684
|
+
* so the recovery never writes: it adopts the baseline only when the remote
|
|
1685
|
+
* event already equals what this record would write, and otherwise reports the
|
|
1686
|
+
* fix. Patching whatever answered the 409 would let a push modify a stranger's
|
|
1687
|
+
* event (Codex review).
|
|
1688
|
+
*
|
|
1689
|
+
* Refusal is not a dead end — a Sync writes both the record and its baseline,
|
|
1690
|
+
* after which the record pushes normally. */
|
|
1691
|
+
async function createOrAdopt(ctx, eventId, record) {
|
|
1692
|
+
try {
|
|
1693
|
+
return await createFromRecord(ctx, eventId, record);
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
if (!isGoogleApiError(error) || error.status !== 409) throw error;
|
|
1696
|
+
const fetched = await getCalendarEvent(ctx.accessToken, {
|
|
1697
|
+
calendarId: ctx.calendarId,
|
|
1698
|
+
eventId
|
|
1699
|
+
});
|
|
1700
|
+
if (fetched === null) throw error;
|
|
1701
|
+
if (mayAdoptExisting(eventId, record, toShadowEvent(fetched.event), ctx.map, ctx.primaryKey, ctx.fields)) return {
|
|
1702
|
+
kind: "unchanged",
|
|
1703
|
+
event: fetched.event
|
|
1704
|
+
};
|
|
1705
|
+
return {
|
|
1706
|
+
kind: "skipped",
|
|
1707
|
+
message: `${eventId}: an event with this id already exists in Google and differs from this record — press Sync to adopt it, then push again`
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
/** A 403 on a write is a permissions answer, but `googleApiError` appends the
|
|
1712
|
+
* "is the API enabled for the Cloud project?" hint to every 403 — true for a
|
|
1713
|
+
* disabled API, misleading here. Reached when the up-front role check could not
|
|
1714
|
+
* run because the calendar is not in the user's list, which is exactly when the
|
|
1715
|
+
* user needs the real reason. */
|
|
1716
|
+
function writeFailure(eventId, error) {
|
|
1717
|
+
if (isGoogleApiError(error) && error.status === 403) return {
|
|
1718
|
+
kind: "skipped",
|
|
1719
|
+
message: `${eventId}: Google refused the write — you may not have permission to change events on this calendar`
|
|
1720
|
+
};
|
|
1721
|
+
return {
|
|
1722
|
+
kind: "error",
|
|
1723
|
+
message: `${eventId}: ${String(error)}`
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
async function pushRecord(ctx, eventId, record) {
|
|
1727
|
+
try {
|
|
1728
|
+
const shadow = ctx.shadow[eventId];
|
|
1729
|
+
const plan = planRecord(eventId, record, shadow, ctx.map, ctx.primaryKey, ctx.fields);
|
|
1730
|
+
if (plan.kind === "unchanged") return { kind: "unchanged" };
|
|
1731
|
+
if (plan.kind === "create" || shadow === void 0) return await createOrAdopt(ctx, eventId, record);
|
|
1732
|
+
return await updateFromRecord(ctx, eventId, record, plan.fields, shadow);
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
return writeFailure(eventId, error);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
/** The baseline an outcome establishes, so the next push sees the record as
|
|
1738
|
+
* unchanged instead of pushing it again. Empty for anything that wrote nothing
|
|
1739
|
+
* and had no baseline to repair. */
|
|
1740
|
+
function pushedShadow(outcomes) {
|
|
1741
|
+
const written = outcomes.flatMap((outcome) => "event" in outcome && outcome.event !== void 0 ? [outcome.event] : []);
|
|
1742
|
+
return Object.fromEntries(written.map((event) => [event.id, toShadowEvent(event)]));
|
|
1743
|
+
}
|
|
1744
|
+
function tally(slug, outcomes, localDeletes) {
|
|
1745
|
+
const count = (kind) => outcomes.filter((outcome) => outcome.kind === kind).length;
|
|
1746
|
+
return {
|
|
1747
|
+
slug,
|
|
1748
|
+
created: count("created"),
|
|
1749
|
+
updated: count("updated"),
|
|
1750
|
+
conflicts: count("conflict"),
|
|
1751
|
+
localDeletes,
|
|
1752
|
+
skipped: outcomes.flatMap((outcome) => outcome.kind === "skipped" ? [outcome.message] : []),
|
|
1753
|
+
errors: outcomes.flatMap((outcome) => outcome.kind === "error" ? [outcome.message] : [])
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
async function pushNow(collection, workspaceRoot, deps) {
|
|
1757
|
+
const { schema, slug } = collection;
|
|
1758
|
+
const calendarId = schema.googleCalendar?.calendarId;
|
|
1759
|
+
const accessToken = await deps.accessToken();
|
|
1760
|
+
const meta = await deps.calendarMeta(accessToken, calendarId);
|
|
1761
|
+
if (isDeniedAccessRole(meta.accessRole)) return {
|
|
1762
|
+
kind: "read-only",
|
|
1763
|
+
accessRole: meta.accessRole ?? ""
|
|
1764
|
+
};
|
|
1765
|
+
const map = pushableMap(schema.googleCalendar?.map ?? {});
|
|
1766
|
+
const shadow = await loadCalendarShadow(calendarId, workspaceRoot);
|
|
1767
|
+
const records = await require_discovery.storeFor(collection, { workspaceRoot }).list();
|
|
1768
|
+
const ctx = {
|
|
1769
|
+
accessToken,
|
|
1770
|
+
calendarId,
|
|
1771
|
+
timeZone: meta.timeZone,
|
|
1772
|
+
map,
|
|
1773
|
+
bySource: bySourceField(map),
|
|
1774
|
+
primaryKey: schema.primaryKey,
|
|
1775
|
+
fields: schema.fields,
|
|
1776
|
+
shadow
|
|
1777
|
+
};
|
|
1778
|
+
const outcomes = [];
|
|
1779
|
+
for (const record of records) {
|
|
1780
|
+
const outcome = await pushRecord(ctx, fieldText(record[schema.primaryKey]), record);
|
|
1781
|
+
outcomes.push(outcome);
|
|
1782
|
+
await saveCalendarShadow(calendarId, pushedShadow([outcome]), workspaceRoot);
|
|
1783
|
+
}
|
|
1784
|
+
const result = tally(slug, outcomes, locallyDeletedIds(shadow, records.map((record) => fieldText(record[schema.primaryKey]))).length);
|
|
1785
|
+
if (result.errors.length > 0) log.warn("google", "calendar push finished with errors", {
|
|
1786
|
+
slug,
|
|
1787
|
+
errors: result.errors.length
|
|
1788
|
+
});
|
|
1789
|
+
return {
|
|
1790
|
+
kind: "pushed",
|
|
1791
|
+
result
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
/** Push ONE collection's records to the calendar it declares.
|
|
1795
|
+
*
|
|
1796
|
+
* "Does this collection sync at all" is answered before "is Google linked", for
|
|
1797
|
+
* the reason `syncCalendarForCollection` gives: telling someone to link their
|
|
1798
|
+
* account for a collection that never declared a calendar sends them fixing the
|
|
1799
|
+
* wrong thing. */
|
|
1800
|
+
async function pushCalendarForCollection(slug, workspaceRoot, deps = liveDeps) {
|
|
1801
|
+
try {
|
|
1802
|
+
const collection = await deps.findCollection(slug, workspaceRoot);
|
|
1803
|
+
if (collection === null) return { kind: "not-a-calendar" };
|
|
1804
|
+
if (!await deps.isLinked()) return { kind: "not-linked" };
|
|
1805
|
+
return await withCalendarLock(collection.schema.googleCalendar?.calendarId, () => pushNow(collection, workspaceRoot, deps));
|
|
1806
|
+
} catch (error) {
|
|
1807
|
+
log.warn("google", "calendar push could not start", {
|
|
1808
|
+
slug,
|
|
1809
|
+
error: String(error)
|
|
1810
|
+
});
|
|
1811
|
+
return {
|
|
1812
|
+
kind: "failed",
|
|
1813
|
+
message: String(error)
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
//#endregion
|
|
1177
1818
|
//#region src/google/tasks.ts
|
|
1178
1819
|
var TASKS_BASE_URL = "https://tasks.googleapis.com/tasks/v1";
|
|
1179
1820
|
var TASKS_API_LABEL = "Google Tasks API";
|
|
@@ -1241,12 +1882,20 @@ async function updateTask(accessToken, input) {
|
|
|
1241
1882
|
body: JSON.stringify(buildTaskPatch(input))
|
|
1242
1883
|
}));
|
|
1243
1884
|
}
|
|
1244
|
-
|
|
1885
|
+
/** Shared body of the two status transitions. PATCH keeps the rest of the task
|
|
1886
|
+
* intact — a PUT would need the full body and would silently drop fields the
|
|
1887
|
+
* caller never read. Private on purpose: the exported `completeTask` /
|
|
1888
|
+
* `uncompleteTask` names are the API, so no caller can invent a third target
|
|
1889
|
+
* state by passing an arbitrary status through. */
|
|
1890
|
+
async function patchTaskStatus(accessToken, input, status) {
|
|
1245
1891
|
return toTaskSummary(await googleRequest(TASKS_API_LABEL, accessToken, tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`), {
|
|
1246
1892
|
method: "PATCH",
|
|
1247
|
-
body: JSON.stringify({ status
|
|
1893
|
+
body: JSON.stringify({ status })
|
|
1248
1894
|
}));
|
|
1249
1895
|
}
|
|
1896
|
+
async function completeTask(accessToken, input) {
|
|
1897
|
+
return patchTaskStatus(accessToken, input, TASK_STATUS_COMPLETED);
|
|
1898
|
+
}
|
|
1250
1899
|
/** Send a completed task back to the to-do list.
|
|
1251
1900
|
*
|
|
1252
1901
|
* Its own function rather than a flag on `completeTask`, and deliberately not
|
|
@@ -1261,10 +1910,7 @@ async function completeTask(accessToken, input) {
|
|
|
1261
1910
|
* reopened task ever displays a completion date in Google's own UI, this is
|
|
1262
1911
|
* the place to add `completed: null` to the patch. */
|
|
1263
1912
|
async function uncompleteTask(accessToken, input) {
|
|
1264
|
-
return
|
|
1265
|
-
method: "PATCH",
|
|
1266
|
-
body: JSON.stringify({ status: TASK_STATUS_NEEDS_ACTION })
|
|
1267
|
-
}));
|
|
1913
|
+
return patchTaskStatus(accessToken, input, TASK_STATUS_NEEDS_ACTION);
|
|
1268
1914
|
}
|
|
1269
1915
|
async function deleteTask(accessToken, input) {
|
|
1270
1916
|
await googleRequest(TASKS_API_LABEL, accessToken, tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`), { method: "DELETE" });
|
|
@@ -1363,6 +2009,7 @@ async function deleteDriveFile(accessToken, input) {
|
|
|
1363
2009
|
await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: "DELETE" });
|
|
1364
2010
|
}
|
|
1365
2011
|
//#endregion
|
|
2012
|
+
exports.CANCELLED_EVENT_STATUS = CANCELLED_EVENT_STATUS;
|
|
1366
2013
|
exports.DEFAULT_LIST_MAX_RESULTS = DEFAULT_LIST_MAX_RESULTS;
|
|
1367
2014
|
exports.GOOGLE_AUTH_CANCELLED = GOOGLE_AUTH_CANCELLED;
|
|
1368
2015
|
exports.GOOGLE_CALENDARLIST_SCOPE = GOOGLE_CALENDARLIST_SCOPE;
|
|
@@ -1372,10 +2019,15 @@ exports.GOOGLE_DRIVE_FILE_SCOPE = GOOGLE_DRIVE_FILE_SCOPE;
|
|
|
1372
2019
|
exports.GOOGLE_SCOPES = GOOGLE_SCOPES;
|
|
1373
2020
|
exports.GOOGLE_TASKS_SCOPE = GOOGLE_TASKS_SCOPE;
|
|
1374
2021
|
exports.GoogleApiError = GoogleApiError;
|
|
2022
|
+
exports.HTTP_CONFLICT = HTTP_CONFLICT;
|
|
2023
|
+
exports.HTTP_FORBIDDEN = HTTP_FORBIDDEN;
|
|
2024
|
+
exports.HTTP_PRECONDITION_FAILED = HTTP_PRECONDITION_FAILED;
|
|
1375
2025
|
exports.MAX_LIST_RESULTS = MAX_LIST_RESULTS;
|
|
2026
|
+
exports.PUSHABLE_SOURCE_FIELDS = PUSHABLE_SOURCE_FIELDS;
|
|
1376
2027
|
exports.anySyncedCollectionSurvives = anySyncedCollectionSurvives;
|
|
1377
2028
|
exports.assertSafeMimeType = assertSafeMimeType;
|
|
1378
2029
|
exports.authorizeGoogle = authorizeGoogle;
|
|
2030
|
+
exports.baselineRecord = baselineRecord;
|
|
1379
2031
|
exports.brokerBaseUrl = brokerBaseUrl;
|
|
1380
2032
|
exports.brokerExchange = brokerExchange;
|
|
1381
2033
|
exports.brokerRefresh = brokerRefresh;
|
|
@@ -1383,18 +2035,22 @@ exports.brokerStart = brokerStart;
|
|
|
1383
2035
|
exports.buildEventPatch = buildEventPatch;
|
|
1384
2036
|
exports.buildMultipartBody = buildMultipartBody;
|
|
1385
2037
|
exports.buildTaskPatch = buildTaskPatch;
|
|
2038
|
+
exports.bySourceField = bySourceField;
|
|
1386
2039
|
exports.calendarApiError = calendarApiError;
|
|
2040
|
+
exports.calendarPushStatePath = calendarPushStatePath;
|
|
1387
2041
|
exports.calendarSyncStatePath = calendarSyncStatePath;
|
|
1388
2042
|
exports.canonicalCalendarId = canonicalCalendarId;
|
|
1389
2043
|
exports.canonicalTaskListId = canonicalTaskListId;
|
|
1390
2044
|
exports.classifyDelete = classifyDelete;
|
|
1391
2045
|
exports.classifyWrite = classifyWrite;
|
|
2046
|
+
exports.clearCalendarShadow = clearCalendarShadow;
|
|
1392
2047
|
exports.clearCalendarSyncToken = clearCalendarSyncToken;
|
|
1393
2048
|
exports.clientSecretPresence = clientSecretPresence;
|
|
1394
2049
|
exports.collectCalendarPages = collectCalendarPages;
|
|
1395
2050
|
exports.commitLinkedTokens = commitLinkedTokens;
|
|
1396
2051
|
exports.completeTask = completeTask;
|
|
1397
2052
|
exports.configureGoogleHost = configureGoogleHost;
|
|
2053
|
+
exports.conflictingFields = conflictingFields;
|
|
1398
2054
|
exports.createCalendarEvent = createCalendarEvent;
|
|
1399
2055
|
exports.createDriveFile = createDriveFile;
|
|
1400
2056
|
exports.createGoogleAuthFlow = createGoogleAuthFlow;
|
|
@@ -1403,8 +2059,11 @@ exports.deleteCalendarEvent = deleteCalendarEvent;
|
|
|
1403
2059
|
exports.deleteDriveFile = deleteDriveFile;
|
|
1404
2060
|
exports.deleteGoogleTokens = deleteGoogleTokens;
|
|
1405
2061
|
exports.deleteTask = deleteTask;
|
|
2062
|
+
exports.fieldText = fieldText;
|
|
1406
2063
|
exports.findClientSecretPath = findClientSecretPath;
|
|
2064
|
+
exports.getCalendar = getCalendar;
|
|
1407
2065
|
exports.getCalendarColors = getCalendarColors;
|
|
2066
|
+
exports.getCalendarEvent = getCalendarEvent;
|
|
1408
2067
|
exports.getGoogleAccessToken = getGoogleAccessToken;
|
|
1409
2068
|
exports.googleApiError = googleApiError;
|
|
1410
2069
|
exports.googleAuthFlow = googleAuthFlow;
|
|
@@ -1413,6 +2072,8 @@ exports.googleConfigDir = googleConfigDir;
|
|
|
1413
2072
|
exports.googleSecretsDir = googleSecretsDir;
|
|
1414
2073
|
exports.googleTokenPath = googleTokenPath;
|
|
1415
2074
|
exports.groupByCalendar = groupByCalendar;
|
|
2075
|
+
exports.isClientSettableEventId = isClientSettableEventId;
|
|
2076
|
+
exports.isDeniedAccessRole = isDeniedAccessRole;
|
|
1416
2077
|
exports.isGoogleApiError = isGoogleApiError;
|
|
1417
2078
|
exports.isIsoDateTimeWithOffset = isIsoDateTimeWithOffset;
|
|
1418
2079
|
exports.isTextMimeType = isTextMimeType;
|
|
@@ -1422,16 +2083,27 @@ exports.listCalendars = listCalendars;
|
|
|
1422
2083
|
exports.listDriveFiles = listDriveFiles;
|
|
1423
2084
|
exports.listTaskLists = listTaskLists;
|
|
1424
2085
|
exports.listTasks = listTasks;
|
|
2086
|
+
exports.loadCalendarShadow = loadCalendarShadow;
|
|
1425
2087
|
exports.loadCalendarSyncToken = loadCalendarSyncToken;
|
|
1426
2088
|
exports.loadClientSecret = loadClientSecret;
|
|
1427
2089
|
exports.loadGoogleTokens = loadGoogleTokens;
|
|
2090
|
+
exports.locallyChangedFields = locallyChangedFields;
|
|
2091
|
+
exports.locallyDeletedIds = locallyDeletedIds;
|
|
2092
|
+
exports.mayAdoptExisting = mayAdoptExisting;
|
|
1428
2093
|
exports.mergeGoogleTokens = mergeGoogleTokens;
|
|
2094
|
+
exports.mergeShadow = mergeShadow;
|
|
1429
2095
|
exports.orphanedCalendarId = orphanedCalendarId;
|
|
1430
2096
|
exports.pickBoundary = pickBoundary;
|
|
2097
|
+
exports.planRecord = planRecord;
|
|
2098
|
+
exports.pushCalendarForCollection = pushCalendarForCollection;
|
|
2099
|
+
exports.pushableMap = pushableMap;
|
|
1431
2100
|
exports.readDriveFile = readDriveFile;
|
|
1432
2101
|
exports.releaseOrphanedCalendarToken = releaseOrphanedCalendarToken;
|
|
2102
|
+
exports.resolveEventSpan = resolveEventSpan;
|
|
2103
|
+
exports.saveCalendarShadow = saveCalendarShadow;
|
|
1433
2104
|
exports.saveCalendarSyncToken = saveCalendarSyncToken;
|
|
1434
2105
|
exports.saveGoogleTokens = saveGoogleTokens;
|
|
2106
|
+
exports.shadowUpdates = shadowUpdates;
|
|
1435
2107
|
exports.syncCalendarEvents = syncCalendarEvents;
|
|
1436
2108
|
exports.syncCalendarForCollection = syncCalendarForCollection;
|
|
1437
2109
|
exports.syncCalendarGroup = syncCalendarGroup;
|
|
@@ -1442,6 +2114,8 @@ exports.toCollectionDateTime = toCollectionDateTime;
|
|
|
1442
2114
|
exports.toCollectionRecord = toCollectionRecord;
|
|
1443
2115
|
exports.toDriveFileSummary = toDriveFileSummary;
|
|
1444
2116
|
exports.toEventSummary = toEventSummary;
|
|
2117
|
+
exports.toGoogleEventTime = toGoogleEventTime;
|
|
2118
|
+
exports.toShadowEvent = toShadowEvent;
|
|
1445
2119
|
exports.toTaskListSummary = toTaskListSummary;
|
|
1446
2120
|
exports.toTaskSummary = toTaskSummary;
|
|
1447
2121
|
exports.uncompleteTask = uncompleteTask;
|
|
@@ -1450,6 +2124,8 @@ exports.unsyncedGroups = unsyncedGroups;
|
|
|
1450
2124
|
exports.updateCalendarEvent = updateCalendarEvent;
|
|
1451
2125
|
exports.updateTask = updateTask;
|
|
1452
2126
|
exports.waitForAuthCode = waitForAuthCode;
|
|
2127
|
+
exports.withCalendarLock = withCalendarLock;
|
|
1453
2128
|
exports.withKeyedLock = withKeyedLock;
|
|
2129
|
+
exports.zoneSuffixOf = zoneSuffixOf;
|
|
1454
2130
|
|
|
1455
2131
|
//# sourceMappingURL=index.cjs.map
|