veryfront 0.0.45 → 0.0.47
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/ai/components.js +3 -3
- package/dist/ai/components.js.map +2 -2
- package/dist/ai/index.d.ts +18 -3
- package/dist/ai/index.js +12 -2
- package/dist/ai/index.js.map +2 -2
- package/dist/cli.js +4 -5
- package/dist/components.js +1 -1
- package/dist/components.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/data.js +1 -1
- package/dist/data.js.map +1 -1
- package/dist/index.js +2 -5
- package/dist/index.js.map +2 -2
- package/dist/integrations/_base/connector.json +11 -0
- package/dist/integrations/_base/files/SETUP.md +132 -0
- package/dist/integrations/_base/files/app/api/integrations/status/route.ts +38 -0
- package/dist/integrations/_base/files/app/setup/page.tsx +461 -0
- package/dist/integrations/_base/files/lib/oauth.ts +145 -0
- package/dist/integrations/_base/files/lib/token-store.ts +109 -0
- package/dist/integrations/calendar/connector.json +77 -0
- package/dist/integrations/calendar/files/ai/tools/create-event.ts +83 -0
- package/dist/integrations/calendar/files/ai/tools/find-free-time.ts +108 -0
- package/dist/integrations/calendar/files/ai/tools/list-events.ts +98 -0
- package/dist/integrations/calendar/files/app/api/auth/calendar/callback/route.ts +114 -0
- package/dist/integrations/calendar/files/app/api/auth/calendar/route.ts +29 -0
- package/dist/integrations/calendar/files/lib/calendar-client.ts +309 -0
- package/dist/integrations/calendar/files/lib/oauth.ts +145 -0
- package/dist/integrations/calendar/files/lib/token-store.ts +109 -0
- package/dist/integrations/github/connector.json +84 -0
- package/dist/integrations/github/files/ai/tools/create-issue.ts +75 -0
- package/dist/integrations/github/files/ai/tools/get-pr-diff.ts +82 -0
- package/dist/integrations/github/files/ai/tools/list-prs.ts +93 -0
- package/dist/integrations/github/files/ai/tools/list-repos.ts +81 -0
- package/dist/integrations/github/files/app/api/auth/github/callback/route.ts +132 -0
- package/dist/integrations/github/files/app/api/auth/github/route.ts +29 -0
- package/dist/integrations/github/files/lib/github-client.ts +282 -0
- package/dist/integrations/github/files/lib/oauth.ts +145 -0
- package/dist/integrations/github/files/lib/token-store.ts +109 -0
- package/dist/integrations/gmail/connector.json +78 -0
- package/dist/integrations/gmail/files/ai/tools/list-emails.ts +92 -0
- package/dist/integrations/gmail/files/ai/tools/search-emails.ts +92 -0
- package/dist/integrations/gmail/files/ai/tools/send-email.ts +77 -0
- package/dist/integrations/gmail/files/app/api/auth/gmail/callback/route.ts +114 -0
- package/dist/integrations/gmail/files/app/api/auth/gmail/route.ts +29 -0
- package/dist/integrations/gmail/files/lib/gmail-client.ts +259 -0
- package/dist/integrations/gmail/files/lib/oauth.ts +145 -0
- package/dist/integrations/gmail/files/lib/token-store.ts +109 -0
- package/dist/integrations/slack/connector.json +74 -0
- package/dist/integrations/slack/files/ai/tools/get-messages.ts +65 -0
- package/dist/integrations/slack/files/ai/tools/list-channels.ts +63 -0
- package/dist/integrations/slack/files/ai/tools/send-message.ts +49 -0
- package/dist/integrations/slack/files/app/api/auth/slack/callback/route.ts +132 -0
- package/dist/integrations/slack/files/app/api/auth/slack/route.ts +29 -0
- package/dist/integrations/slack/files/lib/oauth.ts +145 -0
- package/dist/integrations/slack/files/lib/slack-client.ts +215 -0
- package/dist/integrations/slack/files/lib/token-store.ts +109 -0
- package/dist/templates/ai/app/page.tsx +4 -4
- package/dist/templates/ai/tsconfig.json +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Helper Functions
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for OAuth 2.0 authorization flows.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type OAuthToken, tokenStore } from "./token-store.ts";
|
|
8
|
+
|
|
9
|
+
export interface OAuthProvider {
|
|
10
|
+
name: string;
|
|
11
|
+
authorizationUrl: string;
|
|
12
|
+
tokenUrl: string;
|
|
13
|
+
clientId: string;
|
|
14
|
+
clientSecret: string;
|
|
15
|
+
scopes: string[];
|
|
16
|
+
callbackPath: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate OAuth authorization URL
|
|
21
|
+
*/
|
|
22
|
+
export function getAuthorizationUrl(
|
|
23
|
+
provider: OAuthProvider,
|
|
24
|
+
state: string,
|
|
25
|
+
redirectUri: string,
|
|
26
|
+
): string {
|
|
27
|
+
const params = new URLSearchParams({
|
|
28
|
+
client_id: provider.clientId,
|
|
29
|
+
redirect_uri: redirectUri,
|
|
30
|
+
response_type: "code",
|
|
31
|
+
scope: provider.scopes.join(" "),
|
|
32
|
+
state,
|
|
33
|
+
access_type: "offline",
|
|
34
|
+
prompt: "consent",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return `${provider.authorizationUrl}?${params.toString()}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Exchange authorization code for tokens
|
|
42
|
+
*/
|
|
43
|
+
export async function exchangeCodeForTokens(
|
|
44
|
+
provider: OAuthProvider,
|
|
45
|
+
code: string,
|
|
46
|
+
redirectUri: string,
|
|
47
|
+
): Promise<OAuthToken> {
|
|
48
|
+
const response = await fetch(provider.tokenUrl, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: {
|
|
51
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
52
|
+
},
|
|
53
|
+
body: new URLSearchParams({
|
|
54
|
+
client_id: provider.clientId,
|
|
55
|
+
client_secret: provider.clientSecret,
|
|
56
|
+
code,
|
|
57
|
+
grant_type: "authorization_code",
|
|
58
|
+
redirect_uri: redirectUri,
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const error = await response.text();
|
|
64
|
+
throw new Error(`Token exchange failed: ${response.status} - ${error}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const data = await response.json();
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
accessToken: data.access_token,
|
|
71
|
+
refreshToken: data.refresh_token,
|
|
72
|
+
expiresAt: data.expires_in ? Date.now() + (data.expires_in * 1000) : undefined,
|
|
73
|
+
tokenType: data.token_type || "Bearer",
|
|
74
|
+
scope: data.scope,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Refresh an expired access token
|
|
80
|
+
*/
|
|
81
|
+
export async function refreshAccessToken(
|
|
82
|
+
provider: OAuthProvider,
|
|
83
|
+
refreshToken: string,
|
|
84
|
+
): Promise<OAuthToken> {
|
|
85
|
+
const response = await fetch(provider.tokenUrl, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
89
|
+
},
|
|
90
|
+
body: new URLSearchParams({
|
|
91
|
+
client_id: provider.clientId,
|
|
92
|
+
client_secret: provider.clientSecret,
|
|
93
|
+
refresh_token: refreshToken,
|
|
94
|
+
grant_type: "refresh_token",
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
const error = await response.text();
|
|
100
|
+
throw new Error(`Token refresh failed: ${response.status} - ${error}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const data = await response.json();
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
accessToken: data.access_token,
|
|
107
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
108
|
+
expiresAt: data.expires_in ? Date.now() + (data.expires_in * 1000) : undefined,
|
|
109
|
+
tokenType: data.token_type || "Bearer",
|
|
110
|
+
scope: data.scope,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get a valid access token, refreshing if necessary
|
|
116
|
+
*/
|
|
117
|
+
export async function getValidToken(
|
|
118
|
+
provider: OAuthProvider,
|
|
119
|
+
userId: string,
|
|
120
|
+
service: string,
|
|
121
|
+
): Promise<string | null> {
|
|
122
|
+
const token = await tokenStore.getToken(userId, service);
|
|
123
|
+
|
|
124
|
+
if (!token) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check if token is expired (with 5 minute buffer)
|
|
129
|
+
// If no expiresAt, token doesn't expire (e.g., GitHub)
|
|
130
|
+
const isExpired = token.expiresAt ? token.expiresAt < Date.now() + 5 * 60 * 1000 : false;
|
|
131
|
+
|
|
132
|
+
if (isExpired && token.refreshToken) {
|
|
133
|
+
try {
|
|
134
|
+
const newToken = await refreshAccessToken(provider, token.refreshToken);
|
|
135
|
+
await tokenStore.setToken(userId, service, newToken);
|
|
136
|
+
return newToken.accessToken;
|
|
137
|
+
} catch {
|
|
138
|
+
// Refresh failed, user needs to re-authorize
|
|
139
|
+
await tokenStore.revokeToken(userId, service);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return token.accessToken;
|
|
145
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Token Store
|
|
3
|
+
*
|
|
4
|
+
* Simple in-memory token store for development.
|
|
5
|
+
* Replace with a database or KV store for production.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface OAuthToken {
|
|
9
|
+
accessToken: string;
|
|
10
|
+
refreshToken?: string;
|
|
11
|
+
expiresAt?: number;
|
|
12
|
+
tokenType?: string;
|
|
13
|
+
scope?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TokenStore {
|
|
17
|
+
getToken(userId: string, service: string): Promise<OAuthToken | null>;
|
|
18
|
+
setToken(userId: string, service: string, token: OAuthToken): Promise<void>;
|
|
19
|
+
revokeToken(userId: string, service: string): Promise<void>;
|
|
20
|
+
isConnected(userId: string, service: string): Promise<boolean>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// In-memory storage for development
|
|
24
|
+
const tokens = new Map<string, OAuthToken>();
|
|
25
|
+
|
|
26
|
+
function getKey(userId: string, service: string): string {
|
|
27
|
+
return `${userId}:${service}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Simple in-memory token store
|
|
32
|
+
*
|
|
33
|
+
* NOTE: This is for development only. In production, use:
|
|
34
|
+
* - Database (Postgres, SQLite, etc.)
|
|
35
|
+
* - KV store (Cloudflare Workers KV, Vercel KV, etc.)
|
|
36
|
+
* - Encrypted file storage
|
|
37
|
+
*/
|
|
38
|
+
export const tokenStore: TokenStore = {
|
|
39
|
+
getToken(userId: string, service: string): Promise<OAuthToken | null> {
|
|
40
|
+
const key = getKey(userId, service);
|
|
41
|
+
return Promise.resolve(tokens.get(key) || null);
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
setToken(
|
|
45
|
+
userId: string,
|
|
46
|
+
service: string,
|
|
47
|
+
token: OAuthToken,
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
const key = getKey(userId, service);
|
|
50
|
+
tokens.set(key, token);
|
|
51
|
+
return Promise.resolve();
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
revokeToken(userId: string, service: string): Promise<void> {
|
|
55
|
+
const key = getKey(userId, service);
|
|
56
|
+
tokens.delete(key);
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async isConnected(userId: string, service: string): Promise<boolean> {
|
|
61
|
+
const token = await this.getToken(userId, service);
|
|
62
|
+
if (!token) return false;
|
|
63
|
+
// Check if token is not expired (if no expiry, token doesn't expire)
|
|
64
|
+
return !token.expiresAt || token.expiresAt > Date.now();
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Factory function to create a custom token store
|
|
70
|
+
*/
|
|
71
|
+
export function createTokenStore(options: {
|
|
72
|
+
get: (key: string) => Promise<string | null>;
|
|
73
|
+
set: (key: string, value: string) => Promise<void>;
|
|
74
|
+
delete: (key: string) => Promise<void>;
|
|
75
|
+
}): TokenStore {
|
|
76
|
+
return {
|
|
77
|
+
async getToken(userId: string, service: string): Promise<OAuthToken | null> {
|
|
78
|
+
const key = getKey(userId, service);
|
|
79
|
+
const data = await options.get(key);
|
|
80
|
+
if (!data) return null;
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(data) as OAuthToken;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async setToken(
|
|
89
|
+
userId: string,
|
|
90
|
+
service: string,
|
|
91
|
+
token: OAuthToken,
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
const key = getKey(userId, service);
|
|
94
|
+
await options.set(key, JSON.stringify(token));
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async revokeToken(userId: string, service: string): Promise<void> {
|
|
98
|
+
const key = getKey(userId, service);
|
|
99
|
+
await options.delete(key);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async isConnected(userId: string, service: string): Promise<boolean> {
|
|
103
|
+
const token = await this.getToken(userId, service);
|
|
104
|
+
if (!token) return false;
|
|
105
|
+
// Check if token is not expired (if no expiry, token doesn't expire)
|
|
106
|
+
return !token.expiresAt || token.expiresAt > Date.now();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "slack",
|
|
3
|
+
"displayName": "Slack",
|
|
4
|
+
"icon": "slack.svg",
|
|
5
|
+
"description": "Send messages, read channels, and manage Slack workspace",
|
|
6
|
+
"auth": {
|
|
7
|
+
"type": "oauth2",
|
|
8
|
+
"provider": "slack",
|
|
9
|
+
"authorizationUrl": "https://slack.com/oauth/v2/authorize",
|
|
10
|
+
"tokenUrl": "https://slack.com/api/oauth.v2.access",
|
|
11
|
+
"scopes": [
|
|
12
|
+
"channels:history",
|
|
13
|
+
"channels:read",
|
|
14
|
+
"chat:write",
|
|
15
|
+
"users:read",
|
|
16
|
+
"im:history",
|
|
17
|
+
"im:read"
|
|
18
|
+
],
|
|
19
|
+
"callbackPath": "/api/auth/slack/callback"
|
|
20
|
+
},
|
|
21
|
+
"envVars": [
|
|
22
|
+
{
|
|
23
|
+
"name": "SLACK_CLIENT_ID",
|
|
24
|
+
"description": "Slack App Client ID",
|
|
25
|
+
"required": true,
|
|
26
|
+
"sensitive": false,
|
|
27
|
+
"docsUrl": "https://api.slack.com/apps"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "SLACK_CLIENT_SECRET",
|
|
31
|
+
"description": "Slack App Client Secret",
|
|
32
|
+
"required": true,
|
|
33
|
+
"sensitive": true,
|
|
34
|
+
"docsUrl": "https://api.slack.com/apps"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"tools": [
|
|
38
|
+
{
|
|
39
|
+
"id": "list-channels",
|
|
40
|
+
"name": "List Channels",
|
|
41
|
+
"description": "Get list of Slack channels",
|
|
42
|
+
"requiresWrite": false
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "send-message",
|
|
46
|
+
"name": "Send Message",
|
|
47
|
+
"description": "Send a message to a Slack channel",
|
|
48
|
+
"requiresWrite": true
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "get-messages",
|
|
52
|
+
"name": "Get Messages",
|
|
53
|
+
"description": "Get recent messages from a channel",
|
|
54
|
+
"requiresWrite": false
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"prompts": [
|
|
58
|
+
{
|
|
59
|
+
"id": "catch-up-slack",
|
|
60
|
+
"title": "Catch up on Slack",
|
|
61
|
+
"prompt": "Summarize the important messages from my Slack channels from today. Highlight any mentions or urgent items.",
|
|
62
|
+
"category": "productivity",
|
|
63
|
+
"icon": "slack"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "post-update",
|
|
67
|
+
"title": "Post team update",
|
|
68
|
+
"prompt": "Help me write and post a team update to Slack about my current work progress.",
|
|
69
|
+
"category": "productivity",
|
|
70
|
+
"icon": "message"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"suggestedWith": ["gmail", "calendar", "jira"]
|
|
74
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { tool } from "veryfront/ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createSlackClient } from "../../lib/slack-client.ts";
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
id: "get-messages",
|
|
7
|
+
description: "Get recent messages from a Slack channel",
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
channel: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Channel ID (e.g., 'C1234567890')"),
|
|
12
|
+
limit: z
|
|
13
|
+
.number()
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(100)
|
|
16
|
+
.default(20)
|
|
17
|
+
.describe("Maximum number of messages to return"),
|
|
18
|
+
}),
|
|
19
|
+
execute: async ({ channel, limit }, context) => {
|
|
20
|
+
const userId = context?.userId as string | undefined;
|
|
21
|
+
if (!userId) {
|
|
22
|
+
return {
|
|
23
|
+
error: "User not authenticated. Please log in first.",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const slack = createSlackClient(userId);
|
|
29
|
+
const messages = await slack.getMessages(channel, { limit });
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
messages: messages.map((
|
|
33
|
+
msg: {
|
|
34
|
+
text?: string;
|
|
35
|
+
user?: string;
|
|
36
|
+
ts: string;
|
|
37
|
+
thread_ts?: string;
|
|
38
|
+
reply_count?: number;
|
|
39
|
+
reactions?: Array<{ name: string; count: number }>;
|
|
40
|
+
},
|
|
41
|
+
) => ({
|
|
42
|
+
text: msg.text || "",
|
|
43
|
+
user: msg.user || "unknown",
|
|
44
|
+
timestamp: msg.ts,
|
|
45
|
+
threadTs: msg.thread_ts,
|
|
46
|
+
replyCount: msg.reply_count || 0,
|
|
47
|
+
reactions: msg.reactions?.map((r: { name: string; count: number }) =>
|
|
48
|
+
`${r.name} (${r.count})`
|
|
49
|
+
) || [],
|
|
50
|
+
})),
|
|
51
|
+
count: messages.length,
|
|
52
|
+
channel,
|
|
53
|
+
message: `Retrieved ${messages.length} message(s) from channel.`,
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof Error && error.message.includes("not connected")) {
|
|
57
|
+
return {
|
|
58
|
+
error: "Slack not connected. Please connect your Slack account.",
|
|
59
|
+
connectUrl: "/api/auth/slack",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { tool } from "veryfront/ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createSlackClient } from "../../lib/slack-client.ts";
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
id: "list-channels",
|
|
7
|
+
description: "List Slack channels the user is a member of",
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
limit: z
|
|
10
|
+
.number()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(100)
|
|
13
|
+
.default(20)
|
|
14
|
+
.describe("Maximum number of channels to return"),
|
|
15
|
+
excludeArchived: z
|
|
16
|
+
.boolean()
|
|
17
|
+
.default(true)
|
|
18
|
+
.describe("Exclude archived channels"),
|
|
19
|
+
}),
|
|
20
|
+
execute: async ({ limit, excludeArchived }, context) => {
|
|
21
|
+
const userId = context?.userId as string | undefined;
|
|
22
|
+
if (!userId) {
|
|
23
|
+
return {
|
|
24
|
+
error: "User not authenticated. Please log in first.",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const slack = createSlackClient(userId);
|
|
30
|
+
const channels = await slack.listChannels({ limit, excludeArchived });
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
channels: channels.map((
|
|
34
|
+
ch: {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
is_private: boolean;
|
|
38
|
+
is_member: boolean;
|
|
39
|
+
topic?: { value: string };
|
|
40
|
+
purpose?: { value: string };
|
|
41
|
+
},
|
|
42
|
+
) => ({
|
|
43
|
+
id: ch.id,
|
|
44
|
+
name: ch.name,
|
|
45
|
+
isPrivate: ch.is_private,
|
|
46
|
+
isMember: ch.is_member,
|
|
47
|
+
topic: ch.topic?.value || null,
|
|
48
|
+
purpose: ch.purpose?.value || null,
|
|
49
|
+
})),
|
|
50
|
+
count: channels.length,
|
|
51
|
+
message: `Found ${channels.length} channel(s).`,
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error instanceof Error && error.message.includes("not connected")) {
|
|
55
|
+
return {
|
|
56
|
+
error: "Slack not connected. Please connect your Slack account.",
|
|
57
|
+
connectUrl: "/api/auth/slack",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { tool } from "veryfront/ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createSlackClient } from "../../lib/slack-client.ts";
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
id: "send-message",
|
|
7
|
+
description: "Send a message to a Slack channel",
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
channel: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Channel ID or name (e.g., 'C1234567890' or '#general')"),
|
|
12
|
+
text: z
|
|
13
|
+
.string()
|
|
14
|
+
.min(1)
|
|
15
|
+
.describe("Message text to send"),
|
|
16
|
+
threadTs: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Thread timestamp to reply to (for threaded messages)"),
|
|
20
|
+
}),
|
|
21
|
+
execute: async ({ channel, text, threadTs }, context) => {
|
|
22
|
+
const userId = context?.userId as string | undefined;
|
|
23
|
+
if (!userId) {
|
|
24
|
+
return {
|
|
25
|
+
error: "User not authenticated. Please log in first.",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const slack = createSlackClient(userId);
|
|
31
|
+
const result = await slack.sendMessage(channel, text, { threadTs });
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
success: true,
|
|
35
|
+
messageTs: result.ts,
|
|
36
|
+
channel: result.channel,
|
|
37
|
+
message: threadTs ? `Reply sent to thread in ${channel}.` : `Message sent to ${channel}.`,
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof Error && error.message.includes("not connected")) {
|
|
41
|
+
return {
|
|
42
|
+
error: "Slack not connected. Please connect your Slack account.",
|
|
43
|
+
connectUrl: "/api/auth/slack",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack OAuth Callback
|
|
3
|
+
*
|
|
4
|
+
* Handles the OAuth callback from Slack, exchanges code for tokens,
|
|
5
|
+
* and stores them securely.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { tokenStore } from "../../../../../lib/token-store.ts";
|
|
9
|
+
import { slackOAuthProvider } from "../../../../../lib/slack-client.ts";
|
|
10
|
+
|
|
11
|
+
export async function GET(req: Request) {
|
|
12
|
+
const url = new URL(req.url);
|
|
13
|
+
const code = url.searchParams.get("code");
|
|
14
|
+
const state = url.searchParams.get("state");
|
|
15
|
+
const error = url.searchParams.get("error");
|
|
16
|
+
|
|
17
|
+
// Handle OAuth errors
|
|
18
|
+
if (error) {
|
|
19
|
+
const errorDescription = url.searchParams.get("error_description") || error;
|
|
20
|
+
return new Response(
|
|
21
|
+
`
|
|
22
|
+
<html>
|
|
23
|
+
<head><title>Connection Failed</title></head>
|
|
24
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
25
|
+
<h1>Slack Connection Failed</h1>
|
|
26
|
+
<p style="color: #666;">${errorDescription}</p>
|
|
27
|
+
<a href="/" style="color: #0066cc;">Return to App</a>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
`,
|
|
31
|
+
{
|
|
32
|
+
status: 400,
|
|
33
|
+
headers: { "Content-Type": "text/html" },
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!code || !state) {
|
|
39
|
+
return new Response("Missing code or state parameter", { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Validate state from cookie
|
|
43
|
+
const cookies = req.headers.get("cookie") || "";
|
|
44
|
+
const stateCookie = cookies
|
|
45
|
+
.split(";")
|
|
46
|
+
.find((c) => c.trim().startsWith("slack_oauth_state="));
|
|
47
|
+
const savedState = stateCookie?.split("=")[1]?.trim();
|
|
48
|
+
|
|
49
|
+
if (state !== savedState) {
|
|
50
|
+
return new Response("Invalid state parameter", { status: 400 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const origin = url.origin;
|
|
55
|
+
const redirectUri = `${origin}${slackOAuthProvider.callbackPath}`;
|
|
56
|
+
|
|
57
|
+
// Exchange code for tokens using Slack's specific OAuth flow
|
|
58
|
+
const response = await fetch(slackOAuthProvider.tokenUrl, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
62
|
+
},
|
|
63
|
+
body: new URLSearchParams({
|
|
64
|
+
client_id: slackOAuthProvider.clientId,
|
|
65
|
+
client_secret: slackOAuthProvider.clientSecret,
|
|
66
|
+
code,
|
|
67
|
+
redirect_uri: redirectUri,
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const data = await response.json();
|
|
72
|
+
|
|
73
|
+
if (!data.ok) {
|
|
74
|
+
throw new Error(data.error || "Token exchange failed");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Get actual user ID from session in production
|
|
78
|
+
const userId = "current-user";
|
|
79
|
+
|
|
80
|
+
// Store tokens (Slack returns access_token in authed_user or at top level)
|
|
81
|
+
const accessToken = data.authed_user?.access_token || data.access_token;
|
|
82
|
+
await tokenStore.setToken(userId, "slack", {
|
|
83
|
+
accessToken,
|
|
84
|
+
refreshToken: data.refresh_token,
|
|
85
|
+
scope: data.scope,
|
|
86
|
+
tokenType: data.token_type,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Clear state cookie and redirect to success page
|
|
90
|
+
return new Response(
|
|
91
|
+
`
|
|
92
|
+
<html>
|
|
93
|
+
<head>
|
|
94
|
+
<title>Slack Connected</title>
|
|
95
|
+
<meta http-equiv="refresh" content="2;url=/" />
|
|
96
|
+
</head>
|
|
97
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
98
|
+
<h1 style="color: #22c55e;">✓ Slack Connected!</h1>
|
|
99
|
+
<p style="color: #666;">Your Slack workspace has been connected successfully.</p>
|
|
100
|
+
<p style="color: #999; font-size: 14px;">Redirecting...</p>
|
|
101
|
+
<a href="/" style="color: #0066cc;">Return to App</a>
|
|
102
|
+
</body>
|
|
103
|
+
</html>
|
|
104
|
+
`,
|
|
105
|
+
{
|
|
106
|
+
status: 200,
|
|
107
|
+
headers: {
|
|
108
|
+
"Content-Type": "text/html",
|
|
109
|
+
"Set-Cookie": "slack_oauth_state=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error("Slack OAuth error:", err);
|
|
115
|
+
return new Response(
|
|
116
|
+
`
|
|
117
|
+
<html>
|
|
118
|
+
<head><title>Connection Failed</title></head>
|
|
119
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
120
|
+
<h1>Slack Connection Failed</h1>
|
|
121
|
+
<p style="color: #666;">Unable to complete Slack authorization. Please try again.</p>
|
|
122
|
+
<a href="/api/auth/slack" style="color: #0066cc;">Try Again</a>
|
|
123
|
+
</body>
|
|
124
|
+
</html>
|
|
125
|
+
`,
|
|
126
|
+
{
|
|
127
|
+
status: 500,
|
|
128
|
+
headers: { "Content-Type": "text/html" },
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack OAuth Initiation
|
|
3
|
+
*
|
|
4
|
+
* Redirects to Slack OAuth consent screen
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getAuthorizationUrl } from "../../../../lib/oauth.ts";
|
|
8
|
+
import { slackOAuthProvider } from "../../../../lib/slack-client.ts";
|
|
9
|
+
|
|
10
|
+
export function GET(req: Request) {
|
|
11
|
+
const url = new URL(req.url);
|
|
12
|
+
const origin = url.origin;
|
|
13
|
+
|
|
14
|
+
// Generate a random state for CSRF protection
|
|
15
|
+
const state = crypto.randomUUID();
|
|
16
|
+
|
|
17
|
+
// Store state in a cookie for validation
|
|
18
|
+
const redirectUri = `${origin}${slackOAuthProvider.callbackPath}`;
|
|
19
|
+
const authUrl = getAuthorizationUrl(slackOAuthProvider, state, redirectUri);
|
|
20
|
+
|
|
21
|
+
// Set state cookie and redirect to Slack
|
|
22
|
+
return new Response(null, {
|
|
23
|
+
status: 302,
|
|
24
|
+
headers: {
|
|
25
|
+
Location: authUrl,
|
|
26
|
+
"Set-Cookie": `slack_oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|