patchwork-os 0.2.0-alpha.0 → 0.2.0-alpha.11
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/README.md +41 -46
- package/dist/bridge.js +23 -10
- package/dist/bridge.js.map +1 -1
- package/dist/claudeDriver.d.ts +3 -1
- package/dist/claudeDriver.js +48 -0
- package/dist/claudeDriver.js.map +1 -1
- package/dist/commands/dashboard.d.ts +47 -0
- package/dist/commands/dashboard.js +319 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +5 -2
- package/dist/config.js.map +1 -1
- package/dist/connectors/github.d.ts +94 -0
- package/dist/connectors/github.js +350 -0
- package/dist/connectors/github.js.map +1 -0
- package/dist/connectors/gmail.d.ts +40 -0
- package/dist/connectors/gmail.js +304 -0
- package/dist/connectors/gmail.js.map +1 -0
- package/dist/connectors/googleCalendar.d.ts +57 -0
- package/dist/connectors/googleCalendar.js +308 -0
- package/dist/connectors/googleCalendar.js.map +1 -0
- package/dist/connectors/linear.d.ts +117 -0
- package/dist/connectors/linear.js +248 -0
- package/dist/connectors/linear.js.map +1 -0
- package/dist/connectors/mcpClient.d.ts +56 -0
- package/dist/connectors/mcpClient.js +189 -0
- package/dist/connectors/mcpClient.js.map +1 -0
- package/dist/connectors/mcpOAuth.d.ts +83 -0
- package/dist/connectors/mcpOAuth.js +363 -0
- package/dist/connectors/mcpOAuth.js.map +1 -0
- package/dist/connectors/sentry.d.ts +43 -0
- package/dist/connectors/sentry.js +197 -0
- package/dist/connectors/sentry.js.map +1 -0
- package/dist/connectors/slack.d.ts +50 -0
- package/dist/connectors/slack.js +254 -0
- package/dist/connectors/slack.js.map +1 -0
- package/dist/drivers/claude/api.d.ts +11 -0
- package/dist/drivers/claude/api.js +54 -0
- package/dist/drivers/claude/api.js.map +1 -0
- package/dist/drivers/claude/envSanitizer.d.ts +7 -0
- package/dist/drivers/claude/envSanitizer.js +18 -0
- package/dist/drivers/claude/envSanitizer.js.map +1 -0
- package/dist/drivers/claude/streamParser.d.ts +38 -0
- package/dist/drivers/claude/streamParser.js +34 -0
- package/dist/drivers/claude/streamParser.js.map +1 -0
- package/dist/drivers/claude/subprocess.d.ts +19 -0
- package/dist/drivers/claude/subprocess.js +216 -0
- package/dist/drivers/claude/subprocess.js.map +1 -0
- package/dist/drivers/claude/subprocessSettings.d.ts +9 -0
- package/dist/drivers/claude/subprocessSettings.js +55 -0
- package/dist/drivers/claude/subprocessSettings.js.map +1 -0
- package/dist/drivers/gemini/index.d.ts +14 -0
- package/dist/drivers/gemini/index.js +176 -0
- package/dist/drivers/gemini/index.js.map +1 -0
- package/dist/drivers/grok/index.d.ts +11 -0
- package/dist/drivers/grok/index.js +22 -0
- package/dist/drivers/grok/index.js.map +1 -0
- package/dist/drivers/index.d.ts +18 -0
- package/dist/drivers/index.js +31 -0
- package/dist/drivers/index.js.map +1 -0
- package/dist/drivers/openai/index.d.ts +24 -0
- package/dist/drivers/openai/index.js +110 -0
- package/dist/drivers/openai/index.js.map +1 -0
- package/dist/drivers/types.d.ts +72 -0
- package/dist/drivers/types.js +30 -0
- package/dist/drivers/types.js.map +1 -0
- package/dist/index.js +116 -22
- package/dist/index.js.map +1 -1
- package/dist/recipes/yamlRunner.d.ts +104 -0
- package/dist/recipes/yamlRunner.js +683 -0
- package/dist/recipes/yamlRunner.js.map +1 -0
- package/dist/recipesHttp.d.ts +13 -1
- package/dist/recipesHttp.js +9 -1
- package/dist/recipesHttp.js.map +1 -1
- package/dist/runLog.d.ts +5 -0
- package/dist/runLog.js +44 -0
- package/dist/runLog.js.map +1 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +490 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/addLinearComment.d.ts +55 -0
- package/dist/tools/addLinearComment.js +70 -0
- package/dist/tools/addLinearComment.js.map +1 -0
- package/dist/tools/createLinearIssue.d.ts +84 -0
- package/dist/tools/createLinearIssue.js +146 -0
- package/dist/tools/createLinearIssue.js.map +1 -0
- package/dist/tools/ctxGetTaskContext.d.ts +4 -1
- package/dist/tools/ctxGetTaskContext.js +45 -2
- package/dist/tools/ctxGetTaskContext.js.map +1 -1
- package/dist/tools/fetchCalendarEvents.d.ts +94 -0
- package/dist/tools/fetchCalendarEvents.js +97 -0
- package/dist/tools/fetchCalendarEvents.js.map +1 -0
- package/dist/tools/fetchGithubIssue.d.ts +80 -0
- package/dist/tools/fetchGithubIssue.js +84 -0
- package/dist/tools/fetchGithubIssue.js.map +1 -0
- package/dist/tools/fetchGithubPR.d.ts +89 -0
- package/dist/tools/fetchGithubPR.js +96 -0
- package/dist/tools/fetchGithubPR.js.map +1 -0
- package/dist/tools/fetchLinearIssue.d.ts +112 -0
- package/dist/tools/fetchLinearIssue.js +129 -0
- package/dist/tools/fetchLinearIssue.js.map +1 -0
- package/dist/tools/fetchSentryIssue.d.ts +143 -0
- package/dist/tools/fetchSentryIssue.js +150 -0
- package/dist/tools/fetchSentryIssue.js.map +1 -0
- package/dist/tools/fetchSlackProfile.d.ts +43 -0
- package/dist/tools/fetchSlackProfile.js +43 -0
- package/dist/tools/fetchSlackProfile.js.map +1 -0
- package/dist/tools/getConnectorStatus.d.ts +58 -0
- package/dist/tools/getConnectorStatus.js +56 -0
- package/dist/tools/getConnectorStatus.js.map +1 -0
- package/dist/tools/github/index.d.ts +1 -1
- package/dist/tools/github/index.js +1 -1
- package/dist/tools/github/index.js.map +1 -1
- package/dist/tools/github/pr.d.ts +122 -0
- package/dist/tools/github/pr.js +152 -0
- package/dist/tools/github/pr.js.map +1 -1
- package/dist/tools/index.js +27 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/slackListChannels.d.ts +65 -0
- package/dist/tools/slackListChannels.js +70 -0
- package/dist/tools/slackListChannels.js.map +1 -0
- package/dist/tools/slackPostMessage.d.ts +57 -0
- package/dist/tools/slackPostMessage.js +72 -0
- package/dist/tools/slackPostMessage.js.map +1 -0
- package/dist/tools/updateLinearIssue.d.ts +89 -0
- package/dist/tools/updateLinearIssue.js +103 -0
- package/dist/tools/updateLinearIssue.js.map +1 -0
- package/package.json +1 -1
- package/scripts/start-all.sh +56 -19
- package/templates/recipes/ctx-loop-test.yaml +75 -0
- package/templates/recipes/gmail-health-check.yaml +19 -0
- package/templates/recipes/inbox-triage.yaml +15 -0
- package/templates/recipes/morning-brief-slack.yaml +54 -0
- package/templates/recipes/morning-brief.yaml +72 -0
- package/templates/recipes/sentry-to-linear.yaml +77 -0
- package/templates/scheduled-tasks/morning-brief/SKILL.md +37 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail OAuth 2.0 connector.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* GET /connections/gmail/auth — redirect to Google consent screen
|
|
6
|
+
* GET /connections/gmail/callback — exchange code for tokens, store locally
|
|
7
|
+
* POST /connections/gmail/test — verify stored token works
|
|
8
|
+
* DELETE /connections/gmail — revoke + delete stored token
|
|
9
|
+
* GET /connections — list connector statuses
|
|
10
|
+
*
|
|
11
|
+
* Tokens stored at ~/.patchwork/tokens/gmail.json (mode 0600, never leaves machine).
|
|
12
|
+
* Client credentials read from env: GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET.
|
|
13
|
+
*/
|
|
14
|
+
import crypto from "node:crypto";
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
const SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"];
|
|
19
|
+
const REDIRECT_URI = "http://localhost:3100/connections/gmail/callback";
|
|
20
|
+
const TOKEN_PATH = path.join(homedir(), ".patchwork", "tokens", "gmail.json");
|
|
21
|
+
function clientId() {
|
|
22
|
+
return process.env.GMAIL_CLIENT_ID ?? "";
|
|
23
|
+
}
|
|
24
|
+
function clientSecret() {
|
|
25
|
+
return process.env.GMAIL_CLIENT_SECRET ?? "";
|
|
26
|
+
}
|
|
27
|
+
function isConfigured() {
|
|
28
|
+
return Boolean(clientId() && clientSecret());
|
|
29
|
+
}
|
|
30
|
+
// ── Token storage ─────────────────────────────────────────────────────────────
|
|
31
|
+
export function loadTokens() {
|
|
32
|
+
if (!existsSync(TOKEN_PATH))
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(TOKEN_PATH, "utf-8"));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function saveTokens(tokens) {
|
|
42
|
+
mkdirSync(path.dirname(TOKEN_PATH), { recursive: true, mode: 0o700 });
|
|
43
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
44
|
+
}
|
|
45
|
+
function deleteTokens() {
|
|
46
|
+
if (existsSync(TOKEN_PATH))
|
|
47
|
+
unlinkSync(TOKEN_PATH);
|
|
48
|
+
}
|
|
49
|
+
// ── OAuth helpers ─────────────────────────────────────────────────────────────
|
|
50
|
+
function buildAuthUrl(state) {
|
|
51
|
+
const params = new URLSearchParams({
|
|
52
|
+
client_id: clientId(),
|
|
53
|
+
redirect_uri: REDIRECT_URI,
|
|
54
|
+
response_type: "code",
|
|
55
|
+
scope: SCOPES.join(" "),
|
|
56
|
+
access_type: "offline",
|
|
57
|
+
prompt: "consent",
|
|
58
|
+
state,
|
|
59
|
+
});
|
|
60
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
|
61
|
+
}
|
|
62
|
+
async function exchangeCode(code) {
|
|
63
|
+
const res = await fetch("https://oauth2.googleapis.com/token", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
66
|
+
body: new URLSearchParams({
|
|
67
|
+
code,
|
|
68
|
+
client_id: clientId(),
|
|
69
|
+
client_secret: clientSecret(),
|
|
70
|
+
redirect_uri: REDIRECT_URI,
|
|
71
|
+
grant_type: "authorization_code",
|
|
72
|
+
}).toString(),
|
|
73
|
+
});
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
const body = await res.text();
|
|
76
|
+
throw new Error(`Token exchange failed: ${res.status} ${body}`);
|
|
77
|
+
}
|
|
78
|
+
const json = (await res.json());
|
|
79
|
+
return {
|
|
80
|
+
access_token: json.access_token,
|
|
81
|
+
refresh_token: json.refresh_token,
|
|
82
|
+
expiry_date: json.expires_in
|
|
83
|
+
? Date.now() + json.expires_in * 1000
|
|
84
|
+
: undefined,
|
|
85
|
+
token_type: json.token_type,
|
|
86
|
+
scope: json.scope,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async function refreshAccessToken(tokens) {
|
|
90
|
+
if (!tokens.refresh_token)
|
|
91
|
+
throw new Error("No refresh token available");
|
|
92
|
+
const res = await fetch("https://oauth2.googleapis.com/token", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
95
|
+
body: new URLSearchParams({
|
|
96
|
+
refresh_token: tokens.refresh_token,
|
|
97
|
+
client_id: clientId(),
|
|
98
|
+
client_secret: clientSecret(),
|
|
99
|
+
grant_type: "refresh_token",
|
|
100
|
+
}).toString(),
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
const body = await res.text();
|
|
104
|
+
throw new Error(`Token refresh failed: ${res.status} ${body}`);
|
|
105
|
+
}
|
|
106
|
+
const json = (await res.json());
|
|
107
|
+
const updated = {
|
|
108
|
+
...tokens,
|
|
109
|
+
access_token: json.access_token,
|
|
110
|
+
expiry_date: json.expires_in
|
|
111
|
+
? Date.now() + json.expires_in * 1000
|
|
112
|
+
: tokens.expiry_date,
|
|
113
|
+
};
|
|
114
|
+
saveTokens(updated);
|
|
115
|
+
return updated;
|
|
116
|
+
}
|
|
117
|
+
/** Returns a valid access token, refreshing if needed. */
|
|
118
|
+
export async function getValidAccessToken() {
|
|
119
|
+
let tokens = loadTokens();
|
|
120
|
+
if (!tokens)
|
|
121
|
+
throw new Error("Gmail not connected");
|
|
122
|
+
const bufferMs = 60_000;
|
|
123
|
+
if (tokens.expiry_date && Date.now() > tokens.expiry_date - bufferMs) {
|
|
124
|
+
tokens = await refreshAccessToken(tokens);
|
|
125
|
+
}
|
|
126
|
+
return tokens.access_token;
|
|
127
|
+
}
|
|
128
|
+
async function revokeToken(token) {
|
|
129
|
+
await fetch(`https://oauth2.googleapis.com/revoke?token=${encodeURIComponent(token)}`, { method: "POST" }).catch(() => { });
|
|
130
|
+
}
|
|
131
|
+
async function fetchUserEmail(accessToken) {
|
|
132
|
+
const res = await fetch("https://gmail.googleapis.com/gmail/v1/users/me/profile", { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
133
|
+
if (!res.ok)
|
|
134
|
+
return "";
|
|
135
|
+
const json = (await res.json());
|
|
136
|
+
return json.emailAddress ?? "";
|
|
137
|
+
}
|
|
138
|
+
// ── State map (in-memory CSRF protection) ────────────────────────────────────
|
|
139
|
+
const pendingStates = new Set();
|
|
140
|
+
function generateState() {
|
|
141
|
+
const state = crypto.randomBytes(32).toString("hex");
|
|
142
|
+
pendingStates.add(state);
|
|
143
|
+
setTimeout(() => pendingStates.delete(state), 10 * 60 * 1000); // 10 min TTL
|
|
144
|
+
return state;
|
|
145
|
+
}
|
|
146
|
+
export async function handleConnectionsList() {
|
|
147
|
+
const tokens = loadTokens();
|
|
148
|
+
const { getStatus: getGitHubStatus } = await import("./github.js");
|
|
149
|
+
const { getStatus: getSentryStatus } = await import("./sentry.js");
|
|
150
|
+
const { getStatus: getLinearStatus } = await import("./linear.js");
|
|
151
|
+
const { getStatus: getCalendarStatus } = await import("./googleCalendar.js");
|
|
152
|
+
const { isConnected: isSlackConnected, getProfile: getSlackProfile } = await import("./slack.js");
|
|
153
|
+
const gh = getGitHubStatus();
|
|
154
|
+
const sentry = getSentryStatus();
|
|
155
|
+
const linear = getLinearStatus();
|
|
156
|
+
const calendar = getCalendarStatus();
|
|
157
|
+
const slackConnected = isSlackConnected();
|
|
158
|
+
const slackProfile = getSlackProfile();
|
|
159
|
+
const connectors = [
|
|
160
|
+
{
|
|
161
|
+
id: "gmail",
|
|
162
|
+
status: tokens ? "connected" : "disconnected",
|
|
163
|
+
lastSync: tokens ? new Date().toISOString() : undefined,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "github",
|
|
167
|
+
status: gh.connected ? "connected" : "disconnected",
|
|
168
|
+
lastSync: gh.connected ? new Date().toISOString() : undefined,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "sentry",
|
|
172
|
+
status: sentry.status,
|
|
173
|
+
lastSync: sentry.lastSync,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "linear",
|
|
177
|
+
status: linear.status,
|
|
178
|
+
lastSync: linear.lastSync,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "google-calendar",
|
|
182
|
+
status: calendar.status,
|
|
183
|
+
lastSync: calendar.lastSync,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "slack",
|
|
187
|
+
status: slackConnected ? "connected" : "disconnected",
|
|
188
|
+
lastSync: slackConnected && slackProfile ? new Date().toISOString() : undefined,
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
return {
|
|
192
|
+
status: 200,
|
|
193
|
+
contentType: "application/json",
|
|
194
|
+
body: JSON.stringify({ connectors }),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export function handleGmailAuthRedirect() {
|
|
198
|
+
if (!isConfigured()) {
|
|
199
|
+
return {
|
|
200
|
+
status: 503,
|
|
201
|
+
contentType: "application/json",
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
error: "Gmail connector not configured. Set GMAIL_CLIENT_ID and GMAIL_CLIENT_SECRET.",
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const state = generateState();
|
|
208
|
+
const url = buildAuthUrl(state);
|
|
209
|
+
return { status: 302, redirect: url, body: "" };
|
|
210
|
+
}
|
|
211
|
+
export async function handleGmailCallback(code, state, error) {
|
|
212
|
+
if (error) {
|
|
213
|
+
return {
|
|
214
|
+
status: 400,
|
|
215
|
+
contentType: "text/html",
|
|
216
|
+
body: callbackHtml("Connection cancelled", `Google returned: ${error}`, false),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (!code || !state || !pendingStates.has(state)) {
|
|
220
|
+
return {
|
|
221
|
+
status: 400,
|
|
222
|
+
contentType: "text/html",
|
|
223
|
+
body: callbackHtml("Invalid request", "Missing or expired state.", false),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
pendingStates.delete(state);
|
|
227
|
+
try {
|
|
228
|
+
const tokens = await exchangeCode(code);
|
|
229
|
+
saveTokens(tokens);
|
|
230
|
+
const email = await fetchUserEmail(tokens.access_token);
|
|
231
|
+
return {
|
|
232
|
+
status: 200,
|
|
233
|
+
contentType: "text/html",
|
|
234
|
+
body: callbackHtml("Gmail connected", `Connected${email ? ` as ${email}` : ""}. You can close this tab.`, true),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
return {
|
|
239
|
+
status: 500,
|
|
240
|
+
contentType: "text/html",
|
|
241
|
+
body: callbackHtml("Connection failed", err instanceof Error ? err.message : String(err), false),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
export async function handleGmailTest() {
|
|
246
|
+
try {
|
|
247
|
+
const token = await getValidAccessToken();
|
|
248
|
+
const email = await fetchUserEmail(token);
|
|
249
|
+
return {
|
|
250
|
+
status: 200,
|
|
251
|
+
contentType: "application/json",
|
|
252
|
+
body: JSON.stringify({ ok: true, email }),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
return {
|
|
257
|
+
status: 400,
|
|
258
|
+
contentType: "application/json",
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
ok: false,
|
|
261
|
+
error: err instanceof Error ? err.message : String(err),
|
|
262
|
+
}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
export async function handleGmailDisconnect() {
|
|
267
|
+
const tokens = loadTokens();
|
|
268
|
+
if (tokens?.access_token) {
|
|
269
|
+
await revokeToken(tokens.access_token);
|
|
270
|
+
}
|
|
271
|
+
deleteTokens();
|
|
272
|
+
return {
|
|
273
|
+
status: 200,
|
|
274
|
+
contentType: "application/json",
|
|
275
|
+
body: JSON.stringify({ ok: true }),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
// ── Callback HTML ─────────────────────────────────────────────────────────────
|
|
279
|
+
function callbackHtml(title, message, success) {
|
|
280
|
+
const color = success ? "#b8ff57" : "#ff5555";
|
|
281
|
+
return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
|
|
282
|
+
<title>${title} — Patchwork OS</title>
|
|
283
|
+
<style>
|
|
284
|
+
body { background: #040406; color: #e0e0e0; font-family: system-ui, sans-serif;
|
|
285
|
+
display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
|
286
|
+
.card { background: #0d0d14; border: 1px solid #1e1e2e; border-radius: 8px;
|
|
287
|
+
padding: 40px 48px; max-width: 420px; text-align: center; }
|
|
288
|
+
h1 { color: ${color}; font-size: 1.4rem; margin-bottom: 12px; }
|
|
289
|
+
p { color: #888; line-height: 1.6; }
|
|
290
|
+
a { color: ${color}; text-decoration: none; font-size: 0.9rem; }
|
|
291
|
+
</style>
|
|
292
|
+
</head>
|
|
293
|
+
<body><div class="card">
|
|
294
|
+
<h1>${title}</h1>
|
|
295
|
+
<p>${message}</p>
|
|
296
|
+
<br><a href="javascript:window.close()">Close this tab</a>
|
|
297
|
+
</div>
|
|
298
|
+
<script>
|
|
299
|
+
// Notify the opener tab that auth completed so it can poll.
|
|
300
|
+
if (window.opener) { try { window.opener.postMessage('patchwork:gmail:connected', 'http://localhost:3100'); } catch(_) {} }
|
|
301
|
+
</script>
|
|
302
|
+
</body></html>`;
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=gmail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gmail.js","sourceRoot":"","sources":["../../src/connectors/gmail.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Calendar OAuth 2.0 connector.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* GET /connections/google-calendar/auth — redirect to Google consent screen
|
|
6
|
+
* GET /connections/google-calendar/callback — exchange code for tokens, store locally
|
|
7
|
+
* POST /connections/google-calendar/test — verify stored token works
|
|
8
|
+
* DELETE /connections/google-calendar — revoke + delete stored token
|
|
9
|
+
*
|
|
10
|
+
* Tokens stored at ~/.patchwork/tokens/google-calendar.json (mode 0600).
|
|
11
|
+
* Client credentials read from env: GOOGLE_CALENDAR_CLIENT_ID, GOOGLE_CALENDAR_CLIENT_SECRET
|
|
12
|
+
*/
|
|
13
|
+
export interface CalendarTokens {
|
|
14
|
+
access_token: string;
|
|
15
|
+
refresh_token?: string;
|
|
16
|
+
expiry_date?: number;
|
|
17
|
+
token_type?: string;
|
|
18
|
+
scope?: string;
|
|
19
|
+
calendar_id: string;
|
|
20
|
+
connected_at: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ConnectorStatus {
|
|
23
|
+
id: string;
|
|
24
|
+
status: "connected" | "disconnected";
|
|
25
|
+
lastSync?: string;
|
|
26
|
+
calendarId?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface CalendarEvent {
|
|
29
|
+
id: string;
|
|
30
|
+
summary: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
start: string;
|
|
33
|
+
end: string;
|
|
34
|
+
allDay: boolean;
|
|
35
|
+
location?: string;
|
|
36
|
+
htmlLink: string;
|
|
37
|
+
attendees?: string[];
|
|
38
|
+
}
|
|
39
|
+
export interface ConnectorHandlerResult {
|
|
40
|
+
status: number;
|
|
41
|
+
body: string;
|
|
42
|
+
contentType?: string;
|
|
43
|
+
redirect?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function loadTokens(): CalendarTokens | null;
|
|
46
|
+
export declare function getStatus(): ConnectorStatus;
|
|
47
|
+
/** Returns a valid access token, refreshing if needed. */
|
|
48
|
+
export declare function getValidAccessToken(): Promise<string>;
|
|
49
|
+
export declare function listEvents(opts?: {
|
|
50
|
+
daysAhead?: number;
|
|
51
|
+
maxResults?: number;
|
|
52
|
+
calendarId?: string;
|
|
53
|
+
}, signal?: AbortSignal): Promise<CalendarEvent[]>;
|
|
54
|
+
export declare function handleCalendarAuthRedirect(): ConnectorHandlerResult;
|
|
55
|
+
export declare function handleCalendarCallback(code: string | null, state: string | null, error: string | null): Promise<ConnectorHandlerResult>;
|
|
56
|
+
export declare function handleCalendarTest(): Promise<ConnectorHandlerResult>;
|
|
57
|
+
export declare function handleCalendarDisconnect(): Promise<ConnectorHandlerResult>;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Calendar OAuth 2.0 connector.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* GET /connections/google-calendar/auth — redirect to Google consent screen
|
|
6
|
+
* GET /connections/google-calendar/callback — exchange code for tokens, store locally
|
|
7
|
+
* POST /connections/google-calendar/test — verify stored token works
|
|
8
|
+
* DELETE /connections/google-calendar — revoke + delete stored token
|
|
9
|
+
*
|
|
10
|
+
* Tokens stored at ~/.patchwork/tokens/google-calendar.json (mode 0600).
|
|
11
|
+
* Client credentials read from env: GOOGLE_CALENDAR_CLIENT_ID, GOOGLE_CALENDAR_CLIENT_SECRET
|
|
12
|
+
*/
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
const SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"];
|
|
18
|
+
const REDIRECT_URI = process.env.PATCHWORK_DASHBOARD_URL
|
|
19
|
+
? `${process.env.PATCHWORK_DASHBOARD_URL}/connections/google-calendar/callback`
|
|
20
|
+
: "http://localhost:3200/connections/google-calendar/callback";
|
|
21
|
+
const CALENDAR_API = "https://www.googleapis.com/calendar/v3";
|
|
22
|
+
const TOKEN_PATH = path.join(homedir(), ".patchwork", "tokens", "google-calendar.json");
|
|
23
|
+
function clientId() {
|
|
24
|
+
return process.env.GOOGLE_CALENDAR_CLIENT_ID ?? "";
|
|
25
|
+
}
|
|
26
|
+
function clientSecret() {
|
|
27
|
+
return process.env.GOOGLE_CALENDAR_CLIENT_SECRET ?? "";
|
|
28
|
+
}
|
|
29
|
+
function isConfigured() {
|
|
30
|
+
return Boolean(clientId() && clientSecret());
|
|
31
|
+
}
|
|
32
|
+
// ── Token storage ─────────────────────────────────────────────────────────────
|
|
33
|
+
export function loadTokens() {
|
|
34
|
+
if (!existsSync(TOKEN_PATH))
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(TOKEN_PATH, "utf-8"));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function saveTokens(tokens) {
|
|
44
|
+
mkdirSync(path.dirname(TOKEN_PATH), { recursive: true, mode: 0o700 });
|
|
45
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
46
|
+
}
|
|
47
|
+
function deleteTokens() {
|
|
48
|
+
if (existsSync(TOKEN_PATH))
|
|
49
|
+
unlinkSync(TOKEN_PATH);
|
|
50
|
+
}
|
|
51
|
+
export function getStatus() {
|
|
52
|
+
const tokens = loadTokens();
|
|
53
|
+
return {
|
|
54
|
+
id: "google-calendar",
|
|
55
|
+
status: tokens ? "connected" : "disconnected",
|
|
56
|
+
lastSync: tokens?.connected_at,
|
|
57
|
+
calendarId: tokens?.calendar_id,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// ── OAuth helpers ─────────────────────────────────────────────────────────────
|
|
61
|
+
function buildAuthUrl(state) {
|
|
62
|
+
const params = new URLSearchParams({
|
|
63
|
+
client_id: clientId(),
|
|
64
|
+
redirect_uri: REDIRECT_URI,
|
|
65
|
+
response_type: "code",
|
|
66
|
+
scope: SCOPES.join(" "),
|
|
67
|
+
access_type: "offline",
|
|
68
|
+
prompt: "consent",
|
|
69
|
+
state,
|
|
70
|
+
});
|
|
71
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
|
72
|
+
}
|
|
73
|
+
async function exchangeCode(code) {
|
|
74
|
+
const res = await fetch("https://oauth2.googleapis.com/token", {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
77
|
+
body: new URLSearchParams({
|
|
78
|
+
code,
|
|
79
|
+
client_id: clientId(),
|
|
80
|
+
client_secret: clientSecret(),
|
|
81
|
+
redirect_uri: REDIRECT_URI,
|
|
82
|
+
grant_type: "authorization_code",
|
|
83
|
+
}).toString(),
|
|
84
|
+
});
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const body = await res.text();
|
|
87
|
+
throw new Error(`Token exchange failed: ${res.status} ${body}`);
|
|
88
|
+
}
|
|
89
|
+
const json = (await res.json());
|
|
90
|
+
return {
|
|
91
|
+
access_token: json.access_token,
|
|
92
|
+
refresh_token: json.refresh_token,
|
|
93
|
+
expiry_date: json.expires_in
|
|
94
|
+
? Date.now() + json.expires_in * 1000
|
|
95
|
+
: undefined,
|
|
96
|
+
token_type: json.token_type,
|
|
97
|
+
scope: json.scope,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
async function refreshAccessToken(tokens) {
|
|
101
|
+
if (!tokens.refresh_token)
|
|
102
|
+
throw new Error("No refresh token available");
|
|
103
|
+
const res = await fetch("https://oauth2.googleapis.com/token", {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
106
|
+
body: new URLSearchParams({
|
|
107
|
+
refresh_token: tokens.refresh_token,
|
|
108
|
+
client_id: clientId(),
|
|
109
|
+
client_secret: clientSecret(),
|
|
110
|
+
grant_type: "refresh_token",
|
|
111
|
+
}).toString(),
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
const body = await res.text();
|
|
115
|
+
throw new Error(`Token refresh failed: ${res.status} ${body}`);
|
|
116
|
+
}
|
|
117
|
+
const json = (await res.json());
|
|
118
|
+
const updated = {
|
|
119
|
+
...tokens,
|
|
120
|
+
access_token: json.access_token,
|
|
121
|
+
expiry_date: json.expires_in
|
|
122
|
+
? Date.now() + json.expires_in * 1000
|
|
123
|
+
: tokens.expiry_date,
|
|
124
|
+
};
|
|
125
|
+
saveTokens(updated);
|
|
126
|
+
return updated;
|
|
127
|
+
}
|
|
128
|
+
/** Returns a valid access token, refreshing if needed. */
|
|
129
|
+
export async function getValidAccessToken() {
|
|
130
|
+
let tokens = loadTokens();
|
|
131
|
+
if (!tokens)
|
|
132
|
+
throw new Error("Google Calendar not connected");
|
|
133
|
+
const bufferMs = 60_000;
|
|
134
|
+
if (tokens.expiry_date && Date.now() > tokens.expiry_date - bufferMs) {
|
|
135
|
+
tokens = await refreshAccessToken(tokens);
|
|
136
|
+
}
|
|
137
|
+
return tokens.access_token;
|
|
138
|
+
}
|
|
139
|
+
async function revokeToken(token) {
|
|
140
|
+
await fetch(`https://oauth2.googleapis.com/revoke?token=${encodeURIComponent(token)}`, { method: "POST" }).catch(() => { });
|
|
141
|
+
}
|
|
142
|
+
// ── State map (in-memory CSRF protection) ────────────────────────────────────
|
|
143
|
+
const pendingStates = new Set();
|
|
144
|
+
function generateState() {
|
|
145
|
+
const state = crypto.randomBytes(32).toString("hex");
|
|
146
|
+
pendingStates.add(state);
|
|
147
|
+
setTimeout(() => pendingStates.delete(state), 10 * 60 * 1000);
|
|
148
|
+
return state;
|
|
149
|
+
}
|
|
150
|
+
// ── API helpers ───────────────────────────────────────────────────────────────
|
|
151
|
+
async function calendarGet(endpoint, accessToken, params = {}, signal) {
|
|
152
|
+
const qs = new URLSearchParams(params);
|
|
153
|
+
const res = await fetch(`${CALENDAR_API}${endpoint}?${qs}`, {
|
|
154
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
155
|
+
signal,
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
const body = await res.text();
|
|
159
|
+
throw new Error(`Google Calendar API error ${res.status}: ${body.slice(0, 200)}`);
|
|
160
|
+
}
|
|
161
|
+
return res.json();
|
|
162
|
+
}
|
|
163
|
+
async function fetchCalendarSummary(accessToken, calendarId) {
|
|
164
|
+
const data = (await calendarGet(`/calendars/${encodeURIComponent(calendarId)}`, accessToken));
|
|
165
|
+
return data.summary ?? calendarId;
|
|
166
|
+
}
|
|
167
|
+
// ── Event fetching ────────────────────────────────────────────────────────────
|
|
168
|
+
export async function listEvents(opts = {}, signal) {
|
|
169
|
+
const accessToken = await getValidAccessToken();
|
|
170
|
+
const tokens = loadTokens();
|
|
171
|
+
const calendarId = opts.calendarId ?? tokens.calendar_id ?? "primary";
|
|
172
|
+
const daysAhead = Math.min(opts.daysAhead ?? 7, 30);
|
|
173
|
+
const maxResults = Math.min(opts.maxResults ?? 20, 50);
|
|
174
|
+
const now = new Date();
|
|
175
|
+
const end = new Date(now.getTime() + daysAhead * 24 * 60 * 60 * 1000);
|
|
176
|
+
const data = (await calendarGet(`/calendars/${encodeURIComponent(calendarId)}/events`, accessToken, {
|
|
177
|
+
timeMin: now.toISOString(),
|
|
178
|
+
timeMax: end.toISOString(),
|
|
179
|
+
maxResults: String(maxResults),
|
|
180
|
+
singleEvents: "true",
|
|
181
|
+
orderBy: "startTime",
|
|
182
|
+
}, signal));
|
|
183
|
+
return (data.items ?? []).map((item) => {
|
|
184
|
+
const startRaw = item.start?.dateTime ?? item.start?.date ?? "";
|
|
185
|
+
const endRaw = item.end?.dateTime ?? item.end?.date ?? "";
|
|
186
|
+
const allDay = !item.start?.dateTime;
|
|
187
|
+
return {
|
|
188
|
+
id: item.id,
|
|
189
|
+
summary: item.summary ?? "(no title)",
|
|
190
|
+
description: item.description,
|
|
191
|
+
start: startRaw,
|
|
192
|
+
end: endRaw,
|
|
193
|
+
allDay,
|
|
194
|
+
location: item.location,
|
|
195
|
+
htmlLink: item.htmlLink ?? "",
|
|
196
|
+
attendees: (item.attendees ?? [])
|
|
197
|
+
.map((a) => a.displayName ?? a.email ?? "")
|
|
198
|
+
.filter(Boolean),
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// ── HTTP handlers ─────────────────────────────────────────────────────────────
|
|
203
|
+
export function handleCalendarAuthRedirect() {
|
|
204
|
+
if (!isConfigured()) {
|
|
205
|
+
return {
|
|
206
|
+
status: 400,
|
|
207
|
+
contentType: "application/json",
|
|
208
|
+
body: JSON.stringify({
|
|
209
|
+
ok: false,
|
|
210
|
+
error: "GOOGLE_CALENDAR_CLIENT_ID and GOOGLE_CALENDAR_CLIENT_SECRET env vars not set",
|
|
211
|
+
}),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const state = generateState();
|
|
215
|
+
return { status: 302, body: "", redirect: buildAuthUrl(state) };
|
|
216
|
+
}
|
|
217
|
+
export async function handleCalendarCallback(code, state, error) {
|
|
218
|
+
if (error) {
|
|
219
|
+
return {
|
|
220
|
+
status: 400,
|
|
221
|
+
contentType: "application/json",
|
|
222
|
+
body: JSON.stringify({ ok: false, error }),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (!code || !state || !pendingStates.has(state)) {
|
|
226
|
+
return {
|
|
227
|
+
status: 400,
|
|
228
|
+
contentType: "application/json",
|
|
229
|
+
body: JSON.stringify({ ok: false, error: "Invalid OAuth state" }),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
pendingStates.delete(state);
|
|
233
|
+
try {
|
|
234
|
+
const oauthTokens = await exchangeCode(code);
|
|
235
|
+
// Default to "primary" calendar; user can update later
|
|
236
|
+
const calId = "primary";
|
|
237
|
+
const summary = await fetchCalendarSummary(oauthTokens.access_token, calId);
|
|
238
|
+
const tokens = {
|
|
239
|
+
...oauthTokens,
|
|
240
|
+
calendar_id: calId,
|
|
241
|
+
connected_at: new Date().toISOString(),
|
|
242
|
+
};
|
|
243
|
+
saveTokens(tokens);
|
|
244
|
+
return {
|
|
245
|
+
status: 200,
|
|
246
|
+
contentType: "application/json",
|
|
247
|
+
body: JSON.stringify({ ok: true, calendarId: calId, summary }),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
return {
|
|
252
|
+
status: 400,
|
|
253
|
+
contentType: "application/json",
|
|
254
|
+
body: JSON.stringify({
|
|
255
|
+
ok: false,
|
|
256
|
+
error: err instanceof Error ? err.message : String(err),
|
|
257
|
+
}),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
export async function handleCalendarTest() {
|
|
262
|
+
const tokens = loadTokens();
|
|
263
|
+
if (!tokens) {
|
|
264
|
+
return {
|
|
265
|
+
status: 400,
|
|
266
|
+
contentType: "application/json",
|
|
267
|
+
body: JSON.stringify({
|
|
268
|
+
ok: false,
|
|
269
|
+
error: "Google Calendar not connected",
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const accessToken = await getValidAccessToken();
|
|
275
|
+
const summary = await fetchCalendarSummary(accessToken, tokens.calendar_id);
|
|
276
|
+
return {
|
|
277
|
+
status: 200,
|
|
278
|
+
contentType: "application/json",
|
|
279
|
+
body: JSON.stringify({
|
|
280
|
+
ok: true,
|
|
281
|
+
calendarId: tokens.calendar_id,
|
|
282
|
+
summary,
|
|
283
|
+
}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
return {
|
|
288
|
+
status: 400,
|
|
289
|
+
contentType: "application/json",
|
|
290
|
+
body: JSON.stringify({
|
|
291
|
+
ok: false,
|
|
292
|
+
error: err instanceof Error ? err.message : String(err),
|
|
293
|
+
}),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
export async function handleCalendarDisconnect() {
|
|
298
|
+
const tokens = loadTokens();
|
|
299
|
+
if (tokens?.access_token)
|
|
300
|
+
await revokeToken(tokens.access_token);
|
|
301
|
+
deleteTokens();
|
|
302
|
+
return {
|
|
303
|
+
status: 200,
|
|
304
|
+
contentType: "application/json",
|
|
305
|
+
body: JSON.stringify({ ok: true }),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=googleCalendar.js.map
|