@rudderhq/server 0.1.0-canary.8 → 0.1.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.
Files changed (235) hide show
  1. package/dist/bootstrap/register-api-routes.d.ts.map +1 -1
  2. package/dist/bootstrap/register-api-routes.js +2 -0
  3. package/dist/bootstrap/register-api-routes.js.map +1 -1
  4. package/dist/bundled-plugins/plugin-linear/README.md +22 -0
  5. package/dist/bundled-plugins/plugin-linear/dist/manifest.js +183 -0
  6. package/dist/bundled-plugins/plugin-linear/dist/manifest.js.map +7 -0
  7. package/dist/bundled-plugins/plugin-linear/dist/ui/index.js +1229 -0
  8. package/dist/bundled-plugins/plugin-linear/dist/ui/index.js.map +7 -0
  9. package/dist/bundled-plugins/plugin-linear/dist/worker.js +8251 -0
  10. package/dist/bundled-plugins/plugin-linear/dist/worker.js.map +7 -0
  11. package/dist/bundled-plugins/plugin-linear/package.json +42 -0
  12. package/dist/dev-server-status.d.ts +1 -7
  13. package/dist/dev-server-status.d.ts.map +1 -1
  14. package/dist/dev-server-status.js +1 -4
  15. package/dist/dev-server-status.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +77 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/langfuse-transcript.d.ts +1 -0
  21. package/dist/langfuse-transcript.d.ts.map +1 -1
  22. package/dist/langfuse-transcript.js +24 -0
  23. package/dist/langfuse-transcript.js.map +1 -1
  24. package/dist/onboarding-assets/ceo/MEMORY.md +13 -0
  25. package/dist/onboarding-assets/ceo/SOUL.md +28 -0
  26. package/dist/onboarding-assets/ceo/TOOLS.md +1 -1
  27. package/dist/onboarding-assets/default/MEMORY.md +13 -0
  28. package/dist/onboarding-assets/default/SOUL.md +29 -0
  29. package/dist/onboarding-assets/default/TOOLS.md +1 -1
  30. package/dist/routes/agents.d.ts.map +1 -1
  31. package/dist/routes/agents.js +4 -3
  32. package/dist/routes/agents.js.map +1 -1
  33. package/dist/routes/calendar.d.ts +3 -0
  34. package/dist/routes/calendar.d.ts.map +1 -0
  35. package/dist/routes/calendar.js +265 -0
  36. package/dist/routes/calendar.js.map +1 -0
  37. package/dist/routes/chats.d.ts.map +1 -1
  38. package/dist/routes/chats.js +149 -21
  39. package/dist/routes/chats.js.map +1 -1
  40. package/dist/routes/dashboard.d.ts.map +1 -1
  41. package/dist/routes/dashboard.js +24 -0
  42. package/dist/routes/dashboard.js.map +1 -1
  43. package/dist/routes/goals.d.ts.map +1 -1
  44. package/dist/routes/goals.js +10 -0
  45. package/dist/routes/goals.js.map +1 -1
  46. package/dist/routes/health.d.ts.map +1 -1
  47. package/dist/routes/health.js +3 -12
  48. package/dist/routes/health.js.map +1 -1
  49. package/dist/routes/index.d.ts +1 -0
  50. package/dist/routes/index.d.ts.map +1 -1
  51. package/dist/routes/index.js +1 -0
  52. package/dist/routes/index.js.map +1 -1
  53. package/dist/routes/instance-settings.d.ts.map +1 -1
  54. package/dist/routes/instance-settings.js +1 -26
  55. package/dist/routes/instance-settings.js.map +1 -1
  56. package/dist/routes/issues.d.ts.map +1 -1
  57. package/dist/routes/issues.js +74 -34
  58. package/dist/routes/issues.js.map +1 -1
  59. package/dist/routes/orgs.d.ts.map +1 -1
  60. package/dist/routes/orgs.js +161 -145
  61. package/dist/routes/orgs.js.map +1 -1
  62. package/dist/routes/plugins.d.ts.map +1 -1
  63. package/dist/routes/plugins.js +30 -7
  64. package/dist/routes/plugins.js.map +1 -1
  65. package/dist/services/agent-instructions.d.ts.map +1 -1
  66. package/dist/services/agent-instructions.js +23 -7
  67. package/dist/services/agent-instructions.js.map +1 -1
  68. package/dist/services/agent-run-context.d.ts +1 -1
  69. package/dist/services/agent-run-context.js +1 -1
  70. package/dist/services/agent-run-context.js.map +1 -1
  71. package/dist/services/agents.d.ts +13 -13
  72. package/dist/services/assets.d.ts +2 -2
  73. package/dist/services/calendar.d.ts +137 -0
  74. package/dist/services/calendar.d.ts.map +1 -0
  75. package/dist/services/calendar.js +1279 -0
  76. package/dist/services/calendar.js.map +1 -0
  77. package/dist/services/chat-assistant.d.ts.map +1 -1
  78. package/dist/services/chat-assistant.js +75 -15
  79. package/dist/services/chat-assistant.js.map +1 -1
  80. package/dist/services/chat-generation-locks.d.ts +2 -1
  81. package/dist/services/chat-generation-locks.d.ts.map +1 -1
  82. package/dist/services/chat-generation-locks.js +12 -3
  83. package/dist/services/chat-generation-locks.js.map +1 -1
  84. package/dist/services/chats.d.ts +4 -2
  85. package/dist/services/chats.d.ts.map +1 -1
  86. package/dist/services/chats.js +2 -15
  87. package/dist/services/chats.js.map +1 -1
  88. package/dist/services/costs.d.ts +2 -2
  89. package/dist/services/default-agent-instructions.d.ts +2 -2
  90. package/dist/services/default-agent-instructions.js +2 -2
  91. package/dist/services/documents.d.ts +23 -0
  92. package/dist/services/documents.d.ts.map +1 -1
  93. package/dist/services/documents.js +17 -1
  94. package/dist/services/documents.js.map +1 -1
  95. package/dist/services/export-jobs.d.ts +16 -0
  96. package/dist/services/export-jobs.d.ts.map +1 -0
  97. package/dist/services/export-jobs.js +147 -0
  98. package/dist/services/export-jobs.js.map +1 -0
  99. package/dist/services/finance.d.ts +6 -6
  100. package/dist/services/goals.d.ts +16 -10
  101. package/dist/services/goals.d.ts.map +1 -1
  102. package/dist/services/goals.js +201 -18
  103. package/dist/services/goals.js.map +1 -1
  104. package/dist/services/index.d.ts +3 -0
  105. package/dist/services/index.d.ts.map +1 -1
  106. package/dist/services/index.js +3 -0
  107. package/dist/services/index.js.map +1 -1
  108. package/dist/services/instance-settings.d.ts +1 -3
  109. package/dist/services/instance-settings.d.ts.map +1 -1
  110. package/dist/services/instance-settings.js +1 -38
  111. package/dist/services/instance-settings.js.map +1 -1
  112. package/dist/services/issue-approvals.d.ts +1 -1
  113. package/dist/services/issues.d.ts +12 -0
  114. package/dist/services/issues.d.ts.map +1 -1
  115. package/dist/services/issues.js +107 -1
  116. package/dist/services/issues.js.map +1 -1
  117. package/dist/services/knowledge-portability/organization-portability.d.ts +12 -2
  118. package/dist/services/knowledge-portability/organization-portability.d.ts.map +1 -1
  119. package/dist/services/knowledge-portability/organization-portability.js +77 -4
  120. package/dist/services/knowledge-portability/organization-portability.js.map +1 -1
  121. package/dist/services/messenger.d.ts +2 -2
  122. package/dist/services/messenger.d.ts.map +1 -1
  123. package/dist/services/messenger.js +67 -27
  124. package/dist/services/messenger.js.map +1 -1
  125. package/dist/services/organization-workspace-browser.d.ts.map +1 -1
  126. package/dist/services/organization-workspace-browser.js +3 -0
  127. package/dist/services/organization-workspace-browser.js.map +1 -1
  128. package/dist/services/plugin-registry.d.ts +8 -8
  129. package/dist/services/runtime-kernel/heartbeat.d.ts +6 -0
  130. package/dist/services/runtime-kernel/heartbeat.d.ts.map +1 -1
  131. package/dist/services/runtime-kernel/heartbeat.js +236 -99
  132. package/dist/services/runtime-kernel/heartbeat.js.map +1 -1
  133. package/dist/services/runtime-kernel/model-fallback.d.ts +10 -0
  134. package/dist/services/runtime-kernel/model-fallback.d.ts.map +1 -0
  135. package/dist/services/runtime-kernel/model-fallback.js +147 -0
  136. package/dist/services/runtime-kernel/model-fallback.js.map +1 -0
  137. package/dist/services/secrets.d.ts +1 -3
  138. package/dist/services/secrets.d.ts.map +1 -1
  139. package/dist/services/secrets.js +55 -30
  140. package/dist/services/secrets.js.map +1 -1
  141. package/dist/services/workspace-backups.d.ts +34 -0
  142. package/dist/services/workspace-backups.d.ts.map +1 -0
  143. package/dist/services/workspace-backups.js +519 -0
  144. package/dist/services/workspace-backups.js.map +1 -0
  145. package/dist/services/workspace-runtime.d.ts +2 -2
  146. package/package.json +14 -14
  147. package/resources/bundled-skills/para-memory-files/SKILL.md +3 -1
  148. package/resources/bundled-skills/rudder-create-agent/SKILL.md +21 -4
  149. package/resources/bundled-skills/rudder-create-agent/references/api-reference.md +8 -3
  150. package/resources/bundled-skills/rudder-create-agent/references/cli-reference.md +8 -2
  151. package/skills/para-memory-files/SKILL.md +3 -1
  152. package/skills/rudder-create-agent/SKILL.md +21 -4
  153. package/skills/rudder-create-agent/references/api-reference.md +8 -3
  154. package/skills/rudder-create-agent/references/cli-reference.md +8 -2
  155. package/ui-dist/assets/{_basePickBy-C5FevVGb.js → _basePickBy-9EA6dBFj.js} +1 -1
  156. package/ui-dist/assets/{_baseUniq-Bp5Cq-Lt.js → _baseUniq-puJRDjRm.js} +1 -1
  157. package/ui-dist/assets/{arc-DxCinQZQ.js → arc-BuvB_2Wz.js} +1 -1
  158. package/ui-dist/assets/{architectureDiagram-2XIMDMQ5-Bt4OB6rg.js → architectureDiagram-2XIMDMQ5-DNH3NcPr.js} +1 -1
  159. package/ui-dist/assets/{blockDiagram-WCTKOSBZ-AfUyCHdW.js → blockDiagram-WCTKOSBZ-CCjA-egI.js} +1 -1
  160. package/ui-dist/assets/{c4Diagram-IC4MRINW-ZQmapm_f.js → c4Diagram-IC4MRINW-DaAxG30_.js} +1 -1
  161. package/ui-dist/assets/channel-BHmUwLHY.js +1 -0
  162. package/ui-dist/assets/{chunk-4BX2VUAB-b-nhg8XG.js → chunk-4BX2VUAB-CuuLnPLx.js} +1 -1
  163. package/ui-dist/assets/{chunk-55IACEB6-D_mWeaWL.js → chunk-55IACEB6-7KqKHU50.js} +1 -1
  164. package/ui-dist/assets/{chunk-FMBD7UC4-CvCBPkxY.js → chunk-FMBD7UC4-CquRnk_C.js} +1 -1
  165. package/ui-dist/assets/{chunk-JSJVCQXG-CyIzde6d.js → chunk-JSJVCQXG-Cub6UI-9.js} +1 -1
  166. package/ui-dist/assets/{chunk-KX2RTZJC-664uOAt1.js → chunk-KX2RTZJC-D-R4Pk61.js} +1 -1
  167. package/ui-dist/assets/{chunk-NQ4KR5QH-zC9eKlQL.js → chunk-NQ4KR5QH-YQLRgLCT.js} +1 -1
  168. package/ui-dist/assets/{chunk-QZHKN3VN-Bso6mrAm.js → chunk-QZHKN3VN-BgxQG6QM.js} +1 -1
  169. package/ui-dist/assets/{chunk-WL4C6EOR-CGgjDf4Q.js → chunk-WL4C6EOR-CVJNOFb-.js} +1 -1
  170. package/ui-dist/assets/classDiagram-VBA2DB6C-BykYYXhO.js +1 -0
  171. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-BykYYXhO.js +1 -0
  172. package/ui-dist/assets/clone-BjbqkGJk.js +1 -0
  173. package/ui-dist/assets/{cose-bilkent-S5V4N54A-ChfhiHs0.js → cose-bilkent-S5V4N54A-BGYYdPRC.js} +1 -1
  174. package/ui-dist/assets/{dagre-KLK3FWXG-BtdGql15.js → dagre-KLK3FWXG-CDgRaJNK.js} +1 -1
  175. package/ui-dist/assets/{diagram-E7M64L7V-CcQq6lyW.js → diagram-E7M64L7V-CQEBiicN.js} +1 -1
  176. package/ui-dist/assets/{diagram-IFDJBPK2-C8MRQ8-O.js → diagram-IFDJBPK2-cGKTVrZq.js} +1 -1
  177. package/ui-dist/assets/{diagram-P4PSJMXO-wDtyafSS.js → diagram-P4PSJMXO-fGAfKBU_.js} +1 -1
  178. package/ui-dist/assets/{erDiagram-INFDFZHY-DSPOGKs9.js → erDiagram-INFDFZHY-DW5vJI98.js} +1 -1
  179. package/ui-dist/assets/{flowDiagram-PKNHOUZH-CMRO_o51.js → flowDiagram-PKNHOUZH-CikVuzCR.js} +1 -1
  180. package/ui-dist/assets/{ganttDiagram-A5KZAMGK-ByVpG5X7.js → ganttDiagram-A5KZAMGK-Ca4perbO.js} +1 -1
  181. package/ui-dist/assets/{gitGraphDiagram-K3NZZRJ6-C0hZhA2f.js → gitGraphDiagram-K3NZZRJ6-hkDkX0wB.js} +1 -1
  182. package/ui-dist/assets/{graph-8ZSpiLvu.js → graph-CKVwuNpm.js} +1 -1
  183. package/ui-dist/assets/{index-Jl3ZTphD.js → index-B24_1Y25.js} +1 -1
  184. package/ui-dist/assets/{index-Bnqrds93.js → index-BCSq0Y_A.js} +1 -1
  185. package/ui-dist/assets/{index-LxYtcd2q.js → index-BbX5RwLL.js} +1 -1
  186. package/ui-dist/assets/{index-BuxAGDe1.js → index-Bj5f8srw.js} +1 -1
  187. package/ui-dist/assets/{index-CrjKYwlq.js → index-Bm5RRuGQ.js} +1 -1
  188. package/ui-dist/assets/{index-Byt3a14a.js → index-BzHEDVXA.js} +1 -1
  189. package/ui-dist/assets/{index-C96r3ncF.js → index-C5IbLmrM.js} +1 -1
  190. package/ui-dist/assets/{index-DSa_Y_jA.js → index-CBuiHrHJ.js} +1 -1
  191. package/ui-dist/assets/{index-CsgWTWOx.js → index-CGtsmbZm.js} +1 -1
  192. package/ui-dist/assets/{index-Dolr9Kee.js → index-CIlRDiw5.js} +1 -1
  193. package/ui-dist/assets/{index-tGztn4Is.js → index-CT8eqX9W.js} +1 -1
  194. package/ui-dist/assets/{index-CeJdOYIF.js → index-CjD2xZdW.js} +1 -1
  195. package/ui-dist/assets/{index-C7DEZ3Ju.js → index-DFeHRm34.js} +1 -1
  196. package/ui-dist/assets/{index-ChJl_hqp.js → index-DI-FLO2Z.js} +1 -1
  197. package/ui-dist/assets/{index-D083o6by.js → index-DJ84yjUf.js} +1 -1
  198. package/ui-dist/assets/index-DTw34fFZ.js +1398 -0
  199. package/ui-dist/assets/{index--8IW0gQi.js → index-DZ6kUIBM.js} +1 -1
  200. package/ui-dist/assets/{index-D5fB3OrO.js → index-DdFp0EEO.js} +1 -1
  201. package/ui-dist/assets/{index-BYlbpnGO.js → index-Dm4kNTCW.js} +1 -1
  202. package/ui-dist/assets/{index-DIlroFT7.js → index-aK5eezHP.js} +1 -1
  203. package/ui-dist/assets/{index-DoCNo7J9.js → index-dd4k0fyq.js} +1 -1
  204. package/ui-dist/assets/index-jnv9Ql_2.css +1 -0
  205. package/ui-dist/assets/{index-Do2QEU2O.js → index-kGMjx6qb.js} +1 -1
  206. package/ui-dist/assets/{index-DGliz_Zl.js → index-qEEWalog.js} +1 -1
  207. package/ui-dist/assets/{infoDiagram-LFFYTUFH-CRObxa1Q.js → infoDiagram-LFFYTUFH-aDNdkSKW.js} +1 -1
  208. package/ui-dist/assets/{ishikawaDiagram-PHBUUO56-ksXzVP6h.js → ishikawaDiagram-PHBUUO56-CmclzHhC.js} +1 -1
  209. package/ui-dist/assets/{journeyDiagram-4ABVD52K-DhLhkeS3.js → journeyDiagram-4ABVD52K-BFnBxKuG.js} +1 -1
  210. package/ui-dist/assets/{kanban-definition-K7BYSVSG-CJPHwSur.js → kanban-definition-K7BYSVSG-eJfOZg7R.js} +1 -1
  211. package/ui-dist/assets/{layout-CbB6lAw2.js → layout-CLRiNHgA.js} +1 -1
  212. package/ui-dist/assets/{linear-HPte01nq.js → linear-B-J9sUer.js} +1 -1
  213. package/ui-dist/assets/{mermaid.core-CaHTquLw.js → mermaid.core-C1MjBOIN.js} +4 -4
  214. package/ui-dist/assets/{mindmap-definition-YRQLILUH-CeZ9z-BE.js → mindmap-definition-YRQLILUH-BdvCmP6e.js} +1 -1
  215. package/ui-dist/assets/{pieDiagram-SKSYHLDU-YB621clF.js → pieDiagram-SKSYHLDU-BAITPD_t.js} +1 -1
  216. package/ui-dist/assets/{quadrantDiagram-337W2JSQ-KPDGBXfE.js → quadrantDiagram-337W2JSQ-BFnjyhzq.js} +1 -1
  217. package/ui-dist/assets/{requirementDiagram-Z7DCOOCP-CnMP-_Rj.js → requirementDiagram-Z7DCOOCP-Bxg6tlLh.js} +1 -1
  218. package/ui-dist/assets/{sankeyDiagram-WA2Y5GQK-rWDbj38-.js → sankeyDiagram-WA2Y5GQK-LPpklLQK.js} +1 -1
  219. package/ui-dist/assets/{sequenceDiagram-2WXFIKYE-D5IlEfYm.js → sequenceDiagram-2WXFIKYE-D-W6lss0.js} +1 -1
  220. package/ui-dist/assets/{stateDiagram-RAJIS63D-CI6m7yMI.js → stateDiagram-RAJIS63D-Bzo5M8P7.js} +1 -1
  221. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-DJ1MxF2S.js +1 -0
  222. package/ui-dist/assets/{timeline-definition-YZTLITO2-Bl1-YzON.js → timeline-definition-YZTLITO2-luuVqyTW.js} +1 -1
  223. package/ui-dist/assets/{treemap-KZPCXAKY-CcFSGzuM.js → treemap-KZPCXAKY-ChGqzx5u.js} +1 -1
  224. package/ui-dist/assets/{vennDiagram-LZ73GAT5-DpgfFxeZ.js → vennDiagram-LZ73GAT5-BCEjZinK.js} +1 -1
  225. package/ui-dist/assets/{xychartDiagram-JWTSCODW-Bas4tWGP.js → xychartDiagram-JWTSCODW-mAsE6hMg.js} +1 -1
  226. package/ui-dist/index.html +2 -2
  227. package/dist/onboarding-assets/ceo/AGENTS.md +0 -33
  228. package/dist/onboarding-assets/default/AGENTS.md +0 -9
  229. package/ui-dist/assets/channel-B-3UKZ6E.js +0 -1
  230. package/ui-dist/assets/classDiagram-VBA2DB6C-DJbF61vn.js +0 -1
  231. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-DJbF61vn.js +0 -1
  232. package/ui-dist/assets/clone-B7Z_Fd8l.js +0 -1
  233. package/ui-dist/assets/index-B4jXCLTd.js +0 -1358
  234. package/ui-dist/assets/index-C187WwUh.css +0 -1
  235. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-DMNsLapT.js +0 -1
@@ -0,0 +1,1279 @@
1
+ import { and, asc, desc, eq, gt, inArray, isNull, lt, sql } from "drizzle-orm";
2
+ import { activityLog, agents, approvals, calendarEvents, calendarSources, goals, heartbeatRuns, issues, projects, } from "@rudderhq/db";
3
+ import { SECRET_PROVIDERS } from "@rudderhq/shared";
4
+ import { conflict, forbidden, notFound, unprocessable } from "../errors.js";
5
+ import { asBoolean, asNumber, parseObject } from "../agent-runtimes/utils.js";
6
+ import { secretService } from "./secrets.js";
7
+ const PROJECTED_HEARTBEAT_DURATION_MS = 15 * 60 * 1000;
8
+ const PROJECTED_HEARTBEAT_MAX_PER_AGENT = 96;
9
+ const GOOGLE_CALENDAR_OAUTH_SECRET_NAME = "google_calendar_oauth_credentials";
10
+ const GOOGLE_CALENDAR_REQUIRED_ENV = ["GOOGLE_CALENDAR_CLIENT_ID", "GOOGLE_CALENDAR_CLIENT_SECRET"];
11
+ const GOOGLE_CALENDAR_ACCEPTED_ENV_ALIASES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
12
+ function sanitizeSource(row) {
13
+ const rawCursor = row.syncCursorJson ?? null;
14
+ const cursor = rawCursor && typeof rawCursor === "object"
15
+ ? {
16
+ ...rawCursor,
17
+ accessToken: typeof rawCursor.accessToken === "string" ? "[redacted]" : undefined,
18
+ refreshToken: typeof rawCursor.refreshToken === "string" ? "[redacted]" : undefined,
19
+ }
20
+ : null;
21
+ return {
22
+ ...row,
23
+ type: row.type,
24
+ ownerType: row.ownerType,
25
+ visibilityDefault: row.visibilityDefault,
26
+ status: row.status,
27
+ syncCursorJson: cursor,
28
+ };
29
+ }
30
+ function csvIncludes(filters, value) {
31
+ return !filters || filters.length === 0 || filters.includes(value);
32
+ }
33
+ function parseSyncCursor(value) {
34
+ if (!value)
35
+ return {};
36
+ return value;
37
+ }
38
+ function envGoogleCredentials() {
39
+ const clientId = process.env.GOOGLE_CALENDAR_CLIENT_ID?.trim() || process.env.GOOGLE_CLIENT_ID?.trim() || "";
40
+ const clientSecret = process.env.GOOGLE_CALENDAR_CLIENT_SECRET?.trim() || process.env.GOOGLE_CLIENT_SECRET?.trim() || "";
41
+ return clientId && clientSecret ? { clientId, clientSecret, managedByEnv: true } : null;
42
+ }
43
+ function parseStoredGoogleCredentials(value) {
44
+ try {
45
+ const parsed = JSON.parse(value);
46
+ const clientId = typeof parsed.clientId === "string" ? parsed.clientId.trim() : "";
47
+ const clientSecret = typeof parsed.clientSecret === "string" ? parsed.clientSecret.trim() : "";
48
+ return clientId && clientSecret ? { clientId, clientSecret } : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function serializeGoogleCredentials(credentials) {
55
+ return JSON.stringify({
56
+ clientId: credentials.clientId,
57
+ clientSecret: credentials.clientSecret,
58
+ });
59
+ }
60
+ function defaultSecretProvider() {
61
+ const configured = process.env.RUDDER_SECRETS_PROVIDER;
62
+ return (configured && SECRET_PROVIDERS.includes(configured)
63
+ ? configured
64
+ : "local_encrypted");
65
+ }
66
+ function googleCalendarIdForListItem(item) {
67
+ return item.primary ? "primary" : item.id?.trim() || null;
68
+ }
69
+ function sourceHasGoogleToken(source) {
70
+ const cursor = parseSyncCursor(source?.syncCursorJson);
71
+ return typeof cursor.accessToken === "string" || typeof cursor.refreshToken === "string";
72
+ }
73
+ function parseHeartbeatPolicy(agent) {
74
+ const runtimeConfig = parseObject(agent.runtimeConfig);
75
+ const heartbeat = parseObject(runtimeConfig.heartbeat);
76
+ return {
77
+ enabled: asBoolean(heartbeat.enabled, true),
78
+ intervalSec: Math.max(0, asNumber(heartbeat.intervalSec, 0)),
79
+ };
80
+ }
81
+ function eventSummary(event) {
82
+ return {
83
+ title: event.title,
84
+ eventKind: event.eventKind,
85
+ eventStatus: event.eventStatus,
86
+ startAt: event.startAt.toISOString(),
87
+ endAt: event.endAt.toISOString(),
88
+ ownerAgentId: event.ownerAgentId,
89
+ issueId: event.issueId,
90
+ };
91
+ }
92
+ function createEventValues(orgId, input, actor) {
93
+ return {
94
+ orgId,
95
+ sourceId: input.sourceId ?? null,
96
+ eventKind: input.eventKind,
97
+ eventStatus: input.eventStatus,
98
+ ownerType: input.ownerType,
99
+ ownerUserId: input.ownerUserId ?? null,
100
+ ownerAgentId: input.ownerAgentId ?? null,
101
+ title: input.title,
102
+ description: input.description ?? null,
103
+ startAt: input.startAt,
104
+ endAt: input.endAt,
105
+ timezone: input.timezone,
106
+ allDay: input.allDay,
107
+ visibility: input.visibility,
108
+ issueId: input.issueId ?? null,
109
+ projectId: input.projectId ?? null,
110
+ goalId: input.goalId ?? null,
111
+ approvalId: input.approvalId ?? null,
112
+ heartbeatRunId: input.heartbeatRunId ?? null,
113
+ activityId: input.activityId ?? null,
114
+ sourceMode: input.sourceMode,
115
+ externalProvider: input.externalProvider ?? null,
116
+ externalCalendarId: input.externalCalendarId ?? null,
117
+ externalEventId: input.externalEventId ?? null,
118
+ externalEtag: input.externalEtag ?? null,
119
+ externalUpdatedAt: input.externalUpdatedAt ?? null,
120
+ createdByUserId: actor?.userId ?? null,
121
+ updatedByUserId: actor?.userId ?? null,
122
+ };
123
+ }
124
+ function updateEventValues(input, actor) {
125
+ const values = {
126
+ updatedByUserId: actor?.userId ?? null,
127
+ };
128
+ if (input.sourceId !== undefined)
129
+ values.sourceId = input.sourceId ?? null;
130
+ if (input.eventKind !== undefined)
131
+ values.eventKind = input.eventKind;
132
+ if (input.eventStatus !== undefined)
133
+ values.eventStatus = input.eventStatus;
134
+ if (input.ownerType !== undefined)
135
+ values.ownerType = input.ownerType;
136
+ if (input.ownerUserId !== undefined)
137
+ values.ownerUserId = input.ownerUserId ?? null;
138
+ if (input.ownerAgentId !== undefined)
139
+ values.ownerAgentId = input.ownerAgentId ?? null;
140
+ if (input.title !== undefined)
141
+ values.title = input.title;
142
+ if (input.description !== undefined)
143
+ values.description = input.description ?? null;
144
+ if (input.startAt !== undefined)
145
+ values.startAt = input.startAt;
146
+ if (input.endAt !== undefined)
147
+ values.endAt = input.endAt;
148
+ if (input.timezone !== undefined)
149
+ values.timezone = input.timezone;
150
+ if (input.allDay !== undefined)
151
+ values.allDay = input.allDay;
152
+ if (input.visibility !== undefined)
153
+ values.visibility = input.visibility;
154
+ if (input.issueId !== undefined)
155
+ values.issueId = input.issueId ?? null;
156
+ if (input.projectId !== undefined)
157
+ values.projectId = input.projectId ?? null;
158
+ if (input.goalId !== undefined)
159
+ values.goalId = input.goalId ?? null;
160
+ if (input.approvalId !== undefined)
161
+ values.approvalId = input.approvalId ?? null;
162
+ if (input.heartbeatRunId !== undefined)
163
+ values.heartbeatRunId = input.heartbeatRunId ?? null;
164
+ if (input.activityId !== undefined)
165
+ values.activityId = input.activityId ?? null;
166
+ if (input.sourceMode !== undefined)
167
+ values.sourceMode = input.sourceMode;
168
+ if (input.externalProvider !== undefined)
169
+ values.externalProvider = input.externalProvider ?? null;
170
+ if (input.externalCalendarId !== undefined)
171
+ values.externalCalendarId = input.externalCalendarId ?? null;
172
+ if (input.externalEventId !== undefined)
173
+ values.externalEventId = input.externalEventId ?? null;
174
+ if (input.externalEtag !== undefined)
175
+ values.externalEtag = input.externalEtag ?? null;
176
+ if (input.externalUpdatedAt !== undefined)
177
+ values.externalUpdatedAt = input.externalUpdatedAt ?? null;
178
+ return values;
179
+ }
180
+ function mergeEventInput(existing, input) {
181
+ return {
182
+ sourceId: input.sourceId === undefined ? existing.sourceId : input.sourceId,
183
+ eventKind: (input.eventKind ?? existing.eventKind),
184
+ eventStatus: (input.eventStatus ?? existing.eventStatus),
185
+ ownerType: (input.ownerType ?? existing.ownerType),
186
+ ownerUserId: input.ownerUserId === undefined ? existing.ownerUserId : input.ownerUserId,
187
+ ownerAgentId: input.ownerAgentId === undefined ? existing.ownerAgentId : input.ownerAgentId,
188
+ title: input.title ?? existing.title,
189
+ description: input.description === undefined ? existing.description : input.description,
190
+ startAt: input.startAt ?? existing.startAt,
191
+ endAt: input.endAt ?? existing.endAt,
192
+ timezone: input.timezone ?? existing.timezone,
193
+ allDay: input.allDay ?? existing.allDay,
194
+ visibility: (input.visibility ?? existing.visibility),
195
+ issueId: input.issueId === undefined ? existing.issueId : input.issueId,
196
+ projectId: input.projectId === undefined ? existing.projectId : input.projectId,
197
+ goalId: input.goalId === undefined ? existing.goalId : input.goalId,
198
+ approvalId: input.approvalId === undefined ? existing.approvalId : input.approvalId,
199
+ heartbeatRunId: input.heartbeatRunId === undefined ? existing.heartbeatRunId : input.heartbeatRunId,
200
+ activityId: input.activityId === undefined ? existing.activityId : input.activityId,
201
+ sourceMode: (input.sourceMode ?? existing.sourceMode),
202
+ externalProvider: input.externalProvider === undefined ? existing.externalProvider : input.externalProvider,
203
+ externalCalendarId: input.externalCalendarId === undefined ? existing.externalCalendarId : input.externalCalendarId,
204
+ externalEventId: input.externalEventId === undefined ? existing.externalEventId : input.externalEventId,
205
+ externalEtag: input.externalEtag === undefined ? existing.externalEtag : input.externalEtag,
206
+ externalUpdatedAt: input.externalUpdatedAt === undefined ? existing.externalUpdatedAt : input.externalUpdatedAt,
207
+ };
208
+ }
209
+ export function calendarService(db) {
210
+ const issueIdAsText = sql `${issues.id}::text`;
211
+ const contextIssueId = sql `${heartbeatRuns.contextSnapshot} ->> 'issueId'`;
212
+ const secrets = secretService(db);
213
+ async function storedGoogleCredentials(orgId) {
214
+ const secret = await secrets.getByName(orgId, GOOGLE_CALENDAR_OAUTH_SECRET_NAME);
215
+ if (!secret)
216
+ return null;
217
+ const value = await secrets.resolveSecretValue(orgId, secret.id, "latest");
218
+ const credentials = parseStoredGoogleCredentials(value);
219
+ return credentials ? { ...credentials, managedByEnv: false } : null;
220
+ }
221
+ async function googleCredentials(orgId) {
222
+ return envGoogleCredentials() ?? await storedGoogleCredentials(orgId);
223
+ }
224
+ async function googleOAuthConfig(orgId, redirectUri) {
225
+ const credentials = await googleCredentials(orgId);
226
+ return {
227
+ clientId: credentials?.clientId ?? "",
228
+ clientSecretConfigured: !!credentials,
229
+ managedByEnv: credentials?.managedByEnv ?? false,
230
+ redirectUri,
231
+ requiredEnv: [...GOOGLE_CALENDAR_REQUIRED_ENV],
232
+ acceptedAliases: [...GOOGLE_CALENDAR_ACCEPTED_ENV_ALIASES],
233
+ };
234
+ }
235
+ async function updateGoogleOAuthConfig(orgId, input, redirectUri, actor) {
236
+ if (envGoogleCredentials()) {
237
+ throw unprocessable("Google Calendar OAuth is managed by server environment variables");
238
+ }
239
+ const existingSecret = await secrets.getByName(orgId, GOOGLE_CALENDAR_OAUTH_SECRET_NAME);
240
+ if (input.clear) {
241
+ if (existingSecret)
242
+ await secrets.remove(existingSecret.id);
243
+ return googleOAuthConfig(orgId, redirectUri);
244
+ }
245
+ const existingCredentials = existingSecret
246
+ ? parseStoredGoogleCredentials(await secrets.resolveSecretValue(orgId, existingSecret.id, "latest"))
247
+ : null;
248
+ const clientId = input.clientId?.trim() || existingCredentials?.clientId || "";
249
+ const clientSecret = input.clientSecret?.trim() || existingCredentials?.clientSecret || "";
250
+ if (!clientId)
251
+ throw unprocessable("Google Calendar client ID is required");
252
+ if (!clientSecret)
253
+ throw unprocessable("Google Calendar client secret is required");
254
+ const value = serializeGoogleCredentials({ clientId, clientSecret });
255
+ if (existingSecret) {
256
+ await secrets.rotate(existingSecret.id, { value }, { userId: actor?.userId ?? "board", agentId: null });
257
+ }
258
+ else {
259
+ await secrets.create(orgId, {
260
+ name: GOOGLE_CALENDAR_OAUTH_SECRET_NAME,
261
+ provider: defaultSecretProvider(),
262
+ value,
263
+ description: "Google Calendar OAuth client credentials used for read-only calendar import.",
264
+ }, { userId: actor?.userId ?? "board", agentId: null });
265
+ }
266
+ return googleOAuthConfig(orgId, redirectUri);
267
+ }
268
+ async function assertSourceOrg(orgId, sourceId) {
269
+ if (!sourceId)
270
+ return null;
271
+ const source = await db
272
+ .select()
273
+ .from(calendarSources)
274
+ .where(and(eq(calendarSources.id, sourceId), eq(calendarSources.orgId, orgId)))
275
+ .then((rows) => rows[0] ?? null);
276
+ if (!source)
277
+ throw notFound("Calendar source not found");
278
+ return source;
279
+ }
280
+ async function assertAgentOrg(orgId, agentId) {
281
+ if (!agentId)
282
+ return null;
283
+ const agent = await db
284
+ .select({ id: agents.id, orgId: agents.orgId, status: agents.status })
285
+ .from(agents)
286
+ .where(eq(agents.id, agentId))
287
+ .then((rows) => rows[0] ?? null);
288
+ if (!agent)
289
+ throw notFound("Agent not found");
290
+ if (agent.orgId !== orgId)
291
+ throw unprocessable("Agent must belong to same organization");
292
+ if (agent.status === "terminated")
293
+ throw conflict("Cannot create calendar blocks for terminated agents");
294
+ return agent;
295
+ }
296
+ async function assertIssueOrg(orgId, issueId) {
297
+ if (!issueId)
298
+ return null;
299
+ const issue = await db
300
+ .select({ id: issues.id, orgId: issues.orgId, hiddenAt: issues.hiddenAt })
301
+ .from(issues)
302
+ .where(eq(issues.id, issueId))
303
+ .then((rows) => rows[0] ?? null);
304
+ if (!issue || issue.hiddenAt)
305
+ throw notFound("Issue not found");
306
+ if (issue.orgId !== orgId)
307
+ throw unprocessable("Issue must belong to same organization");
308
+ return issue;
309
+ }
310
+ async function assertProjectOrg(orgId, projectId) {
311
+ if (!projectId)
312
+ return null;
313
+ const project = await db
314
+ .select({ id: projects.id, orgId: projects.orgId })
315
+ .from(projects)
316
+ .where(eq(projects.id, projectId))
317
+ .then((rows) => rows[0] ?? null);
318
+ if (!project)
319
+ throw notFound("Project not found");
320
+ if (project.orgId !== orgId)
321
+ throw unprocessable("Project must belong to same organization");
322
+ return project;
323
+ }
324
+ async function assertGoalOrg(orgId, goalId) {
325
+ if (!goalId)
326
+ return null;
327
+ const goal = await db
328
+ .select({ id: goals.id, orgId: goals.orgId })
329
+ .from(goals)
330
+ .where(eq(goals.id, goalId))
331
+ .then((rows) => rows[0] ?? null);
332
+ if (!goal)
333
+ throw notFound("Goal not found");
334
+ if (goal.orgId !== orgId)
335
+ throw unprocessable("Goal must belong to same organization");
336
+ return goal;
337
+ }
338
+ async function assertApprovalOrg(orgId, approvalId) {
339
+ if (!approvalId)
340
+ return null;
341
+ const approval = await db
342
+ .select({ id: approvals.id, orgId: approvals.orgId })
343
+ .from(approvals)
344
+ .where(eq(approvals.id, approvalId))
345
+ .then((rows) => rows[0] ?? null);
346
+ if (!approval)
347
+ throw notFound("Approval not found");
348
+ if (approval.orgId !== orgId)
349
+ throw unprocessable("Approval must belong to same organization");
350
+ return approval;
351
+ }
352
+ async function assertRunOrg(orgId, runId) {
353
+ if (!runId)
354
+ return null;
355
+ const run = await db
356
+ .select({ id: heartbeatRuns.id, orgId: heartbeatRuns.orgId })
357
+ .from(heartbeatRuns)
358
+ .where(eq(heartbeatRuns.id, runId))
359
+ .then((rows) => rows[0] ?? null);
360
+ if (!run)
361
+ throw notFound("Heartbeat run not found");
362
+ if (run.orgId !== orgId)
363
+ throw unprocessable("Heartbeat run must belong to same organization");
364
+ return run;
365
+ }
366
+ async function assertActivityOrg(orgId, activityId) {
367
+ if (!activityId)
368
+ return null;
369
+ const activity = await db
370
+ .select({ id: activityLog.id, orgId: activityLog.orgId })
371
+ .from(activityLog)
372
+ .where(eq(activityLog.id, activityId))
373
+ .then((rows) => rows[0] ?? null);
374
+ if (!activity)
375
+ throw notFound("Activity event not found");
376
+ if (activity.orgId !== orgId)
377
+ throw unprocessable("Activity event must belong to same organization");
378
+ return activity;
379
+ }
380
+ async function assertCalendarEventShape(orgId, input) {
381
+ if (input.eventKind === "agent_work_block") {
382
+ if (input.ownerAgentId === null)
383
+ throw unprocessable("Agent work blocks require an agent");
384
+ if (input.ownerType && input.ownerType !== "agent")
385
+ throw unprocessable("Agent work blocks must be owned by an agent");
386
+ }
387
+ if (input.eventKind === "human_event" && input.ownerType && input.ownerType !== "user") {
388
+ throw unprocessable("Human calendar events must be owned by a user");
389
+ }
390
+ if (input.sourceMode && input.sourceMode !== "manual" && input.sourceMode !== "imported") {
391
+ throw forbidden("Derived calendar events are read-only");
392
+ }
393
+ await Promise.all([
394
+ assertSourceOrg(orgId, input.sourceId),
395
+ assertAgentOrg(orgId, input.ownerAgentId),
396
+ assertIssueOrg(orgId, input.issueId),
397
+ assertProjectOrg(orgId, input.projectId),
398
+ assertGoalOrg(orgId, input.goalId),
399
+ assertApprovalOrg(orgId, input.approvalId),
400
+ assertRunOrg(orgId, input.heartbeatRunId),
401
+ assertActivityOrg(orgId, input.activityId),
402
+ ]);
403
+ }
404
+ function mapPersistedEvent(row) {
405
+ return {
406
+ ...row.event,
407
+ eventKind: row.event.eventKind,
408
+ eventStatus: row.event.eventStatus,
409
+ ownerType: row.event.ownerType,
410
+ visibility: row.event.visibility,
411
+ sourceMode: row.event.sourceMode,
412
+ source: row.sourceId
413
+ ? {
414
+ id: row.sourceId,
415
+ type: row.sourceType,
416
+ name: row.sourceName ?? "Calendar",
417
+ visibilityDefault: (row.sourceVisibilityDefault ?? "full"),
418
+ externalProvider: row.sourceExternalProvider,
419
+ }
420
+ : null,
421
+ agent: row.event.ownerAgentId && row.agentName
422
+ ? {
423
+ id: row.event.ownerAgentId,
424
+ name: row.agentName,
425
+ role: row.agentRole ?? "general",
426
+ title: row.agentTitle,
427
+ urlKey: row.agentUrlKey,
428
+ }
429
+ : null,
430
+ issue: row.event.issueId && row.issueTitle
431
+ ? {
432
+ id: row.event.issueId,
433
+ identifier: row.issueIdentifier,
434
+ title: row.issueTitle,
435
+ status: row.issueStatus ?? "todo",
436
+ priority: row.issuePriority ?? "medium",
437
+ }
438
+ : null,
439
+ };
440
+ }
441
+ async function listPersistedEvents(orgId, filters) {
442
+ const conditions = [
443
+ eq(calendarEvents.orgId, orgId),
444
+ isNull(calendarEvents.deletedAt),
445
+ lt(calendarEvents.startAt, filters.end),
446
+ gt(calendarEvents.endAt, filters.start),
447
+ ];
448
+ if (filters.agentIds?.length) {
449
+ conditions.push(inArray(calendarEvents.ownerAgentId, filters.agentIds));
450
+ }
451
+ if (filters.sourceIds?.length) {
452
+ conditions.push(inArray(calendarEvents.sourceId, filters.sourceIds));
453
+ }
454
+ if (filters.eventKinds?.length) {
455
+ conditions.push(inArray(calendarEvents.eventKind, filters.eventKinds));
456
+ }
457
+ if (filters.statuses?.length) {
458
+ conditions.push(inArray(calendarEvents.eventStatus, filters.statuses));
459
+ }
460
+ const rows = await db
461
+ .select({
462
+ event: calendarEvents,
463
+ sourceId: calendarSources.id,
464
+ sourceType: calendarSources.type,
465
+ sourceName: calendarSources.name,
466
+ sourceVisibilityDefault: calendarSources.visibilityDefault,
467
+ sourceExternalProvider: calendarSources.externalProvider,
468
+ agentName: agents.name,
469
+ agentRole: agents.role,
470
+ agentTitle: agents.title,
471
+ agentUrlKey: agents.workspaceKey,
472
+ issueIdentifier: issues.identifier,
473
+ issueTitle: issues.title,
474
+ issueStatus: issues.status,
475
+ issuePriority: issues.priority,
476
+ })
477
+ .from(calendarEvents)
478
+ .leftJoin(calendarSources, eq(calendarEvents.sourceId, calendarSources.id))
479
+ .leftJoin(agents, eq(calendarEvents.ownerAgentId, agents.id))
480
+ .leftJoin(issues, eq(calendarEvents.issueId, issues.id))
481
+ .where(and(...conditions))
482
+ .orderBy(asc(calendarEvents.startAt), asc(calendarEvents.title));
483
+ return rows.map(mapPersistedEvent);
484
+ }
485
+ async function listRunIssueFallbacks(orgId, runIds) {
486
+ if (runIds.length === 0)
487
+ return new Map();
488
+ const rows = await db
489
+ .selectDistinctOn([activityLog.runId], {
490
+ runId: activityLog.runId,
491
+ activityId: activityLog.id,
492
+ issueId: issues.id,
493
+ identifier: issues.identifier,
494
+ title: issues.title,
495
+ status: issues.status,
496
+ priority: issues.priority,
497
+ })
498
+ .from(activityLog)
499
+ .innerJoin(issues, eq(activityLog.entityId, issueIdAsText))
500
+ .where(and(eq(activityLog.orgId, orgId), inArray(activityLog.runId, runIds), eq(activityLog.entityType, "issue"), isNull(issues.hiddenAt)))
501
+ .orderBy(activityLog.runId, desc(activityLog.createdAt));
502
+ return new Map(rows
503
+ .filter((row) => row.runId)
504
+ .map((row) => [
505
+ row.runId,
506
+ {
507
+ activityId: row.activityId,
508
+ issue: {
509
+ id: row.issueId,
510
+ identifier: row.identifier,
511
+ title: row.title,
512
+ status: row.status,
513
+ priority: row.priority,
514
+ },
515
+ },
516
+ ]));
517
+ }
518
+ async function listDerivedRunEvents(orgId, filters) {
519
+ if (filters.sourceIds?.length)
520
+ return [];
521
+ if (!csvIncludes(filters.eventKinds, "agent_work_block"))
522
+ return [];
523
+ const conditions = [
524
+ eq(heartbeatRuns.orgId, orgId),
525
+ sql `coalesce(${heartbeatRuns.startedAt}, ${heartbeatRuns.createdAt}) < ${filters.end.toISOString()}::timestamptz`,
526
+ sql `coalesce(${heartbeatRuns.finishedAt}, now()) > ${filters.start.toISOString()}::timestamptz`,
527
+ ];
528
+ if (filters.agentIds?.length) {
529
+ conditions.push(inArray(heartbeatRuns.agentId, filters.agentIds));
530
+ }
531
+ if (filters.runId) {
532
+ conditions.push(eq(heartbeatRuns.id, filters.runId));
533
+ }
534
+ const runRows = await db
535
+ .select({
536
+ id: heartbeatRuns.id,
537
+ orgId: heartbeatRuns.orgId,
538
+ agentId: heartbeatRuns.agentId,
539
+ status: heartbeatRuns.status,
540
+ startedAt: heartbeatRuns.startedAt,
541
+ finishedAt: heartbeatRuns.finishedAt,
542
+ createdAt: heartbeatRuns.createdAt,
543
+ updatedAt: heartbeatRuns.updatedAt,
544
+ invocationSource: heartbeatRuns.invocationSource,
545
+ triggerDetail: heartbeatRuns.triggerDetail,
546
+ contextSnapshot: heartbeatRuns.contextSnapshot,
547
+ agentName: agents.name,
548
+ agentRole: agents.role,
549
+ agentTitle: agents.title,
550
+ agentUrlKey: agents.workspaceKey,
551
+ issueId: issues.id,
552
+ issueIdentifier: issues.identifier,
553
+ issueTitle: issues.title,
554
+ issueStatus: issues.status,
555
+ issuePriority: issues.priority,
556
+ })
557
+ .from(heartbeatRuns)
558
+ .innerJoin(agents, eq(heartbeatRuns.agentId, agents.id))
559
+ .leftJoin(issues, and(eq(issueIdAsText, contextIssueId), isNull(issues.hiddenAt)))
560
+ .where(and(...conditions))
561
+ .orderBy(asc(sql `coalesce(${heartbeatRuns.startedAt}, ${heartbeatRuns.createdAt})`));
562
+ const fallbackByRunId = await listRunIssueFallbacks(orgId, runRows.map((row) => row.id));
563
+ const now = new Date();
564
+ return runRows.flatMap((row) => {
565
+ const startAt = row.startedAt ?? row.createdAt;
566
+ const endAt = row.finishedAt ?? now;
567
+ const eventStatus = row.status === "queued" || row.status === "running"
568
+ ? "in_progress"
569
+ : "actual";
570
+ if (!csvIncludes(filters.statuses, eventStatus))
571
+ return [];
572
+ const fallback = fallbackByRunId.get(row.id);
573
+ const issue = row.issueId
574
+ ? {
575
+ id: row.issueId,
576
+ identifier: row.issueIdentifier,
577
+ title: row.issueTitle ?? "Untitled issue",
578
+ status: row.issueStatus ?? "todo",
579
+ priority: row.issuePriority ?? "medium",
580
+ }
581
+ : fallback?.issue ?? null;
582
+ const title = issue ? `${row.agentName} · ${issue.title}` : `${row.agentName} · Heartbeat run`;
583
+ return [{
584
+ id: `run:${row.id}`,
585
+ orgId: row.orgId,
586
+ sourceId: null,
587
+ eventKind: "agent_work_block",
588
+ eventStatus,
589
+ ownerType: "agent",
590
+ ownerUserId: null,
591
+ ownerAgentId: row.agentId,
592
+ title,
593
+ description: row.triggerDetail ? `Run trigger: ${row.triggerDetail}` : null,
594
+ startAt,
595
+ endAt,
596
+ timezone: "UTC",
597
+ allDay: false,
598
+ visibility: "full",
599
+ issueId: issue?.id ?? null,
600
+ projectId: null,
601
+ goalId: null,
602
+ approvalId: null,
603
+ heartbeatRunId: row.id,
604
+ activityId: fallback?.activityId ?? null,
605
+ sourceMode: "derived",
606
+ externalProvider: null,
607
+ externalCalendarId: null,
608
+ externalEventId: null,
609
+ externalEtag: null,
610
+ externalUpdatedAt: null,
611
+ createdByUserId: null,
612
+ updatedByUserId: null,
613
+ createdAt: row.createdAt,
614
+ updatedAt: row.updatedAt,
615
+ deletedAt: null,
616
+ source: {
617
+ id: "derived:agent-work",
618
+ type: "agent_work",
619
+ name: "Agent work history",
620
+ visibilityDefault: "full",
621
+ externalProvider: null,
622
+ },
623
+ agent: {
624
+ id: row.agentId,
625
+ name: row.agentName,
626
+ role: row.agentRole,
627
+ title: row.agentTitle,
628
+ urlKey: row.agentUrlKey,
629
+ },
630
+ issue,
631
+ }];
632
+ });
633
+ }
634
+ async function listProjectedHeartbeatEvents(orgId, filters) {
635
+ if (filters.sourceIds?.length)
636
+ return [];
637
+ if (!csvIncludes(filters.eventKinds, "agent_work_block"))
638
+ return [];
639
+ if (!csvIncludes(filters.statuses, "projected"))
640
+ return [];
641
+ const now = new Date();
642
+ const projectionStart = new Date(Math.max(filters.start.getTime(), now.getTime()));
643
+ if (filters.end.getTime() <= projectionStart.getTime())
644
+ return [];
645
+ const conditions = [
646
+ eq(agents.orgId, orgId),
647
+ sql `${agents.status} not in ('paused', 'terminated', 'pending_approval')`,
648
+ ];
649
+ if (filters.agentIds?.length) {
650
+ conditions.push(inArray(agents.id, filters.agentIds));
651
+ }
652
+ const rows = await db
653
+ .select()
654
+ .from(agents)
655
+ .where(and(...conditions))
656
+ .orderBy(asc(agents.name));
657
+ const projected = [];
658
+ for (const row of rows) {
659
+ const policy = parseHeartbeatPolicy(row);
660
+ if (!policy.enabled || policy.intervalSec <= 0)
661
+ continue;
662
+ const intervalMs = policy.intervalSec * 1000;
663
+ const baselineMs = new Date(row.lastHeartbeatAt ?? row.createdAt).getTime();
664
+ let nextMs = baselineMs + intervalMs;
665
+ if (nextMs < projectionStart.getTime()) {
666
+ const elapsed = projectionStart.getTime() - baselineMs;
667
+ nextMs = baselineMs + Math.ceil(elapsed / intervalMs) * intervalMs;
668
+ }
669
+ let count = 0;
670
+ while (nextMs < filters.end.getTime() && count < PROJECTED_HEARTBEAT_MAX_PER_AGENT) {
671
+ const startAt = new Date(nextMs);
672
+ const endAt = new Date(Math.min(nextMs + PROJECTED_HEARTBEAT_DURATION_MS, filters.end.getTime()));
673
+ projected.push({
674
+ id: `projected-heartbeat:${row.id}:${startAt.toISOString()}`,
675
+ orgId: row.orgId,
676
+ sourceId: null,
677
+ eventKind: "agent_work_block",
678
+ eventStatus: "projected",
679
+ ownerType: "agent",
680
+ ownerUserId: null,
681
+ ownerAgentId: row.id,
682
+ title: `${row.name} · Projected heartbeat`,
683
+ description: `Projected from this agent's ${policy.intervalSec}s timer heartbeat. This does not schedule or guarantee execution.`,
684
+ startAt,
685
+ endAt,
686
+ timezone: "UTC",
687
+ allDay: false,
688
+ visibility: "full",
689
+ issueId: null,
690
+ projectId: null,
691
+ goalId: null,
692
+ approvalId: null,
693
+ heartbeatRunId: null,
694
+ activityId: null,
695
+ sourceMode: "derived",
696
+ externalProvider: null,
697
+ externalCalendarId: null,
698
+ externalEventId: null,
699
+ externalEtag: null,
700
+ externalUpdatedAt: null,
701
+ createdByUserId: null,
702
+ updatedByUserId: null,
703
+ createdAt: row.createdAt,
704
+ updatedAt: row.updatedAt,
705
+ deletedAt: null,
706
+ source: {
707
+ id: "derived:projected-heartbeats",
708
+ type: "system",
709
+ name: "Projected heartbeats",
710
+ visibilityDefault: "full",
711
+ externalProvider: null,
712
+ },
713
+ agent: {
714
+ id: row.id,
715
+ name: row.name,
716
+ role: row.role,
717
+ title: row.title,
718
+ urlKey: row.workspaceKey,
719
+ },
720
+ issue: null,
721
+ });
722
+ nextMs += intervalMs;
723
+ count += 1;
724
+ }
725
+ }
726
+ return projected;
727
+ }
728
+ async function getPersistedEvent(orgId, id) {
729
+ const rows = await db
730
+ .select({
731
+ event: calendarEvents,
732
+ sourceId: calendarSources.id,
733
+ sourceType: calendarSources.type,
734
+ sourceName: calendarSources.name,
735
+ sourceVisibilityDefault: calendarSources.visibilityDefault,
736
+ sourceExternalProvider: calendarSources.externalProvider,
737
+ agentName: agents.name,
738
+ agentRole: agents.role,
739
+ agentTitle: agents.title,
740
+ agentUrlKey: agents.workspaceKey,
741
+ issueIdentifier: issues.identifier,
742
+ issueTitle: issues.title,
743
+ issueStatus: issues.status,
744
+ issuePriority: issues.priority,
745
+ })
746
+ .from(calendarEvents)
747
+ .leftJoin(calendarSources, eq(calendarEvents.sourceId, calendarSources.id))
748
+ .leftJoin(agents, eq(calendarEvents.ownerAgentId, agents.id))
749
+ .leftJoin(issues, eq(calendarEvents.issueId, issues.id))
750
+ .where(and(eq(calendarEvents.id, id), eq(calendarEvents.orgId, orgId), isNull(calendarEvents.deletedAt)));
751
+ return rows[0] ? mapPersistedEvent(rows[0]) : null;
752
+ }
753
+ async function writableEvent(orgId, id) {
754
+ const event = await db
755
+ .select()
756
+ .from(calendarEvents)
757
+ .where(and(eq(calendarEvents.id, id), eq(calendarEvents.orgId, orgId), isNull(calendarEvents.deletedAt)))
758
+ .then((rows) => rows[0] ?? null);
759
+ if (!event)
760
+ throw notFound("Calendar event not found");
761
+ if (event.sourceMode !== "manual") {
762
+ throw conflict("Imported and derived calendar events are read-only");
763
+ }
764
+ if (event.eventKind !== "human_event") {
765
+ throw conflict("Only My Calendar events can be edited");
766
+ }
767
+ return event;
768
+ }
769
+ async function getOrCreateGoogleSource(orgId, actor, status = "disconnected") {
770
+ const existing = await db
771
+ .select()
772
+ .from(calendarSources)
773
+ .where(and(eq(calendarSources.orgId, orgId), eq(calendarSources.type, "google_calendar"), eq(calendarSources.externalProvider, "google_calendar"), eq(calendarSources.externalCalendarId, "primary")))
774
+ .then((rows) => rows[0] ?? null);
775
+ if (existing)
776
+ return existing;
777
+ const [created] = await db
778
+ .insert(calendarSources)
779
+ .values({
780
+ orgId,
781
+ type: "google_calendar",
782
+ name: "Google Calendar",
783
+ ownerType: "user",
784
+ ownerUserId: actor?.userId ?? "board",
785
+ externalProvider: "google_calendar",
786
+ externalCalendarId: "primary",
787
+ visibilityDefault: "full",
788
+ status,
789
+ })
790
+ .returning();
791
+ return created;
792
+ }
793
+ async function listGoogleSourceRows(orgId) {
794
+ const rows = await db
795
+ .select()
796
+ .from(calendarSources)
797
+ .where(and(eq(calendarSources.orgId, orgId), eq(calendarSources.type, "google_calendar"), eq(calendarSources.externalProvider, "google_calendar")));
798
+ return rows.sort((a, b) => {
799
+ const aPrimary = a.externalCalendarId === "primary" ? 0 : 1;
800
+ const bPrimary = b.externalCalendarId === "primary" ? 0 : 1;
801
+ if (aPrimary !== bPrimary)
802
+ return aPrimary - bPrimary;
803
+ return a.name.localeCompare(b.name);
804
+ });
805
+ }
806
+ async function findGoogleCredentialSource(orgId, preferred) {
807
+ if (preferred && preferred.type === "google_calendar" && sourceHasGoogleToken(preferred))
808
+ return preferred;
809
+ const sources = await listGoogleSourceRows(orgId);
810
+ return sources.find(sourceHasGoogleToken) ?? preferred ?? sources[0] ?? null;
811
+ }
812
+ async function ensureGoogleAccessToken(orgId, source) {
813
+ if (!source)
814
+ return null;
815
+ const cursor = parseSyncCursor(source.syncCursorJson);
816
+ const accessToken = typeof cursor.accessToken === "string" ? cursor.accessToken : null;
817
+ const refreshToken = typeof cursor.refreshToken === "string" ? cursor.refreshToken : null;
818
+ const expiresAt = typeof cursor.expiresAt === "string" ? new Date(cursor.expiresAt).getTime() : null;
819
+ if (accessToken && source.status !== "error" && (!expiresAt || expiresAt > Date.now() + 60_000)) {
820
+ return { source, accessToken };
821
+ }
822
+ if (!refreshToken) {
823
+ return accessToken ? { source, accessToken } : null;
824
+ }
825
+ const credentials = await googleCredentials(orgId);
826
+ if (!credentials)
827
+ return null;
828
+ const response = await fetch("https://oauth2.googleapis.com/token", {
829
+ method: "POST",
830
+ headers: { "content-type": "application/x-www-form-urlencoded" },
831
+ body: new URLSearchParams({
832
+ client_id: credentials.clientId,
833
+ client_secret: credentials.clientSecret,
834
+ refresh_token: refreshToken,
835
+ grant_type: "refresh_token",
836
+ }),
837
+ });
838
+ if (!response.ok) {
839
+ await db
840
+ .update(calendarSources)
841
+ .set({ status: "error", updatedAt: new Date() })
842
+ .where(eq(calendarSources.id, source.id));
843
+ return null;
844
+ }
845
+ const token = await response.json();
846
+ const nextAccessToken = token.access_token ?? accessToken;
847
+ if (!nextAccessToken)
848
+ return null;
849
+ const [updated] = await db
850
+ .update(calendarSources)
851
+ .set({
852
+ syncCursorJson: {
853
+ ...cursor,
854
+ accessToken: nextAccessToken,
855
+ refreshToken,
856
+ tokenType: token.token_type ?? cursor.tokenType,
857
+ scope: token.scope ?? cursor.scope,
858
+ expiresAt: token.expires_in ? new Date(Date.now() + token.expires_in * 1000).toISOString() : cursor.expiresAt ?? null,
859
+ },
860
+ status: source.status === "paused" ? "paused" : "active",
861
+ updatedAt: new Date(),
862
+ })
863
+ .where(eq(calendarSources.id, source.id))
864
+ .returning();
865
+ return { source: updated, accessToken: nextAccessToken };
866
+ }
867
+ async function refreshGoogleCalendarSources(orgId, credentialSource, accessToken, actor) {
868
+ const response = await fetch("https://www.googleapis.com/calendar/v3/users/me/calendarList", {
869
+ headers: { authorization: `Bearer ${accessToken}` },
870
+ });
871
+ if (!response.ok)
872
+ return [];
873
+ const body = await response.json();
874
+ const existingSources = await listGoogleSourceRows(orgId);
875
+ const existingByExternalId = new Map(existingSources
876
+ .filter((source) => source.externalCalendarId)
877
+ .map((source) => [source.externalCalendarId, source]));
878
+ const refreshed = [];
879
+ for (const item of body.items ?? []) {
880
+ const externalCalendarId = googleCalendarIdForListItem(item);
881
+ if (!externalCalendarId)
882
+ continue;
883
+ const existing = existingByExternalId.get(externalCalendarId)
884
+ ?? (item.primary ? credentialSource : null);
885
+ const name = item.summary?.trim() || (item.primary ? "Primary calendar" : externalCalendarId);
886
+ const metadataCursor = {
887
+ ...(existing?.syncCursorJson ?? {}),
888
+ googleCalendarPrimary: item.primary === true,
889
+ googleCalendarHidden: item.hidden === true,
890
+ googleCalendarColor: item.backgroundColor ?? null,
891
+ };
892
+ const shouldEnableByDefault = item.primary === true || item.selected !== false;
893
+ const status = existing
894
+ ? existing.status === "paused" ? "paused" : "active"
895
+ : shouldEnableByDefault ? "active" : "paused";
896
+ const values = {
897
+ orgId,
898
+ type: "google_calendar",
899
+ name,
900
+ ownerType: "user",
901
+ ownerUserId: existing?.ownerUserId ?? credentialSource.ownerUserId ?? actor?.userId ?? "board",
902
+ ownerAgentId: null,
903
+ externalProvider: "google_calendar",
904
+ externalCalendarId,
905
+ visibilityDefault: existing?.visibilityDefault ?? credentialSource.visibilityDefault ?? "full",
906
+ status,
907
+ syncCursorJson: externalCalendarId === "primary"
908
+ ? {
909
+ ...metadataCursor,
910
+ ...parseSyncCursor(credentialSource.syncCursorJson),
911
+ googleCalendarPrimary: true,
912
+ googleCalendarHidden: item.hidden === true,
913
+ googleCalendarColor: item.backgroundColor ?? null,
914
+ }
915
+ : metadataCursor,
916
+ updatedAt: new Date(),
917
+ };
918
+ if (existing) {
919
+ const [updated] = await db
920
+ .update(calendarSources)
921
+ .set(values)
922
+ .where(eq(calendarSources.id, existing.id))
923
+ .returning();
924
+ refreshed.push(updated);
925
+ }
926
+ else {
927
+ const [created] = await db
928
+ .insert(calendarSources)
929
+ .values(values)
930
+ .returning();
931
+ refreshed.push(created);
932
+ }
933
+ }
934
+ return refreshed;
935
+ }
936
+ async function syncOneGoogleCalendar(orgId, source, accessToken) {
937
+ const timeMin = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
938
+ const timeMax = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString();
939
+ const params = new URLSearchParams({
940
+ singleEvents: "true",
941
+ orderBy: "startTime",
942
+ timeMin,
943
+ timeMax,
944
+ });
945
+ const calendarId = source.externalCalendarId ?? "primary";
946
+ const response = await fetch(`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?${params.toString()}`, {
947
+ headers: { authorization: `Bearer ${accessToken}` },
948
+ });
949
+ if (!response.ok) {
950
+ const [updated] = await db
951
+ .update(calendarSources)
952
+ .set({ status: "error", updatedAt: new Date() })
953
+ .where(eq(calendarSources.id, source.id))
954
+ .returning();
955
+ return { source: updated, importedCount: 0 };
956
+ }
957
+ const body = await response.json();
958
+ let importedCount = 0;
959
+ for (const item of body.items ?? []) {
960
+ if (!item.id || item.status === "cancelled")
961
+ continue;
962
+ const startRaw = item.start?.dateTime ?? item.start?.date;
963
+ const endRaw = item.end?.dateTime ?? item.end?.date;
964
+ if (!startRaw || !endRaw)
965
+ continue;
966
+ const allDay = !item.start?.dateTime;
967
+ const visibility = item.visibility === "private"
968
+ ? "private"
969
+ : source.visibilityDefault;
970
+ const title = visibility !== "full"
971
+ ? "Busy"
972
+ : item.summary?.trim() || "Busy";
973
+ const created = await upsertImportedGoogleEvent({
974
+ orgId,
975
+ sourceId: source.id,
976
+ calendarId,
977
+ title,
978
+ startAt: new Date(startRaw),
979
+ endAt: new Date(endRaw),
980
+ timezone: item.start?.timeZone ?? "UTC",
981
+ allDay,
982
+ visibility,
983
+ externalEventId: item.id,
984
+ externalEtag: item.etag ?? null,
985
+ externalUpdatedAt: item.updated ? new Date(item.updated) : null,
986
+ });
987
+ if (created)
988
+ importedCount += 1;
989
+ }
990
+ const [updated] = await db
991
+ .update(calendarSources)
992
+ .set({
993
+ status: "active",
994
+ lastSyncedAt: new Date(),
995
+ updatedAt: new Date(),
996
+ })
997
+ .where(eq(calendarSources.id, source.id))
998
+ .returning();
999
+ return { source: updated, importedCount };
1000
+ }
1001
+ async function upsertImportedGoogleEvent(params) {
1002
+ const existing = await db
1003
+ .select({ id: calendarEvents.id })
1004
+ .from(calendarEvents)
1005
+ .where(and(eq(calendarEvents.orgId, params.orgId), eq(calendarEvents.externalProvider, "google_calendar"), eq(calendarEvents.externalCalendarId, params.calendarId), eq(calendarEvents.externalEventId, params.externalEventId)))
1006
+ .then((rows) => rows[0] ?? null);
1007
+ const values = {
1008
+ orgId: params.orgId,
1009
+ sourceId: params.sourceId,
1010
+ eventKind: "external_event",
1011
+ eventStatus: "external",
1012
+ ownerType: "user",
1013
+ title: params.title,
1014
+ startAt: params.startAt,
1015
+ endAt: params.endAt,
1016
+ timezone: params.timezone,
1017
+ allDay: params.allDay,
1018
+ visibility: params.visibility,
1019
+ sourceMode: "imported",
1020
+ externalProvider: "google_calendar",
1021
+ externalCalendarId: params.calendarId,
1022
+ externalEventId: params.externalEventId,
1023
+ externalEtag: params.externalEtag,
1024
+ externalUpdatedAt: params.externalUpdatedAt,
1025
+ updatedAt: new Date(),
1026
+ deletedAt: null,
1027
+ };
1028
+ if (existing) {
1029
+ await db.update(calendarEvents).set(values).where(eq(calendarEvents.id, existing.id));
1030
+ return false;
1031
+ }
1032
+ await db.insert(calendarEvents).values(values);
1033
+ return true;
1034
+ }
1035
+ return {
1036
+ eventSummary,
1037
+ async listSources(orgId) {
1038
+ const rows = await db
1039
+ .select()
1040
+ .from(calendarSources)
1041
+ .where(eq(calendarSources.orgId, orgId))
1042
+ .orderBy(asc(calendarSources.type), asc(calendarSources.name));
1043
+ return rows.map(sanitizeSource);
1044
+ },
1045
+ async createSource(orgId, input, actor) {
1046
+ await assertAgentOrg(orgId, input.ownerAgentId);
1047
+ const [created] = await db
1048
+ .insert(calendarSources)
1049
+ .values({
1050
+ orgId,
1051
+ ...input,
1052
+ ownerUserId: input.ownerUserId ?? actor?.userId ?? null,
1053
+ ownerAgentId: input.ownerAgentId ?? null,
1054
+ externalProvider: input.externalProvider ?? null,
1055
+ externalCalendarId: input.externalCalendarId ?? null,
1056
+ syncCursorJson: input.syncCursorJson ?? null,
1057
+ })
1058
+ .returning();
1059
+ return sanitizeSource(created);
1060
+ },
1061
+ async updateSource(orgId, sourceId, input, actor) {
1062
+ const existing = await assertSourceOrg(orgId, sourceId);
1063
+ await assertAgentOrg(orgId, input.ownerAgentId);
1064
+ const [updated] = await db
1065
+ .update(calendarSources)
1066
+ .set({
1067
+ ...input,
1068
+ ownerUserId: input.ownerUserId ?? existing?.ownerUserId ?? actor?.userId ?? null,
1069
+ ownerAgentId: input.ownerAgentId === undefined ? existing?.ownerAgentId ?? null : input.ownerAgentId,
1070
+ externalProvider: input.externalProvider === undefined ? existing?.externalProvider ?? null : input.externalProvider,
1071
+ externalCalendarId: input.externalCalendarId === undefined ? existing?.externalCalendarId ?? null : input.externalCalendarId,
1072
+ syncCursorJson: input.syncCursorJson === undefined ? existing?.syncCursorJson ?? null : input.syncCursorJson,
1073
+ updatedAt: new Date(),
1074
+ })
1075
+ .where(eq(calendarSources.id, sourceId))
1076
+ .returning();
1077
+ return sanitizeSource(updated);
1078
+ },
1079
+ async deleteSource(orgId, sourceId) {
1080
+ await assertSourceOrg(orgId, sourceId);
1081
+ await db.delete(calendarSources).where(and(eq(calendarSources.id, sourceId), eq(calendarSources.orgId, orgId)));
1082
+ return { ok: true };
1083
+ },
1084
+ async listEvents(orgId, filters) {
1085
+ const [persisted, derived, projected] = await Promise.all([
1086
+ listPersistedEvents(orgId, filters),
1087
+ listDerivedRunEvents(orgId, filters),
1088
+ listProjectedHeartbeatEvents(orgId, filters),
1089
+ ]);
1090
+ return [...persisted, ...derived, ...projected].sort((a, b) => {
1091
+ const time = new Date(a.startAt).getTime() - new Date(b.startAt).getTime();
1092
+ return time !== 0 ? time : a.title.localeCompare(b.title);
1093
+ });
1094
+ },
1095
+ async getEvent(orgId, eventId) {
1096
+ if (eventId.startsWith("run:")) {
1097
+ const runId = eventId.slice("run:".length);
1098
+ const derived = await listDerivedRunEvents(orgId, {
1099
+ start: new Date(0),
1100
+ end: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
1101
+ runId,
1102
+ });
1103
+ return derived[0] ?? null;
1104
+ }
1105
+ return getPersistedEvent(orgId, eventId);
1106
+ },
1107
+ async createEvent(orgId, input, actor) {
1108
+ await assertCalendarEventShape(orgId, input);
1109
+ const [created] = await db
1110
+ .insert(calendarEvents)
1111
+ .values(createEventValues(orgId, input, actor))
1112
+ .returning();
1113
+ return getPersistedEvent(orgId, created.id);
1114
+ },
1115
+ async updateEvent(orgId, eventId, input, actor) {
1116
+ const existing = await writableEvent(orgId, eventId);
1117
+ const merged = mergeEventInput(existing, input);
1118
+ await assertCalendarEventShape(orgId, merged);
1119
+ if (merged.endAt.getTime() <= merged.startAt.getTime()) {
1120
+ throw unprocessable("End time must be after start time");
1121
+ }
1122
+ const [updated] = await db
1123
+ .update(calendarEvents)
1124
+ .set({
1125
+ ...updateEventValues(input, actor),
1126
+ updatedAt: new Date(),
1127
+ })
1128
+ .where(eq(calendarEvents.id, eventId))
1129
+ .returning();
1130
+ return { previous: existing, event: await getPersistedEvent(orgId, updated.id) };
1131
+ },
1132
+ async deleteEvent(orgId, eventId, actor) {
1133
+ const existing = await writableEvent(orgId, eventId);
1134
+ await db
1135
+ .update(calendarEvents)
1136
+ .set({
1137
+ deletedAt: new Date(),
1138
+ updatedAt: new Date(),
1139
+ updatedByUserId: actor?.userId ?? null,
1140
+ eventStatus: "cancelled",
1141
+ })
1142
+ .where(eq(calendarEvents.id, eventId));
1143
+ return existing;
1144
+ },
1145
+ getGoogleOAuthConfig: googleOAuthConfig,
1146
+ updateGoogleOAuthConfig,
1147
+ async connectGoogle(orgId, redirectUri, actor) {
1148
+ const credentials = await googleCredentials(orgId);
1149
+ let source = await getOrCreateGoogleSource(orgId, actor, credentials ? "disconnected" : "error");
1150
+ if (!credentials) {
1151
+ return {
1152
+ status: "configuration_required",
1153
+ authUrl: null,
1154
+ source: sanitizeSource(source),
1155
+ redirectUri,
1156
+ requiredEnv: [...GOOGLE_CALENDAR_REQUIRED_ENV],
1157
+ acceptedAliases: [...GOOGLE_CALENDAR_ACCEPTED_ENV_ALIASES],
1158
+ config: await googleOAuthConfig(orgId, redirectUri),
1159
+ };
1160
+ }
1161
+ if (source.status === "error" && !sourceHasGoogleToken(source)) {
1162
+ const [updated] = await db
1163
+ .update(calendarSources)
1164
+ .set({ status: "disconnected", updatedAt: new Date() })
1165
+ .where(eq(calendarSources.id, source.id))
1166
+ .returning();
1167
+ source = updated ?? source;
1168
+ }
1169
+ const state = Buffer.from(JSON.stringify({ orgId, sourceId: source.id })).toString("base64url");
1170
+ const params = new URLSearchParams({
1171
+ client_id: credentials.clientId,
1172
+ redirect_uri: redirectUri,
1173
+ response_type: "code",
1174
+ scope: "https://www.googleapis.com/auth/calendar.readonly",
1175
+ access_type: "offline",
1176
+ include_granted_scopes: "true",
1177
+ prompt: "consent",
1178
+ state,
1179
+ });
1180
+ return {
1181
+ status: "authorization_required",
1182
+ authUrl: `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`,
1183
+ source: sanitizeSource(source),
1184
+ config: await googleOAuthConfig(orgId, redirectUri),
1185
+ };
1186
+ },
1187
+ async completeGoogleCallback(orgId, input, actor) {
1188
+ const credentials = await googleCredentials(orgId);
1189
+ if (!credentials)
1190
+ throw unprocessable("Google Calendar OAuth is not configured");
1191
+ let sourceId = null;
1192
+ if (input.state) {
1193
+ try {
1194
+ const parsed = JSON.parse(Buffer.from(input.state, "base64url").toString("utf8"));
1195
+ if (parsed.orgId === orgId && typeof parsed.sourceId === "string") {
1196
+ sourceId = parsed.sourceId;
1197
+ }
1198
+ }
1199
+ catch {
1200
+ sourceId = null;
1201
+ }
1202
+ }
1203
+ const source = sourceId
1204
+ ? await assertSourceOrg(orgId, sourceId)
1205
+ : await getOrCreateGoogleSource(orgId, actor);
1206
+ const response = await fetch("https://oauth2.googleapis.com/token", {
1207
+ method: "POST",
1208
+ headers: { "content-type": "application/x-www-form-urlencoded" },
1209
+ body: new URLSearchParams({
1210
+ code: input.code,
1211
+ client_id: credentials.clientId,
1212
+ client_secret: credentials.clientSecret,
1213
+ redirect_uri: input.redirectUri,
1214
+ grant_type: "authorization_code",
1215
+ }),
1216
+ });
1217
+ if (!response.ok) {
1218
+ throw unprocessable(`Google Calendar authorization failed: ${response.status}`);
1219
+ }
1220
+ const token = await response.json();
1221
+ const cursor = parseSyncCursor(source?.syncCursorJson);
1222
+ const [updated] = await db
1223
+ .update(calendarSources)
1224
+ .set({
1225
+ status: "active",
1226
+ syncCursorJson: {
1227
+ ...cursor,
1228
+ accessToken: token.access_token,
1229
+ refreshToken: token.refresh_token ?? cursor.refreshToken,
1230
+ tokenType: token.token_type,
1231
+ scope: token.scope,
1232
+ expiresAt: token.expires_in ? new Date(Date.now() + token.expires_in * 1000).toISOString() : null,
1233
+ },
1234
+ updatedAt: new Date(),
1235
+ })
1236
+ .where(eq(calendarSources.id, source.id))
1237
+ .returning();
1238
+ if (token.access_token) {
1239
+ await refreshGoogleCalendarSources(orgId, updated, token.access_token, actor);
1240
+ }
1241
+ return sanitizeSource(updated);
1242
+ },
1243
+ async syncGoogle(orgId, sourceId) {
1244
+ const requestedSource = sourceId
1245
+ ? await assertSourceOrg(orgId, sourceId)
1246
+ : await getOrCreateGoogleSource(orgId);
1247
+ if (!requestedSource || requestedSource.type !== "google_calendar") {
1248
+ throw unprocessable("Calendar source is not a Google Calendar source");
1249
+ }
1250
+ const credentialSource = await findGoogleCredentialSource(orgId, requestedSource);
1251
+ const token = await ensureGoogleAccessToken(orgId, credentialSource);
1252
+ if (!token) {
1253
+ return { source: sanitizeSource(requestedSource), importedCount: 0, syncedSourceCount: 0 };
1254
+ }
1255
+ await refreshGoogleCalendarSources(orgId, token.source, token.accessToken);
1256
+ const refreshedRequestedSource = sourceId
1257
+ ? (await assertSourceOrg(orgId, sourceId))
1258
+ : requestedSource;
1259
+ const targets = sourceId
1260
+ ? [refreshedRequestedSource]
1261
+ : (await listGoogleSourceRows(orgId)).filter((source) => source.status === "active");
1262
+ let importedCount = 0;
1263
+ let responseSource = refreshedRequestedSource;
1264
+ let syncedSourceCount = 0;
1265
+ for (const target of targets) {
1266
+ if (target.type !== "google_calendar")
1267
+ continue;
1268
+ const result = await syncOneGoogleCalendar(orgId, target, token.accessToken);
1269
+ importedCount += result.importedCount;
1270
+ syncedSourceCount += 1;
1271
+ if (target.id === refreshedRequestedSource.id || !sourceId) {
1272
+ responseSource = result.source;
1273
+ }
1274
+ }
1275
+ return { source: sanitizeSource(responseSource), importedCount, syncedSourceCount };
1276
+ },
1277
+ };
1278
+ }
1279
+ //# sourceMappingURL=calendar.js.map