@mulmoclaude/core 0.22.1 → 0.23.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/collection-skills.md +102 -1
- package/assets/helps/custom-view.md +34 -0
- package/assets/helps/error-recovery.md +36 -0
- package/dist/collection/core/queryZ.d.ts +90 -0
- package/dist/collection/core/schema.d.ts +14 -1
- package/dist/collection/core/schemaZ.d.ts +18 -1
- package/dist/collection/index.cjs +3 -2
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.d.ts +1 -0
- package/dist/collection/index.js +3 -3
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +12 -4
- package/dist/collection/registry/server/index.cjs.map +1 -1
- package/dist/collection/registry/server/index.js +12 -4
- package/dist/collection/registry/server/index.js.map +1 -1
- package/dist/collection/server/csvQuery.d.ts +19 -0
- package/dist/collection/server/csvStore.d.ts +49 -0
- package/dist/collection/server/discoveredCollection.d.ts +9 -1
- package/dist/collection/server/discovery.d.ts +7 -3
- package/dist/collection/server/index.cjs +15 -2
- package/dist/collection/server/index.d.ts +5 -0
- package/dist/collection/server/index.js +3 -3
- package/dist/collection/server/manageTool.d.ts +4 -0
- package/dist/collection/server/store.d.ts +31 -0
- package/dist/collection-watchers/index.cjs +104 -3
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.js +102 -3
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/feeds/index.cjs +2 -2
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/index.cjs +3 -3
- package/dist/feeds/server/index.js +3 -3
- package/dist/google/auth.d.ts +8 -2
- package/dist/google/calendar.d.ts +44 -0
- package/dist/google/index.cjs +86 -9
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.d.ts +2 -2
- package/dist/google/index.js +82 -10
- package/dist/google/index.js.map +1 -1
- package/dist/{ingestTypes-DEjpiZGM.js → ingestTypes-B-dXxUF9.js} +2 -2
- package/dist/{ingestTypes-DEjpiZGM.js.map → ingestTypes-B-dXxUF9.js.map} +1 -1
- package/dist/{ingestTypes-Dh5lniBL.cjs → ingestTypes-Cev9q77C.cjs} +2 -2
- package/dist/{ingestTypes-Dh5lniBL.cjs.map → ingestTypes-Cev9q77C.cjs.map} +1 -1
- package/dist/{promptSafety-DN5V__Ku.cjs → promptSafety-DRd15gQ6.cjs} +15 -1
- package/dist/promptSafety-DRd15gQ6.cjs.map +1 -0
- package/dist/{promptSafety-DyG3EuC7.js → promptSafety-rDWA9_JS.js} +10 -2
- package/dist/promptSafety-rDWA9_JS.js.map +1 -0
- package/dist/{server-Bd8l1bhv.js → server--FgDORd3.js} +678 -79
- package/dist/server--FgDORd3.js.map +1 -0
- package/dist/{server-Bh-DPK-H.cjs → server-Q7ld-FlZ.cjs} +754 -76
- package/dist/server-Q7ld-FlZ.cjs.map +1 -0
- package/package.json +3 -1
- package/dist/promptSafety-DN5V__Ku.cjs.map +0 -1
- package/dist/promptSafety-DyG3EuC7.js.map +0 -1
- package/dist/server-Bd8l1bhv.js.map +0 -1
- package/dist/server-Bh-DPK-H.cjs.map +0 -1
package/dist/google/index.js
CHANGED
|
@@ -343,13 +343,20 @@ async function brokerRefresh(refreshToken, baseUrl = brokerBaseUrl()) {
|
|
|
343
343
|
//#endregion
|
|
344
344
|
//#region src/google/auth.ts
|
|
345
345
|
var GOOGLE_CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.events";
|
|
346
|
+
/** Minimal read scope for the calendar list + per-calendar colours.
|
|
347
|
+
* `calendar.events` already covers reading/writing events on any calendar, but
|
|
348
|
+
* discovering WHICH calendars the user has (CalendarList.list) needs a
|
|
349
|
+
* calendar-list read scope — this is the narrowest one that grants it. Kept in
|
|
350
|
+
* step with the broker's consent scopes so local- and broker-linked accounts
|
|
351
|
+
* grant the same set. */
|
|
352
|
+
var GOOGLE_CALENDARLIST_SCOPE = "https://www.googleapis.com/auth/calendar.calendarlist.readonly";
|
|
346
353
|
var GOOGLE_TASKS_SCOPE = "https://www.googleapis.com/auth/tasks";
|
|
347
354
|
var GOOGLE_DRIVE_FILE_SCOPE = "https://www.googleapis.com/auth/drive.file";
|
|
348
355
|
/** Requested at consent as one set — matches the scopes registered on the
|
|
349
|
-
* OAuth consent screen, so a single re-link covers every supported API
|
|
350
|
-
* (Calendar now; Tasks / Drive tools ride the same grant later). */
|
|
356
|
+
* OAuth consent screen, so a single re-link covers every supported API. */
|
|
351
357
|
var GOOGLE_SCOPES = [
|
|
352
358
|
GOOGLE_CALENDAR_SCOPE,
|
|
359
|
+
GOOGLE_CALENDARLIST_SCOPE,
|
|
353
360
|
GOOGLE_TASKS_SCOPE,
|
|
354
361
|
GOOGLE_DRIVE_FILE_SCOPE
|
|
355
362
|
];
|
|
@@ -594,8 +601,12 @@ var itemsOf = (value) => {
|
|
|
594
601
|
};
|
|
595
602
|
//#endregion
|
|
596
603
|
//#region src/google/calendar.ts
|
|
597
|
-
var
|
|
604
|
+
var CALENDAR_BASE_URL = "https://www.googleapis.com/calendar/v3";
|
|
598
605
|
var CALENDAR_API_LABEL = "Google Calendar API";
|
|
606
|
+
var DEFAULT_CALENDAR_ID = "primary";
|
|
607
|
+
var CALENDAR_LIST_PAGE_SIZE = 250;
|
|
608
|
+
var MAX_CALENDAR_LIST_PAGES = 40;
|
|
609
|
+
var eventsUrl = (calendarId) => `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(calendarId || DEFAULT_CALENDAR_ID)}/events`;
|
|
599
610
|
var eventTime = (value) => {
|
|
600
611
|
if (!isRecord(value)) return "";
|
|
601
612
|
if (typeof value.dateTime === "string") return value.dateTime;
|
|
@@ -610,9 +621,33 @@ var toEventSummary = (value) => {
|
|
|
610
621
|
start: eventTime(record.start),
|
|
611
622
|
end: eventTime(record.end),
|
|
612
623
|
htmlLink: stringField(record, "htmlLink"),
|
|
613
|
-
status: stringField(record, "status")
|
|
624
|
+
status: stringField(record, "status"),
|
|
625
|
+
colorId: stringField(record, "colorId")
|
|
614
626
|
};
|
|
615
627
|
};
|
|
628
|
+
var toCalendarSummary = (value) => {
|
|
629
|
+
const record = asRecord(value);
|
|
630
|
+
return {
|
|
631
|
+
id: stringField(record, "id"),
|
|
632
|
+
summary: stringField(record, "summary"),
|
|
633
|
+
description: stringField(record, "description"),
|
|
634
|
+
primary: record.primary === true,
|
|
635
|
+
accessRole: stringField(record, "accessRole"),
|
|
636
|
+
backgroundColor: stringField(record, "backgroundColor"),
|
|
637
|
+
foregroundColor: stringField(record, "foregroundColor"),
|
|
638
|
+
colorId: stringField(record, "colorId")
|
|
639
|
+
};
|
|
640
|
+
};
|
|
641
|
+
var toColorMap = (value) => {
|
|
642
|
+
const entries = Object.entries(asRecord(value)).map(([colorId, entry]) => {
|
|
643
|
+
const record = asRecord(entry);
|
|
644
|
+
return [colorId, {
|
|
645
|
+
background: stringField(record, "background"),
|
|
646
|
+
foreground: stringField(record, "foreground")
|
|
647
|
+
}];
|
|
648
|
+
});
|
|
649
|
+
return Object.fromEntries(entries);
|
|
650
|
+
};
|
|
616
651
|
/** Kept as a named export for the existing unit tests / callers; the shared
|
|
617
652
|
* helper now carries the wording. */
|
|
618
653
|
var calendarApiError = (status, body) => googleApiError(CALENDAR_API_LABEL, status, body);
|
|
@@ -621,21 +656,58 @@ async function createCalendarEvent(accessToken, input) {
|
|
|
621
656
|
summary: input.summary,
|
|
622
657
|
description: input.description,
|
|
623
658
|
start: { dateTime: input.startDateTime },
|
|
624
|
-
end: { dateTime: input.endDateTime }
|
|
659
|
+
end: { dateTime: input.endDateTime },
|
|
660
|
+
...input.colorId ? { colorId: input.colorId } : {}
|
|
625
661
|
};
|
|
626
|
-
return toEventSummary(await googleRequest(CALENDAR_API_LABEL, accessToken,
|
|
662
|
+
return toEventSummary(await googleRequest(CALENDAR_API_LABEL, accessToken, eventsUrl(input.calendarId), {
|
|
627
663
|
method: "POST",
|
|
628
664
|
body: JSON.stringify(body)
|
|
629
665
|
}));
|
|
630
666
|
}
|
|
631
667
|
async function listCalendarEvents(accessToken, input = {}) {
|
|
632
|
-
const
|
|
668
|
+
const params = new URLSearchParams({
|
|
633
669
|
timeMin: input.timeMin ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
634
670
|
maxResults: String(input.maxResults ?? 10),
|
|
635
671
|
singleEvents: "true",
|
|
636
672
|
orderBy: "startTime"
|
|
637
|
-
})
|
|
638
|
-
return (
|
|
673
|
+
});
|
|
674
|
+
return itemsOf(await googleRequest(CALENDAR_API_LABEL, accessToken, `${eventsUrl(input.calendarId)}?${params.toString()}`)).map(toEventSummary);
|
|
675
|
+
}
|
|
676
|
+
/** Pagination loop for CalendarList.list, extracted so it can be tested without
|
|
677
|
+
* network. Stops at the last page (no token) or the runaway page cap. */
|
|
678
|
+
async function collectCalendarPages(fetchPage, maxPages = MAX_CALENDAR_LIST_PAGES) {
|
|
679
|
+
const calendars = [];
|
|
680
|
+
let pageToken;
|
|
681
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
682
|
+
const { items, nextPageToken } = await fetchPage(pageToken);
|
|
683
|
+
calendars.push(...items.map(toCalendarSummary));
|
|
684
|
+
if (!nextPageToken) break;
|
|
685
|
+
pageToken = nextPageToken;
|
|
686
|
+
}
|
|
687
|
+
return calendars;
|
|
688
|
+
}
|
|
689
|
+
/** The calendars the user has added/subscribed to (primary + secondary +
|
|
690
|
+
* shared), each with its id, name and colour, following pagination. Needs the
|
|
691
|
+
* calendar-list read scope (GOOGLE_CALENDARLIST_SCOPE). */
|
|
692
|
+
async function listCalendars(accessToken) {
|
|
693
|
+
return collectCalendarPages(async (pageToken) => {
|
|
694
|
+
const params = new URLSearchParams({ maxResults: String(CALENDAR_LIST_PAGE_SIZE) });
|
|
695
|
+
if (pageToken) params.set("pageToken", pageToken);
|
|
696
|
+
const payload = await googleRequest(CALENDAR_API_LABEL, accessToken, `${CALENDAR_BASE_URL}/users/me/calendarList?${params.toString()}`);
|
|
697
|
+
const record = asRecord(payload);
|
|
698
|
+
return {
|
|
699
|
+
items: itemsOf(payload),
|
|
700
|
+
nextPageToken: typeof record.nextPageToken === "string" ? record.nextPageToken : void 0
|
|
701
|
+
};
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
/** Resolve a `colorId` (on an event or calendar) to its hex background/foreground. */
|
|
705
|
+
async function getCalendarColors(accessToken) {
|
|
706
|
+
const record = asRecord(await googleRequest(CALENDAR_API_LABEL, accessToken, `${CALENDAR_BASE_URL}/colors`));
|
|
707
|
+
return {
|
|
708
|
+
event: toColorMap(record.event),
|
|
709
|
+
calendar: toColorMap(record.calendar)
|
|
710
|
+
};
|
|
639
711
|
}
|
|
640
712
|
//#endregion
|
|
641
713
|
//#region src/google/tasks.ts
|
|
@@ -786,6 +858,6 @@ async function deleteDriveFile(accessToken, input) {
|
|
|
786
858
|
await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: "DELETE" });
|
|
787
859
|
}
|
|
788
860
|
//#endregion
|
|
789
|
-
export { DEFAULT_LIST_MAX_RESULTS, GOOGLE_CALENDAR_SCOPE, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, GOOGLE_TASKS_SCOPE, MAX_LIST_RESULTS, assertSafeMimeType, authorizeGoogle, brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, buildMultipartBody, calendarApiError, clientSecretPresence, completeTask, configureGoogleHost, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteDriveFile, deleteGoogleTokens, deleteTask, findClientSecretPath, getGoogleAccessToken, googleApiError, googleAuthFlow, googleConfigDir, googleSecretsDir, googleTokenPath, isIsoDateTimeWithOffset, isTextMimeType, legacyGoogleTokenPath, listCalendarEvents, listDriveFiles, listTaskLists, listTasks, loadClientSecret, loadGoogleTokens, mergeGoogleTokens, pickBoundary, readDriveFile, saveGoogleTokens, toDriveFileSummary, toEventSummary, toTaskListSummary, toTaskSummary, unlinkGoogle, waitForAuthCode };
|
|
861
|
+
export { DEFAULT_LIST_MAX_RESULTS, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_CALENDAR_SCOPE, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, GOOGLE_TASKS_SCOPE, MAX_LIST_RESULTS, assertSafeMimeType, authorizeGoogle, brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, buildMultipartBody, calendarApiError, clientSecretPresence, collectCalendarPages, completeTask, configureGoogleHost, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteDriveFile, deleteGoogleTokens, deleteTask, findClientSecretPath, getCalendarColors, getGoogleAccessToken, googleApiError, googleAuthFlow, googleConfigDir, googleSecretsDir, googleTokenPath, isIsoDateTimeWithOffset, isTextMimeType, legacyGoogleTokenPath, listCalendarEvents, listCalendars, listDriveFiles, listTaskLists, listTasks, loadClientSecret, loadGoogleTokens, mergeGoogleTokens, pickBoundary, readDriveFile, saveGoogleTokens, toCalendarSummary, toDriveFileSummary, toEventSummary, toTaskListSummary, toTaskSummary, unlinkGoogle, waitForAuthCode };
|
|
790
862
|
|
|
791
863
|
//# sourceMappingURL=index.js.map
|
package/dist/google/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/google/host.ts","../../src/google/datetime.ts","../../src/google/paths.ts","../../src/google/util.ts","../../src/google/clientSecret.ts","../../src/google/fsJson.ts","../../src/google/tokenStore.ts","../../src/google/fetch.ts","../../src/google/broker.ts","../../src/google/auth.ts","../../src/google/authFlow.ts","../../src/google/apiClient.ts","../../src/google/calendar.ts","../../src/google/tasks.ts","../../src/google/driveFile.ts"],"sourcesContent":["// Host binding for the Google engine. The engine logs through the host's\n// logger, but a package-level import of the host logger would be an uphill\n// dependency — so the host injects it once at startup (same pattern as\n// `collection/server/host.ts`). The default is silent so the engine works\n// unconfigured in unit tests.\n\nexport interface GoogleLogger {\n error: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n warn: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n info: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n debug: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n}\n\nconst silentLogger: GoogleLogger = {\n error: () => undefined,\n warn: () => undefined,\n info: () => undefined,\n debug: () => undefined,\n};\n\nlet hostLog: GoogleLogger = silentLogger;\n\nexport function configureGoogleHost(binding: { log: GoogleLogger }): void {\n hostLog = binding.log;\n}\n\nexport const log: GoogleLogger = {\n error: (prefix, message, data) => hostLog.error(prefix, message, data),\n warn: (prefix, message, data) => hostLog.warn(prefix, message, data),\n info: (prefix, message, data) => hostLog.info(prefix, message, data),\n debug: (prefix, message, data) => hostLog.debug(prefix, message, data),\n};\n","// Strict RFC3339 date-time validation shared by every surface that feeds\n// Calendar `dateTime`/`timeMin` values (agent tool args, remote-host command\n// params). Calendar rejects date-only / offset-less values with an opaque\n// 400, so callers validate here and return an actionable message instead.\n\n// Fractional seconds are normalized away first — an optional `(\\.\\d+)?`\n// group inside the main pattern trips security/detect-unsafe-regex.\nconst ISO_DATE_TIME_WITH_OFFSET_RE = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:Z|[+-](\\d{2}):(\\d{2}))$/;\nconst FRACTIONAL_SECONDS_RE = /\\.\\d+(?=Z|[+-])/;\n\nconst MAX_HOUR = 23;\nconst MAX_MINUTE = 59;\nconst MAX_SECOND = 59;\n\n// JS Date normalizes overflowed components (2026-02-31 parses as\n// 2026-03-03), so `new Date(value)` alone cannot reject impossible dates —\n// a UTC round-trip of the raw components can.\nconst isRealCalendarDate = (year: number, month: number, day: number): boolean => {\n const roundTrip = new Date(Date.UTC(year, month - 1, day));\n return roundTrip.getUTCFullYear() === year && roundTrip.getUTCMonth() === month - 1 && roundTrip.getUTCDate() === day;\n};\n\nexport const isIsoDateTimeWithOffset = (value: string): boolean => {\n const match = ISO_DATE_TIME_WITH_OFFSET_RE.exec(value.replace(FRACTIONAL_SECONDS_RE, \"\"));\n if (!match) return false;\n const [year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0] = match.slice(1, 7).map(Number);\n const timeInRange = hour <= MAX_HOUR && minute <= MAX_MINUTE && second <= MAX_SECOND;\n // RFC3339 bounds the offset to 00-23:59; groups 7/8 are undefined for \"Z\".\n const offsetInRange = match[7] === undefined || (Number(match[7]) <= MAX_HOUR && Number(match[8]) <= MAX_MINUTE);\n return isRealCalendarDate(year, month, day) && timeInRange && offsetInRange;\n};\n","// Google OAuth material lives OUTSIDE the workspace: the client secret is\n// machine-only (never synced) and the refresh token must survive workspace\n// resets — same reasoning as the gcloud / gh CLI model. The dir is the\n// host-NEUTRAL `~/.config/mulmo` because this engine is shared by both\n// MulmoClaude and MulmoTerminal, which deliberately share one grant per\n// machine. The `home` parameter exists so tests can thread a fake home.\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function googleConfigDir(home?: string): string {\n return join(home ?? homedir(), \".config\", \"mulmo\");\n}\n\n/** Pre-0.20.1 token dir (mulmoclaude-branded); reads migrate away from it. */\nexport function legacyGoogleTokenPath(home?: string): string {\n return join(home ?? homedir(), \".config\", \"mulmoclaude\", \"google-token.json\");\n}\n\nexport function googleTokenPath(home?: string): string {\n return join(googleConfigDir(home), \"google-token.json\");\n}\n\nexport function googleSecretsDir(home?: string): string {\n return join(home ?? homedir(), \".secrets\");\n}\n","// Small helpers ported from the host (`server/utils/{errors,text,time,types}.ts`)\n// so the Google engine carries no dependency on host utils — same convention\n// as `collection/registry/server/fetch.ts`.\n\nexport const ONE_SECOND_MS = 1_000;\nexport const ONE_MINUTE_MS = 60_000;\n\nexport function errorMessage(err: unknown, fallback = \"unknown error\"): string {\n if (err instanceof Error) return err.message || fallback;\n const text = String(err);\n return text === \"\" || text === \"[object Object]\" ? fallback : text;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Clip a string to at most `max` chars; the ellipsis is included in the\n * budget so output never exceeds `max`. */\nexport function truncate(text: string, max: number, ellipsis = \"…\"): string {\n if (max <= 0) return \"\";\n if (text.length <= max) return text;\n return `${text.slice(0, Math.max(0, max - ellipsis.length))}${ellipsis}`;\n}\n","// Loads the Google OAuth **desktop-app** client credentials a user may have\n// downloaded from the Cloud Console into `~/.secrets/client_secret_*.json`.\n// Files are discovered by prefix so nobody has to rename Google's long default\n// filename.\n//\n// Only `{\"installed\": …}` (desktop) files count. A `{\"web\": …}` client cannot\n// drive the loopback consent this engine runs — and one legitimately sits in\n// the same directory for anyone who also deploys the broker — so web clients\n// are skipped rather than treated as a competing choice.\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { isRecord } from \"./util.js\";\nimport { googleSecretsDir } from \"./paths.js\";\n\nexport interface InstalledClientSecret {\n client_id: string;\n client_secret: string;\n}\n\n// Empty strings must not count: they would satisfy a `typeof` check, get\n// picked as the client, and fail at Google with an opaque invalid_client.\nconst isNonEmptyString = (value: unknown): value is string => typeof value === \"string\" && value !== \"\";\n\nconst isInstalledClientSecret = (value: unknown): value is { installed: InstalledClientSecret } => {\n if (!isRecord(value) || !isRecord(value.installed)) return false;\n return isNonEmptyString(value.installed.client_id) && isNonEmptyString(value.installed.client_secret);\n};\n\nconst isClientSecretFileName = (name: string): boolean => name.startsWith(\"client_secret_\") && name.endsWith(\".json\");\n\nconst readIfDesktopClient = async (filePath: string): Promise<string | null> => {\n try {\n const parsed: unknown = JSON.parse(await readFile(filePath, \"utf-8\"));\n return isInstalledClientSecret(parsed) ? filePath : null;\n } catch {\n // Unreadable or malformed: not a usable desktop client, and refusing the\n // whole directory over one stray file would be worse than ignoring it.\n return null;\n }\n};\n\n/** Absolute paths of every desktop-app client JSON in `~/.secrets/`. */\nasync function listDesktopClientSecretFiles(home?: string): Promise<string[]> {\n const dir = googleSecretsDir(home);\n const entries = await readdir(dir).catch((): string[] => []);\n const candidates = entries.filter(isClientSecretFileName).sort();\n const checked = await Promise.all(candidates.map((name) => readIfDesktopClient(join(dir, name))));\n return checked.filter((path): path is string => path !== null);\n}\n\n/** `missing` is the ordinary case — the broker supplies the client, so no user\n * action is needed. `ambiguous` (2+ desktop clients) still needs a human:\n * a stored refresh token pairs with exactly one client_id, so picking for\n * them could silently break the link. */\nexport type ClientSecretPresence = \"found\" | \"missing\" | \"ambiguous\";\n\nexport async function clientSecretPresence(home?: string): Promise<ClientSecretPresence> {\n const matches = await listDesktopClientSecretFiles(home);\n if (matches.length === 0) return \"missing\";\n return matches.length === 1 ? \"found\" : \"ambiguous\";\n}\n\nexport async function findClientSecretPath(home?: string): Promise<string> {\n const dir = googleSecretsDir(home);\n const matches = await listDesktopClientSecretFiles(home);\n const [first, ...rest] = matches;\n if (!first) {\n throw new Error(`no desktop-app client_secret_*.json found in ${dir} — this host links through the sign-in service instead`);\n }\n if (rest.length > 0) {\n const names = matches.map((path) => path.slice(dir.length + 1)).join(\", \");\n throw new Error(`multiple desktop-app client_secret_*.json files found in ${dir} (${names}) — keep exactly one`);\n }\n return first;\n}\n\nexport async function loadClientSecret(home?: string): Promise<InstalledClientSecret> {\n const filePath = await findClientSecretPath(home);\n const parsed: unknown = JSON.parse(await readFile(filePath, \"utf-8\"));\n if (!isInstalledClientSecret(parsed)) {\n throw new Error(`${filePath} is not a desktop-app OAuth client secret (missing \"installed\" with client_id / client_secret)`);\n }\n return { client_id: parsed.installed.client_id, client_secret: parsed.installed.client_secret };\n}\n","// Minimal JSON file I/O for the token store. Unlike the host's\n// `writeJsonAtomic` (and core's collection `atomic.ts`), this one supports a\n// file mode — the token file must be 0600.\nimport { promises as fsp } from \"node:fs\";\nimport path from \"node:path\";\n\nexport async function readJsonOrNull<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await fsp.readFile(filePath, \"utf-8\");\n const parsed: T = JSON.parse(raw);\n return parsed;\n } catch {\n return null;\n }\n}\n\nconst IS_WINDOWS = process.platform === \"win32\";\nconst RENAME_RETRY_DELAYS_MS = [30, 100, 300];\n\nconst hasErrnoCode = (err: unknown): err is { code: string } =>\n typeof err === \"object\" && err !== null && \"code\" in err && typeof (err as { code: unknown }).code === \"string\";\n\n// On Windows, AV / Search Indexer / Defender briefly hold handles and rename\n// trips EPERM/EBUSY/EACCES. The retry is gated to Windows because POSIX EPERM\n// means a real permission problem — retrying would just delay the throw.\n// Ported from the host's server/utils/files/atomic.ts.\nconst isTransientRenameError = (err: unknown): boolean =>\n IS_WINDOWS && hasErrnoCode(err) && (err.code === \"EPERM\" || err.code === \"EBUSY\" || err.code === \"EACCES\");\n\nasync function renameWithWindowsRetry(fromPath: string, toPath: string): Promise<void> {\n for (const delayMs of RENAME_RETRY_DELAYS_MS) {\n try {\n await fsp.rename(fromPath, toPath);\n return;\n } catch (err) {\n if (!isTransientRenameError(err)) throw err;\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n }\n await fsp.rename(fromPath, toPath);\n}\n\n/** tmp-write + rename so readers never see a half-written file; `mode`\n * applies to the tmp file and survives the rename. */\nexport async function writeJsonAtomicWithMode(filePath: string, data: unknown, mode: number): Promise<void> {\n const tmp = `${filePath}.tmp`;\n await fsp.mkdir(path.dirname(filePath), { recursive: true });\n try {\n await fsp.writeFile(tmp, JSON.stringify(data, null, 2), { encoding: \"utf-8\", mode });\n await renameWithWindowsRetry(tmp, filePath);\n } catch (err) {\n await fsp.unlink(tmp).catch(() => undefined);\n throw err;\n }\n}\n","// Persistence for the Google OAuth tokens (refresh + access) at\n// `~/.config/mulmo/google-token.json`, mode 600. Google omits\n// `refresh_token` from refresh responses, so merges must preserve the one we\n// already hold — losing it forces the user through the browser consent again.\nimport { constants as fsConstants, copyFile, mkdir, rm, stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Credentials } from \"google-auth-library\";\nimport { readJsonOrNull, writeJsonAtomicWithMode } from \"./fsJson.js\";\nimport { googleTokenPath, legacyGoogleTokenPath } from \"./paths.js\";\n\nconst TOKEN_FILE_MODE = 0o600;\n\n/** Which client minted the grant: `local` = the user's own client JSON in\n * `~/.secrets/`, `broker` = the mulmoserver broker. Renewal must use the same\n * path, since only that client's secret can refresh its tokens. Absent on\n * tokens written before the broker existed — those are all `local`. */\nexport type IssuedVia = \"local\" | \"broker\";\n\nexport interface StoredGoogleTokens extends Credentials {\n issuedVia?: IssuedVia;\n}\n\nexport function mergeGoogleTokens(existing: StoredGoogleTokens | null, incoming: StoredGoogleTokens): StoredGoogleTokens {\n const merged = { ...existing, ...incoming };\n if (!incoming.refresh_token && existing?.refresh_token) merged.refresh_token = existing.refresh_token;\n // A broker refresh response carries no marker; keep the one from the link.\n if (!incoming.issuedVia && existing?.issuedVia) merged.issuedVia = existing.issuedVia;\n return merged;\n}\n\nconst fileExists = async (filePath: string): Promise<boolean> =>\n await stat(filePath).then(\n () => true,\n () => false,\n );\n\n// Tokens written before 0.20.1 live under the mulmoclaude-branded dir; move\n// them once. COPYFILE_EXCL makes the create atomic-and-non-clobbering — an\n// exists+rename sequence could overwrite a token a concurrent process wrote\n// to the new path in between (TOCTOU). The legacy file is deleted only after\n// a successful copy; on EEXIST (new path won a race, or both files already\n// exist) it is left for any older install still reading it. copyFile\n// preserves the 600 mode.\nasync function migrateLegacyTokenFile(home?: string): Promise<void> {\n const current = googleTokenPath(home);\n const legacy = legacyGoogleTokenPath(home);\n if (!(await fileExists(legacy))) return;\n await mkdir(path.dirname(current), { recursive: true });\n try {\n await copyFile(legacy, current, fsConstants.COPYFILE_EXCL);\n } catch {\n return;\n }\n await rm(legacy, { force: true });\n}\n\nexport async function loadGoogleTokens(home?: string): Promise<StoredGoogleTokens | null> {\n await migrateLegacyTokenFile(home).catch(() => undefined);\n const current = await readJsonOrNull<StoredGoogleTokens>(googleTokenPath(home));\n if (current) return current;\n // Migration is best-effort — a valid legacy token must still count as\n // linked even when the move failed (permissions, read-only fs, …).\n return await readJsonOrNull<StoredGoogleTokens>(legacyGoogleTokenPath(home));\n}\n\nexport async function saveGoogleTokens(incoming: StoredGoogleTokens, home?: string): Promise<StoredGoogleTokens> {\n const merged = mergeGoogleTokens(await loadGoogleTokens(home), incoming);\n await writeJsonAtomicWithMode(googleTokenPath(home), merged, TOKEN_FILE_MODE);\n return merged;\n}\n\nexport async function deleteGoogleTokens(home?: string): Promise<void> {\n await rm(googleTokenPath(home), { force: true });\n}\n","// `fetch` with a finite timeout, ported from the host (`server/utils/fetch.ts`)\n// so the Google engine carries no dependency on host utils (same convention as\n// `collection/registry/server/fetch.ts`).\n\nimport { ONE_SECOND_MS } from \"./util.js\";\n\nexport const DEFAULT_FETCH_TIMEOUT_MS = 10 * ONE_SECOND_MS;\n\n// `Parameters<typeof fetch>[1]` avoids referencing the ambient `RequestInit`\n// type, which ESLint's `no-undef` rule trips over in the server config.\nexport type FetchWithTimeoutInit = Parameters<typeof fetch>[1] & { timeoutMs?: number };\n\n/** `fetch` with a finite timeout. Rejects with a `TimeoutError` once\n * `timeoutMs` elapses. Composes with a caller-supplied `signal` so external\n * cancellation still works. */\nexport async function fetchWithTimeout(url: string | URL, init: FetchWithTimeoutInit = {}): Promise<Response> {\n const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, signal: callerSignal, ...rest } = init;\n\n if (callerSignal?.aborted) {\n throw callerSignal.reason ?? new DOMException(\"Aborted\", \"AbortError\");\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, \"TimeoutError\"));\n }, timeoutMs);\n\n const unsubscribeCaller = bridgeExternalSignal(callerSignal, controller);\n\n try {\n return await fetch(url, { ...rest, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n unsubscribeCaller?.();\n }\n}\n\nfunction bridgeExternalSignal(external: AbortSignal | null | undefined, controller: AbortController): (() => void) | null {\n if (!external) return null;\n const onAbort = () => controller.abort(external.reason);\n external.addEventListener(\"abort\", onAbort, { once: true });\n return () => external.removeEventListener(\"abort\", onAbort);\n}\n","// Client for the mulmoserver OAuth broker (receptron/mulmoserver#54).\n//\n// Why a broker: Google requires a client_secret at the token endpoint even for\n// PKCE flows, so a user without their own Cloud project cannot complete a link\n// on their own. The broker holds the secret and applies it — it never stores or\n// returns it, and tokens still live only on this machine.\n//\n// What proves this host started the flow is the PKCE code_verifier: anyone can\n// mint a `state` at /googleOAuthStart, so the verifier — not the state — is the\n// authorization. See the broker's pkce.ts for the same reasoning server-side.\nimport type { Credentials } from \"google-auth-library\";\nimport { fetchWithTimeout } from \"./fetch.js\";\nimport { errorMessage, isRecord, ONE_SECOND_MS } from \"./util.js\";\n\nconst DEFAULT_BROKER_BASE_URL = \"https://asia-northeast1-mulmoserver.cloudfunctions.net\";\nconst BROKER_TIMEOUT_MS = 20 * ONE_SECOND_MS;\n\n// Trailing slashes are stripped by hand: `/\\/+$/` backtracks super-linearly on\n// a long run of slashes, which lint rejects.\nconst withoutTrailingSlashes = (url: string): string => {\n let end = url.length;\n while (end > 0 && url[end - 1] === \"/\") end -= 1;\n return url.slice(0, end);\n};\n\n/** `MULMO_GOOGLE_BROKER_URL` lets a fork / staging deploy point elsewhere\n * without a code change; unset means the shipped broker. */\nexport const brokerBaseUrl = (override: string | undefined = process.env.MULMO_GOOGLE_BROKER_URL): string =>\n withoutTrailingSlashes(override ?? DEFAULT_BROKER_BASE_URL);\n\nexport interface BrokerStartResponse {\n authUrl: string;\n state: string;\n}\n\nconst LOOPBACK_HOSTNAMES = new Set([\"127.0.0.1\", \"localhost\", \"::1\", \"[::1]\"]);\n\n/** Codes, PKCE verifiers and refresh tokens travel to the broker, so cleartext\n * is refused outright — an override pointed at `http://…` would put them on\n * the wire. Loopback stays allowed: that is where tests stub the broker, and\n * it never leaves the machine. */\nconst assertSecureBrokerUrl = (url: string): void => {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(`invalid Google sign-in service URL: ${url}`);\n }\n if (parsed.protocol === \"https:\") return;\n if (parsed.protocol === \"http:\" && LOOPBACK_HOSTNAMES.has(parsed.hostname)) return;\n throw new Error(`the Google sign-in service must be reached over HTTPS (got ${parsed.protocol}//${parsed.host})`);\n};\n\nconst brokerFetch = async (url: string, init: { method?: string; body?: string } = {}): Promise<unknown> => {\n assertSecureBrokerUrl(url);\n let response: Response;\n try {\n response = await fetchWithTimeout(url, {\n ...init,\n timeoutMs: BROKER_TIMEOUT_MS,\n headers: { \"Content-Type\": \"application/json\" },\n });\n } catch (err) {\n throw new Error(`Google sign-in service unreachable — check the network connection and retry (${errorMessage(err)})`);\n }\n if (!response.ok) {\n // The broker answers deliberately opaque errors (it must not help probe\n // codes / refresh tokens), so there is nothing more specific to surface.\n throw new Error(`Google sign-in service returned HTTP ${response.status}`);\n }\n try {\n return await response.json();\n } catch {\n // A proxy / captive portal answering 200 with HTML would otherwise surface\n // as a raw SyntaxError the agent can't act on.\n throw new Error(\"Google sign-in service returned a malformed response\");\n }\n};\n\nconst GOOGLE_CONSENT_HOST = \"accounts.google.com\";\n\n/** The returned URL is opened in the user's browser, so it must be Google's\n * own consent page and nothing else — a mis-pointed or hostile broker (the\n * base URL is overridable) could otherwise hand back a phishing page wearing\n * a sign-in flow. Cheap to assert, and Google's endpoint host is fixed. */\nconst assertGoogleConsentUrl = (authUrl: string): void => {\n let parsed: URL;\n try {\n parsed = new URL(authUrl);\n } catch {\n throw new Error(\"Google sign-in service returned an unusable authorization URL\");\n }\n if (parsed.protocol !== \"https:\" || parsed.hostname !== GOOGLE_CONSENT_HOST) {\n throw new Error(`Google sign-in service returned an authorization URL that is not Google's consent page (${parsed.protocol}//${parsed.host})`);\n }\n};\n\nexport async function brokerStart(port: number, codeChallenge: string, baseUrl = brokerBaseUrl()): Promise<BrokerStartResponse> {\n const params = new URLSearchParams({ port: String(port), code_challenge: codeChallenge });\n const payload = await brokerFetch(`${baseUrl}/googleOAuthStart?${params.toString()}`);\n const record = isRecord(payload) ? payload : {};\n if (typeof record.auth_url !== \"string\" || typeof record.state !== \"string\") {\n throw new Error(\"Google sign-in service returned an unexpected response (missing auth_url / state)\");\n }\n assertGoogleConsentUrl(record.auth_url);\n return { authUrl: record.auth_url, state: record.state };\n}\n\nconst toCredentials = (payload: unknown, existingRefreshToken?: string): Credentials => {\n const record = isRecord(payload) ? payload : {};\n if (typeof record.access_token !== \"string\") {\n throw new Error(\"Google sign-in service returned no access token\");\n }\n // The refresh endpoint does not echo the refresh_token back — keep the one we\n // already hold so a renewal can't silently unlink the account.\n const refreshToken = typeof record.refresh_token === \"string\" ? record.refresh_token : existingRefreshToken;\n return {\n access_token: record.access_token,\n ...(refreshToken ? { refresh_token: refreshToken } : {}),\n ...(typeof record.expiry_date === \"number\" ? { expiry_date: record.expiry_date } : {}),\n };\n};\n\nexport async function brokerExchange(input: { code: string; state: string; codeVerifier: string }, baseUrl = brokerBaseUrl()): Promise<Credentials> {\n const payload = await brokerFetch(`${baseUrl}/googleOAuthExchange`, {\n method: \"POST\",\n body: JSON.stringify({ code: input.code, state: input.state, code_verifier: input.codeVerifier }),\n });\n const credentials = toCredentials(payload);\n if (!credentials.refresh_token) {\n throw new Error(\"Google returned no refresh token — remove this app under Google Account → Security → Third-party access, then retry\");\n }\n return credentials;\n}\n\nexport async function brokerRefresh(refreshToken: string, baseUrl = brokerBaseUrl()): Promise<Credentials> {\n const payload = await brokerFetch(`${baseUrl}/googleOAuthRefresh`, {\n method: \"POST\",\n body: JSON.stringify({ refresh_token: refreshToken }),\n });\n return toCredentials(payload, refreshToken);\n}\n","// Google OAuth for the host machine, independent of Firebase Auth (which\n// discards refresh tokens). Two ways to reach a linked account:\n//\n// - LOCAL: the user dropped their own desktop-app client JSON in\n// `~/.secrets/`. Everything (consent, exchange, refresh) happens on this\n// machine — nothing but Google is contacted.\n// - BROKER (the default): no client JSON, so the mulmoserver broker applies\n// the client secret for the exchange / refresh it cannot be done without.\n// Tokens still only ever live here. See broker.ts.\n//\n// Either way the loopback listener, the PKCE verifier, and the token file are\n// this machine's. `issuedVia` on the stored token records which path minted it\n// so renewals take the matching one.\nimport { randomBytes } from \"node:crypto\";\nimport http from \"node:http\";\nimport { CodeChallengeMethod, OAuth2Client, type Credentials } from \"google-auth-library\";\nimport { brokerExchange, brokerRefresh, brokerStart } from \"./broker.js\";\nimport { log } from \"./host.js\";\nimport { errorMessage, ONE_MINUTE_MS, ONE_SECOND_MS } from \"./util.js\";\nimport { fetchWithTimeout } from \"./fetch.js\";\nimport { clientSecretPresence, loadClientSecret, type InstalledClientSecret } from \"./clientSecret.js\";\nimport { deleteGoogleTokens, loadGoogleTokens, saveGoogleTokens, type IssuedVia } from \"./tokenStore.js\";\n\nexport const GOOGLE_CALENDAR_SCOPE = \"https://www.googleapis.com/auth/calendar.events\";\nexport const GOOGLE_TASKS_SCOPE = \"https://www.googleapis.com/auth/tasks\";\nexport const GOOGLE_DRIVE_FILE_SCOPE = \"https://www.googleapis.com/auth/drive.file\";\n/** Requested at consent as one set — matches the scopes registered on the\n * OAuth consent screen, so a single re-link covers every supported API\n * (Calendar now; Tasks / Drive tools ride the same grant later). */\nexport const GOOGLE_SCOPES = [GOOGLE_CALENDAR_SCOPE, GOOGLE_TASKS_SCOPE, GOOGLE_DRIVE_FILE_SCOPE];\nconst CALLBACK_PATH = \"/oauth2callback\";\nconst AUTH_TIMEOUT_MS = 5 * ONE_MINUTE_MS;\nconst STATE_BYTES = 16;\n/** Renew a minute early so a call can't start with a token that expires\n * mid-flight. */\nconst EXPIRY_MARGIN_MS = ONE_MINUTE_MS;\n\nexport interface AuthorizeGoogleOptions {\n home?: string;\n /** Called with the consent URL; open it in a browser (and/or print it). */\n onAuthUrl?: (url: string) => void;\n timeoutMs?: number;\n}\n\nconst createClient = (secret: InstalledClientSecret, redirectUri?: string): OAuth2Client =>\n new OAuth2Client({ clientId: secret.client_id, clientSecret: secret.client_secret, redirectUri });\n\nconst persistRotatedTokens = (client: OAuth2Client, home?: string): void => {\n client.on(\"tokens\", (tokens) => {\n saveGoogleTokens(tokens, home).catch((err: unknown) => {\n log.error(\"google\", \"failed to persist rotated tokens\", { error: String(err) });\n });\n });\n};\n\nconst REVOKED_GRANT_MESSAGE = \"could not obtain a Google access token — the grant may have been revoked; re-link the account\";\n\nconst localAccessToken = async (saved: Credentials, home?: string): Promise<string> => {\n const presence = await clientSecretPresence(home);\n // Two desktop clients: the token is still renewable — the engine just can't\n // tell which client minted it. The fix is to remove the duplicate, so raise\n // the loader's own wording rather than sending the user through a re-link.\n if (presence === \"ambiguous\") await loadClientSecret(home);\n // No client at all: the token is bound to the client_id that minted it, so\n // nothing — not even the broker — can renew it. Re-linking is the only way\n // out; the loader's \"no desktop client\" wording would read like a setup\n // problem instead.\n if (presence === \"missing\") {\n throw new Error(\n \"the saved Google link was created with an OAuth client that is no longer configured on this host — ask the user to link their Google account again in this app's settings\",\n );\n }\n const client = createClient(await loadClientSecret(home));\n client.setCredentials(saved);\n persistRotatedTokens(client, home);\n const { token } = await client.getAccessToken();\n if (!token) throw new Error(REVOKED_GRANT_MESSAGE);\n return token;\n};\n\n// The broker mints access tokens because only it holds the client secret. The\n// refreshed token is written back so the next call can reuse it until expiry\n// instead of hitting the broker every time.\nconst brokerAccessToken = async (saved: Credentials, home?: string): Promise<string> => {\n if (typeof saved.expiry_date === \"number\" && saved.access_token && saved.expiry_date - EXPIRY_MARGIN_MS > Date.now()) {\n return saved.access_token;\n }\n const refreshed = await brokerRefresh(saved.refresh_token ?? \"\");\n if (!refreshed.access_token) throw new Error(REVOKED_GRANT_MESSAGE);\n await saveGoogleTokens(refreshed, home);\n return refreshed.access_token;\n};\n\nexport async function getGoogleAccessToken(home?: string): Promise<string> {\n const saved = await loadGoogleTokens(home);\n if (!saved?.refresh_token) {\n // Host-neutral wording — this engine ships to multiple hosts whose link\n // flows differ (#2128); each host's own help carries the specific steps.\n throw new Error(\"Google account not linked on this host — ask the user to link their Google account in this app's settings, then retry\");\n }\n // Tokens written before the broker existed carry no marker; they were all\n // minted from a local client, so that stays the default.\n return saved.issuedVia === \"broker\" ? await brokerAccessToken(saved, home) : await localAccessToken(saved, home);\n}\n\nconst REVOKE_URL = \"https://oauth2.googleapis.com/revoke\";\n\n/** The revoke POST, injectable for tests. */\nexport type RevokeFetch = typeof fetchWithTimeout;\n\n/** Revoke the grant at Google (best-effort) and delete the local token file.\n * Revoke failures are logged but never block the local delete — Google may\n * already consider the token invalid, and keeping the file would leave the\n * user unable to unlink. */\nexport async function unlinkGoogle(home?: string, revokeFetch: RevokeFetch = fetchWithTimeout): Promise<void> {\n const saved = await loadGoogleTokens(home);\n const token = saved?.refresh_token ?? saved?.access_token;\n if (token) {\n try {\n const response = await revokeFetch(REVOKE_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({ token }).toString(),\n });\n if (!response.ok) log.warn(\"google\", \"token revoke returned non-ok\", { status: response.status });\n } catch (err) {\n log.warn(\"google\", \"token revoke failed, deleting local tokens anyway\", { error: errorMessage(err) });\n }\n }\n await deleteGoogleTokens(home);\n}\n\nconst startLoopbackServer = (): Promise<{ server: http.Server; port: number }> =>\n new Promise((resolve, reject) => {\n const server = http.createServer();\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n reject(new Error(\"loopback server has no port\"));\n return;\n }\n resolve({ server, port: address.port });\n });\n });\n\n// State is validated before error/code — a callback that can't prove it\n// belongs to this flow must not influence it (its `error` text would\n// otherwise reach the terminal attacker-controlled).\nconst authCodeFromCallback = (url: URL, expectedState: string): string => {\n if (url.searchParams.get(\"state\") !== expectedState) throw new Error(\"OAuth state mismatch — possible CSRF, aborting\");\n const error = url.searchParams.get(\"error\");\n if (error) throw new Error(`Google authorization failed: ${error}`);\n const code = url.searchParams.get(\"code\");\n if (!code) throw new Error(\"authorization callback carried no code\");\n return code;\n};\n\nconst respondHtml = (res: http.ServerResponse, status: number, message: string): void => {\n res.writeHead(status, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(`<html><body><h3>${message}</h3></body></html>`);\n};\n\nexport const waitForAuthCode = (server: http.Server, expectedState: string, timeoutMs: number): Promise<string> =>\n new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(`authorization timed out after ${timeoutMs / ONE_SECOND_MS}s`)), timeoutMs);\n server.on(\"request\", (req, res) => {\n const url = new URL(req.url ?? \"/\", \"http://127.0.0.1\");\n if (url.pathname !== CALLBACK_PATH) {\n res.writeHead(404);\n res.end();\n return;\n }\n // A wrong-state request is not our callback (drive-by localhost probe\n // or stale tab) — answer it but keep waiting for the real redirect, so\n // an unauthenticated request can't abort the pending flow.\n if (url.searchParams.get(\"state\") !== expectedState) {\n respondHtml(res, 400, \"Invalid authorization callback. You can close this tab.\");\n return;\n }\n clearTimeout(timer);\n try {\n const code = authCodeFromCallback(url, expectedState);\n respondHtml(res, 200, \"Authorization complete — you can close this tab.\");\n resolve(code);\n } catch (err) {\n // Static text only — the failure detail echoes query-string content,\n // which must not be reflected into HTML. The CLI prints the detail.\n respondHtml(res, 400, \"Authorization failed — see the terminal for details. You can close this tab.\");\n reject(err);\n }\n });\n });\n\n// `access_type: offline` + `prompt: consent` force Google to return a refresh\n// token on every run (repeat consents otherwise omit it).\nconst buildConsentUrl = (client: OAuth2Client, codeChallenge: string, state: string): string =>\n client.generateAuthUrl({\n access_type: \"offline\",\n prompt: \"consent\",\n scope: GOOGLE_SCOPES,\n code_challenge_method: CodeChallengeMethod.S256,\n code_challenge: codeChallenge,\n state,\n });\n\n// PKCE material is generated by an OAuth2Client with no credentials — the\n// helper is pure crypto, and in broker mode there is no client to construct.\nconst generatePkce = async (): Promise<{ codeVerifier: string; codeChallenge: string }> => {\n const { codeVerifier, codeChallenge } = await new OAuth2Client().generateCodeVerifierAsync();\n if (!codeChallenge) throw new Error(\"failed to derive a PKCE code challenge\");\n return { codeVerifier, codeChallenge };\n};\n\nconst authorizeWithLocalClient = async (\n secret: InstalledClientSecret,\n server: http.Server,\n port: number,\n opts: AuthorizeGoogleOptions,\n): Promise<Credentials> => {\n const client = createClient(secret, `http://127.0.0.1:${port}${CALLBACK_PATH}`);\n const { codeVerifier, codeChallenge } = await client.generateCodeVerifierAsync();\n if (!codeChallenge) throw new Error(\"failed to derive a PKCE code challenge\");\n const state = randomBytes(STATE_BYTES).toString(\"hex\");\n opts.onAuthUrl?.(buildConsentUrl(client, codeChallenge, state));\n const code = await waitForAuthCode(server, state, opts.timeoutMs ?? AUTH_TIMEOUT_MS);\n const { tokens } = await client.getToken({ code, codeVerifier });\n if (!tokens.refresh_token) {\n throw new Error(\"Google returned no refresh token — remove this app under Google Account → Security → Third-party access, then retry\");\n }\n return tokens;\n};\n\n// The broker signs `state` with a key it never releases, so it — not this\n// host — builds the authorization URL. `state` then round-trips through the\n// browser and comes back to our loopback, where waitForAuthCode matches it.\nconst authorizeWithBroker = async (server: http.Server, port: number, opts: AuthorizeGoogleOptions): Promise<Credentials> => {\n const { codeVerifier, codeChallenge } = await generatePkce();\n const { authUrl, state } = await brokerStart(port, codeChallenge);\n opts.onAuthUrl?.(authUrl);\n const code = await waitForAuthCode(server, state, opts.timeoutMs ?? AUTH_TIMEOUT_MS);\n return await brokerExchange({ code, state, codeVerifier });\n};\n\nexport async function authorizeGoogle(opts: AuthorizeGoogleOptions = {}): Promise<Credentials> {\n // A user-supplied client wins: it keeps the whole flow on this machine, and\n // silently preferring the broker would ignore a deliberate setup. Two of\n // them is unresolvable rather than a reason to fall back — the user meant to\n // use one of theirs, and a broker link would quietly not be it.\n const presence = await clientSecretPresence(opts.home);\n if (presence === \"ambiguous\") {\n // Same wording the loader raises, so the CLI and the settings UI (which\n // disables linking in this state) agree on the fix.\n await loadClientSecret(opts.home);\n }\n const useLocalClient = presence === \"found\";\n const { server, port } = await startLoopbackServer();\n try {\n const issuedVia: IssuedVia = useLocalClient ? \"local\" : \"broker\";\n const tokens = useLocalClient\n ? await authorizeWithLocalClient(await loadClientSecret(opts.home), server, port, opts)\n : await authorizeWithBroker(server, port, opts);\n return await saveGoogleTokens({ ...tokens, issuedVia }, opts.home);\n } finally {\n server.close();\n }\n}\n","// In-flight manager for the settings-UI OAuth flow. authorizeGoogle()\n// resolves only after the user finishes the browser consent, so the HTTP\n// layer starts it in the background, returns the consent URL immediately,\n// and reports progress via status polling. One flow at a time — the guard\n// is the in-flight start promise itself (set synchronously before any\n// await), so concurrent authorize requests share one flow instead of\n// spawning parallel loopback listeners.\nimport { log } from \"./host.js\";\nimport { errorMessage } from \"./util.js\";\nimport { authorizeGoogle } from \"./auth.js\";\n\nexport interface GoogleAuthFlowStatus {\n pending: boolean;\n lastError: string | null;\n}\n\nexport interface GoogleAuthFlow {\n start: () => Promise<{ authUrl: string }>;\n status: () => GoogleAuthFlowStatus;\n}\n\nexport const createGoogleAuthFlow = (authorize: typeof authorizeGoogle): GoogleAuthFlow => {\n let inFlightStart: Promise<{ authUrl: string }> | null = null;\n let flowRunning = false;\n let lastError: string | null = null;\n\n const launchFlow = (): Promise<{ authUrl: string }> =>\n new Promise((resolve, reject) => {\n flowRunning = true;\n authorize({\n onAuthUrl: (url) => resolve({ authUrl: url }),\n })\n .then(() => log.info(\"google\", \"authorize flow completed\"))\n .catch((err: unknown) => {\n lastError = errorMessage(err);\n log.warn(\"google\", \"authorize flow failed\", { error: lastError });\n // No-op when the URL already resolved; covers pre-URL failures\n // (missing client secret, port bind error).\n reject(err instanceof Error ? err : new Error(String(err)));\n })\n .finally(() => {\n flowRunning = false;\n inFlightStart = null;\n });\n });\n\n const start = (): Promise<{ authUrl: string }> => {\n if (inFlightStart) return inFlightStart;\n lastError = null;\n inFlightStart = launchFlow();\n return inFlightStart;\n };\n\n const status = (): GoogleAuthFlowStatus => ({ pending: flowRunning, lastError });\n\n return { start, status };\n};\n\nexport const googleAuthFlow = createGoogleAuthFlow(authorizeGoogle);\n","// Shared REST plumbing for the Google APIs this engine wraps (Calendar,\n// Tasks, Drive). Plain fetch instead of the `googleapis` SDK — a handful of\n// endpoints don't justify the dependency (see\n// plans/done/feat-google-oauth-calendar.md).\nimport { errorMessage, isRecord, ONE_SECOND_MS, truncate } from \"./util.js\";\nimport { fetchWithTimeout } from \"./fetch.js\";\n\nexport const GOOGLE_API_TIMEOUT_MS = 30 * ONE_SECOND_MS;\nconst ERROR_BODY_MAX_CHARS = 300;\nconst HTTP_FORBIDDEN = 403;\nexport const DEFAULT_LIST_MAX_RESULTS = 10;\nexport const MAX_LIST_RESULTS = 50;\n\n/** 403 usually means the API is not enabled for the user's Cloud project —\n * name the API so the agent's recovery guidance can be specific. */\nexport const googleApiError = (apiLabel: string, status: number, body: string): Error => {\n const hint = status === HTTP_FORBIDDEN ? ` (is the ${apiLabel} enabled for the Cloud project?)` : \"\";\n const detail = body ? ` — ${truncate(body, ERROR_BODY_MAX_CHARS)}` : \"\";\n return new Error(`${apiLabel}: HTTP ${status}${hint}${detail}`);\n};\n\nexport interface GoogleRequestInit {\n method?: string;\n body?: string;\n /** Overrides the default JSON content type (multipart upload, …). */\n contentType?: string;\n /** Response is not JSON (Drive media download) — return the raw text. */\n expectText?: boolean;\n}\n\nexport async function googleRequest(apiLabel: string, accessToken: string, url: string, init: GoogleRequestInit = {}): Promise<unknown> {\n const { contentType = \"application/json\", expectText = false, ...rest } = init;\n const response = await fetchWithTimeout(url, {\n ...rest,\n timeoutMs: GOOGLE_API_TIMEOUT_MS,\n headers: { Authorization: `Bearer ${accessToken}`, \"Content-Type\": contentType },\n });\n if (!response.ok) {\n const body = await response.text().catch((err: unknown) => errorMessage(err));\n throw googleApiError(apiLabel, response.status, body);\n }\n if (expectText) return await response.text();\n // 204 (Drive delete, …) has no body to parse.\n if (response.status === 204) return {};\n return await response.json();\n}\n\nexport const stringField = (record: Record<string, unknown>, key: string): string => (typeof record[key] === \"string\" ? record[key] : \"\");\n\nexport const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});\n\nexport const itemsOf = (value: unknown): unknown[] => {\n const record = asRecord(value);\n return Array.isArray(record.items) ? record.items : [];\n};\n","// Google Calendar v3 REST calls against the user's primary calendar.\nimport { asRecord, googleApiError, googleRequest, stringField, DEFAULT_LIST_MAX_RESULTS } from \"./apiClient.js\";\nimport { isRecord } from \"./util.js\";\n\nconst CALENDAR_EVENTS_URL = \"https://www.googleapis.com/calendar/v3/calendars/primary/events\";\nconst CALENDAR_API_LABEL = \"Google Calendar API\";\n\nexport interface CalendarEventInput {\n summary: string;\n startDateTime: string;\n endDateTime: string;\n description?: string;\n}\n\nexport interface ListEventsInput {\n timeMin?: string;\n maxResults?: number;\n}\n\nexport interface CalendarEventSummary {\n id: string;\n summary: string;\n start: string;\n end: string;\n htmlLink: string;\n status: string;\n}\n\n// All-day events carry `date`, timed events carry `dateTime`.\nconst eventTime = (value: unknown): string => {\n if (!isRecord(value)) return \"\";\n if (typeof value.dateTime === \"string\") return value.dateTime;\n if (typeof value.date === \"string\") return value.date;\n return \"\";\n};\n\nexport const toEventSummary = (value: unknown): CalendarEventSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n summary: stringField(record, \"summary\"),\n start: eventTime(record.start),\n end: eventTime(record.end),\n htmlLink: stringField(record, \"htmlLink\"),\n status: stringField(record, \"status\"),\n };\n};\n\n/** Kept as a named export for the existing unit tests / callers; the shared\n * helper now carries the wording. */\nexport const calendarApiError = (status: number, body: string): Error => googleApiError(CALENDAR_API_LABEL, status, body);\n\nexport async function createCalendarEvent(accessToken: string, input: CalendarEventInput): Promise<CalendarEventSummary> {\n const body = {\n summary: input.summary,\n description: input.description,\n start: { dateTime: input.startDateTime },\n end: { dateTime: input.endDateTime },\n };\n const created = await googleRequest(CALENDAR_API_LABEL, accessToken, CALENDAR_EVENTS_URL, { method: \"POST\", body: JSON.stringify(body) });\n return toEventSummary(created);\n}\n\nexport async function listCalendarEvents(accessToken: string, input: ListEventsInput = {}): Promise<CalendarEventSummary[]> {\n const params = new URLSearchParams({\n timeMin: input.timeMin ?? new Date().toISOString(),\n maxResults: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n singleEvents: \"true\",\n orderBy: \"startTime\",\n });\n const listed = await googleRequest(CALENDAR_API_LABEL, accessToken, `${CALENDAR_EVENTS_URL}?${params.toString()}`);\n const record = asRecord(listed);\n const items = Array.isArray(record.items) ? record.items : [];\n return items.map(toEventSummary);\n}\n","// Google Tasks v1 REST calls. `@default` is Google's alias for the user's\n// default task list, so callers can stay list-agnostic.\nimport { asRecord, googleRequest, itemsOf, stringField, DEFAULT_LIST_MAX_RESULTS } from \"./apiClient.js\";\n\nconst TASKS_BASE_URL = \"https://tasks.googleapis.com/tasks/v1\";\nconst TASKS_API_LABEL = \"Google Tasks API\";\nconst DEFAULT_TASK_LIST_ID = \"@default\";\nconst TASK_STATUS_COMPLETED = \"completed\";\nconst MAX_TASK_LISTS = 50;\n\nexport interface TaskListSummary {\n id: string;\n title: string;\n}\n\nexport interface TaskSummary {\n id: string;\n title: string;\n status: string;\n due: string;\n notes: string;\n}\n\nexport interface ListTasksInput {\n taskListId?: string;\n maxResults?: number;\n showCompleted?: boolean;\n}\n\nexport interface CreateTaskInput {\n title: string;\n notes?: string;\n /** RFC3339. Google stores a DATE only — the time part is recorded but\n * ignored by the UI, so callers should not promise time-of-day fidelity. */\n due?: string;\n taskListId?: string;\n}\n\nexport interface CompleteTaskInput {\n taskId: string;\n taskListId?: string;\n}\n\nexport const toTaskListSummary = (value: unknown): TaskListSummary => {\n const record = asRecord(value);\n return { id: stringField(record, \"id\"), title: stringField(record, \"title\") };\n};\n\nexport const toTaskSummary = (value: unknown): TaskSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n title: stringField(record, \"title\"),\n status: stringField(record, \"status\"),\n due: stringField(record, \"due\"),\n notes: stringField(record, \"notes\"),\n };\n};\n\nconst tasksUrl = (taskListId: string | undefined, suffix = \"\"): string =>\n `${TASKS_BASE_URL}/lists/${encodeURIComponent(taskListId ?? DEFAULT_TASK_LIST_ID)}/tasks${suffix}`;\n\nexport async function listTaskLists(accessToken: string): Promise<TaskListSummary[]> {\n const listed = await googleRequest(TASKS_API_LABEL, accessToken, `${TASKS_BASE_URL}/users/@me/lists?maxResults=${MAX_TASK_LISTS}`);\n return itemsOf(listed).map(toTaskListSummary);\n}\n\nexport async function listTasks(accessToken: string, input: ListTasksInput = {}): Promise<TaskSummary[]> {\n const params = new URLSearchParams({\n maxResults: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n showCompleted: String(input.showCompleted ?? false),\n });\n const listed = await googleRequest(TASKS_API_LABEL, accessToken, `${tasksUrl(input.taskListId)}?${params.toString()}`);\n return itemsOf(listed).map(toTaskSummary);\n}\n\nexport async function createTask(accessToken: string, input: CreateTaskInput): Promise<TaskSummary> {\n const body = { title: input.title, notes: input.notes, due: input.due };\n const created = await googleRequest(TASKS_API_LABEL, accessToken, tasksUrl(input.taskListId), { method: \"POST\", body: JSON.stringify(body) });\n return toTaskSummary(created);\n}\n\nexport async function completeTask(accessToken: string, input: CompleteTaskInput): Promise<TaskSummary> {\n // PATCH keeps the rest of the task intact — a PUT would need the full body\n // and would silently drop fields the caller never read.\n const url = tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`);\n const updated = await googleRequest(TASKS_API_LABEL, accessToken, url, {\n method: \"PATCH\",\n body: JSON.stringify({ status: TASK_STATUS_COMPLETED }),\n });\n return toTaskSummary(updated);\n}\n\nexport async function deleteTask(accessToken: string, input: CompleteTaskInput): Promise<void> {\n const url = tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`);\n await googleRequest(TASKS_API_LABEL, accessToken, url, { method: \"DELETE\" });\n}\n","// Google Drive v3 REST calls under the `drive.file` scope — the app only\n// ever sees files IT created, never the user's wider Drive. That narrow\n// scope is why this is non-sensitive and needs no Google verification\n// review; keep every call inside it.\nimport { randomBytes } from \"node:crypto\";\nimport { asRecord, googleRequest, stringField, DEFAULT_LIST_MAX_RESULTS } from \"./apiClient.js\";\n\nconst DRIVE_FILES_URL = \"https://www.googleapis.com/drive/v3/files\";\nconst DRIVE_UPLOAD_URL = \"https://www.googleapis.com/upload/drive/v3/files\";\nconst DRIVE_API_LABEL = \"Google Drive API\";\nconst DEFAULT_MIME_TYPE = \"text/plain\";\nconst FILE_FIELDS = \"id,name,mimeType,webViewLink,modifiedTime\";\n// Reading a huge blob into a tool result would blow the context window; the\n// tool surface is for app-created text documents, not media.\nconst MAX_READ_CHARS = 100_000;\nconst TEXT_MIME_PREFIXES = [\"text/\", \"application/json\", \"application/xml\", \"application/javascript\"];\n\nexport interface DriveFileSummary {\n id: string;\n name: string;\n mimeType: string;\n webViewLink: string;\n modifiedTime: string;\n}\n\nexport interface ListDriveFilesInput {\n maxResults?: number;\n}\n\nexport interface CreateDriveFileInput {\n name: string;\n content: string;\n mimeType?: string;\n}\n\nexport interface ReadDriveFileInput {\n fileId: string;\n}\n\nexport const toDriveFileSummary = (value: unknown): DriveFileSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n name: stringField(record, \"name\"),\n mimeType: stringField(record, \"mimeType\"),\n webViewLink: stringField(record, \"webViewLink\"),\n modifiedTime: stringField(record, \"modifiedTime\"),\n };\n};\n\nexport const isTextMimeType = (mimeType: string): boolean => TEXT_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix));\n\nexport async function listDriveFiles(accessToken: string, input: ListDriveFilesInput = {}): Promise<DriveFileSummary[]> {\n const params = new URLSearchParams({\n pageSize: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n fields: `files(${FILE_FIELDS})`,\n orderBy: \"modifiedTime desc\",\n });\n const listed = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}?${params.toString()}`);\n const record = asRecord(listed);\n const files = Array.isArray(record.files) ? record.files : [];\n return files.map(toDriveFileSummary);\n}\n\n// Multipart upload: metadata part + media part in one request. Built by hand\n// because the `googleapis` SDK is the only alternative and this is the sole\n// multipart call in the engine.\nconst BOUNDARY_BYTES = 16;\n\n/** Fresh per request: a fixed boundary appearing inside file content would\n * split the payload at the wrong place, so Drive would store a truncated or\n * mangled file. Random 128 bits makes an accidental — or crafted — collision\n * infeasible; `newMultipartBoundary` is re-derived until it is absent from\n * the body it must delimit. */\nconst newMultipartBoundary = (): string => `mulmo-drive-${randomBytes(BOUNDARY_BYTES).toString(\"hex\")}`;\n\n// RFC 2045 token + parameters: no CR/LF (header injection into the part\n// headers) and no quotes/semicolons that would let a crafted value forge\n// extra headers or parameters.\nconst MIME_TYPE_RE = /^[A-Za-z0-9!#$&^_.+-]+\\/[A-Za-z0-9!#$&^_.+-]+$/;\n\nexport const assertSafeMimeType = (mimeType: string): string => {\n if (!MIME_TYPE_RE.test(mimeType)) throw new Error(`Google Drive API: invalid mimeType '${mimeType}' — expected a plain type/subtype such as text/plain`);\n return mimeType;\n};\n\nexport const buildMultipartBody = (metadata: Record<string, string>, content: string, mimeType: string, boundary: string): string =>\n [\n `--${boundary}`,\n \"Content-Type: application/json; charset=UTF-8\",\n \"\",\n JSON.stringify(metadata),\n `--${boundary}`,\n `Content-Type: ${mimeType}`,\n \"\",\n content,\n `--${boundary}--`,\n \"\",\n ].join(\"\\r\\n\");\n\n/** A boundary that appears nowhere in the parts it delimits. */\nexport const pickBoundary = (parts: string[], generate: () => string = newMultipartBoundary): string => {\n const collides = (candidate: string): boolean => parts.some((part) => part.includes(candidate));\n let boundary = generate();\n while (collides(boundary)) boundary = generate();\n return boundary;\n};\n\nexport async function createDriveFile(accessToken: string, input: CreateDriveFileInput): Promise<DriveFileSummary> {\n const mimeType = assertSafeMimeType(input.mimeType ?? DEFAULT_MIME_TYPE);\n const metadata = { name: input.name, mimeType };\n const boundary = pickBoundary([input.content, JSON.stringify(metadata)]);\n const params = new URLSearchParams({ uploadType: \"multipart\", fields: FILE_FIELDS });\n const created = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_UPLOAD_URL}?${params.toString()}`, {\n method: \"POST\",\n contentType: `multipart/related; boundary=${boundary}`,\n body: buildMultipartBody(metadata, input.content, mimeType, boundary),\n });\n return toDriveFileSummary(created);\n}\n\nexport async function readDriveFile(accessToken: string, input: ReadDriveFileInput): Promise<{ file: DriveFileSummary; content: string }> {\n const fileId = encodeURIComponent(input.fileId);\n const metadata = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${fileId}?fields=${FILE_FIELDS}`);\n const file = toDriveFileSummary(metadata);\n if (!isTextMimeType(file.mimeType)) {\n throw new Error(`Google Drive API: '${file.name}' is ${file.mimeType || \"a binary file\"} — only text files can be read as content`);\n }\n const raw = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${fileId}?alt=media`, { expectText: true });\n const content = typeof raw === \"string\" ? raw.slice(0, MAX_READ_CHARS) : \"\";\n return { file, content };\n}\n\nexport async function deleteDriveFile(accessToken: string, input: ReadDriveFileInput): Promise<void> {\n await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: \"DELETE\" });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/google/host.ts","../../src/google/datetime.ts","../../src/google/paths.ts","../../src/google/util.ts","../../src/google/clientSecret.ts","../../src/google/fsJson.ts","../../src/google/tokenStore.ts","../../src/google/fetch.ts","../../src/google/broker.ts","../../src/google/auth.ts","../../src/google/authFlow.ts","../../src/google/apiClient.ts","../../src/google/calendar.ts","../../src/google/tasks.ts","../../src/google/driveFile.ts"],"sourcesContent":["// Host binding for the Google engine. The engine logs through the host's\n// logger, but a package-level import of the host logger would be an uphill\n// dependency — so the host injects it once at startup (same pattern as\n// `collection/server/host.ts`). The default is silent so the engine works\n// unconfigured in unit tests.\n\nexport interface GoogleLogger {\n error: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n warn: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n info: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n debug: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n}\n\nconst silentLogger: GoogleLogger = {\n error: () => undefined,\n warn: () => undefined,\n info: () => undefined,\n debug: () => undefined,\n};\n\nlet hostLog: GoogleLogger = silentLogger;\n\nexport function configureGoogleHost(binding: { log: GoogleLogger }): void {\n hostLog = binding.log;\n}\n\nexport const log: GoogleLogger = {\n error: (prefix, message, data) => hostLog.error(prefix, message, data),\n warn: (prefix, message, data) => hostLog.warn(prefix, message, data),\n info: (prefix, message, data) => hostLog.info(prefix, message, data),\n debug: (prefix, message, data) => hostLog.debug(prefix, message, data),\n};\n","// Strict RFC3339 date-time validation shared by every surface that feeds\n// Calendar `dateTime`/`timeMin` values (agent tool args, remote-host command\n// params). Calendar rejects date-only / offset-less values with an opaque\n// 400, so callers validate here and return an actionable message instead.\n\n// Fractional seconds are normalized away first — an optional `(\\.\\d+)?`\n// group inside the main pattern trips security/detect-unsafe-regex.\nconst ISO_DATE_TIME_WITH_OFFSET_RE = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:Z|[+-](\\d{2}):(\\d{2}))$/;\nconst FRACTIONAL_SECONDS_RE = /\\.\\d+(?=Z|[+-])/;\n\nconst MAX_HOUR = 23;\nconst MAX_MINUTE = 59;\nconst MAX_SECOND = 59;\n\n// JS Date normalizes overflowed components (2026-02-31 parses as\n// 2026-03-03), so `new Date(value)` alone cannot reject impossible dates —\n// a UTC round-trip of the raw components can.\nconst isRealCalendarDate = (year: number, month: number, day: number): boolean => {\n const roundTrip = new Date(Date.UTC(year, month - 1, day));\n return roundTrip.getUTCFullYear() === year && roundTrip.getUTCMonth() === month - 1 && roundTrip.getUTCDate() === day;\n};\n\nexport const isIsoDateTimeWithOffset = (value: string): boolean => {\n const match = ISO_DATE_TIME_WITH_OFFSET_RE.exec(value.replace(FRACTIONAL_SECONDS_RE, \"\"));\n if (!match) return false;\n const [year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0] = match.slice(1, 7).map(Number);\n const timeInRange = hour <= MAX_HOUR && minute <= MAX_MINUTE && second <= MAX_SECOND;\n // RFC3339 bounds the offset to 00-23:59; groups 7/8 are undefined for \"Z\".\n const offsetInRange = match[7] === undefined || (Number(match[7]) <= MAX_HOUR && Number(match[8]) <= MAX_MINUTE);\n return isRealCalendarDate(year, month, day) && timeInRange && offsetInRange;\n};\n","// Google OAuth material lives OUTSIDE the workspace: the client secret is\n// machine-only (never synced) and the refresh token must survive workspace\n// resets — same reasoning as the gcloud / gh CLI model. The dir is the\n// host-NEUTRAL `~/.config/mulmo` because this engine is shared by both\n// MulmoClaude and MulmoTerminal, which deliberately share one grant per\n// machine. The `home` parameter exists so tests can thread a fake home.\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function googleConfigDir(home?: string): string {\n return join(home ?? homedir(), \".config\", \"mulmo\");\n}\n\n/** Pre-0.20.1 token dir (mulmoclaude-branded); reads migrate away from it. */\nexport function legacyGoogleTokenPath(home?: string): string {\n return join(home ?? homedir(), \".config\", \"mulmoclaude\", \"google-token.json\");\n}\n\nexport function googleTokenPath(home?: string): string {\n return join(googleConfigDir(home), \"google-token.json\");\n}\n\nexport function googleSecretsDir(home?: string): string {\n return join(home ?? homedir(), \".secrets\");\n}\n","// Small helpers ported from the host (`server/utils/{errors,text,time,types}.ts`)\n// so the Google engine carries no dependency on host utils — same convention\n// as `collection/registry/server/fetch.ts`.\n\nexport const ONE_SECOND_MS = 1_000;\nexport const ONE_MINUTE_MS = 60_000;\n\nexport function errorMessage(err: unknown, fallback = \"unknown error\"): string {\n if (err instanceof Error) return err.message || fallback;\n const text = String(err);\n return text === \"\" || text === \"[object Object]\" ? fallback : text;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Clip a string to at most `max` chars; the ellipsis is included in the\n * budget so output never exceeds `max`. */\nexport function truncate(text: string, max: number, ellipsis = \"…\"): string {\n if (max <= 0) return \"\";\n if (text.length <= max) return text;\n return `${text.slice(0, Math.max(0, max - ellipsis.length))}${ellipsis}`;\n}\n","// Loads the Google OAuth **desktop-app** client credentials a user may have\n// downloaded from the Cloud Console into `~/.secrets/client_secret_*.json`.\n// Files are discovered by prefix so nobody has to rename Google's long default\n// filename.\n//\n// Only `{\"installed\": …}` (desktop) files count. A `{\"web\": …}` client cannot\n// drive the loopback consent this engine runs — and one legitimately sits in\n// the same directory for anyone who also deploys the broker — so web clients\n// are skipped rather than treated as a competing choice.\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { isRecord } from \"./util.js\";\nimport { googleSecretsDir } from \"./paths.js\";\n\nexport interface InstalledClientSecret {\n client_id: string;\n client_secret: string;\n}\n\n// Empty strings must not count: they would satisfy a `typeof` check, get\n// picked as the client, and fail at Google with an opaque invalid_client.\nconst isNonEmptyString = (value: unknown): value is string => typeof value === \"string\" && value !== \"\";\n\nconst isInstalledClientSecret = (value: unknown): value is { installed: InstalledClientSecret } => {\n if (!isRecord(value) || !isRecord(value.installed)) return false;\n return isNonEmptyString(value.installed.client_id) && isNonEmptyString(value.installed.client_secret);\n};\n\nconst isClientSecretFileName = (name: string): boolean => name.startsWith(\"client_secret_\") && name.endsWith(\".json\");\n\nconst readIfDesktopClient = async (filePath: string): Promise<string | null> => {\n try {\n const parsed: unknown = JSON.parse(await readFile(filePath, \"utf-8\"));\n return isInstalledClientSecret(parsed) ? filePath : null;\n } catch {\n // Unreadable or malformed: not a usable desktop client, and refusing the\n // whole directory over one stray file would be worse than ignoring it.\n return null;\n }\n};\n\n/** Absolute paths of every desktop-app client JSON in `~/.secrets/`. */\nasync function listDesktopClientSecretFiles(home?: string): Promise<string[]> {\n const dir = googleSecretsDir(home);\n const entries = await readdir(dir).catch((): string[] => []);\n const candidates = entries.filter(isClientSecretFileName).sort();\n const checked = await Promise.all(candidates.map((name) => readIfDesktopClient(join(dir, name))));\n return checked.filter((path): path is string => path !== null);\n}\n\n/** `missing` is the ordinary case — the broker supplies the client, so no user\n * action is needed. `ambiguous` (2+ desktop clients) still needs a human:\n * a stored refresh token pairs with exactly one client_id, so picking for\n * them could silently break the link. */\nexport type ClientSecretPresence = \"found\" | \"missing\" | \"ambiguous\";\n\nexport async function clientSecretPresence(home?: string): Promise<ClientSecretPresence> {\n const matches = await listDesktopClientSecretFiles(home);\n if (matches.length === 0) return \"missing\";\n return matches.length === 1 ? \"found\" : \"ambiguous\";\n}\n\nexport async function findClientSecretPath(home?: string): Promise<string> {\n const dir = googleSecretsDir(home);\n const matches = await listDesktopClientSecretFiles(home);\n const [first, ...rest] = matches;\n if (!first) {\n throw new Error(`no desktop-app client_secret_*.json found in ${dir} — this host links through the sign-in service instead`);\n }\n if (rest.length > 0) {\n const names = matches.map((path) => path.slice(dir.length + 1)).join(\", \");\n throw new Error(`multiple desktop-app client_secret_*.json files found in ${dir} (${names}) — keep exactly one`);\n }\n return first;\n}\n\nexport async function loadClientSecret(home?: string): Promise<InstalledClientSecret> {\n const filePath = await findClientSecretPath(home);\n const parsed: unknown = JSON.parse(await readFile(filePath, \"utf-8\"));\n if (!isInstalledClientSecret(parsed)) {\n throw new Error(`${filePath} is not a desktop-app OAuth client secret (missing \"installed\" with client_id / client_secret)`);\n }\n return { client_id: parsed.installed.client_id, client_secret: parsed.installed.client_secret };\n}\n","// Minimal JSON file I/O for the token store. Unlike the host's\n// `writeJsonAtomic` (and core's collection `atomic.ts`), this one supports a\n// file mode — the token file must be 0600.\nimport { promises as fsp } from \"node:fs\";\nimport path from \"node:path\";\n\nexport async function readJsonOrNull<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await fsp.readFile(filePath, \"utf-8\");\n const parsed: T = JSON.parse(raw);\n return parsed;\n } catch {\n return null;\n }\n}\n\nconst IS_WINDOWS = process.platform === \"win32\";\nconst RENAME_RETRY_DELAYS_MS = [30, 100, 300];\n\nconst hasErrnoCode = (err: unknown): err is { code: string } =>\n typeof err === \"object\" && err !== null && \"code\" in err && typeof (err as { code: unknown }).code === \"string\";\n\n// On Windows, AV / Search Indexer / Defender briefly hold handles and rename\n// trips EPERM/EBUSY/EACCES. The retry is gated to Windows because POSIX EPERM\n// means a real permission problem — retrying would just delay the throw.\n// Ported from the host's server/utils/files/atomic.ts.\nconst isTransientRenameError = (err: unknown): boolean =>\n IS_WINDOWS && hasErrnoCode(err) && (err.code === \"EPERM\" || err.code === \"EBUSY\" || err.code === \"EACCES\");\n\nasync function renameWithWindowsRetry(fromPath: string, toPath: string): Promise<void> {\n for (const delayMs of RENAME_RETRY_DELAYS_MS) {\n try {\n await fsp.rename(fromPath, toPath);\n return;\n } catch (err) {\n if (!isTransientRenameError(err)) throw err;\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n }\n await fsp.rename(fromPath, toPath);\n}\n\n/** tmp-write + rename so readers never see a half-written file; `mode`\n * applies to the tmp file and survives the rename. */\nexport async function writeJsonAtomicWithMode(filePath: string, data: unknown, mode: number): Promise<void> {\n const tmp = `${filePath}.tmp`;\n await fsp.mkdir(path.dirname(filePath), { recursive: true });\n try {\n await fsp.writeFile(tmp, JSON.stringify(data, null, 2), { encoding: \"utf-8\", mode });\n await renameWithWindowsRetry(tmp, filePath);\n } catch (err) {\n await fsp.unlink(tmp).catch(() => undefined);\n throw err;\n }\n}\n","// Persistence for the Google OAuth tokens (refresh + access) at\n// `~/.config/mulmo/google-token.json`, mode 600. Google omits\n// `refresh_token` from refresh responses, so merges must preserve the one we\n// already hold — losing it forces the user through the browser consent again.\nimport { constants as fsConstants, copyFile, mkdir, rm, stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Credentials } from \"google-auth-library\";\nimport { readJsonOrNull, writeJsonAtomicWithMode } from \"./fsJson.js\";\nimport { googleTokenPath, legacyGoogleTokenPath } from \"./paths.js\";\n\nconst TOKEN_FILE_MODE = 0o600;\n\n/** Which client minted the grant: `local` = the user's own client JSON in\n * `~/.secrets/`, `broker` = the mulmoserver broker. Renewal must use the same\n * path, since only that client's secret can refresh its tokens. Absent on\n * tokens written before the broker existed — those are all `local`. */\nexport type IssuedVia = \"local\" | \"broker\";\n\nexport interface StoredGoogleTokens extends Credentials {\n issuedVia?: IssuedVia;\n}\n\nexport function mergeGoogleTokens(existing: StoredGoogleTokens | null, incoming: StoredGoogleTokens): StoredGoogleTokens {\n const merged = { ...existing, ...incoming };\n if (!incoming.refresh_token && existing?.refresh_token) merged.refresh_token = existing.refresh_token;\n // A broker refresh response carries no marker; keep the one from the link.\n if (!incoming.issuedVia && existing?.issuedVia) merged.issuedVia = existing.issuedVia;\n return merged;\n}\n\nconst fileExists = async (filePath: string): Promise<boolean> =>\n await stat(filePath).then(\n () => true,\n () => false,\n );\n\n// Tokens written before 0.20.1 live under the mulmoclaude-branded dir; move\n// them once. COPYFILE_EXCL makes the create atomic-and-non-clobbering — an\n// exists+rename sequence could overwrite a token a concurrent process wrote\n// to the new path in between (TOCTOU). The legacy file is deleted only after\n// a successful copy; on EEXIST (new path won a race, or both files already\n// exist) it is left for any older install still reading it. copyFile\n// preserves the 600 mode.\nasync function migrateLegacyTokenFile(home?: string): Promise<void> {\n const current = googleTokenPath(home);\n const legacy = legacyGoogleTokenPath(home);\n if (!(await fileExists(legacy))) return;\n await mkdir(path.dirname(current), { recursive: true });\n try {\n await copyFile(legacy, current, fsConstants.COPYFILE_EXCL);\n } catch {\n return;\n }\n await rm(legacy, { force: true });\n}\n\nexport async function loadGoogleTokens(home?: string): Promise<StoredGoogleTokens | null> {\n await migrateLegacyTokenFile(home).catch(() => undefined);\n const current = await readJsonOrNull<StoredGoogleTokens>(googleTokenPath(home));\n if (current) return current;\n // Migration is best-effort — a valid legacy token must still count as\n // linked even when the move failed (permissions, read-only fs, …).\n return await readJsonOrNull<StoredGoogleTokens>(legacyGoogleTokenPath(home));\n}\n\nexport async function saveGoogleTokens(incoming: StoredGoogleTokens, home?: string): Promise<StoredGoogleTokens> {\n const merged = mergeGoogleTokens(await loadGoogleTokens(home), incoming);\n await writeJsonAtomicWithMode(googleTokenPath(home), merged, TOKEN_FILE_MODE);\n return merged;\n}\n\nexport async function deleteGoogleTokens(home?: string): Promise<void> {\n await rm(googleTokenPath(home), { force: true });\n}\n","// `fetch` with a finite timeout, ported from the host (`server/utils/fetch.ts`)\n// so the Google engine carries no dependency on host utils (same convention as\n// `collection/registry/server/fetch.ts`).\n\nimport { ONE_SECOND_MS } from \"./util.js\";\n\nexport const DEFAULT_FETCH_TIMEOUT_MS = 10 * ONE_SECOND_MS;\n\n// `Parameters<typeof fetch>[1]` avoids referencing the ambient `RequestInit`\n// type, which ESLint's `no-undef` rule trips over in the server config.\nexport type FetchWithTimeoutInit = Parameters<typeof fetch>[1] & { timeoutMs?: number };\n\n/** `fetch` with a finite timeout. Rejects with a `TimeoutError` once\n * `timeoutMs` elapses. Composes with a caller-supplied `signal` so external\n * cancellation still works. */\nexport async function fetchWithTimeout(url: string | URL, init: FetchWithTimeoutInit = {}): Promise<Response> {\n const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, signal: callerSignal, ...rest } = init;\n\n if (callerSignal?.aborted) {\n throw callerSignal.reason ?? new DOMException(\"Aborted\", \"AbortError\");\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, \"TimeoutError\"));\n }, timeoutMs);\n\n const unsubscribeCaller = bridgeExternalSignal(callerSignal, controller);\n\n try {\n return await fetch(url, { ...rest, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n unsubscribeCaller?.();\n }\n}\n\nfunction bridgeExternalSignal(external: AbortSignal | null | undefined, controller: AbortController): (() => void) | null {\n if (!external) return null;\n const onAbort = () => controller.abort(external.reason);\n external.addEventListener(\"abort\", onAbort, { once: true });\n return () => external.removeEventListener(\"abort\", onAbort);\n}\n","// Client for the mulmoserver OAuth broker (receptron/mulmoserver#54).\n//\n// Why a broker: Google requires a client_secret at the token endpoint even for\n// PKCE flows, so a user without their own Cloud project cannot complete a link\n// on their own. The broker holds the secret and applies it — it never stores or\n// returns it, and tokens still live only on this machine.\n//\n// What proves this host started the flow is the PKCE code_verifier: anyone can\n// mint a `state` at /googleOAuthStart, so the verifier — not the state — is the\n// authorization. See the broker's pkce.ts for the same reasoning server-side.\nimport type { Credentials } from \"google-auth-library\";\nimport { fetchWithTimeout } from \"./fetch.js\";\nimport { errorMessage, isRecord, ONE_SECOND_MS } from \"./util.js\";\n\nconst DEFAULT_BROKER_BASE_URL = \"https://asia-northeast1-mulmoserver.cloudfunctions.net\";\nconst BROKER_TIMEOUT_MS = 20 * ONE_SECOND_MS;\n\n// Trailing slashes are stripped by hand: `/\\/+$/` backtracks super-linearly on\n// a long run of slashes, which lint rejects.\nconst withoutTrailingSlashes = (url: string): string => {\n let end = url.length;\n while (end > 0 && url[end - 1] === \"/\") end -= 1;\n return url.slice(0, end);\n};\n\n/** `MULMO_GOOGLE_BROKER_URL` lets a fork / staging deploy point elsewhere\n * without a code change; unset means the shipped broker. */\nexport const brokerBaseUrl = (override: string | undefined = process.env.MULMO_GOOGLE_BROKER_URL): string =>\n withoutTrailingSlashes(override ?? DEFAULT_BROKER_BASE_URL);\n\nexport interface BrokerStartResponse {\n authUrl: string;\n state: string;\n}\n\nconst LOOPBACK_HOSTNAMES = new Set([\"127.0.0.1\", \"localhost\", \"::1\", \"[::1]\"]);\n\n/** Codes, PKCE verifiers and refresh tokens travel to the broker, so cleartext\n * is refused outright — an override pointed at `http://…` would put them on\n * the wire. Loopback stays allowed: that is where tests stub the broker, and\n * it never leaves the machine. */\nconst assertSecureBrokerUrl = (url: string): void => {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(`invalid Google sign-in service URL: ${url}`);\n }\n if (parsed.protocol === \"https:\") return;\n if (parsed.protocol === \"http:\" && LOOPBACK_HOSTNAMES.has(parsed.hostname)) return;\n throw new Error(`the Google sign-in service must be reached over HTTPS (got ${parsed.protocol}//${parsed.host})`);\n};\n\nconst brokerFetch = async (url: string, init: { method?: string; body?: string } = {}): Promise<unknown> => {\n assertSecureBrokerUrl(url);\n let response: Response;\n try {\n response = await fetchWithTimeout(url, {\n ...init,\n timeoutMs: BROKER_TIMEOUT_MS,\n headers: { \"Content-Type\": \"application/json\" },\n });\n } catch (err) {\n throw new Error(`Google sign-in service unreachable — check the network connection and retry (${errorMessage(err)})`);\n }\n if (!response.ok) {\n // The broker answers deliberately opaque errors (it must not help probe\n // codes / refresh tokens), so there is nothing more specific to surface.\n throw new Error(`Google sign-in service returned HTTP ${response.status}`);\n }\n try {\n return await response.json();\n } catch {\n // A proxy / captive portal answering 200 with HTML would otherwise surface\n // as a raw SyntaxError the agent can't act on.\n throw new Error(\"Google sign-in service returned a malformed response\");\n }\n};\n\nconst GOOGLE_CONSENT_HOST = \"accounts.google.com\";\n\n/** The returned URL is opened in the user's browser, so it must be Google's\n * own consent page and nothing else — a mis-pointed or hostile broker (the\n * base URL is overridable) could otherwise hand back a phishing page wearing\n * a sign-in flow. Cheap to assert, and Google's endpoint host is fixed. */\nconst assertGoogleConsentUrl = (authUrl: string): void => {\n let parsed: URL;\n try {\n parsed = new URL(authUrl);\n } catch {\n throw new Error(\"Google sign-in service returned an unusable authorization URL\");\n }\n if (parsed.protocol !== \"https:\" || parsed.hostname !== GOOGLE_CONSENT_HOST) {\n throw new Error(`Google sign-in service returned an authorization URL that is not Google's consent page (${parsed.protocol}//${parsed.host})`);\n }\n};\n\nexport async function brokerStart(port: number, codeChallenge: string, baseUrl = brokerBaseUrl()): Promise<BrokerStartResponse> {\n const params = new URLSearchParams({ port: String(port), code_challenge: codeChallenge });\n const payload = await brokerFetch(`${baseUrl}/googleOAuthStart?${params.toString()}`);\n const record = isRecord(payload) ? payload : {};\n if (typeof record.auth_url !== \"string\" || typeof record.state !== \"string\") {\n throw new Error(\"Google sign-in service returned an unexpected response (missing auth_url / state)\");\n }\n assertGoogleConsentUrl(record.auth_url);\n return { authUrl: record.auth_url, state: record.state };\n}\n\nconst toCredentials = (payload: unknown, existingRefreshToken?: string): Credentials => {\n const record = isRecord(payload) ? payload : {};\n if (typeof record.access_token !== \"string\") {\n throw new Error(\"Google sign-in service returned no access token\");\n }\n // The refresh endpoint does not echo the refresh_token back — keep the one we\n // already hold so a renewal can't silently unlink the account.\n const refreshToken = typeof record.refresh_token === \"string\" ? record.refresh_token : existingRefreshToken;\n return {\n access_token: record.access_token,\n ...(refreshToken ? { refresh_token: refreshToken } : {}),\n ...(typeof record.expiry_date === \"number\" ? { expiry_date: record.expiry_date } : {}),\n };\n};\n\nexport async function brokerExchange(input: { code: string; state: string; codeVerifier: string }, baseUrl = brokerBaseUrl()): Promise<Credentials> {\n const payload = await brokerFetch(`${baseUrl}/googleOAuthExchange`, {\n method: \"POST\",\n body: JSON.stringify({ code: input.code, state: input.state, code_verifier: input.codeVerifier }),\n });\n const credentials = toCredentials(payload);\n if (!credentials.refresh_token) {\n throw new Error(\"Google returned no refresh token — remove this app under Google Account → Security → Third-party access, then retry\");\n }\n return credentials;\n}\n\nexport async function brokerRefresh(refreshToken: string, baseUrl = brokerBaseUrl()): Promise<Credentials> {\n const payload = await brokerFetch(`${baseUrl}/googleOAuthRefresh`, {\n method: \"POST\",\n body: JSON.stringify({ refresh_token: refreshToken }),\n });\n return toCredentials(payload, refreshToken);\n}\n","// Google OAuth for the host machine, independent of Firebase Auth (which\n// discards refresh tokens). Two ways to reach a linked account:\n//\n// - LOCAL: the user dropped their own desktop-app client JSON in\n// `~/.secrets/`. Everything (consent, exchange, refresh) happens on this\n// machine — nothing but Google is contacted.\n// - BROKER (the default): no client JSON, so the mulmoserver broker applies\n// the client secret for the exchange / refresh it cannot be done without.\n// Tokens still only ever live here. See broker.ts.\n//\n// Either way the loopback listener, the PKCE verifier, and the token file are\n// this machine's. `issuedVia` on the stored token records which path minted it\n// so renewals take the matching one.\nimport { randomBytes } from \"node:crypto\";\nimport http from \"node:http\";\nimport { CodeChallengeMethod, OAuth2Client, type Credentials } from \"google-auth-library\";\nimport { brokerExchange, brokerRefresh, brokerStart } from \"./broker.js\";\nimport { log } from \"./host.js\";\nimport { errorMessage, ONE_MINUTE_MS, ONE_SECOND_MS } from \"./util.js\";\nimport { fetchWithTimeout } from \"./fetch.js\";\nimport { clientSecretPresence, loadClientSecret, type InstalledClientSecret } from \"./clientSecret.js\";\nimport { deleteGoogleTokens, loadGoogleTokens, saveGoogleTokens, type IssuedVia } from \"./tokenStore.js\";\n\nexport const GOOGLE_CALENDAR_SCOPE = \"https://www.googleapis.com/auth/calendar.events\";\n/** Minimal read scope for the calendar list + per-calendar colours.\n * `calendar.events` already covers reading/writing events on any calendar, but\n * discovering WHICH calendars the user has (CalendarList.list) needs a\n * calendar-list read scope — this is the narrowest one that grants it. Kept in\n * step with the broker's consent scopes so local- and broker-linked accounts\n * grant the same set. */\nexport const GOOGLE_CALENDARLIST_SCOPE = \"https://www.googleapis.com/auth/calendar.calendarlist.readonly\";\nexport const GOOGLE_TASKS_SCOPE = \"https://www.googleapis.com/auth/tasks\";\nexport const GOOGLE_DRIVE_FILE_SCOPE = \"https://www.googleapis.com/auth/drive.file\";\n/** Requested at consent as one set — matches the scopes registered on the\n * OAuth consent screen, so a single re-link covers every supported API. */\nexport const GOOGLE_SCOPES = [GOOGLE_CALENDAR_SCOPE, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_TASKS_SCOPE, GOOGLE_DRIVE_FILE_SCOPE];\nconst CALLBACK_PATH = \"/oauth2callback\";\nconst AUTH_TIMEOUT_MS = 5 * ONE_MINUTE_MS;\nconst STATE_BYTES = 16;\n/** Renew a minute early so a call can't start with a token that expires\n * mid-flight. */\nconst EXPIRY_MARGIN_MS = ONE_MINUTE_MS;\n\nexport interface AuthorizeGoogleOptions {\n home?: string;\n /** Called with the consent URL; open it in a browser (and/or print it). */\n onAuthUrl?: (url: string) => void;\n timeoutMs?: number;\n}\n\nconst createClient = (secret: InstalledClientSecret, redirectUri?: string): OAuth2Client =>\n new OAuth2Client({ clientId: secret.client_id, clientSecret: secret.client_secret, redirectUri });\n\nconst persistRotatedTokens = (client: OAuth2Client, home?: string): void => {\n client.on(\"tokens\", (tokens) => {\n saveGoogleTokens(tokens, home).catch((err: unknown) => {\n log.error(\"google\", \"failed to persist rotated tokens\", { error: String(err) });\n });\n });\n};\n\nconst REVOKED_GRANT_MESSAGE = \"could not obtain a Google access token — the grant may have been revoked; re-link the account\";\n\nconst localAccessToken = async (saved: Credentials, home?: string): Promise<string> => {\n const presence = await clientSecretPresence(home);\n // Two desktop clients: the token is still renewable — the engine just can't\n // tell which client minted it. The fix is to remove the duplicate, so raise\n // the loader's own wording rather than sending the user through a re-link.\n if (presence === \"ambiguous\") await loadClientSecret(home);\n // No client at all: the token is bound to the client_id that minted it, so\n // nothing — not even the broker — can renew it. Re-linking is the only way\n // out; the loader's \"no desktop client\" wording would read like a setup\n // problem instead.\n if (presence === \"missing\") {\n throw new Error(\n \"the saved Google link was created with an OAuth client that is no longer configured on this host — ask the user to link their Google account again in this app's settings\",\n );\n }\n const client = createClient(await loadClientSecret(home));\n client.setCredentials(saved);\n persistRotatedTokens(client, home);\n const { token } = await client.getAccessToken();\n if (!token) throw new Error(REVOKED_GRANT_MESSAGE);\n return token;\n};\n\n// The broker mints access tokens because only it holds the client secret. The\n// refreshed token is written back so the next call can reuse it until expiry\n// instead of hitting the broker every time.\nconst brokerAccessToken = async (saved: Credentials, home?: string): Promise<string> => {\n if (typeof saved.expiry_date === \"number\" && saved.access_token && saved.expiry_date - EXPIRY_MARGIN_MS > Date.now()) {\n return saved.access_token;\n }\n const refreshed = await brokerRefresh(saved.refresh_token ?? \"\");\n if (!refreshed.access_token) throw new Error(REVOKED_GRANT_MESSAGE);\n await saveGoogleTokens(refreshed, home);\n return refreshed.access_token;\n};\n\nexport async function getGoogleAccessToken(home?: string): Promise<string> {\n const saved = await loadGoogleTokens(home);\n if (!saved?.refresh_token) {\n // Host-neutral wording — this engine ships to multiple hosts whose link\n // flows differ (#2128); each host's own help carries the specific steps.\n throw new Error(\"Google account not linked on this host — ask the user to link their Google account in this app's settings, then retry\");\n }\n // Tokens written before the broker existed carry no marker; they were all\n // minted from a local client, so that stays the default.\n return saved.issuedVia === \"broker\" ? await brokerAccessToken(saved, home) : await localAccessToken(saved, home);\n}\n\nconst REVOKE_URL = \"https://oauth2.googleapis.com/revoke\";\n\n/** The revoke POST, injectable for tests. */\nexport type RevokeFetch = typeof fetchWithTimeout;\n\n/** Revoke the grant at Google (best-effort) and delete the local token file.\n * Revoke failures are logged but never block the local delete — Google may\n * already consider the token invalid, and keeping the file would leave the\n * user unable to unlink. */\nexport async function unlinkGoogle(home?: string, revokeFetch: RevokeFetch = fetchWithTimeout): Promise<void> {\n const saved = await loadGoogleTokens(home);\n const token = saved?.refresh_token ?? saved?.access_token;\n if (token) {\n try {\n const response = await revokeFetch(REVOKE_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({ token }).toString(),\n });\n if (!response.ok) log.warn(\"google\", \"token revoke returned non-ok\", { status: response.status });\n } catch (err) {\n log.warn(\"google\", \"token revoke failed, deleting local tokens anyway\", { error: errorMessage(err) });\n }\n }\n await deleteGoogleTokens(home);\n}\n\nconst startLoopbackServer = (): Promise<{ server: http.Server; port: number }> =>\n new Promise((resolve, reject) => {\n const server = http.createServer();\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n reject(new Error(\"loopback server has no port\"));\n return;\n }\n resolve({ server, port: address.port });\n });\n });\n\n// State is validated before error/code — a callback that can't prove it\n// belongs to this flow must not influence it (its `error` text would\n// otherwise reach the terminal attacker-controlled).\nconst authCodeFromCallback = (url: URL, expectedState: string): string => {\n if (url.searchParams.get(\"state\") !== expectedState) throw new Error(\"OAuth state mismatch — possible CSRF, aborting\");\n const error = url.searchParams.get(\"error\");\n if (error) throw new Error(`Google authorization failed: ${error}`);\n const code = url.searchParams.get(\"code\");\n if (!code) throw new Error(\"authorization callback carried no code\");\n return code;\n};\n\nconst respondHtml = (res: http.ServerResponse, status: number, message: string): void => {\n res.writeHead(status, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(`<html><body><h3>${message}</h3></body></html>`);\n};\n\nexport const waitForAuthCode = (server: http.Server, expectedState: string, timeoutMs: number): Promise<string> =>\n new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(`authorization timed out after ${timeoutMs / ONE_SECOND_MS}s`)), timeoutMs);\n server.on(\"request\", (req, res) => {\n const url = new URL(req.url ?? \"/\", \"http://127.0.0.1\");\n if (url.pathname !== CALLBACK_PATH) {\n res.writeHead(404);\n res.end();\n return;\n }\n // A wrong-state request is not our callback (drive-by localhost probe\n // or stale tab) — answer it but keep waiting for the real redirect, so\n // an unauthenticated request can't abort the pending flow.\n if (url.searchParams.get(\"state\") !== expectedState) {\n respondHtml(res, 400, \"Invalid authorization callback. You can close this tab.\");\n return;\n }\n clearTimeout(timer);\n try {\n const code = authCodeFromCallback(url, expectedState);\n respondHtml(res, 200, \"Authorization complete — you can close this tab.\");\n resolve(code);\n } catch (err) {\n // Static text only — the failure detail echoes query-string content,\n // which must not be reflected into HTML. The CLI prints the detail.\n respondHtml(res, 400, \"Authorization failed — see the terminal for details. You can close this tab.\");\n reject(err);\n }\n });\n });\n\n// `access_type: offline` + `prompt: consent` force Google to return a refresh\n// token on every run (repeat consents otherwise omit it).\nconst buildConsentUrl = (client: OAuth2Client, codeChallenge: string, state: string): string =>\n client.generateAuthUrl({\n access_type: \"offline\",\n prompt: \"consent\",\n scope: GOOGLE_SCOPES,\n code_challenge_method: CodeChallengeMethod.S256,\n code_challenge: codeChallenge,\n state,\n });\n\n// PKCE material is generated by an OAuth2Client with no credentials — the\n// helper is pure crypto, and in broker mode there is no client to construct.\nconst generatePkce = async (): Promise<{ codeVerifier: string; codeChallenge: string }> => {\n const { codeVerifier, codeChallenge } = await new OAuth2Client().generateCodeVerifierAsync();\n if (!codeChallenge) throw new Error(\"failed to derive a PKCE code challenge\");\n return { codeVerifier, codeChallenge };\n};\n\nconst authorizeWithLocalClient = async (\n secret: InstalledClientSecret,\n server: http.Server,\n port: number,\n opts: AuthorizeGoogleOptions,\n): Promise<Credentials> => {\n const client = createClient(secret, `http://127.0.0.1:${port}${CALLBACK_PATH}`);\n const { codeVerifier, codeChallenge } = await client.generateCodeVerifierAsync();\n if (!codeChallenge) throw new Error(\"failed to derive a PKCE code challenge\");\n const state = randomBytes(STATE_BYTES).toString(\"hex\");\n opts.onAuthUrl?.(buildConsentUrl(client, codeChallenge, state));\n const code = await waitForAuthCode(server, state, opts.timeoutMs ?? AUTH_TIMEOUT_MS);\n const { tokens } = await client.getToken({ code, codeVerifier });\n if (!tokens.refresh_token) {\n throw new Error(\"Google returned no refresh token — remove this app under Google Account → Security → Third-party access, then retry\");\n }\n return tokens;\n};\n\n// The broker signs `state` with a key it never releases, so it — not this\n// host — builds the authorization URL. `state` then round-trips through the\n// browser and comes back to our loopback, where waitForAuthCode matches it.\nconst authorizeWithBroker = async (server: http.Server, port: number, opts: AuthorizeGoogleOptions): Promise<Credentials> => {\n const { codeVerifier, codeChallenge } = await generatePkce();\n const { authUrl, state } = await brokerStart(port, codeChallenge);\n opts.onAuthUrl?.(authUrl);\n const code = await waitForAuthCode(server, state, opts.timeoutMs ?? AUTH_TIMEOUT_MS);\n return await brokerExchange({ code, state, codeVerifier });\n};\n\nexport async function authorizeGoogle(opts: AuthorizeGoogleOptions = {}): Promise<Credentials> {\n // A user-supplied client wins: it keeps the whole flow on this machine, and\n // silently preferring the broker would ignore a deliberate setup. Two of\n // them is unresolvable rather than a reason to fall back — the user meant to\n // use one of theirs, and a broker link would quietly not be it.\n const presence = await clientSecretPresence(opts.home);\n if (presence === \"ambiguous\") {\n // Same wording the loader raises, so the CLI and the settings UI (which\n // disables linking in this state) agree on the fix.\n await loadClientSecret(opts.home);\n }\n const useLocalClient = presence === \"found\";\n const { server, port } = await startLoopbackServer();\n try {\n const issuedVia: IssuedVia = useLocalClient ? \"local\" : \"broker\";\n const tokens = useLocalClient\n ? await authorizeWithLocalClient(await loadClientSecret(opts.home), server, port, opts)\n : await authorizeWithBroker(server, port, opts);\n return await saveGoogleTokens({ ...tokens, issuedVia }, opts.home);\n } finally {\n server.close();\n }\n}\n","// In-flight manager for the settings-UI OAuth flow. authorizeGoogle()\n// resolves only after the user finishes the browser consent, so the HTTP\n// layer starts it in the background, returns the consent URL immediately,\n// and reports progress via status polling. One flow at a time — the guard\n// is the in-flight start promise itself (set synchronously before any\n// await), so concurrent authorize requests share one flow instead of\n// spawning parallel loopback listeners.\nimport { log } from \"./host.js\";\nimport { errorMessage } from \"./util.js\";\nimport { authorizeGoogle } from \"./auth.js\";\n\nexport interface GoogleAuthFlowStatus {\n pending: boolean;\n lastError: string | null;\n}\n\nexport interface GoogleAuthFlow {\n start: () => Promise<{ authUrl: string }>;\n status: () => GoogleAuthFlowStatus;\n}\n\nexport const createGoogleAuthFlow = (authorize: typeof authorizeGoogle): GoogleAuthFlow => {\n let inFlightStart: Promise<{ authUrl: string }> | null = null;\n let flowRunning = false;\n let lastError: string | null = null;\n\n const launchFlow = (): Promise<{ authUrl: string }> =>\n new Promise((resolve, reject) => {\n flowRunning = true;\n authorize({\n onAuthUrl: (url) => resolve({ authUrl: url }),\n })\n .then(() => log.info(\"google\", \"authorize flow completed\"))\n .catch((err: unknown) => {\n lastError = errorMessage(err);\n log.warn(\"google\", \"authorize flow failed\", { error: lastError });\n // No-op when the URL already resolved; covers pre-URL failures\n // (missing client secret, port bind error).\n reject(err instanceof Error ? err : new Error(String(err)));\n })\n .finally(() => {\n flowRunning = false;\n inFlightStart = null;\n });\n });\n\n const start = (): Promise<{ authUrl: string }> => {\n if (inFlightStart) return inFlightStart;\n lastError = null;\n inFlightStart = launchFlow();\n return inFlightStart;\n };\n\n const status = (): GoogleAuthFlowStatus => ({ pending: flowRunning, lastError });\n\n return { start, status };\n};\n\nexport const googleAuthFlow = createGoogleAuthFlow(authorizeGoogle);\n","// Shared REST plumbing for the Google APIs this engine wraps (Calendar,\n// Tasks, Drive). Plain fetch instead of the `googleapis` SDK — a handful of\n// endpoints don't justify the dependency (see\n// plans/done/feat-google-oauth-calendar.md).\nimport { errorMessage, isRecord, ONE_SECOND_MS, truncate } from \"./util.js\";\nimport { fetchWithTimeout } from \"./fetch.js\";\n\nexport const GOOGLE_API_TIMEOUT_MS = 30 * ONE_SECOND_MS;\nconst ERROR_BODY_MAX_CHARS = 300;\nconst HTTP_FORBIDDEN = 403;\nexport const DEFAULT_LIST_MAX_RESULTS = 10;\nexport const MAX_LIST_RESULTS = 50;\n\n/** 403 usually means the API is not enabled for the user's Cloud project —\n * name the API so the agent's recovery guidance can be specific. */\nexport const googleApiError = (apiLabel: string, status: number, body: string): Error => {\n const hint = status === HTTP_FORBIDDEN ? ` (is the ${apiLabel} enabled for the Cloud project?)` : \"\";\n const detail = body ? ` — ${truncate(body, ERROR_BODY_MAX_CHARS)}` : \"\";\n return new Error(`${apiLabel}: HTTP ${status}${hint}${detail}`);\n};\n\nexport interface GoogleRequestInit {\n method?: string;\n body?: string;\n /** Overrides the default JSON content type (multipart upload, …). */\n contentType?: string;\n /** Response is not JSON (Drive media download) — return the raw text. */\n expectText?: boolean;\n}\n\nexport async function googleRequest(apiLabel: string, accessToken: string, url: string, init: GoogleRequestInit = {}): Promise<unknown> {\n const { contentType = \"application/json\", expectText = false, ...rest } = init;\n const response = await fetchWithTimeout(url, {\n ...rest,\n timeoutMs: GOOGLE_API_TIMEOUT_MS,\n headers: { Authorization: `Bearer ${accessToken}`, \"Content-Type\": contentType },\n });\n if (!response.ok) {\n const body = await response.text().catch((err: unknown) => errorMessage(err));\n throw googleApiError(apiLabel, response.status, body);\n }\n if (expectText) return await response.text();\n // 204 (Drive delete, …) has no body to parse.\n if (response.status === 204) return {};\n return await response.json();\n}\n\nexport const stringField = (record: Record<string, unknown>, key: string): string => (typeof record[key] === \"string\" ? record[key] : \"\");\n\nexport const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});\n\nexport const itemsOf = (value: unknown): unknown[] => {\n const record = asRecord(value);\n return Array.isArray(record.items) ? record.items : [];\n};\n","// Google Calendar v3 REST calls. Events read/write against any calendar the\n// user can access (default: their primary); the calendar list and colour\n// palette let callers show non-primary calendars and their colours.\nimport { asRecord, googleApiError, googleRequest, itemsOf, stringField, DEFAULT_LIST_MAX_RESULTS } from \"./apiClient.js\";\nimport { isRecord } from \"./util.js\";\n\nconst CALENDAR_BASE_URL = \"https://www.googleapis.com/calendar/v3\";\nconst CALENDAR_API_LABEL = \"Google Calendar API\";\nconst DEFAULT_CALENDAR_ID = \"primary\";\n// CalendarList.list is paginated; page through nextPageToken so an account\n// with many subscribed calendars isn't silently truncated. The page cap is a\n// runaway guard — 250 * 40 = 10k calendars, far beyond any real account.\nconst CALENDAR_LIST_PAGE_SIZE = 250;\nconst MAX_CALENDAR_LIST_PAGES = 40;\n\n// `||` (not `??`) so an empty-string calendarId also falls back to primary\n// instead of building a malformed `/calendars//events` URL.\nconst eventsUrl = (calendarId: string | undefined): string => `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(calendarId || DEFAULT_CALENDAR_ID)}/events`;\n\nexport interface CalendarEventInput {\n summary: string;\n startDateTime: string;\n endDateTime: string;\n description?: string;\n /** Calendar to create the event on; defaults to the user's primary. */\n calendarId?: string;\n /** Event colour (Google event palette id \"1\"..\"11\"); omit to inherit the calendar's colour. */\n colorId?: string;\n}\n\nexport interface ListEventsInput {\n timeMin?: string;\n maxResults?: number;\n /** Calendar to read; defaults to the user's primary. */\n calendarId?: string;\n}\n\nexport interface CalendarEventSummary {\n id: string;\n summary: string;\n start: string;\n end: string;\n htmlLink: string;\n status: string;\n /** Google event palette id (\"1\"..\"11\"), or \"\" when the event inherits the calendar's colour. */\n colorId: string;\n}\n\nexport interface CalendarSummary {\n id: string;\n summary: string;\n description: string;\n /** True only for the user's primary calendar. */\n primary: boolean;\n accessRole: string;\n backgroundColor: string;\n foregroundColor: string;\n /** Calendar palette id backing background/foregroundColor. */\n colorId: string;\n}\n\nexport interface CalendarColorEntry {\n background: string;\n foreground: string;\n}\n\n/** Palettes from Google's `/colors` endpoint: `event` maps an event's `colorId`\n * and `calendar` maps a calendar's `colorId` to hex background/foreground. */\nexport interface CalendarColors {\n event: Record<string, CalendarColorEntry>;\n calendar: Record<string, CalendarColorEntry>;\n}\n\n// All-day events carry `date`, timed events carry `dateTime`.\nconst eventTime = (value: unknown): string => {\n if (!isRecord(value)) return \"\";\n if (typeof value.dateTime === \"string\") return value.dateTime;\n if (typeof value.date === \"string\") return value.date;\n return \"\";\n};\n\nexport const toEventSummary = (value: unknown): CalendarEventSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n summary: stringField(record, \"summary\"),\n start: eventTime(record.start),\n end: eventTime(record.end),\n htmlLink: stringField(record, \"htmlLink\"),\n status: stringField(record, \"status\"),\n colorId: stringField(record, \"colorId\"),\n };\n};\n\nexport const toCalendarSummary = (value: unknown): CalendarSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n summary: stringField(record, \"summary\"),\n description: stringField(record, \"description\"),\n primary: record.primary === true,\n accessRole: stringField(record, \"accessRole\"),\n backgroundColor: stringField(record, \"backgroundColor\"),\n foregroundColor: stringField(record, \"foregroundColor\"),\n colorId: stringField(record, \"colorId\"),\n };\n};\n\nconst toColorMap = (value: unknown): Record<string, CalendarColorEntry> => {\n const entries = Object.entries(asRecord(value)).map(([colorId, entry]): [string, CalendarColorEntry] => {\n const record = asRecord(entry);\n return [colorId, { background: stringField(record, \"background\"), foreground: stringField(record, \"foreground\") }];\n });\n return Object.fromEntries(entries);\n};\n\n/** Kept as a named export for the existing unit tests / callers; the shared\n * helper now carries the wording. */\nexport const calendarApiError = (status: number, body: string): Error => googleApiError(CALENDAR_API_LABEL, status, body);\n\nexport async function createCalendarEvent(accessToken: string, input: CalendarEventInput): Promise<CalendarEventSummary> {\n const body = {\n summary: input.summary,\n description: input.description,\n start: { dateTime: input.startDateTime },\n end: { dateTime: input.endDateTime },\n ...(input.colorId ? { colorId: input.colorId } : {}),\n };\n const created = await googleRequest(CALENDAR_API_LABEL, accessToken, eventsUrl(input.calendarId), { method: \"POST\", body: JSON.stringify(body) });\n return toEventSummary(created);\n}\n\nexport async function listCalendarEvents(accessToken: string, input: ListEventsInput = {}): Promise<CalendarEventSummary[]> {\n const params = new URLSearchParams({\n timeMin: input.timeMin ?? new Date().toISOString(),\n maxResults: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n singleEvents: \"true\",\n orderBy: \"startTime\",\n });\n const listed = await googleRequest(CALENDAR_API_LABEL, accessToken, `${eventsUrl(input.calendarId)}?${params.toString()}`);\n return itemsOf(listed).map(toEventSummary);\n}\n\nexport interface CalendarListPage {\n items: unknown[];\n nextPageToken?: string;\n}\n\n/** Pagination loop for CalendarList.list, extracted so it can be tested without\n * network. Stops at the last page (no token) or the runaway page cap. */\nexport async function collectCalendarPages(\n fetchPage: (pageToken?: string) => Promise<CalendarListPage>,\n maxPages = MAX_CALENDAR_LIST_PAGES,\n): Promise<CalendarSummary[]> {\n const calendars: CalendarSummary[] = [];\n let pageToken: string | undefined;\n for (let page = 0; page < maxPages; page += 1) {\n const { items, nextPageToken } = await fetchPage(pageToken);\n calendars.push(...items.map(toCalendarSummary));\n if (!nextPageToken) break;\n pageToken = nextPageToken;\n }\n return calendars;\n}\n\n/** The calendars the user has added/subscribed to (primary + secondary +\n * shared), each with its id, name and colour, following pagination. Needs the\n * calendar-list read scope (GOOGLE_CALENDARLIST_SCOPE). */\nexport async function listCalendars(accessToken: string): Promise<CalendarSummary[]> {\n return collectCalendarPages(async (pageToken) => {\n const params = new URLSearchParams({ maxResults: String(CALENDAR_LIST_PAGE_SIZE) });\n if (pageToken) params.set(\"pageToken\", pageToken);\n const payload = await googleRequest(CALENDAR_API_LABEL, accessToken, `${CALENDAR_BASE_URL}/users/me/calendarList?${params.toString()}`);\n const record = asRecord(payload);\n return { items: itemsOf(payload), nextPageToken: typeof record.nextPageToken === \"string\" ? record.nextPageToken : undefined };\n });\n}\n\n/** Resolve a `colorId` (on an event or calendar) to its hex background/foreground. */\nexport async function getCalendarColors(accessToken: string): Promise<CalendarColors> {\n const payload = await googleRequest(CALENDAR_API_LABEL, accessToken, `${CALENDAR_BASE_URL}/colors`);\n const record = asRecord(payload);\n return { event: toColorMap(record.event), calendar: toColorMap(record.calendar) };\n}\n","// Google Tasks v1 REST calls. `@default` is Google's alias for the user's\n// default task list, so callers can stay list-agnostic.\nimport { asRecord, googleRequest, itemsOf, stringField, DEFAULT_LIST_MAX_RESULTS } from \"./apiClient.js\";\n\nconst TASKS_BASE_URL = \"https://tasks.googleapis.com/tasks/v1\";\nconst TASKS_API_LABEL = \"Google Tasks API\";\nconst DEFAULT_TASK_LIST_ID = \"@default\";\nconst TASK_STATUS_COMPLETED = \"completed\";\nconst MAX_TASK_LISTS = 50;\n\nexport interface TaskListSummary {\n id: string;\n title: string;\n}\n\nexport interface TaskSummary {\n id: string;\n title: string;\n status: string;\n due: string;\n notes: string;\n}\n\nexport interface ListTasksInput {\n taskListId?: string;\n maxResults?: number;\n showCompleted?: boolean;\n}\n\nexport interface CreateTaskInput {\n title: string;\n notes?: string;\n /** RFC3339. Google stores a DATE only — the time part is recorded but\n * ignored by the UI, so callers should not promise time-of-day fidelity. */\n due?: string;\n taskListId?: string;\n}\n\nexport interface CompleteTaskInput {\n taskId: string;\n taskListId?: string;\n}\n\nexport const toTaskListSummary = (value: unknown): TaskListSummary => {\n const record = asRecord(value);\n return { id: stringField(record, \"id\"), title: stringField(record, \"title\") };\n};\n\nexport const toTaskSummary = (value: unknown): TaskSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n title: stringField(record, \"title\"),\n status: stringField(record, \"status\"),\n due: stringField(record, \"due\"),\n notes: stringField(record, \"notes\"),\n };\n};\n\nconst tasksUrl = (taskListId: string | undefined, suffix = \"\"): string =>\n `${TASKS_BASE_URL}/lists/${encodeURIComponent(taskListId ?? DEFAULT_TASK_LIST_ID)}/tasks${suffix}`;\n\nexport async function listTaskLists(accessToken: string): Promise<TaskListSummary[]> {\n const listed = await googleRequest(TASKS_API_LABEL, accessToken, `${TASKS_BASE_URL}/users/@me/lists?maxResults=${MAX_TASK_LISTS}`);\n return itemsOf(listed).map(toTaskListSummary);\n}\n\nexport async function listTasks(accessToken: string, input: ListTasksInput = {}): Promise<TaskSummary[]> {\n const params = new URLSearchParams({\n maxResults: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n showCompleted: String(input.showCompleted ?? false),\n });\n const listed = await googleRequest(TASKS_API_LABEL, accessToken, `${tasksUrl(input.taskListId)}?${params.toString()}`);\n return itemsOf(listed).map(toTaskSummary);\n}\n\nexport async function createTask(accessToken: string, input: CreateTaskInput): Promise<TaskSummary> {\n const body = { title: input.title, notes: input.notes, due: input.due };\n const created = await googleRequest(TASKS_API_LABEL, accessToken, tasksUrl(input.taskListId), { method: \"POST\", body: JSON.stringify(body) });\n return toTaskSummary(created);\n}\n\nexport async function completeTask(accessToken: string, input: CompleteTaskInput): Promise<TaskSummary> {\n // PATCH keeps the rest of the task intact — a PUT would need the full body\n // and would silently drop fields the caller never read.\n const url = tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`);\n const updated = await googleRequest(TASKS_API_LABEL, accessToken, url, {\n method: \"PATCH\",\n body: JSON.stringify({ status: TASK_STATUS_COMPLETED }),\n });\n return toTaskSummary(updated);\n}\n\nexport async function deleteTask(accessToken: string, input: CompleteTaskInput): Promise<void> {\n const url = tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`);\n await googleRequest(TASKS_API_LABEL, accessToken, url, { method: \"DELETE\" });\n}\n","// Google Drive v3 REST calls under the `drive.file` scope — the app only\n// ever sees files IT created, never the user's wider Drive. That narrow\n// scope is why this is non-sensitive and needs no Google verification\n// review; keep every call inside it.\nimport { randomBytes } from \"node:crypto\";\nimport { asRecord, googleRequest, stringField, DEFAULT_LIST_MAX_RESULTS } from \"./apiClient.js\";\n\nconst DRIVE_FILES_URL = \"https://www.googleapis.com/drive/v3/files\";\nconst DRIVE_UPLOAD_URL = \"https://www.googleapis.com/upload/drive/v3/files\";\nconst DRIVE_API_LABEL = \"Google Drive API\";\nconst DEFAULT_MIME_TYPE = \"text/plain\";\nconst FILE_FIELDS = \"id,name,mimeType,webViewLink,modifiedTime\";\n// Reading a huge blob into a tool result would blow the context window; the\n// tool surface is for app-created text documents, not media.\nconst MAX_READ_CHARS = 100_000;\nconst TEXT_MIME_PREFIXES = [\"text/\", \"application/json\", \"application/xml\", \"application/javascript\"];\n\nexport interface DriveFileSummary {\n id: string;\n name: string;\n mimeType: string;\n webViewLink: string;\n modifiedTime: string;\n}\n\nexport interface ListDriveFilesInput {\n maxResults?: number;\n}\n\nexport interface CreateDriveFileInput {\n name: string;\n content: string;\n mimeType?: string;\n}\n\nexport interface ReadDriveFileInput {\n fileId: string;\n}\n\nexport const toDriveFileSummary = (value: unknown): DriveFileSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n name: stringField(record, \"name\"),\n mimeType: stringField(record, \"mimeType\"),\n webViewLink: stringField(record, \"webViewLink\"),\n modifiedTime: stringField(record, \"modifiedTime\"),\n };\n};\n\nexport const isTextMimeType = (mimeType: string): boolean => TEXT_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix));\n\nexport async function listDriveFiles(accessToken: string, input: ListDriveFilesInput = {}): Promise<DriveFileSummary[]> {\n const params = new URLSearchParams({\n pageSize: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n fields: `files(${FILE_FIELDS})`,\n orderBy: \"modifiedTime desc\",\n });\n const listed = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}?${params.toString()}`);\n const record = asRecord(listed);\n const files = Array.isArray(record.files) ? record.files : [];\n return files.map(toDriveFileSummary);\n}\n\n// Multipart upload: metadata part + media part in one request. Built by hand\n// because the `googleapis` SDK is the only alternative and this is the sole\n// multipart call in the engine.\nconst BOUNDARY_BYTES = 16;\n\n/** Fresh per request: a fixed boundary appearing inside file content would\n * split the payload at the wrong place, so Drive would store a truncated or\n * mangled file. Random 128 bits makes an accidental — or crafted — collision\n * infeasible; `newMultipartBoundary` is re-derived until it is absent from\n * the body it must delimit. */\nconst newMultipartBoundary = (): string => `mulmo-drive-${randomBytes(BOUNDARY_BYTES).toString(\"hex\")}`;\n\n// RFC 2045 token + parameters: no CR/LF (header injection into the part\n// headers) and no quotes/semicolons that would let a crafted value forge\n// extra headers or parameters.\nconst MIME_TYPE_RE = /^[A-Za-z0-9!#$&^_.+-]+\\/[A-Za-z0-9!#$&^_.+-]+$/;\n\nexport const assertSafeMimeType = (mimeType: string): string => {\n if (!MIME_TYPE_RE.test(mimeType)) throw new Error(`Google Drive API: invalid mimeType '${mimeType}' — expected a plain type/subtype such as text/plain`);\n return mimeType;\n};\n\nexport const buildMultipartBody = (metadata: Record<string, string>, content: string, mimeType: string, boundary: string): string =>\n [\n `--${boundary}`,\n \"Content-Type: application/json; charset=UTF-8\",\n \"\",\n JSON.stringify(metadata),\n `--${boundary}`,\n `Content-Type: ${mimeType}`,\n \"\",\n content,\n `--${boundary}--`,\n \"\",\n ].join(\"\\r\\n\");\n\n/** A boundary that appears nowhere in the parts it delimits. */\nexport const pickBoundary = (parts: string[], generate: () => string = newMultipartBoundary): string => {\n const collides = (candidate: string): boolean => parts.some((part) => part.includes(candidate));\n let boundary = generate();\n while (collides(boundary)) boundary = generate();\n return boundary;\n};\n\nexport async function createDriveFile(accessToken: string, input: CreateDriveFileInput): Promise<DriveFileSummary> {\n const mimeType = assertSafeMimeType(input.mimeType ?? DEFAULT_MIME_TYPE);\n const metadata = { name: input.name, mimeType };\n const boundary = pickBoundary([input.content, JSON.stringify(metadata)]);\n const params = new URLSearchParams({ uploadType: \"multipart\", fields: FILE_FIELDS });\n const created = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_UPLOAD_URL}?${params.toString()}`, {\n method: \"POST\",\n contentType: `multipart/related; boundary=${boundary}`,\n body: buildMultipartBody(metadata, input.content, mimeType, boundary),\n });\n return toDriveFileSummary(created);\n}\n\nexport async function readDriveFile(accessToken: string, input: ReadDriveFileInput): Promise<{ file: DriveFileSummary; content: string }> {\n const fileId = encodeURIComponent(input.fileId);\n const metadata = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${fileId}?fields=${FILE_FIELDS}`);\n const file = toDriveFileSummary(metadata);\n if (!isTextMimeType(file.mimeType)) {\n throw new Error(`Google Drive API: '${file.name}' is ${file.mimeType || \"a binary file\"} — only text files can be read as content`);\n }\n const raw = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${fileId}?alt=media`, { expectText: true });\n const content = typeof raw === \"string\" ? raw.slice(0, MAX_READ_CHARS) : \"\";\n return { file, content };\n}\n\nexport async function deleteDriveFile(accessToken: string, input: ReadDriveFileInput): Promise<void> {\n await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: \"DELETE\" });\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { L as FEED_SCHEDULES } from "./promptSafety-
|
|
1
|
+
import { L as FEED_SCHEDULES } from "./promptSafety-rDWA9_JS.js";
|
|
2
2
|
import "./collection/index.js";
|
|
3
3
|
//#region src/feeds/ingestTypes.ts
|
|
4
4
|
var AGENT_INGEST_KIND = "agent";
|
|
@@ -12,4 +12,4 @@ var DEFAULT_FEED_MAX_ITEMS = 100;
|
|
|
12
12
|
//#endregion
|
|
13
13
|
export { DEFAULT_FEED_MAX_ITEMS as n, isFeedSchedule as r, AGENT_INGEST_KIND as t };
|
|
14
14
|
|
|
15
|
-
//# sourceMappingURL=ingestTypes-
|
|
15
|
+
//# sourceMappingURL=ingestTypes-B-dXxUF9.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ingestTypes-
|
|
1
|
+
{"version":3,"file":"ingestTypes-B-dXxUF9.js","names":[],"sources":["../src/feeds/ingestTypes.ts"],"sourcesContent":["// Declarative retrieval config for the \"Feeds\" mechanism. A Feed is a\n// CollectionSchema plus this `ingest` block, registered as data (NOT as\n// a skill) under `<workspace>/feeds/<slug>/schema.json`. The host's\n// retrieval engine reads it to periodically refill the collection's\n// records via the shared collections io layer.\n//\n// The ingest vocab (INGEST_KINDS / FEED_SCHEDULES + their literal-union types)\n// lives in the sibling `../collection` subpath alongside the schema contract, so\n// the package's schema validator can enforce it. Re-exported here so the feeds\n// engine's existing importers resolve them unchanged.\nimport { INGEST_KINDS, FEED_SCHEDULES, type IngestKind, type FeedSchedule } from \"../collection/index.js\";\n// Type-only: keeps zod out of this module's runtime graph (the feeds engine\n// imports it browser-free through `../collection`'s type surface).\nimport type { z } from \"zod\";\nimport type { AgentIngestZ, DeclarativeIngestZ } from \"../collection/core/schemaZ\";\n//\n// Two flavours: the declarative kinds (`rss`/`atom`/`http-json`) fetch-and-map,\n// and `agent` dispatches a hidden worker. The `code` kind (LLM-generated\n// deterministic transform) is still reserved for a future retriever.\n\n// The agent ingest kind. Defined HERE (not imported from `../collection`) on\n// purpose: the engine only needs the literal to branch, and re-importing it as a\n// VALUE keeps this module free of a value dependency on the collection vocab.\n// Core owns its own copy for the schema validator; this matches the same literal.\nexport const AGENT_INGEST_KIND = \"agent\" as const;\nexport type AgentIngestKind = typeof AGENT_INGEST_KIND;\n\nexport { INGEST_KINDS, FEED_SCHEDULES, type IngestKind, type FeedSchedule };\n\nconst FEED_SCHEDULE_SET: ReadonlySet<string> = new Set(FEED_SCHEDULES);\n\nexport function isFeedSchedule(value: unknown): value is FeedSchedule {\n return typeof value === \"string\" && FEED_SCHEDULE_SET.has(value);\n}\n\n/** Default cap on stored records per feed when `ingest.maxItems` is\n * omitted. Keeps high-volume feeds (news / podcasts) bounded. */\nexport const DEFAULT_FEED_MAX_ITEMS = 100;\n\n/** Declarative field map: target collection field name → source path\n * into the raw item (dot/bracket path, e.g. `\"title\"` or\n * `\"data.name\"`). */\nexport type IngestFieldMap = Record<string, string>;\n\n/** Declarative retrieval (`rss`/`atom`/`http-json`): the host fetches `url`\n * and projects each item through `map` (target field → source path; a\n * `http-json` feed may add `itemsAt`, a dot/bracket path to the items\n * array; `idFrom` derives the primaryKey; `maxItems` caps stored records,\n * default {@link DEFAULT_FEED_MAX_ITEMS}, `0` = keep everything). The\n * canonical loose contract stays the minimal `CollectionIngest`; this\n * feeds subtype is derived from the zod source of truth\n * (`collection/core/schemaZ` `DeclarativeIngestZ`), so the engine and the\n * schema validator can never drift. */\nexport type DeclarativeIngestSpec = z.infer<typeof DeclarativeIngestZ>;\n\n/** Agent-performed retrieval (`kind: \"agent\"`). No `url`/`map`: the host seeds\n * a hidden background worker (origin `system`) in `role` with `template` + a\n * summary of every record, and the worker edits the records itself via the\n * collections io layer. Valid on any collection (primarily skill-backed).\n * Derived from `AgentIngestZ` in `collection/core/schemaZ`. */\nexport type AgentIngestSpec = z.infer<typeof AgentIngestZ>;\n\n/** The `ingest` block carried on a `CollectionSchema`, discriminated on `kind`. */\nexport type IngestSpec = DeclarativeIngestSpec | AgentIngestSpec;\n"],"mappings":";;;AAwBA,IAAa,oBAAoB;AAKjC,IAAM,oBAAyC,IAAI,IAAI,cAAc;AAErE,SAAgB,eAAe,OAAuC;CACpE,OAAO,OAAO,UAAU,YAAY,kBAAkB,IAAI,KAAK;AACjE;;;AAIA,IAAa,yBAAyB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_promptSafety = require("./promptSafety-
|
|
1
|
+
const require_promptSafety = require("./promptSafety-DRd15gQ6.cjs");
|
|
2
2
|
require("./collection/index.cjs");
|
|
3
3
|
//#region src/feeds/ingestTypes.ts
|
|
4
4
|
var AGENT_INGEST_KIND = "agent";
|
|
@@ -29,4 +29,4 @@ Object.defineProperty(exports, "isFeedSchedule", {
|
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
//# sourceMappingURL=ingestTypes-
|
|
32
|
+
//# sourceMappingURL=ingestTypes-Cev9q77C.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ingestTypes-
|
|
1
|
+
{"version":3,"file":"ingestTypes-Cev9q77C.cjs","names":[],"sources":["../src/feeds/ingestTypes.ts"],"sourcesContent":["// Declarative retrieval config for the \"Feeds\" mechanism. A Feed is a\n// CollectionSchema plus this `ingest` block, registered as data (NOT as\n// a skill) under `<workspace>/feeds/<slug>/schema.json`. The host's\n// retrieval engine reads it to periodically refill the collection's\n// records via the shared collections io layer.\n//\n// The ingest vocab (INGEST_KINDS / FEED_SCHEDULES + their literal-union types)\n// lives in the sibling `../collection` subpath alongside the schema contract, so\n// the package's schema validator can enforce it. Re-exported here so the feeds\n// engine's existing importers resolve them unchanged.\nimport { INGEST_KINDS, FEED_SCHEDULES, type IngestKind, type FeedSchedule } from \"../collection/index.js\";\n// Type-only: keeps zod out of this module's runtime graph (the feeds engine\n// imports it browser-free through `../collection`'s type surface).\nimport type { z } from \"zod\";\nimport type { AgentIngestZ, DeclarativeIngestZ } from \"../collection/core/schemaZ\";\n//\n// Two flavours: the declarative kinds (`rss`/`atom`/`http-json`) fetch-and-map,\n// and `agent` dispatches a hidden worker. The `code` kind (LLM-generated\n// deterministic transform) is still reserved for a future retriever.\n\n// The agent ingest kind. Defined HERE (not imported from `../collection`) on\n// purpose: the engine only needs the literal to branch, and re-importing it as a\n// VALUE keeps this module free of a value dependency on the collection vocab.\n// Core owns its own copy for the schema validator; this matches the same literal.\nexport const AGENT_INGEST_KIND = \"agent\" as const;\nexport type AgentIngestKind = typeof AGENT_INGEST_KIND;\n\nexport { INGEST_KINDS, FEED_SCHEDULES, type IngestKind, type FeedSchedule };\n\nconst FEED_SCHEDULE_SET: ReadonlySet<string> = new Set(FEED_SCHEDULES);\n\nexport function isFeedSchedule(value: unknown): value is FeedSchedule {\n return typeof value === \"string\" && FEED_SCHEDULE_SET.has(value);\n}\n\n/** Default cap on stored records per feed when `ingest.maxItems` is\n * omitted. Keeps high-volume feeds (news / podcasts) bounded. */\nexport const DEFAULT_FEED_MAX_ITEMS = 100;\n\n/** Declarative field map: target collection field name → source path\n * into the raw item (dot/bracket path, e.g. `\"title\"` or\n * `\"data.name\"`). */\nexport type IngestFieldMap = Record<string, string>;\n\n/** Declarative retrieval (`rss`/`atom`/`http-json`): the host fetches `url`\n * and projects each item through `map` (target field → source path; a\n * `http-json` feed may add `itemsAt`, a dot/bracket path to the items\n * array; `idFrom` derives the primaryKey; `maxItems` caps stored records,\n * default {@link DEFAULT_FEED_MAX_ITEMS}, `0` = keep everything). The\n * canonical loose contract stays the minimal `CollectionIngest`; this\n * feeds subtype is derived from the zod source of truth\n * (`collection/core/schemaZ` `DeclarativeIngestZ`), so the engine and the\n * schema validator can never drift. */\nexport type DeclarativeIngestSpec = z.infer<typeof DeclarativeIngestZ>;\n\n/** Agent-performed retrieval (`kind: \"agent\"`). No `url`/`map`: the host seeds\n * a hidden background worker (origin `system`) in `role` with `template` + a\n * summary of every record, and the worker edits the records itself via the\n * collections io layer. Valid on any collection (primarily skill-backed).\n * Derived from `AgentIngestZ` in `collection/core/schemaZ`. */\nexport type AgentIngestSpec = z.infer<typeof AgentIngestZ>;\n\n/** The `ingest` block carried on a `CollectionSchema`, discriminated on `kind`. */\nexport type IngestSpec = DeclarativeIngestSpec | AgentIngestSpec;\n"],"mappings":";;;AAwBA,IAAa,oBAAoB;AAKjC,IAAM,oBAAyC,IAAI,IAAI,qBAAA,cAAc;AAErE,SAAgB,eAAe,OAAuC;CACpE,OAAO,OAAO,UAAU,YAAY,kBAAkB,IAAI,KAAK;AACjE;;;AAIA,IAAa,yBAAyB"}
|