@mulmoclaude/core 1.13.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/assets/helps/error-recovery.md +12 -1
  2. package/dist/atomic-C_7YpMiM.cjs.map +1 -1
  3. package/dist/atomic-DPpdrJzO.js.map +1 -1
  4. package/dist/{calendarGrid-CrGxeCOG.js → calendarGrid-Csy2rpjp.js} +65 -36
  5. package/dist/{calendarGrid-CrGxeCOG.js.map → calendarGrid-Csy2rpjp.js.map} +1 -1
  6. package/dist/{calendarGrid-CaS9er8i.cjs → calendarGrid-p8K-7cuB.cjs} +65 -36
  7. package/dist/{calendarGrid-CaS9er8i.cjs.map → calendarGrid-p8K-7cuB.cjs.map} +1 -1
  8. package/dist/collection/core/actionVisible.d.ts +3 -3
  9. package/dist/collection/core/completion.d.ts +5 -5
  10. package/dist/collection/core/schemaZ.d.ts +4 -4
  11. package/dist/collection/index.cjs +7 -10
  12. package/dist/collection/index.cjs.map +1 -1
  13. package/dist/collection/index.js +7 -10
  14. package/dist/collection/index.js.map +1 -1
  15. package/dist/collection/registry/registryIndex.d.ts +1 -1
  16. package/dist/collection/registry/server/index.cjs +15 -13
  17. package/dist/collection/registry/server/index.cjs.map +1 -1
  18. package/dist/collection/registry/server/index.js +15 -13
  19. package/dist/collection/registry/server/index.js.map +1 -1
  20. package/dist/collection/server/csvQuery.d.ts +7 -2
  21. package/dist/collection/server/csvStore.d.ts +2 -1
  22. package/dist/collection/server/discovery.d.ts +2 -2
  23. package/dist/collection/server/index.cjs +3 -3
  24. package/dist/collection/server/index.js +3 -3
  25. package/dist/collection/server/io.d.ts +3 -3
  26. package/dist/collection/server/manageTool.d.ts +3 -3
  27. package/dist/collection/server/store.d.ts +3 -3
  28. package/dist/collection/server/validate.d.ts +1 -1
  29. package/dist/collection-watchers/index.cjs +10 -25
  30. package/dist/collection-watchers/index.cjs.map +1 -1
  31. package/dist/collection-watchers/index.js +10 -25
  32. package/dist/collection-watchers/index.js.map +1 -1
  33. package/dist/{discovery-D-ZJ6a6z.js → discovery-BSedPW8A.js} +64 -36
  34. package/dist/discovery-BSedPW8A.js.map +1 -0
  35. package/dist/{discovery-DAoMnBYM.cjs → discovery-DSLgRQhN.cjs} +64 -36
  36. package/dist/discovery-DSLgRQhN.cjs.map +1 -0
  37. package/dist/{dist-21pMsI1a.cjs → dist-CsgSfWwR.cjs} +22 -1
  38. package/dist/{dist-21pMsI1a.cjs.map → dist-CsgSfWwR.cjs.map} +1 -1
  39. package/dist/{dist-DP1FTGnO.js → dist-D8zokgGo.js} +11 -2
  40. package/dist/{dist-DP1FTGnO.js.map → dist-D8zokgGo.js.map} +1 -1
  41. package/dist/feeds/index.cjs +2 -2
  42. package/dist/feeds/index.js +2 -2
  43. package/dist/feeds/server/host.d.ts +2 -2
  44. package/dist/feeds/server/index.cjs +35 -29
  45. package/dist/feeds/server/index.cjs.map +1 -1
  46. package/dist/feeds/server/index.js +35 -29
  47. package/dist/feeds/server/index.js.map +1 -1
  48. package/dist/feeds/server/refreshResult.d.ts +2 -2
  49. package/dist/files/index.cjs +3 -2
  50. package/dist/files/index.cjs.map +1 -1
  51. package/dist/files/index.js +3 -2
  52. package/dist/files/index.js.map +1 -1
  53. package/dist/google/calendar.d.ts +47 -33
  54. package/dist/google/collectionPush.d.ts +13 -8
  55. package/dist/google/collectionSync.d.ts +2 -2
  56. package/dist/google/index.cjs +63 -38
  57. package/dist/google/index.cjs.map +1 -1
  58. package/dist/google/index.d.ts +2 -2
  59. package/dist/google/index.js +61 -38
  60. package/dist/google/index.js.map +1 -1
  61. package/dist/{graph-DjE1Az1C.cjs → graph-BZJxiv1E.cjs} +10 -12
  62. package/dist/graph-BZJxiv1E.cjs.map +1 -0
  63. package/dist/{graph-BcwqiSpR.js → graph-Ce7xsDTt.js} +10 -12
  64. package/dist/graph-Ce7xsDTt.js.map +1 -0
  65. package/dist/{ingestTypes-D1GQdG8e.cjs → ingestTypes-CPR-h-OZ.cjs} +2 -2
  66. package/dist/{ingestTypes-D1GQdG8e.cjs.map → ingestTypes-CPR-h-OZ.cjs.map} +1 -1
  67. package/dist/{ingestTypes-XReG-li7.js → ingestTypes-F2IJhOLJ.js} +2 -2
  68. package/dist/{ingestTypes-XReG-li7.js.map → ingestTypes-F2IJhOLJ.js.map} +1 -1
  69. package/dist/notifier/index.cjs +1 -1
  70. package/dist/notifier/index.js +1 -1
  71. package/dist/notifier/types.d.ts +4 -4
  72. package/dist/{notifier-C3IhYxqh.cjs → notifier-CKFVVwDP.cjs} +36 -25
  73. package/dist/notifier-CKFVVwDP.cjs.map +1 -0
  74. package/dist/{notifier-CMFlw74b.js → notifier-CQD8NljP.js} +36 -25
  75. package/dist/notifier-CQD8NljP.js.map +1 -0
  76. package/dist/plugin-vue/index.cjs +4 -2
  77. package/dist/plugin-vue/index.cjs.map +1 -1
  78. package/dist/plugin-vue/index.js +4 -2
  79. package/dist/plugin-vue/index.js.map +1 -1
  80. package/dist/{project-B9NSyr6L.js → project-B0j_Vf_Q.js} +7 -2
  81. package/dist/project-B0j_Vf_Q.js.map +1 -0
  82. package/dist/{project-CCtT_fQr.cjs → project-C1ep9pvo.cjs} +7 -2
  83. package/dist/project-C1ep9pvo.cjs.map +1 -0
  84. package/dist/{promptSafety-FD8cQn3e.cjs → promptSafety-DUZ5BiW5.cjs} +106 -88
  85. package/dist/promptSafety-DUZ5BiW5.cjs.map +1 -0
  86. package/dist/{promptSafety-C--op_6a.js → promptSafety-DgHMF4Pz.js} +106 -88
  87. package/dist/promptSafety-DgHMF4Pz.js.map +1 -0
  88. package/dist/remote-host/index.cjs +2 -1
  89. package/dist/remote-host/index.d.ts +10 -0
  90. package/dist/remote-host/index.js +2 -2
  91. package/dist/remote-host/server/hostRunner.d.ts +7 -2
  92. package/dist/remote-host/server/index.cjs +14 -11
  93. package/dist/remote-host/server/index.cjs.map +1 -1
  94. package/dist/remote-host/server/index.js +13 -10
  95. package/dist/remote-host/server/index.js.map +1 -1
  96. package/dist/{remote-host-DkDVxNim.cjs → remote-host-CtQjagPt.cjs} +53 -1
  97. package/dist/remote-host-CtQjagPt.cjs.map +1 -0
  98. package/dist/remote-host-Dba4lF3Z.js +82 -0
  99. package/dist/remote-host-Dba4lF3Z.js.map +1 -0
  100. package/dist/remote-view/index.cjs +6 -4
  101. package/dist/remote-view/index.cjs.map +1 -1
  102. package/dist/remote-view/index.d.ts +1 -1
  103. package/dist/remote-view/index.js +6 -4
  104. package/dist/remote-view/index.js.map +1 -1
  105. package/dist/scheduler/adapter.d.ts +4 -4
  106. package/dist/scheduler/index.cjs +35 -3
  107. package/dist/scheduler/index.cjs.map +1 -1
  108. package/dist/scheduler/index.js +35 -3
  109. package/dist/scheduler/index.js.map +1 -1
  110. package/dist/scheduler/task-manager.d.ts +2 -2
  111. package/dist/{server-B_b0bLSW.cjs → server-1c6gB372.cjs} +54 -31
  112. package/dist/server-1c6gB372.cjs.map +1 -0
  113. package/dist/{server-Ci_A3chZ.js → server-Dv_L7l0y.js} +54 -31
  114. package/dist/server-Dv_L7l0y.js.map +1 -0
  115. package/dist/skill-bridge/index.cjs +3 -4
  116. package/dist/skill-bridge/index.cjs.map +1 -1
  117. package/dist/skill-bridge/index.js +3 -4
  118. package/dist/skill-bridge/index.js.map +1 -1
  119. package/dist/types-BKVZrtyc.js.map +1 -1
  120. package/dist/types-CNqkLT4M.cjs.map +1 -1
  121. package/dist/utils/fetch.cjs.map +1 -1
  122. package/dist/utils/fetch.d.ts +1 -1
  123. package/dist/utils/fetch.js.map +1 -1
  124. package/dist/utils/index.cjs +1 -1
  125. package/dist/utils/index.js +1 -1
  126. package/dist/whisper/client.cjs.map +1 -1
  127. package/dist/whisper/client.d.ts +3 -3
  128. package/dist/whisper/client.js.map +1 -1
  129. package/dist/whisper/index.cjs +2 -6
  130. package/dist/whisper/index.cjs.map +1 -1
  131. package/dist/whisper/index.js +2 -6
  132. package/dist/whisper/index.js.map +1 -1
  133. package/dist/wiki/index.cjs +4 -4
  134. package/dist/wiki/index.cjs.map +1 -1
  135. package/dist/wiki/index.js +4 -4
  136. package/dist/wiki/index.js.map +1 -1
  137. package/dist/wiki/server/index.cjs +15 -12
  138. package/dist/wiki/server/index.cjs.map +1 -1
  139. package/dist/wiki/server/index.js +15 -12
  140. package/dist/wiki/server/index.js.map +1 -1
  141. package/package.json +6 -6
  142. package/dist/discovery-D-ZJ6a6z.js.map +0 -1
  143. package/dist/discovery-DAoMnBYM.cjs.map +0 -1
  144. package/dist/graph-BcwqiSpR.js.map +0 -1
  145. package/dist/graph-DjE1Az1C.cjs.map +0 -1
  146. package/dist/notifier-C3IhYxqh.cjs.map +0 -1
  147. package/dist/notifier-CMFlw74b.js.map +0 -1
  148. package/dist/project-B9NSyr6L.js.map +0 -1
  149. package/dist/project-CCtT_fQr.cjs.map +0 -1
  150. package/dist/promptSafety-C--op_6a.js.map +0 -1
  151. package/dist/promptSafety-FD8cQn3e.cjs.map +0 -1
  152. package/dist/remote-host-D_BRFHcI.js +0 -36
  153. package/dist/remote-host-D_BRFHcI.js.map +0 -1
  154. package/dist/remote-host-DkDVxNim.cjs.map +0 -1
  155. package/dist/server-B_b0bLSW.cjs.map +0 -1
  156. package/dist/server-Ci_A3chZ.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/files/json.ts","../../src/files/safe.ts","../../src/files/byPath.ts","../../src/files/htmlFileRequest.ts"],"sourcesContent":["import { writeFileAtomic, type WriteAtomicOptions } from \"./atomic.js\";\n\nconst JSON_INDENT = 2;\n\n/** Atomic JSON write (2-space indent) — serialize then write through the atomic\n * tmp-file + rename seam so a reader never sees a half-written document. */\nexport async function writeJsonAtomic(filePath: string, data: unknown, opts: WriteAtomicOptions = {}): Promise<void> {\n await writeFileAtomic(filePath, JSON.stringify(data, null, JSON_INDENT), opts);\n}\n","import { realpathSync } from \"node:fs\";\nimport path from \"node:path\";\n\n/** True for a `not found` filesystem error (ENOENT) — lets callers treat a\n * missing file as an empty/default result instead of a thrown error. */\nexport function isEnoent(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && \"code\" in err && err.code === \"ENOENT\";\n}\n\n/** Realpath-based read-time containment: resolve `relPath` against the root's\n * realpath and require the target's realpath to stay inside it. Returns null\n * on ENOENT or traversal (symlink escapes included). `rootReal` MUST already\n * be a realpath. The one implementation for host and plugins (#2461) — this\n * security-critical primitive must not drift per consumer. */\nexport function resolveWithinRoot(rootReal: string, relPath: string): string | null {\n const normalized = path.normalize(relPath || \"\");\n const resolved = path.resolve(rootReal, normalized);\n const resolvedReal = realpathOrNull(resolved);\n if (resolvedReal === null) return null;\n if (resolvedReal !== rootReal && !resolvedReal.startsWith(rootReal + path.sep)) {\n return null;\n }\n return resolvedReal;\n}\n\nfunction realpathOrNull(absPath: string): string | null {\n try {\n return realpathSync(absPath);\n } catch {\n return null;\n }\n}\n","// Read / overwrite files the TOOL CALL named, rather than files the app minted.\n// `presentDocument(path)` / `presentHtml(path)` open a document that already\n// exists — a repo's `README.md`, `docs/report.html`, an absolute path — and the\n// user's edits in the view overwrite it in place.\n//\n// Shared by both hosts (MulmoClaude's `server/utils/files/by-path.ts` and\n// MulmoTerminal's backend bind their own workspace root to it), because the\n// judgement of what a `path` argument may reach is exactly the thing that must\n// not drift between them: a host that accepts what the other refuses turns one\n// tool call into two different behaviours.\n//\n// The rules:\n// - `classifyFilePath` (also what the plugins' `path` gate calls) decides the\n// shape: right extension, no NUL, no `.` / `..` / empty segment. That\n// lexical guard is what stops a vetted path from being re-pointed later.\n// - relative paths resolve against the injected root; absolute paths are taken\n// as given. There is deliberately NO containment check — opening a file\n// outside the workspace is the documented purpose of the `path` form. The\n// agent can already read and write those files directly; what is new is that\n// the view can too.\n// - a value that is absolute only under ANOTHER platform's rules is refused:\n// `classifyFilePath` recognises `C:\\proj\\x.md` everywhere (the value may\n// come from a remote host), but on POSIX `path.resolve(\"C:/proj/x.md\")`\n// lands under the process cwd — a file nobody named.\n// - reads and writes require a REGULAR FILE, judged through `realpath` so a\n// symlink is assessed by what it points at and a directory named `x.md`\n// cannot masquerade as a document.\n// - `write` overwrites only. Neither tool creates a file at a caller-supplied\n// path, so a write to a path that does not exist means the view is stale or\n// the path was wrong — refusing keeps a typo from scattering files.\n// - `readDir` and `unlink` are refused outright: this capability exists to\n// read and re-save ONE named document, not to browse or delete.\n\nimport { readFile, realpath, stat as fsStat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { classifyFilePath } from \"../artifacts/paths.js\";\nimport { writeFileAtomic } from \"./atomic.js\";\n\nexport const MARKDOWN_EXTENSIONS = [\".md\"] as const;\nexport const HTML_EXTENSIONS = [\".html\", \".htm\"] as const;\n\n/** Minimal structural echo of gui-chat-protocol's `FileOps`. Declared here\n * rather than imported so core keeps no dependency on the protocol package;\n * a host assigns the result straight to a `FileOps`-typed capability. */\nexport interface ByPathFileOps {\n read: (rel: string) => Promise<string>;\n readBytes: (rel: string) => Promise<Uint8Array>;\n write: (rel: string, content: string | Uint8Array) => Promise<void>;\n readDir: (rel: string) => Promise<string[]>;\n stat: (rel: string) => Promise<{ mtimeMs: number; size: number }>;\n exists: (rel: string) => Promise<boolean>;\n unlink: (rel: string) => Promise<void>;\n}\n\n/** Absolute on-disk path for a caller-supplied path with one of `extensions`,\n * or null when the value is not usable on this platform. */\nexport function resolveByPath(root: string, value: string, extensions: readonly string[]): string | null {\n const kind = classifyFilePath(value, extensions);\n if (kind === null) return null;\n if (kind === \"relative\") return path.resolve(root, value);\n return path.isAbsolute(value) ? path.resolve(value) : null;\n}\n\n/** True when the path names an existing regular file. */\nexport async function existsAsFile(root: string, value: string, extensions: readonly string[]): Promise<boolean> {\n const absPath = resolveByPath(root, value, extensions);\n return absPath === null ? false : (await regularFileTarget(absPath)) !== null;\n}\n\nexport interface ByPathOptions {\n /** The root relative values resolve against, read PER CALL — hosts inject the\n * workspace after these ops are already bound into plugin closures. */\n rootFor: () => string;\n extensions: readonly string[];\n}\n\nfunction resolveOrThrow({ rootFor, extensions }: ByPathOptions, value: string): string {\n const absPath = resolveByPath(rootFor(), value, extensions);\n if (absPath === null) throw new Error(`invalid path: ${value}`);\n return absPath;\n}\n\n/** The canonical REGULAR FILE a path names, or null when it is missing, is a\n * directory, or is anything else (a FIFO would block a read forever).\n * Resolved through `realpath` so a symlink is judged — and later written — by\n * what it points at: `writeFileAtomic` renames a temp file into place, which\n * through a link would replace the link itself and leave the real document\n * untouched. */\nasync function regularFileTarget(absPath: string): Promise<string | null> {\n try {\n const target = await realpath(absPath);\n return (await fsStat(target)).isFile() ? target : null;\n } catch {\n return null;\n }\n}\n\n/** The file to read or overwrite. Throws rather than creating: neither tool\n * ever writes to a path that does not already hold a document. */\nasync function existingFileFor(options: ByPathOptions, rel: string): Promise<string> {\n const target = await regularFileTarget(resolveOrThrow(options, rel));\n if (target === null) throw new Error(`no file exists at ${rel}`);\n return target;\n}\n\n/** Browsing and deleting are NOT part of this capability: it exists so a view\n * can read and re-save the ONE document the tool call named, not to enumerate\n * a directory it was never pointed at or remove a file. */\nfunction unsupported(operation: string): () => Promise<never> {\n return () => Promise.reject(new Error(`byPath FileOps does not support ${operation}`));\n}\n\n/** FileOps over caller-supplied paths — what a host injects as `files.byPath`\n * for plugins whose `path` argument may leave their artifact directory. Every\n * method takes the same value the tool call carried, not a scope-relative one. */\nexport function createByPathFileOps(options: ByPathOptions): ByPathFileOps {\n return {\n read: async (rel) => readFile(await existingFileFor(options, rel), \"utf-8\"),\n readBytes: async (rel) => {\n const buf = await readFile(await existingFileFor(options, rel));\n return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);\n },\n write: async (rel, content) => writeFileAtomic(await existingFileFor(options, rel), content),\n readDir: unsupported(\"readDir\"),\n stat: async (rel) => {\n const { mtimeMs, size } = await fsStat(resolveOrThrow(options, rel));\n return { mtimeMs, size };\n },\n exists: (rel) => existsAsFile(options.rootFor(), rel, options.extensions),\n unlink: unsupported(\"unlink\"),\n };\n}\n","// Server half of the `/htmlfile` scheme (client half: `src/config/htmlFileUrl.ts`).\n// Turns a request path into the absolute file to serve, or null.\n//\n// Shared by both hosts so the URL the View asks for and the file a host resolves\n// cannot drift apart.\n//\n// This mount is deliberately NOT contained to a root: presentHtml's `path` form\n// may name any HTML page on disk, and serving it is the point. What still holds:\n//\n// - only `ws` (workspace-relative) and `abs` (absolute) scopes exist; anything\n// else 404s, so a bare `/htmlfile/etc/passwd` is not a path at all.\n// - no `.` / `..` / empty segment survives, so a vetted URL cannot be\n// re-pointed by traversal, and no dotfile segment is reachable (same policy\n// as `resolveArtifactRequestPath`, which the artifact mounts share).\n// - extension allowlist, realpath and regular-file checks stay with the host's\n// mount, which is also where the CSP header and rate limit are applied.\n//\n// The trust boundary is whatever the host applies to its `/artifacts/html`\n// mount — in MulmoClaude, the loopback-only listener plus `requireSameOrigin`\n// (bearer auth does not apply outside `/api`, and an iframe `src` request cannot\n// carry an Authorization header anyway).\n\nimport path from \"node:path\";\n// The scope constants are re-declared rather than imported: core must not\n// depend on a plugin (dependencies flow the other way). `html-plugin`'s\n// `htmlFileUrl` builds the URLs these parse, and the round-trip test in the host\n// is what keeps the two halves honest.\nconst HTML_FILE_SCOPE_WORKSPACE = \"ws\";\nconst HTML_FILE_SCOPE_ABSOLUTE = \"abs\";\n\nconst WINDOWS_DRIVE_ONLY_RE = /^[a-zA-Z]:$/;\n\nfunction decodeSegments(reqPath: string): string[] | null {\n let decoded: string[];\n try {\n decoded = reqPath\n .replace(/^\\//, \"\")\n .split(\"/\")\n .map((segment) => decodeURIComponent(segment));\n } catch {\n // Malformed escape (`%ZZ`) — fail closed rather than bubbling a URIError.\n return null;\n }\n // Segmentation happens on the ENCODED path, so a `%2F` (or `%5C`) becomes a\n // separator only after this decode — `a%2F..%2F..%2Ftmp%2Fx.html` would arrive\n // as one segment that the `..` check below never sees, and then split apart\n // inside `path.resolve`. A decoded segment that still contains a separator is\n // never legitimate here, so refuse it.\n return decoded.some((segment) => segment.includes(\"/\") || segment.includes(\"\\\\\")) ? null : decoded;\n}\n\n/** Absolute path for a `/htmlfile/<scope>/<segments…>` request, or null when\n * the URL is malformed, uses an unknown scope, or touches a `.` / `..` /\n * dotfile segment. `workspaceRoot` should already be a realpath. */\nexport function resolveHtmlFileRequestPath(workspaceRoot: string, reqPath: string): string | null {\n const decoded = decodeSegments(reqPath);\n if (decoded === null || decoded.length < 2) return null;\n const [scope, ...rest] = decoded;\n if (scope !== HTML_FILE_SCOPE_WORKSPACE && scope !== HTML_FILE_SCOPE_ABSOLUTE) return null;\n if (rest.length === 0) return null;\n if (rest.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\" || segment.startsWith(\".\") || segment.includes(\"\\0\"))) {\n return null;\n }\n if (scope === HTML_FILE_SCOPE_WORKSPACE) return path.resolve(workspaceRoot, ...rest);\n // `abs/C:/proj/page.html` — the drive letter arrives as its own segment, so\n // rejoin it before resolving; everything else is rooted at `/`.\n const candidate = WINDOWS_DRIVE_ONLY_RE.test(rest[0]) ? rest.join(\"/\") : `/${rest.join(\"/\")}`;\n // Only THIS platform's `path` can say whether that is really absolute: on\n // POSIX, `path.resolve(\"C:/proj/page.html\")` would land under the process cwd\n // rather than at any drive, silently serving the wrong file. Refuse instead.\n return path.isAbsolute(candidate) ? path.resolve(candidate) : null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/files/json.ts","../../src/files/safe.ts","../../src/files/byPath.ts","../../src/files/htmlFileRequest.ts"],"sourcesContent":["import { writeFileAtomic, type WriteAtomicOptions } from \"./atomic.js\";\n\nconst JSON_INDENT = 2;\n\n/** Atomic JSON write (2-space indent) — serialize then write through the atomic\n * tmp-file + rename seam so a reader never sees a half-written document. */\nexport async function writeJsonAtomic(filePath: string, data: unknown, opts: WriteAtomicOptions = {}): Promise<void> {\n await writeFileAtomic(filePath, JSON.stringify(data, null, JSON_INDENT), opts);\n}\n","import { realpathSync } from \"node:fs\";\nimport path from \"node:path\";\n\n/** True for a `not found` filesystem error (ENOENT) — lets callers treat a\n * missing file as an empty/default result instead of a thrown error. */\nexport function isEnoent(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && \"code\" in err && err.code === \"ENOENT\";\n}\n\n/** Realpath-based read-time containment: resolve `relPath` against the root's\n * realpath and require the target's realpath to stay inside it. Returns null\n * on ENOENT or traversal (symlink escapes included). `rootReal` MUST already\n * be a realpath. The one implementation for host and plugins (#2461) — this\n * security-critical primitive must not drift per consumer. */\nexport function resolveWithinRoot(rootReal: string, relPath: string): string | null {\n const normalized = path.normalize(relPath || \"\");\n const resolved = path.resolve(rootReal, normalized);\n const resolvedReal = realpathOrNull(resolved);\n if (resolvedReal === null) return null;\n if (resolvedReal !== rootReal && !resolvedReal.startsWith(rootReal + path.sep)) {\n return null;\n }\n return resolvedReal;\n}\n\nfunction realpathOrNull(absPath: string): string | null {\n try {\n return realpathSync(absPath);\n } catch {\n return null;\n }\n}\n","// Read / overwrite files the TOOL CALL named, rather than files the app minted.\n// `presentDocument(path)` / `presentHtml(path)` open a document that already\n// exists — a repo's `README.md`, `docs/report.html`, an absolute path — and the\n// user's edits in the view overwrite it in place.\n//\n// Shared by both hosts (MulmoClaude's `server/utils/files/by-path.ts` and\n// MulmoTerminal's backend bind their own workspace root to it), because the\n// judgement of what a `path` argument may reach is exactly the thing that must\n// not drift between them: a host that accepts what the other refuses turns one\n// tool call into two different behaviours.\n//\n// The rules:\n// - `classifyFilePath` (also what the plugins' `path` gate calls) decides the\n// shape: right extension, no NUL, no `.` / `..` / empty segment. That\n// lexical guard is what stops a vetted path from being re-pointed later.\n// - relative paths resolve against the injected root; absolute paths are taken\n// as given. There is deliberately NO containment check — opening a file\n// outside the workspace is the documented purpose of the `path` form. The\n// agent can already read and write those files directly; what is new is that\n// the view can too.\n// - a value that is absolute only under ANOTHER platform's rules is refused:\n// `classifyFilePath` recognises `C:\\proj\\x.md` everywhere (the value may\n// come from a remote host), but on POSIX `path.resolve(\"C:/proj/x.md\")`\n// lands under the process cwd — a file nobody named.\n// - reads and writes require a REGULAR FILE, judged through `realpath` so a\n// symlink is assessed by what it points at and a directory named `x.md`\n// cannot masquerade as a document.\n// - `write` overwrites only. Neither tool creates a file at a caller-supplied\n// path, so a write to a path that does not exist means the view is stale or\n// the path was wrong — refusing keeps a typo from scattering files.\n// - `readDir` and `unlink` are refused outright: this capability exists to\n// read and re-save ONE named document, not to browse or delete.\n\nimport { readFile, realpath, stat as fsStat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { classifyFilePath } from \"../artifacts/paths.js\";\nimport { writeFileAtomic } from \"./atomic.js\";\n\nexport const MARKDOWN_EXTENSIONS = [\".md\"] as const;\nexport const HTML_EXTENSIONS = [\".html\", \".htm\"] as const;\n\n/** Minimal structural echo of gui-chat-protocol's `FileOps`. Declared here\n * rather than imported so core keeps no dependency on the protocol package;\n * a host assigns the result straight to a `FileOps`-typed capability. */\nexport interface ByPathFileOps {\n read: (rel: string) => Promise<string>;\n readBytes: (rel: string) => Promise<Uint8Array>;\n write: (rel: string, content: string | Uint8Array) => Promise<void>;\n readDir: (rel: string) => Promise<string[]>;\n stat: (rel: string) => Promise<{ mtimeMs: number; size: number }>;\n exists: (rel: string) => Promise<boolean>;\n unlink: (rel: string) => Promise<void>;\n}\n\n/** Absolute on-disk path for a caller-supplied path with one of `extensions`,\n * or null when the value is not usable on this platform. */\nexport function resolveByPath(root: string, value: string, extensions: readonly string[]): string | null {\n const kind = classifyFilePath(value, extensions);\n if (kind === null) return null;\n if (kind === \"relative\") return path.resolve(root, value);\n return path.isAbsolute(value) ? path.resolve(value) : null;\n}\n\n/** True when the path names an existing regular file. */\nexport async function existsAsFile(root: string, value: string, extensions: readonly string[]): Promise<boolean> {\n const absPath = resolveByPath(root, value, extensions);\n return absPath === null ? false : (await regularFileTarget(absPath)) !== null;\n}\n\nexport interface ByPathOptions {\n /** The root relative values resolve against, read PER CALL — hosts inject the\n * workspace after these ops are already bound into plugin closures. */\n rootFor: () => string;\n extensions: readonly string[];\n}\n\nfunction resolveOrThrow({ rootFor, extensions }: ByPathOptions, value: string): string {\n const absPath = resolveByPath(rootFor(), value, extensions);\n if (absPath === null) throw new Error(`invalid path: ${value}`);\n return absPath;\n}\n\n/** The canonical REGULAR FILE a path names, or null when it is missing, is a\n * directory, or is anything else (a FIFO would block a read forever).\n * Resolved through `realpath` so a symlink is judged — and later written — by\n * what it points at: `writeFileAtomic` renames a temp file into place, which\n * through a link would replace the link itself and leave the real document\n * untouched. */\nasync function regularFileTarget(absPath: string): Promise<string | null> {\n try {\n const target = await realpath(absPath);\n return (await fsStat(target)).isFile() ? target : null;\n } catch {\n return null;\n }\n}\n\n/** The file to read or overwrite. Throws rather than creating: neither tool\n * ever writes to a path that does not already hold a document. */\nasync function existingFileFor(options: ByPathOptions, rel: string): Promise<string> {\n const target = await regularFileTarget(resolveOrThrow(options, rel));\n if (target === null) throw new Error(`no file exists at ${rel}`);\n return target;\n}\n\n/** Browsing and deleting are NOT part of this capability: it exists so a view\n * can read and re-save the ONE document the tool call named, not to enumerate\n * a directory it was never pointed at or remove a file. */\nfunction unsupported(operation: string): () => Promise<never> {\n return () => Promise.reject(new Error(`byPath FileOps does not support ${operation}`));\n}\n\n/** FileOps over caller-supplied paths — what a host injects as `files.byPath`\n * for plugins whose `path` argument may leave their artifact directory. Every\n * method takes the same value the tool call carried, not a scope-relative one. */\nexport function createByPathFileOps(options: ByPathOptions): ByPathFileOps {\n return {\n read: async (rel) => readFile(await existingFileFor(options, rel), \"utf-8\"),\n readBytes: async (rel) => {\n const buf = await readFile(await existingFileFor(options, rel));\n return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);\n },\n write: async (rel, content) => writeFileAtomic(await existingFileFor(options, rel), content),\n readDir: unsupported(\"readDir\"),\n stat: async (rel) => {\n const { mtimeMs, size } = await fsStat(resolveOrThrow(options, rel));\n return { mtimeMs, size };\n },\n exists: (rel) => existsAsFile(options.rootFor(), rel, options.extensions),\n unlink: unsupported(\"unlink\"),\n };\n}\n","// Server half of the `/htmlfile` scheme (client half: `src/config/htmlFileUrl.ts`).\n// Turns a request path into the absolute file to serve, or null.\n//\n// Shared by both hosts so the URL the View asks for and the file a host resolves\n// cannot drift apart.\n//\n// This mount is deliberately NOT contained to a root: presentHtml's `path` form\n// may name any HTML page on disk, and serving it is the point. What still holds:\n//\n// - only `ws` (workspace-relative) and `abs` (absolute) scopes exist; anything\n// else 404s, so a bare `/htmlfile/etc/passwd` is not a path at all.\n// - no `.` / `..` / empty segment survives, so a vetted URL cannot be\n// re-pointed by traversal, and no dotfile segment is reachable (same policy\n// as `resolveArtifactRequestPath`, which the artifact mounts share).\n// - extension allowlist, realpath and regular-file checks stay with the host's\n// mount, which is also where the CSP header and rate limit are applied.\n//\n// The trust boundary is whatever the host applies to its `/artifacts/html`\n// mount — in MulmoClaude, the loopback-only listener plus `requireSameOrigin`\n// (bearer auth does not apply outside `/api`, and an iframe `src` request cannot\n// carry an Authorization header anyway).\n\nimport path from \"node:path\";\n// The scope constants are re-declared rather than imported: core must not\n// depend on a plugin (dependencies flow the other way). `html-plugin`'s\n// `htmlFileUrl` builds the URLs these parse, and the round-trip test in the host\n// is what keeps the two halves honest.\nconst HTML_FILE_SCOPE_WORKSPACE = \"ws\";\nconst HTML_FILE_SCOPE_ABSOLUTE = \"abs\";\n\nconst WINDOWS_DRIVE_ONLY_RE = /^[a-zA-Z]:$/;\n\nfunction decodeSegments(reqPath: string): string[] | null {\n let decoded: string[];\n try {\n decoded = reqPath\n .replace(/^\\//, \"\")\n .split(\"/\")\n .map((segment) => decodeURIComponent(segment));\n } catch {\n // Malformed escape (`%ZZ`) — fail closed rather than bubbling a URIError.\n return null;\n }\n // Segmentation happens on the ENCODED path, so a `%2F` (or `%5C`) becomes a\n // separator only after this decode — `a%2F..%2F..%2Ftmp%2Fx.html` would arrive\n // as one segment that the `..` check below never sees, and then split apart\n // inside `path.resolve`. A decoded segment that still contains a separator is\n // never legitimate here, so refuse it.\n return decoded.some((segment) => segment.includes(\"/\") || segment.includes(\"\\\\\")) ? null : decoded;\n}\n\n/** Absolute path for a `/htmlfile/<scope>/<segments…>` request, or null when\n * the URL is malformed, uses an unknown scope, or touches a `.` / `..` /\n * dotfile segment. `workspaceRoot` should already be a realpath. */\nexport function resolveHtmlFileRequestPath(workspaceRoot: string, reqPath: string): string | null {\n const decoded = decodeSegments(reqPath);\n if (decoded === null || decoded.length < 2) return null;\n const [scope, ...rest] = decoded;\n if (scope !== HTML_FILE_SCOPE_WORKSPACE && scope !== HTML_FILE_SCOPE_ABSOLUTE) return null;\n const [firstRest] = rest;\n if (firstRest === undefined) return null;\n if (rest.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\" || segment.startsWith(\".\") || segment.includes(\"\\0\"))) {\n return null;\n }\n if (scope === HTML_FILE_SCOPE_WORKSPACE) return path.resolve(workspaceRoot, ...rest);\n // `abs/C:/proj/page.html` — the drive letter arrives as its own segment, so\n // rejoin it before resolving; everything else is rooted at `/`.\n const candidate = WINDOWS_DRIVE_ONLY_RE.test(firstRest) ? rest.join(\"/\") : `/${rest.join(\"/\")}`;\n // Only THIS platform's `path` can say whether that is really absolute: on\n // POSIX, `path.resolve(\"C:/proj/page.html\")` would land under the process cwd\n // rather than at any drive, silently serving the wrong file. Refuse instead.\n return path.isAbsolute(candidate) ? path.resolve(candidate) : null;\n}\n"],"mappings":"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"}
@@ -39,47 +39,47 @@ export type CalendarEventSpan = {
39
39
  };
40
40
  export type CalendarEventInput = {
41
41
  summary: string;
42
- description?: string;
43
- location?: string;
42
+ description?: string | undefined;
43
+ location?: string | undefined;
44
44
  /** Calendar to create the event on; defaults to the user's primary. */
45
- calendarId?: string;
45
+ calendarId?: string | undefined;
46
46
  /** Event colour (Google event palette id "1".."11"); omit to inherit the calendar's colour. */
47
- colorId?: string;
47
+ colorId?: string | undefined;
48
48
  /** Caller-chosen event id. Google requires base32hex (`0-9a-v`), 5-1024 chars,
49
49
  * and answers 409 when it is already taken. A collection push sets this so a
50
50
  * locally-created record keeps its own record id as the event id, instead of
51
51
  * being re-keyed to Google's after the fact — a re-key that is missed leaves
52
52
  * a duplicate record on the next pull. */
53
- eventId?: string;
53
+ eventId?: string | undefined;
54
54
  } & CalendarEventSpan;
55
55
  export interface UpdateCalendarEventInput {
56
56
  eventId: string;
57
- summary?: string;
58
- startDateTime?: string;
59
- endDateTime?: string;
57
+ summary?: string | undefined;
58
+ startDateTime?: string | undefined;
59
+ endDateTime?: string | undefined;
60
60
  /** Structured span; wins over the flat pair when both are present. */
61
- start?: CalendarEventTime;
62
- end?: CalendarEventTime;
61
+ start?: CalendarEventTime | undefined;
62
+ end?: CalendarEventTime | undefined;
63
63
  /** `""` clears the description; omit to leave it untouched. */
64
- description?: string;
64
+ description?: string | undefined;
65
65
  /** `""` clears the location; omit to leave it untouched. */
66
- location?: string;
67
- calendarId?: string;
68
- colorId?: string;
66
+ location?: string | undefined;
67
+ calendarId?: string | undefined;
68
+ colorId?: string | undefined;
69
69
  /** Etag of the version this edit was computed against. Sent as `If-Match`, so
70
70
  * Google answers 412 rather than letting the PATCH clobber a change that
71
71
  * landed after the caller read the event. Omit for an unconditional write. */
72
- ifMatch?: string;
72
+ ifMatch?: string | undefined;
73
73
  }
74
74
  export interface DeleteCalendarEventInput {
75
75
  eventId: string;
76
- calendarId?: string;
76
+ calendarId?: string | undefined;
77
77
  }
78
78
  export interface ListEventsInput {
79
- timeMin?: string;
80
- maxResults?: number;
79
+ timeMin?: string | undefined;
80
+ maxResults?: number | undefined;
81
81
  /** Calendar to read; defaults to the user's primary. */
82
- calendarId?: string;
82
+ calendarId?: string | undefined;
83
83
  }
84
84
  export interface CalendarEventSummary {
85
85
  id: string;
@@ -160,18 +160,18 @@ export declare function deleteCalendarEvent(accessToken: string, input: DeleteCa
160
160
  export declare function listCalendarEvents(accessToken: string, input?: ListEventsInput): Promise<CalendarEventSummary[]>;
161
161
  export interface SyncEventsInput {
162
162
  /** Calendar to sync; defaults to the user's primary. */
163
- calendarId?: string;
163
+ calendarId?: string | undefined;
164
164
  /** Token from the previous sync. Omit for a full sync. */
165
- syncToken?: string;
165
+ syncToken?: string | undefined;
166
166
  /** Page size for the underlying list calls. */
167
- maxResults?: number;
167
+ maxResults?: number | undefined;
168
168
  }
169
169
  export interface CalendarSyncResult {
170
170
  /** Changed events since `syncToken` (all of them on a full sync). Deletions
171
171
  * arrive here too, as `status: "cancelled"`. */
172
172
  events: CalendarEventSummary[];
173
173
  /** Token to pass to the NEXT sync. Absent only if Google omitted it. */
174
- nextSyncToken?: string;
174
+ nextSyncToken?: string | undefined;
175
175
  /** The stored token had expired (410) — the caller must drop it and re-sync
176
176
  * from scratch; no events are returned in that case. */
177
177
  fullResyncRequired: boolean;
@@ -189,23 +189,37 @@ export interface CalendarSyncResult {
189
189
  export declare function syncCalendarEvents(accessToken: string, input?: SyncEventsInput): Promise<CalendarSyncResult>;
190
190
  export interface CalendarListPage {
191
191
  items: unknown[];
192
- nextPageToken?: string;
192
+ nextPageToken?: string | undefined;
193
193
  }
194
194
  /** Pagination loop for CalendarList.list, extracted so it can be tested without
195
195
  * network. Stops at the last page (no token) or the runaway page cap. */
196
196
  export declare function collectCalendarPages(fetchPage: (pageToken?: string) => Promise<CalendarListPage>, maxPages?: number): Promise<CalendarSummary[]>;
197
+ /** What a calendar absent from the user's list can still tell about itself. */
198
+ export interface CalendarMeta {
199
+ /** IANA zone (`Asia/Tokyo`), `""` when Google omits it. */
200
+ timeZone: string;
201
+ /** `owner` / `writer` / `reader` / `freeBusyReader`, `""` when unreported. */
202
+ accessRole: string;
203
+ }
204
+ export declare const toCalendarMeta: (value: unknown) => CalendarMeta;
205
+ /** One calendar's zone and the caller's role on it, read by id rather than
206
+ * looked up in the user's list.
207
+ *
208
+ * `calendarList` only holds calendars the user has ADDED; a calendar shared
209
+ * with them can be readable and writable by id without appearing there, and a
210
+ * push still needs its zone to send an offset-less `dateTime`.
211
+ *
212
+ * Deliberately `events.list` and NOT `calendars.get`: the latter takes only
213
+ * full-calendar scopes (`calendar`, `calendar.readonly`, `calendar.calendars*`)
214
+ * and this app asks for none of them, so it answers 403 for every account it
215
+ * links (#2735). The events list carries the same `timeZone` — plus the
216
+ * `accessRole` the calendar resource does not have — under `calendar.events`,
217
+ * which the push already holds because it writes with it. Only the envelope is
218
+ * read; the one event asked for is the smallest page Google will serve. */
219
+ export declare function getCalendarMeta(accessToken: string, calendarId: string | undefined): Promise<CalendarMeta>;
197
220
  /** The calendars the user has added/subscribed to (primary + secondary +
198
221
  * shared), each with its id, name and colour, following pagination. Needs the
199
222
  * calendar-list read scope (GOOGLE_CALENDARLIST_SCOPE). */
200
- /** One calendar's own resource, addressed by id rather than looked up in the
201
- * user's list.
202
- *
203
- * `calendarList` only holds calendars the user has ADDED; a calendar shared
204
- * with them can be readable and writable by id without appearing there. This is
205
- * how a push learns such a calendar's `timeZone`. Note the resource carries no
206
- * `accessRole` — that is a calendarList property — so reachability here says
207
- * nothing about writability. */
208
- export declare function getCalendar(accessToken: string, calendarId: string | undefined): Promise<CalendarSummary>;
209
223
  export declare function listCalendars(accessToken: string): Promise<CalendarSummary[]>;
210
224
  /** Resolve a `colorId` (on an event or calendar) to its hex background/foreground. */
211
225
  export declare function getCalendarColors(accessToken: string): Promise<CalendarColors>;
@@ -5,21 +5,26 @@ import { ShadowEvent } from './calendarPushState.js';
5
5
  import { PushableSourceField } from './pushPlan.js';
6
6
  /** What the up-front writability gate knows about the target calendar. */
7
7
  export interface CalendarWriteTarget {
8
- /** The role from the user's calendar LIST, or null when the calendar is not in
9
- * it. Null means unknown, never read-only: `calendarList` holds only the
10
- * calendars the user has added, so one shared with write access can be absent
11
- * from it (Codex review). */
8
+ /** The caller's role on the calendar, or null when neither source reported one.
9
+ * Null means unknown, never read-only: hard-denying an unknown role would
10
+ * refuse a calendar the user can in fact write to. */
12
11
  accessRole: string | null;
13
12
  /** IANA zone for rebuilding a zone-less stored clock, `""` when unreported. */
14
13
  timeZone: string;
15
14
  }
16
15
  /** Whether to refuse the whole push before touching a single event.
17
16
  *
18
- * Only on POSITIVE evidence of a non-writable role. An unlisted calendar is
19
- * unknown and must fall through: hard-denying it would block the feature
20
- * outright for a calendar the user can in fact write to. Those attempts surface
21
- * Google's own 403 per record if the write really is not allowed. */
17
+ * Only on POSITIVE evidence of a non-writable role. A calendar whose role
18
+ * neither source reported is unknown and must fall through: hard-denying it
19
+ * would block the feature outright for a calendar the user can in fact write
20
+ * to. Those attempts surface Google's own 403 per record if the write really is
21
+ * not allowed. */
22
22
  export declare const isDeniedAccessRole: (accessRole: string | null) => boolean;
23
+ /** A role Google left blank is UNKNOWN, not a denial. `isDeniedAccessRole`
24
+ * refuses every role it does not recognise — `""` included — so passing an
25
+ * unreported one straight through would refuse a calendar Google simply said
26
+ * nothing about. */
27
+ export declare const reportedAccessRole: (accessRole: string) => string | null;
23
28
  export interface CalendarCollectionPushResult {
24
29
  slug: string;
25
30
  created: number;
@@ -194,8 +194,8 @@ export declare function groupByCalendar(collections: readonly LoadedCollection[]
194
194
  * Structural so the rule can be exercised without building a LoadedCollection. */
195
195
  export interface CalendarDeclaring {
196
196
  googleCalendar?: {
197
- calendarId?: string;
198
- };
197
+ calendarId?: string | undefined;
198
+ } | undefined;
199
199
  }
200
200
  /** The canonical calendar whose sync token nothing needs any more, or null.
201
201
  *
@@ -1,12 +1,12 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../rolldown-runtime-D6vf50IK.cjs");
3
- const require_dist = require("../dist-21pMsI1a.cjs");
3
+ const require_dist = require("../dist-CsgSfWwR.cjs");
4
4
  const require_text = require("../text-CaAh9ZSO.cjs");
5
5
  const require_atomic = require("../atomic-C_7YpMiM.cjs");
6
6
  const require_utils_fetch = require("../utils/fetch.cjs");
7
- const require_calendarGrid = require("../calendarGrid-CaS9er8i.cjs");
8
- const require_project = require("../project-CCtT_fQr.cjs");
9
- const require_discovery = require("../discovery-DAoMnBYM.cjs");
7
+ const require_calendarGrid = require("../calendarGrid-p8K-7cuB.cjs");
8
+ const require_project = require("../project-C1ep9pvo.cjs");
9
+ const require_discovery = require("../discovery-DSLgRQhN.cjs");
10
10
  let node_fs = require("node:fs");
11
11
  let node_path = require("node:path");
12
12
  node_path = require_rolldown_runtime.__toESM(node_path, 1);
@@ -305,7 +305,7 @@ var EXPIRY_MARGIN_MS = ONE_MINUTE_MS;
305
305
  var createClient = (secret, redirectUri) => new google_auth_library.OAuth2Client({
306
306
  clientId: secret.client_id,
307
307
  clientSecret: secret.client_secret,
308
- redirectUri
308
+ ...redirectUri !== void 0 ? { redirectUri } : {}
309
309
  });
310
310
  var persistRotatedTokens = (client, home) => {
311
311
  client.on("tokens", (tokens) => {
@@ -593,6 +593,7 @@ var CALENDAR_LIST_PAGE_SIZE = 250;
593
593
  var MAX_CALENDAR_LIST_PAGES = 40;
594
594
  var EVENT_SYNC_PAGE_SIZE = 2500;
595
595
  var MAX_EVENT_SYNC_PAGES = 200;
596
+ var CALENDAR_META_PAGE_SIZE = 1;
596
597
  var HTTP_GONE = 410;
597
598
  var EVENT_ABSENT_STATUSES = [404, HTTP_GONE];
598
599
  /** An `If-Match` write whose etag no longer matches — someone else changed the
@@ -804,20 +805,34 @@ async function collectCalendarPages(fetchPage, maxPages = MAX_CALENDAR_LIST_PAGE
804
805
  }
805
806
  return calendars;
806
807
  }
807
- /** The calendars the user has added/subscribed to (primary + secondary +
808
- * shared), each with its id, name and colour, following pagination. Needs the
809
- * calendar-list read scope (GOOGLE_CALENDARLIST_SCOPE). */
810
- /** One calendar's own resource, addressed by id rather than looked up in the
811
- * user's list.
808
+ var toCalendarMeta = (value) => {
809
+ const record = asRecord(value);
810
+ return {
811
+ timeZone: stringField(record, "timeZone"),
812
+ accessRole: stringField(record, "accessRole")
813
+ };
814
+ };
815
+ /** One calendar's zone and the caller's role on it, read by id rather than
816
+ * looked up in the user's list.
812
817
  *
813
818
  * `calendarList` only holds calendars the user has ADDED; a calendar shared
814
- * with them can be readable and writable by id without appearing there. This is
815
- * how a push learns such a calendar's `timeZone`. Note the resource carries no
816
- * `accessRole` — that is a calendarList property — so reachability here says
817
- * nothing about writability. */
818
- async function getCalendar(accessToken, calendarId) {
819
- return toCalendarSummary(await googleRequest(CALENDAR_API_LABEL, accessToken, `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(canonicalCalendarId(calendarId))}`));
819
+ * with them can be readable and writable by id without appearing there, and a
820
+ * push still needs its zone to send an offset-less `dateTime`.
821
+ *
822
+ * Deliberately `events.list` and NOT `calendars.get`: the latter takes only
823
+ * full-calendar scopes (`calendar`, `calendar.readonly`, `calendar.calendars*`)
824
+ * and this app asks for none of them, so it answers 403 for every account it
825
+ * links (#2735). The events list carries the same `timeZone` — plus the
826
+ * `accessRole` the calendar resource does not have — under `calendar.events`,
827
+ * which the push already holds because it writes with it. Only the envelope is
828
+ * read; the one event asked for is the smallest page Google will serve. */
829
+ async function getCalendarMeta(accessToken, calendarId) {
830
+ const params = new URLSearchParams({ maxResults: String(CALENDAR_META_PAGE_SIZE) });
831
+ return toCalendarMeta(await googleRequest(CALENDAR_API_LABEL, accessToken, `${eventsUrl(calendarId)}?${params.toString()}`));
820
832
  }
833
+ /** The calendars the user has added/subscribed to (primary + secondary +
834
+ * shared), each with its id, name and colour, following pagination. Needs the
835
+ * calendar-list read scope (GOOGLE_CALENDARLIST_SCOPE). */
821
836
  async function listCalendars(accessToken) {
822
837
  return collectCalendarPages(async (pageToken) => {
823
838
  const params = new URLSearchParams({ maxResults: String(CALENDAR_LIST_PAGE_SIZE) });
@@ -868,9 +883,7 @@ async function readLockRaw(lockPath) {
868
883
  function parseLock(raw) {
869
884
  try {
870
885
  const parsed = JSON.parse(raw);
871
- if (typeof parsed !== "object" || parsed === null) return null;
872
- const { holder } = parsed;
873
- return typeof holder === "string" ? { holder } : null;
886
+ return require_dist.hasStringProp(parsed, "holder") ? { holder: parsed.holder } : null;
874
887
  } catch {
875
888
  return null;
876
889
  }
@@ -1369,7 +1382,7 @@ var PUSHABLE_SOURCE_FIELDS = [
1369
1382
  "description",
1370
1383
  "location"
1371
1384
  ];
1372
- var isPushableSource = (source) => PUSHABLE_SOURCE_FIELDS.includes(source);
1385
+ var isPushableSource = (source) => PUSHABLE_SOURCE_FIELDS.some((field) => field === source);
1373
1386
  /** The subset of a schema's `map` a push can act on: collection field → event field. */
1374
1387
  function pushableMap(map) {
1375
1388
  return Object.fromEntries(Object.entries(map).filter((entry) => isPushableSource(entry[1])));
@@ -1491,21 +1504,28 @@ function locallyDeletedIds(shadow, presentIds) {
1491
1504
  var WRITABLE_ACCESS_ROLES = ["owner", "writer"];
1492
1505
  /** Whether to refuse the whole push before touching a single event.
1493
1506
  *
1494
- * Only on POSITIVE evidence of a non-writable role. An unlisted calendar is
1495
- * unknown and must fall through: hard-denying it would block the feature
1496
- * outright for a calendar the user can in fact write to. Those attempts surface
1497
- * Google's own 403 per record if the write really is not allowed. */
1507
+ * Only on POSITIVE evidence of a non-writable role. A calendar whose role
1508
+ * neither source reported is unknown and must fall through: hard-denying it
1509
+ * would block the feature outright for a calendar the user can in fact write
1510
+ * to. Those attempts surface Google's own 403 per record if the write really is
1511
+ * not allowed. */
1498
1512
  var isDeniedAccessRole = (accessRole) => accessRole !== null && !WRITABLE_ACCESS_ROLES.includes(accessRole);
1513
+ /** A role Google left blank is UNKNOWN, not a denial. `isDeniedAccessRole`
1514
+ * refuses every role it does not recognise — `""` included — so passing an
1515
+ * unreported one straight through would refuse a calendar Google simply said
1516
+ * nothing about. */
1517
+ var reportedAccessRole = (accessRole) => accessRole || null;
1499
1518
  /** Resolve the calendar a schema names, for its writability and its timezone.
1500
1519
  *
1501
- * The list is consulted first because only it reports `accessRole`, which is
1502
- * what lets a read-only calendar be refused with the real reason instead of an
1503
- * opaque per-event 403. `"primary"` is matched on the `primary` flag, not the
1504
- * id — the primary calendar's own id is the account's email address, which never
1505
- * equals the literal the schema declares.
1520
+ * The list is consulted first because it answers for every added calendar in
1521
+ * one call the push already makes. `"primary"` is matched on the `primary`
1522
+ * flag, not the id — the primary calendar's own id is the account's email
1523
+ * address, which never equals the literal the schema declares.
1506
1524
  *
1507
- * A calendar absent from the list is NOT denied: it is fetched by id for its
1508
- * timezone and its role left unknown. A 404 there means the calendar really is
1525
+ * A calendar absent from the list is NOT denied: `calendarList` holds only the
1526
+ * calendars the user has added, so one shared with write access can be missing
1527
+ * from it (Codex review). It is asked about itself instead, which reports the
1528
+ * same zone AND the real role. A 404 there means the calendar is genuinely
1509
1529
  * unreachable, and propagates as a `failed` outcome. */
1510
1530
  async function liveCalendarMeta(accessToken, calendarId) {
1511
1531
  const key = canonicalCalendarId(calendarId);
@@ -1515,10 +1535,13 @@ async function liveCalendarMeta(accessToken, calendarId) {
1515
1535
  accessRole: listed.accessRole,
1516
1536
  timeZone: listed.timeZone
1517
1537
  };
1518
- const direct = await getCalendar(accessToken, calendarId);
1519
- log.info("google", "calendar is not in the user's list — pushing without an up-front role check", { calendarId: key });
1538
+ const direct = await getCalendarMeta(accessToken, calendarId);
1539
+ log.info("google", "calendar is not in the user's list — role read from the calendar itself", {
1540
+ calendarId: key,
1541
+ accessRole: direct.accessRole
1542
+ });
1520
1543
  return {
1521
- accessRole: null,
1544
+ accessRole: reportedAccessRole(direct.accessRole),
1522
1545
  timeZone: direct.timeZone
1523
1546
  };
1524
1547
  }
@@ -1703,9 +1726,9 @@ async function createOrAdopt(ctx, eventId, record) {
1703
1726
  }
1704
1727
  /** A 403 on a write is a permissions answer, but `googleApiError` appends the
1705
1728
  * "is the API enabled for the Cloud project?" hint to every 403 — true for a
1706
- * disabled API, misleading here. Reached when the up-front role check could not
1707
- * run because the calendar is not in the user's list, which is exactly when the
1708
- * user needs the real reason. */
1729
+ * disabled API, misleading here. Reached when the up-front gate let the push
1730
+ * through on an unknown role, which is exactly when the user needs the real
1731
+ * reason. */
1709
1732
  function writeFailure(eventId, error) {
1710
1733
  if (isGoogleApiError(error) && error.status === 403) return {
1711
1734
  kind: "skipped",
@@ -2734,9 +2757,9 @@ exports.deleteGoogleTokens = deleteGoogleTokens;
2734
2757
  exports.deleteTask = deleteTask;
2735
2758
  exports.fieldText = fieldText;
2736
2759
  exports.findClientSecretPath = findClientSecretPath;
2737
- exports.getCalendar = getCalendar;
2738
2760
  exports.getCalendarColors = getCalendarColors;
2739
2761
  exports.getCalendarEvent = getCalendarEvent;
2762
+ exports.getCalendarMeta = getCalendarMeta;
2740
2763
  exports.getGoogleAccessToken = getGoogleAccessToken;
2741
2764
  exports.googleApiError = googleApiError;
2742
2765
  exports.googleAuthFlow = googleAuthFlow;
@@ -2783,6 +2806,7 @@ exports.pushCollectionNow = pushCollectionNow;
2783
2806
  exports.pushableMap = pushableMap;
2784
2807
  exports.readDriveFile = readDriveFile;
2785
2808
  exports.releaseOrphanedCalendarToken = releaseOrphanedCalendarToken;
2809
+ exports.reportedAccessRole = reportedAccessRole;
2786
2810
  exports.resolveEventSpan = resolveEventSpan;
2787
2811
  exports.saveCalendarShadow = saveCalendarShadow;
2788
2812
  exports.saveCalendarSyncToken = saveCalendarSyncToken;
@@ -2794,6 +2818,7 @@ exports.syncCalendarForCollection = syncCalendarForCollection;
2794
2818
  exports.syncCalendarGroup = syncCalendarGroup;
2795
2819
  exports.syncDueCalendarCollections = syncDueCalendarCollections;
2796
2820
  exports.syncNewCalendarCollections = syncNewCalendarCollections;
2821
+ exports.toCalendarMeta = toCalendarMeta;
2797
2822
  exports.toCalendarSummary = toCalendarSummary;
2798
2823
  exports.toCollectionDateTime = toCollectionDateTime;
2799
2824
  exports.toCollectionRecord = toCollectionRecord;