veryfront 0.0.44 → 0.0.46
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/index.d.ts +11 -1
- package/dist/ai/index.js +2 -2
- package/dist/ai/index.js.map +2 -2
- package/dist/cli.js +240 -87
- 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 +34 -32
- 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/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,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack API Client
|
|
3
|
+
*
|
|
4
|
+
* Provides a type-safe interface to Slack API operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { tokenStore as _tokenStore } from "./token-store.ts";
|
|
8
|
+
import { getValidToken } from "./oauth.ts";
|
|
9
|
+
|
|
10
|
+
// Helper for Cross-Platform environment access
|
|
11
|
+
function getEnv(key: string): string | undefined {
|
|
12
|
+
// @ts-ignore - Deno global
|
|
13
|
+
if (typeof Deno !== "undefined") {
|
|
14
|
+
// @ts-ignore - Deno global
|
|
15
|
+
return Deno.env.get(key);
|
|
16
|
+
} // @ts-ignore - process global
|
|
17
|
+
else if (typeof process !== "undefined" && process.env) {
|
|
18
|
+
// @ts-ignore - process global
|
|
19
|
+
return process.env[key];
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SLACK_API_BASE = "https://slack.com/api";
|
|
25
|
+
|
|
26
|
+
export interface SlackChannel {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
is_channel: boolean;
|
|
30
|
+
is_private: boolean;
|
|
31
|
+
is_member: boolean;
|
|
32
|
+
topic?: { value: string };
|
|
33
|
+
purpose?: { value: string };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SlackMessage {
|
|
37
|
+
type: string;
|
|
38
|
+
user?: string;
|
|
39
|
+
text: string;
|
|
40
|
+
ts: string;
|
|
41
|
+
thread_ts?: string;
|
|
42
|
+
reply_count?: number;
|
|
43
|
+
reactions?: Array<{ name: string; count: number }>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SlackUser {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
real_name: string;
|
|
50
|
+
profile: {
|
|
51
|
+
display_name: string;
|
|
52
|
+
email?: string;
|
|
53
|
+
image_48?: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Slack OAuth provider configuration
|
|
59
|
+
*/
|
|
60
|
+
export const slackOAuthProvider = {
|
|
61
|
+
name: "slack",
|
|
62
|
+
authorizationUrl: "https://slack.com/oauth/v2/authorize",
|
|
63
|
+
tokenUrl: "https://slack.com/api/oauth.v2.access",
|
|
64
|
+
clientId: getEnv("SLACK_CLIENT_ID") || "",
|
|
65
|
+
clientSecret: getEnv("SLACK_CLIENT_SECRET") || "",
|
|
66
|
+
scopes: [
|
|
67
|
+
"channels:history",
|
|
68
|
+
"channels:read",
|
|
69
|
+
"chat:write",
|
|
70
|
+
"users:read",
|
|
71
|
+
"im:history",
|
|
72
|
+
"im:read",
|
|
73
|
+
],
|
|
74
|
+
callbackPath: "/api/auth/slack/callback",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a Slack client for a specific user
|
|
79
|
+
*/
|
|
80
|
+
export function createSlackClient(userId: string) {
|
|
81
|
+
async function getAccessToken(): Promise<string> {
|
|
82
|
+
const token = await getValidToken(slackOAuthProvider, userId, "slack");
|
|
83
|
+
if (!token) {
|
|
84
|
+
throw new Error("Slack not connected. Please connect your Slack account first.");
|
|
85
|
+
}
|
|
86
|
+
return token;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function apiRequest<T>(
|
|
90
|
+
method: string,
|
|
91
|
+
params: Record<string, unknown> = {},
|
|
92
|
+
): Promise<T> {
|
|
93
|
+
const accessToken = await getAccessToken();
|
|
94
|
+
|
|
95
|
+
const response = await fetch(`${SLACK_API_BASE}/${method}`, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
Authorization: `Bearer ${accessToken}`,
|
|
99
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify(params),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const data = await response.json();
|
|
105
|
+
|
|
106
|
+
if (!data.ok) {
|
|
107
|
+
throw new Error(`Slack API error: ${data.error}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return data as T;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
/**
|
|
115
|
+
* List channels the user is a member of
|
|
116
|
+
*/
|
|
117
|
+
async listChannels(options: {
|
|
118
|
+
limit?: number;
|
|
119
|
+
excludeArchived?: boolean;
|
|
120
|
+
} = {}): Promise<SlackChannel[]> {
|
|
121
|
+
const result = await apiRequest<{ channels: SlackChannel[] }>(
|
|
122
|
+
"conversations.list",
|
|
123
|
+
{
|
|
124
|
+
limit: options.limit || 100,
|
|
125
|
+
exclude_archived: options.excludeArchived ?? true,
|
|
126
|
+
types: "public_channel,private_channel",
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
return result.channels;
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get messages from a channel
|
|
134
|
+
*/
|
|
135
|
+
async getMessages(
|
|
136
|
+
channelId: string,
|
|
137
|
+
options: { limit?: number; oldest?: string } = {},
|
|
138
|
+
): Promise<SlackMessage[]> {
|
|
139
|
+
const result = await apiRequest<{ messages: SlackMessage[] }>(
|
|
140
|
+
"conversations.history",
|
|
141
|
+
{
|
|
142
|
+
channel: channelId,
|
|
143
|
+
limit: options.limit || 20,
|
|
144
|
+
oldest: options.oldest,
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
return result.messages;
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Send a message to a channel
|
|
152
|
+
*/
|
|
153
|
+
async sendMessage(
|
|
154
|
+
channelId: string,
|
|
155
|
+
text: string,
|
|
156
|
+
options: { threadTs?: string; unfurlLinks?: boolean } = {},
|
|
157
|
+
): Promise<{ ts: string; channel: string }> {
|
|
158
|
+
const result = await apiRequest<{ ts: string; channel: string }>(
|
|
159
|
+
"chat.postMessage",
|
|
160
|
+
{
|
|
161
|
+
channel: channelId,
|
|
162
|
+
text,
|
|
163
|
+
thread_ts: options.threadTs,
|
|
164
|
+
unfurl_links: options.unfurlLinks ?? true,
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
return result;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get user info
|
|
172
|
+
*/
|
|
173
|
+
async getUser(userId: string): Promise<SlackUser> {
|
|
174
|
+
const result = await apiRequest<{ user: SlackUser }>("users.info", {
|
|
175
|
+
user: userId,
|
|
176
|
+
});
|
|
177
|
+
return result.user;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get thread replies
|
|
182
|
+
*/
|
|
183
|
+
async getThread(
|
|
184
|
+
channelId: string,
|
|
185
|
+
threadTs: string,
|
|
186
|
+
): Promise<SlackMessage[]> {
|
|
187
|
+
const result = await apiRequest<{ messages: SlackMessage[] }>(
|
|
188
|
+
"conversations.replies",
|
|
189
|
+
{
|
|
190
|
+
channel: channelId,
|
|
191
|
+
ts: threadTs,
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
return result.messages;
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Search messages
|
|
199
|
+
*/
|
|
200
|
+
async searchMessages(
|
|
201
|
+
query: string,
|
|
202
|
+
options: { count?: number } = {},
|
|
203
|
+
): Promise<SlackMessage[]> {
|
|
204
|
+
const result = await apiRequest<{
|
|
205
|
+
messages: { matches: SlackMessage[] };
|
|
206
|
+
}>("search.messages", {
|
|
207
|
+
query,
|
|
208
|
+
count: options.count || 20,
|
|
209
|
+
});
|
|
210
|
+
return result.messages.matches;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export type SlackClient = ReturnType<typeof createSlackClient>;
|
|
@@ -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
|
+
}
|