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