@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.
- package/dist/bootstrap/register-api-routes.d.ts.map +1 -1
- package/dist/bootstrap/register-api-routes.js +2 -0
- package/dist/bootstrap/register-api-routes.js.map +1 -1
- package/dist/bundled-plugins/plugin-linear/README.md +22 -0
- package/dist/bundled-plugins/plugin-linear/dist/manifest.js +183 -0
- package/dist/bundled-plugins/plugin-linear/dist/manifest.js.map +7 -0
- package/dist/bundled-plugins/plugin-linear/dist/ui/index.js +1229 -0
- package/dist/bundled-plugins/plugin-linear/dist/ui/index.js.map +7 -0
- package/dist/bundled-plugins/plugin-linear/dist/worker.js +8251 -0
- package/dist/bundled-plugins/plugin-linear/dist/worker.js.map +7 -0
- package/dist/bundled-plugins/plugin-linear/package.json +42 -0
- package/dist/dev-server-status.d.ts +1 -7
- package/dist/dev-server-status.d.ts.map +1 -1
- package/dist/dev-server-status.js +1 -4
- package/dist/dev-server-status.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +77 -1
- package/dist/index.js.map +1 -1
- package/dist/langfuse-transcript.d.ts +1 -0
- package/dist/langfuse-transcript.d.ts.map +1 -1
- package/dist/langfuse-transcript.js +24 -0
- package/dist/langfuse-transcript.js.map +1 -1
- package/dist/onboarding-assets/ceo/MEMORY.md +13 -0
- package/dist/onboarding-assets/ceo/SOUL.md +28 -0
- package/dist/onboarding-assets/ceo/TOOLS.md +1 -1
- package/dist/onboarding-assets/default/MEMORY.md +13 -0
- package/dist/onboarding-assets/default/SOUL.md +29 -0
- package/dist/onboarding-assets/default/TOOLS.md +1 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +4 -3
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/calendar.d.ts +3 -0
- package/dist/routes/calendar.d.ts.map +1 -0
- package/dist/routes/calendar.js +265 -0
- package/dist/routes/calendar.js.map +1 -0
- package/dist/routes/chats.d.ts.map +1 -1
- package/dist/routes/chats.js +149 -21
- package/dist/routes/chats.js.map +1 -1
- package/dist/routes/dashboard.d.ts.map +1 -1
- package/dist/routes/dashboard.js +24 -0
- package/dist/routes/dashboard.js.map +1 -1
- package/dist/routes/goals.d.ts.map +1 -1
- package/dist/routes/goals.js +10 -0
- package/dist/routes/goals.js.map +1 -1
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +3 -12
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/instance-settings.d.ts.map +1 -1
- package/dist/routes/instance-settings.js +1 -26
- package/dist/routes/instance-settings.js.map +1 -1
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +74 -34
- package/dist/routes/issues.js.map +1 -1
- package/dist/routes/orgs.d.ts.map +1 -1
- package/dist/routes/orgs.js +161 -145
- package/dist/routes/orgs.js.map +1 -1
- package/dist/routes/plugins.d.ts.map +1 -1
- package/dist/routes/plugins.js +30 -7
- package/dist/routes/plugins.js.map +1 -1
- package/dist/services/agent-instructions.d.ts.map +1 -1
- package/dist/services/agent-instructions.js +23 -7
- package/dist/services/agent-instructions.js.map +1 -1
- package/dist/services/agent-run-context.d.ts +1 -1
- package/dist/services/agent-run-context.js +1 -1
- package/dist/services/agent-run-context.js.map +1 -1
- package/dist/services/agents.d.ts +13 -13
- package/dist/services/assets.d.ts +2 -2
- package/dist/services/calendar.d.ts +137 -0
- package/dist/services/calendar.d.ts.map +1 -0
- package/dist/services/calendar.js +1279 -0
- package/dist/services/calendar.js.map +1 -0
- package/dist/services/chat-assistant.d.ts.map +1 -1
- package/dist/services/chat-assistant.js +75 -15
- package/dist/services/chat-assistant.js.map +1 -1
- package/dist/services/chat-generation-locks.d.ts +2 -1
- package/dist/services/chat-generation-locks.d.ts.map +1 -1
- package/dist/services/chat-generation-locks.js +12 -3
- package/dist/services/chat-generation-locks.js.map +1 -1
- package/dist/services/chats.d.ts +4 -2
- package/dist/services/chats.d.ts.map +1 -1
- package/dist/services/chats.js +2 -15
- package/dist/services/chats.js.map +1 -1
- package/dist/services/costs.d.ts +2 -2
- package/dist/services/default-agent-instructions.d.ts +2 -2
- package/dist/services/default-agent-instructions.js +2 -2
- package/dist/services/documents.d.ts +23 -0
- package/dist/services/documents.d.ts.map +1 -1
- package/dist/services/documents.js +17 -1
- package/dist/services/documents.js.map +1 -1
- package/dist/services/export-jobs.d.ts +16 -0
- package/dist/services/export-jobs.d.ts.map +1 -0
- package/dist/services/export-jobs.js +147 -0
- package/dist/services/export-jobs.js.map +1 -0
- package/dist/services/finance.d.ts +6 -6
- package/dist/services/goals.d.ts +16 -10
- package/dist/services/goals.d.ts.map +1 -1
- package/dist/services/goals.js +201 -18
- package/dist/services/goals.js.map +1 -1
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +3 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/instance-settings.d.ts +1 -3
- package/dist/services/instance-settings.d.ts.map +1 -1
- package/dist/services/instance-settings.js +1 -38
- package/dist/services/instance-settings.js.map +1 -1
- package/dist/services/issue-approvals.d.ts +1 -1
- package/dist/services/issues.d.ts +12 -0
- package/dist/services/issues.d.ts.map +1 -1
- package/dist/services/issues.js +107 -1
- package/dist/services/issues.js.map +1 -1
- package/dist/services/knowledge-portability/organization-portability.d.ts +12 -2
- package/dist/services/knowledge-portability/organization-portability.d.ts.map +1 -1
- package/dist/services/knowledge-portability/organization-portability.js +77 -4
- package/dist/services/knowledge-portability/organization-portability.js.map +1 -1
- package/dist/services/messenger.d.ts +2 -2
- package/dist/services/messenger.d.ts.map +1 -1
- package/dist/services/messenger.js +67 -27
- package/dist/services/messenger.js.map +1 -1
- package/dist/services/organization-workspace-browser.d.ts.map +1 -1
- package/dist/services/organization-workspace-browser.js +3 -0
- package/dist/services/organization-workspace-browser.js.map +1 -1
- package/dist/services/plugin-registry.d.ts +8 -8
- package/dist/services/runtime-kernel/heartbeat.d.ts +6 -0
- package/dist/services/runtime-kernel/heartbeat.d.ts.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.js +236 -99
- package/dist/services/runtime-kernel/heartbeat.js.map +1 -1
- package/dist/services/runtime-kernel/model-fallback.d.ts +10 -0
- package/dist/services/runtime-kernel/model-fallback.d.ts.map +1 -0
- package/dist/services/runtime-kernel/model-fallback.js +147 -0
- package/dist/services/runtime-kernel/model-fallback.js.map +1 -0
- package/dist/services/secrets.d.ts +1 -3
- package/dist/services/secrets.d.ts.map +1 -1
- package/dist/services/secrets.js +55 -30
- package/dist/services/secrets.js.map +1 -1
- package/dist/services/workspace-backups.d.ts +34 -0
- package/dist/services/workspace-backups.d.ts.map +1 -0
- package/dist/services/workspace-backups.js +519 -0
- package/dist/services/workspace-backups.js.map +1 -0
- package/dist/services/workspace-runtime.d.ts +2 -2
- package/package.json +14 -14
- package/resources/bundled-skills/para-memory-files/SKILL.md +3 -1
- package/resources/bundled-skills/rudder-create-agent/SKILL.md +21 -4
- package/resources/bundled-skills/rudder-create-agent/references/api-reference.md +8 -3
- package/resources/bundled-skills/rudder-create-agent/references/cli-reference.md +8 -2
- package/skills/para-memory-files/SKILL.md +3 -1
- package/skills/rudder-create-agent/SKILL.md +21 -4
- package/skills/rudder-create-agent/references/api-reference.md +8 -3
- package/skills/rudder-create-agent/references/cli-reference.md +8 -2
- package/ui-dist/assets/{_basePickBy-C5FevVGb.js → _basePickBy-9EA6dBFj.js} +1 -1
- package/ui-dist/assets/{_baseUniq-Bp5Cq-Lt.js → _baseUniq-puJRDjRm.js} +1 -1
- package/ui-dist/assets/{arc-DxCinQZQ.js → arc-BuvB_2Wz.js} +1 -1
- package/ui-dist/assets/{architectureDiagram-2XIMDMQ5-Bt4OB6rg.js → architectureDiagram-2XIMDMQ5-DNH3NcPr.js} +1 -1
- package/ui-dist/assets/{blockDiagram-WCTKOSBZ-AfUyCHdW.js → blockDiagram-WCTKOSBZ-CCjA-egI.js} +1 -1
- package/ui-dist/assets/{c4Diagram-IC4MRINW-ZQmapm_f.js → c4Diagram-IC4MRINW-DaAxG30_.js} +1 -1
- package/ui-dist/assets/channel-BHmUwLHY.js +1 -0
- package/ui-dist/assets/{chunk-4BX2VUAB-b-nhg8XG.js → chunk-4BX2VUAB-CuuLnPLx.js} +1 -1
- package/ui-dist/assets/{chunk-55IACEB6-D_mWeaWL.js → chunk-55IACEB6-7KqKHU50.js} +1 -1
- package/ui-dist/assets/{chunk-FMBD7UC4-CvCBPkxY.js → chunk-FMBD7UC4-CquRnk_C.js} +1 -1
- package/ui-dist/assets/{chunk-JSJVCQXG-CyIzde6d.js → chunk-JSJVCQXG-Cub6UI-9.js} +1 -1
- package/ui-dist/assets/{chunk-KX2RTZJC-664uOAt1.js → chunk-KX2RTZJC-D-R4Pk61.js} +1 -1
- package/ui-dist/assets/{chunk-NQ4KR5QH-zC9eKlQL.js → chunk-NQ4KR5QH-YQLRgLCT.js} +1 -1
- package/ui-dist/assets/{chunk-QZHKN3VN-Bso6mrAm.js → chunk-QZHKN3VN-BgxQG6QM.js} +1 -1
- package/ui-dist/assets/{chunk-WL4C6EOR-CGgjDf4Q.js → chunk-WL4C6EOR-CVJNOFb-.js} +1 -1
- package/ui-dist/assets/classDiagram-VBA2DB6C-BykYYXhO.js +1 -0
- package/ui-dist/assets/classDiagram-v2-RAHNMMFH-BykYYXhO.js +1 -0
- package/ui-dist/assets/clone-BjbqkGJk.js +1 -0
- package/ui-dist/assets/{cose-bilkent-S5V4N54A-ChfhiHs0.js → cose-bilkent-S5V4N54A-BGYYdPRC.js} +1 -1
- package/ui-dist/assets/{dagre-KLK3FWXG-BtdGql15.js → dagre-KLK3FWXG-CDgRaJNK.js} +1 -1
- package/ui-dist/assets/{diagram-E7M64L7V-CcQq6lyW.js → diagram-E7M64L7V-CQEBiicN.js} +1 -1
- package/ui-dist/assets/{diagram-IFDJBPK2-C8MRQ8-O.js → diagram-IFDJBPK2-cGKTVrZq.js} +1 -1
- package/ui-dist/assets/{diagram-P4PSJMXO-wDtyafSS.js → diagram-P4PSJMXO-fGAfKBU_.js} +1 -1
- package/ui-dist/assets/{erDiagram-INFDFZHY-DSPOGKs9.js → erDiagram-INFDFZHY-DW5vJI98.js} +1 -1
- package/ui-dist/assets/{flowDiagram-PKNHOUZH-CMRO_o51.js → flowDiagram-PKNHOUZH-CikVuzCR.js} +1 -1
- package/ui-dist/assets/{ganttDiagram-A5KZAMGK-ByVpG5X7.js → ganttDiagram-A5KZAMGK-Ca4perbO.js} +1 -1
- package/ui-dist/assets/{gitGraphDiagram-K3NZZRJ6-C0hZhA2f.js → gitGraphDiagram-K3NZZRJ6-hkDkX0wB.js} +1 -1
- package/ui-dist/assets/{graph-8ZSpiLvu.js → graph-CKVwuNpm.js} +1 -1
- package/ui-dist/assets/{index-Jl3ZTphD.js → index-B24_1Y25.js} +1 -1
- package/ui-dist/assets/{index-Bnqrds93.js → index-BCSq0Y_A.js} +1 -1
- package/ui-dist/assets/{index-LxYtcd2q.js → index-BbX5RwLL.js} +1 -1
- package/ui-dist/assets/{index-BuxAGDe1.js → index-Bj5f8srw.js} +1 -1
- package/ui-dist/assets/{index-CrjKYwlq.js → index-Bm5RRuGQ.js} +1 -1
- package/ui-dist/assets/{index-Byt3a14a.js → index-BzHEDVXA.js} +1 -1
- package/ui-dist/assets/{index-C96r3ncF.js → index-C5IbLmrM.js} +1 -1
- package/ui-dist/assets/{index-DSa_Y_jA.js → index-CBuiHrHJ.js} +1 -1
- package/ui-dist/assets/{index-CsgWTWOx.js → index-CGtsmbZm.js} +1 -1
- package/ui-dist/assets/{index-Dolr9Kee.js → index-CIlRDiw5.js} +1 -1
- package/ui-dist/assets/{index-tGztn4Is.js → index-CT8eqX9W.js} +1 -1
- package/ui-dist/assets/{index-CeJdOYIF.js → index-CjD2xZdW.js} +1 -1
- package/ui-dist/assets/{index-C7DEZ3Ju.js → index-DFeHRm34.js} +1 -1
- package/ui-dist/assets/{index-ChJl_hqp.js → index-DI-FLO2Z.js} +1 -1
- package/ui-dist/assets/{index-D083o6by.js → index-DJ84yjUf.js} +1 -1
- package/ui-dist/assets/index-DTw34fFZ.js +1398 -0
- package/ui-dist/assets/{index--8IW0gQi.js → index-DZ6kUIBM.js} +1 -1
- package/ui-dist/assets/{index-D5fB3OrO.js → index-DdFp0EEO.js} +1 -1
- package/ui-dist/assets/{index-BYlbpnGO.js → index-Dm4kNTCW.js} +1 -1
- package/ui-dist/assets/{index-DIlroFT7.js → index-aK5eezHP.js} +1 -1
- package/ui-dist/assets/{index-DoCNo7J9.js → index-dd4k0fyq.js} +1 -1
- package/ui-dist/assets/index-jnv9Ql_2.css +1 -0
- package/ui-dist/assets/{index-Do2QEU2O.js → index-kGMjx6qb.js} +1 -1
- package/ui-dist/assets/{index-DGliz_Zl.js → index-qEEWalog.js} +1 -1
- package/ui-dist/assets/{infoDiagram-LFFYTUFH-CRObxa1Q.js → infoDiagram-LFFYTUFH-aDNdkSKW.js} +1 -1
- package/ui-dist/assets/{ishikawaDiagram-PHBUUO56-ksXzVP6h.js → ishikawaDiagram-PHBUUO56-CmclzHhC.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-4ABVD52K-DhLhkeS3.js → journeyDiagram-4ABVD52K-BFnBxKuG.js} +1 -1
- package/ui-dist/assets/{kanban-definition-K7BYSVSG-CJPHwSur.js → kanban-definition-K7BYSVSG-eJfOZg7R.js} +1 -1
- package/ui-dist/assets/{layout-CbB6lAw2.js → layout-CLRiNHgA.js} +1 -1
- package/ui-dist/assets/{linear-HPte01nq.js → linear-B-J9sUer.js} +1 -1
- package/ui-dist/assets/{mermaid.core-CaHTquLw.js → mermaid.core-C1MjBOIN.js} +4 -4
- package/ui-dist/assets/{mindmap-definition-YRQLILUH-CeZ9z-BE.js → mindmap-definition-YRQLILUH-BdvCmP6e.js} +1 -1
- package/ui-dist/assets/{pieDiagram-SKSYHLDU-YB621clF.js → pieDiagram-SKSYHLDU-BAITPD_t.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-337W2JSQ-KPDGBXfE.js → quadrantDiagram-337W2JSQ-BFnjyhzq.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-Z7DCOOCP-CnMP-_Rj.js → requirementDiagram-Z7DCOOCP-Bxg6tlLh.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-WA2Y5GQK-rWDbj38-.js → sankeyDiagram-WA2Y5GQK-LPpklLQK.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-2WXFIKYE-D5IlEfYm.js → sequenceDiagram-2WXFIKYE-D-W6lss0.js} +1 -1
- package/ui-dist/assets/{stateDiagram-RAJIS63D-CI6m7yMI.js → stateDiagram-RAJIS63D-Bzo5M8P7.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-DJ1MxF2S.js +1 -0
- package/ui-dist/assets/{timeline-definition-YZTLITO2-Bl1-YzON.js → timeline-definition-YZTLITO2-luuVqyTW.js} +1 -1
- package/ui-dist/assets/{treemap-KZPCXAKY-CcFSGzuM.js → treemap-KZPCXAKY-ChGqzx5u.js} +1 -1
- package/ui-dist/assets/{vennDiagram-LZ73GAT5-DpgfFxeZ.js → vennDiagram-LZ73GAT5-BCEjZinK.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-JWTSCODW-Bas4tWGP.js → xychartDiagram-JWTSCODW-mAsE6hMg.js} +1 -1
- package/ui-dist/index.html +2 -2
- package/dist/onboarding-assets/ceo/AGENTS.md +0 -33
- package/dist/onboarding-assets/default/AGENTS.md +0 -9
- package/ui-dist/assets/channel-B-3UKZ6E.js +0 -1
- package/ui-dist/assets/classDiagram-VBA2DB6C-DJbF61vn.js +0 -1
- package/ui-dist/assets/classDiagram-v2-RAHNMMFH-DJbF61vn.js +0 -1
- package/ui-dist/assets/clone-B7Z_Fd8l.js +0 -1
- package/ui-dist/assets/index-B4jXCLTd.js +0 -1358
- package/ui-dist/assets/index-C187WwUh.css +0 -1
- 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
|