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,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "github",
|
|
3
|
+
"displayName": "GitHub",
|
|
4
|
+
"icon": "github.svg",
|
|
5
|
+
"description": "Manage repositories, issues, and pull requests",
|
|
6
|
+
"auth": {
|
|
7
|
+
"type": "oauth2",
|
|
8
|
+
"provider": "github",
|
|
9
|
+
"authorizationUrl": "https://github.com/login/oauth/authorize",
|
|
10
|
+
"tokenUrl": "https://github.com/login/oauth/access_token",
|
|
11
|
+
"scopes": [
|
|
12
|
+
"repo",
|
|
13
|
+
"read:user",
|
|
14
|
+
"read:org"
|
|
15
|
+
],
|
|
16
|
+
"callbackPath": "/api/auth/github/callback"
|
|
17
|
+
},
|
|
18
|
+
"envVars": [
|
|
19
|
+
{
|
|
20
|
+
"name": "GITHUB_CLIENT_ID",
|
|
21
|
+
"description": "GitHub OAuth App Client ID",
|
|
22
|
+
"required": true,
|
|
23
|
+
"sensitive": false,
|
|
24
|
+
"docsUrl": "https://github.com/settings/developers"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "GITHUB_CLIENT_SECRET",
|
|
28
|
+
"description": "GitHub OAuth App Client Secret",
|
|
29
|
+
"required": true,
|
|
30
|
+
"sensitive": true,
|
|
31
|
+
"docsUrl": "https://github.com/settings/developers"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"tools": [
|
|
35
|
+
{
|
|
36
|
+
"id": "list-repos",
|
|
37
|
+
"name": "List Repositories",
|
|
38
|
+
"description": "Get list of user's repositories",
|
|
39
|
+
"requiresWrite": false
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "list-prs",
|
|
43
|
+
"name": "List Pull Requests",
|
|
44
|
+
"description": "Get open pull requests",
|
|
45
|
+
"requiresWrite": false
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "create-issue",
|
|
49
|
+
"name": "Create Issue",
|
|
50
|
+
"description": "Create a new issue in a repository",
|
|
51
|
+
"requiresWrite": true
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "get-pr-diff",
|
|
55
|
+
"name": "Get PR Diff",
|
|
56
|
+
"description": "Get the diff for a pull request",
|
|
57
|
+
"requiresWrite": false
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"prompts": [
|
|
61
|
+
{
|
|
62
|
+
"id": "review-prs",
|
|
63
|
+
"title": "Review my open PRs",
|
|
64
|
+
"prompt": "Show me my open pull requests and help me review them. Summarize the changes and any comments.",
|
|
65
|
+
"category": "development",
|
|
66
|
+
"icon": "git-pull-request"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "create-issue",
|
|
70
|
+
"title": "Create GitHub issue",
|
|
71
|
+
"prompt": "Help me create a new GitHub issue with a clear description and appropriate labels.",
|
|
72
|
+
"category": "development",
|
|
73
|
+
"icon": "circle-dot"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": "summarize-commits",
|
|
77
|
+
"title": "Summarize recent commits",
|
|
78
|
+
"prompt": "Summarize the recent commits in my repository and highlight significant changes.",
|
|
79
|
+
"category": "development",
|
|
80
|
+
"icon": "git-commit"
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"suggestedWith": ["jira", "slack"]
|
|
84
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { tool } from "veryfront/ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createGitHubClient } from "../../lib/github-client.ts";
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
id: "create-issue",
|
|
7
|
+
description: "Create a new issue in a GitHub repository",
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
repo: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Repository in format 'owner/repo' (e.g., 'facebook/react')"),
|
|
12
|
+
title: z
|
|
13
|
+
.string()
|
|
14
|
+
.min(1)
|
|
15
|
+
.describe("Issue title"),
|
|
16
|
+
body: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Issue body/description (supports Markdown)"),
|
|
20
|
+
labels: z
|
|
21
|
+
.array(z.string())
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Labels to add to the issue"),
|
|
24
|
+
assignees: z
|
|
25
|
+
.array(z.string())
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("GitHub usernames to assign to the issue"),
|
|
28
|
+
}),
|
|
29
|
+
execute: async ({ repo, title, body, labels, assignees }, context) => {
|
|
30
|
+
const userId = context?.userId as string | undefined;
|
|
31
|
+
if (!userId) {
|
|
32
|
+
return {
|
|
33
|
+
error: "User not authenticated. Please log in first.",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const [owner, repoName] = repo.split("/");
|
|
38
|
+
if (!owner || !repoName) {
|
|
39
|
+
return {
|
|
40
|
+
error: "Invalid repository format. Use 'owner/repo' format.",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const github = createGitHubClient(userId);
|
|
46
|
+
const issue = await github.createIssue(owner, repoName, {
|
|
47
|
+
title,
|
|
48
|
+
body,
|
|
49
|
+
labels,
|
|
50
|
+
assignees,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
issue: {
|
|
56
|
+
number: issue.number,
|
|
57
|
+
title: issue.title,
|
|
58
|
+
url: issue.html_url,
|
|
59
|
+
state: issue.state,
|
|
60
|
+
labels: issue.labels.map((l: { name: string }) => l.name),
|
|
61
|
+
assignees: issue.assignees.map((a: { login: string }) => a.login),
|
|
62
|
+
},
|
|
63
|
+
message: `Issue #${issue.number} created successfully in ${repo}.`,
|
|
64
|
+
};
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error instanceof Error && error.message.includes("not connected")) {
|
|
67
|
+
return {
|
|
68
|
+
error: "GitHub not connected. Please connect your GitHub account.",
|
|
69
|
+
connectUrl: "/api/auth/github",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { tool } from "veryfront/ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createGitHubClient } from "../../lib/github-client.ts";
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
id: "get-pr-diff",
|
|
7
|
+
description: "Get the diff for a pull request to review code changes",
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
repo: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Repository in format 'owner/repo' (e.g., 'facebook/react')"),
|
|
12
|
+
prNumber: z
|
|
13
|
+
.number()
|
|
14
|
+
.int()
|
|
15
|
+
.positive()
|
|
16
|
+
.describe("Pull request number"),
|
|
17
|
+
}),
|
|
18
|
+
execute: async ({ repo, prNumber }, context) => {
|
|
19
|
+
const userId = context?.userId as string | undefined;
|
|
20
|
+
if (!userId) {
|
|
21
|
+
return {
|
|
22
|
+
error: "User not authenticated. Please log in first.",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const [owner, repoName] = repo.split("/");
|
|
27
|
+
if (!owner || !repoName) {
|
|
28
|
+
return {
|
|
29
|
+
error: "Invalid repository format. Use 'owner/repo' format.",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const github = createGitHubClient(userId);
|
|
35
|
+
|
|
36
|
+
// Get PR details first
|
|
37
|
+
const pr = await github.getPullRequest(owner, repoName, prNumber);
|
|
38
|
+
|
|
39
|
+
// Get the diff
|
|
40
|
+
const diff = await github.getPullRequestDiff(owner, repoName, prNumber);
|
|
41
|
+
|
|
42
|
+
// Truncate very long diffs
|
|
43
|
+
const maxDiffLength = 50000;
|
|
44
|
+
const truncatedDiff = diff.length > maxDiffLength
|
|
45
|
+
? diff.substring(0, maxDiffLength) +
|
|
46
|
+
`\n\n... (diff truncated, ${diff.length - maxDiffLength} characters remaining)`
|
|
47
|
+
: diff;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
pullRequest: {
|
|
51
|
+
number: pr.number,
|
|
52
|
+
title: pr.title,
|
|
53
|
+
author: pr.user.login,
|
|
54
|
+
url: pr.html_url,
|
|
55
|
+
sourceBranch: pr.head.ref,
|
|
56
|
+
targetBranch: pr.base.ref,
|
|
57
|
+
additions: pr.additions,
|
|
58
|
+
deletions: pr.deletions,
|
|
59
|
+
changedFiles: pr.changed_files,
|
|
60
|
+
isDraft: pr.draft,
|
|
61
|
+
state: pr.state,
|
|
62
|
+
},
|
|
63
|
+
diff: truncatedDiff,
|
|
64
|
+
stats: {
|
|
65
|
+
additions: pr.additions,
|
|
66
|
+
deletions: pr.deletions,
|
|
67
|
+
changedFiles: pr.changed_files,
|
|
68
|
+
},
|
|
69
|
+
message:
|
|
70
|
+
`Retrieved diff for PR #${prNumber} (${pr.additions} additions, ${pr.deletions} deletions across ${pr.changed_files} files).`,
|
|
71
|
+
};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof Error && error.message.includes("not connected")) {
|
|
74
|
+
return {
|
|
75
|
+
error: "GitHub not connected. Please connect your GitHub account.",
|
|
76
|
+
connectUrl: "/api/auth/github",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { tool } from "veryfront/ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createGitHubClient } from "../../lib/github-client.ts";
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
id: "list-prs",
|
|
7
|
+
description: "List pull requests for a GitHub repository",
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
repo: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Repository in format 'owner/repo' (e.g., 'facebook/react')"),
|
|
12
|
+
state: z
|
|
13
|
+
.enum(["open", "closed", "all"])
|
|
14
|
+
.default("open")
|
|
15
|
+
.describe("State of pull requests to list"),
|
|
16
|
+
limit: z
|
|
17
|
+
.number()
|
|
18
|
+
.min(1)
|
|
19
|
+
.max(100)
|
|
20
|
+
.default(10)
|
|
21
|
+
.describe("Maximum number of pull requests to return"),
|
|
22
|
+
}),
|
|
23
|
+
execute: async ({ repo, state, limit }, context) => {
|
|
24
|
+
const userId = context?.userId as string | undefined;
|
|
25
|
+
if (!userId) {
|
|
26
|
+
return {
|
|
27
|
+
error: "User not authenticated. Please log in first.",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const [owner, repoName] = repo.split("/");
|
|
32
|
+
if (!owner || !repoName) {
|
|
33
|
+
return {
|
|
34
|
+
error: "Invalid repository format. Use 'owner/repo' format.",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const github = createGitHubClient(userId);
|
|
40
|
+
const prs = await github.listPullRequests(owner, repoName, {
|
|
41
|
+
state,
|
|
42
|
+
perPage: limit,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
pullRequests: prs.map((
|
|
47
|
+
pr: {
|
|
48
|
+
number: number;
|
|
49
|
+
title: string;
|
|
50
|
+
state: string;
|
|
51
|
+
draft: boolean;
|
|
52
|
+
html_url: string;
|
|
53
|
+
user: { login: string };
|
|
54
|
+
created_at: string;
|
|
55
|
+
updated_at: string;
|
|
56
|
+
head: { ref: string };
|
|
57
|
+
base: { ref: string };
|
|
58
|
+
additions: number;
|
|
59
|
+
deletions: number;
|
|
60
|
+
changed_files: number;
|
|
61
|
+
labels: Array<{ name: string }>;
|
|
62
|
+
},
|
|
63
|
+
) => ({
|
|
64
|
+
number: pr.number,
|
|
65
|
+
title: pr.title,
|
|
66
|
+
state: pr.state,
|
|
67
|
+
isDraft: pr.draft,
|
|
68
|
+
url: pr.html_url,
|
|
69
|
+
author: pr.user.login,
|
|
70
|
+
createdAt: pr.created_at,
|
|
71
|
+
updatedAt: pr.updated_at,
|
|
72
|
+
sourceBranch: pr.head.ref,
|
|
73
|
+
targetBranch: pr.base.ref,
|
|
74
|
+
additions: pr.additions,
|
|
75
|
+
deletions: pr.deletions,
|
|
76
|
+
changedFiles: pr.changed_files,
|
|
77
|
+
labels: pr.labels.map((l: { name: string }) => l.name),
|
|
78
|
+
})),
|
|
79
|
+
count: prs.length,
|
|
80
|
+
repository: repo,
|
|
81
|
+
message: `Found ${prs.length} ${state} pull request(s) in ${repo}.`,
|
|
82
|
+
};
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error instanceof Error && error.message.includes("not connected")) {
|
|
85
|
+
return {
|
|
86
|
+
error: "GitHub not connected. Please connect your GitHub account.",
|
|
87
|
+
connectUrl: "/api/auth/github",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { tool } from "veryfront/ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createGitHubClient } from "../../lib/github-client.ts";
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
id: "list-repos",
|
|
7
|
+
description: "List GitHub repositories for the authenticated user",
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
type: z
|
|
10
|
+
.enum(["all", "owner", "public", "private", "member"])
|
|
11
|
+
.default("all")
|
|
12
|
+
.describe("Type of repositories to list"),
|
|
13
|
+
sort: z
|
|
14
|
+
.enum(["created", "updated", "pushed", "full_name"])
|
|
15
|
+
.default("updated")
|
|
16
|
+
.describe("How to sort the repositories"),
|
|
17
|
+
limit: z
|
|
18
|
+
.number()
|
|
19
|
+
.min(1)
|
|
20
|
+
.max(100)
|
|
21
|
+
.default(20)
|
|
22
|
+
.describe("Maximum number of repositories to return"),
|
|
23
|
+
}),
|
|
24
|
+
execute: async ({ type, sort, limit }, context) => {
|
|
25
|
+
const userId = context?.userId as string | undefined;
|
|
26
|
+
if (!userId) {
|
|
27
|
+
return {
|
|
28
|
+
error: "User not authenticated. Please log in first.",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const github = createGitHubClient(userId);
|
|
34
|
+
const repos = await github.listRepos({
|
|
35
|
+
type,
|
|
36
|
+
sort,
|
|
37
|
+
perPage: limit,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
repositories: repos.map((
|
|
42
|
+
repo: {
|
|
43
|
+
name: string;
|
|
44
|
+
full_name: string;
|
|
45
|
+
description: string | null;
|
|
46
|
+
private: boolean;
|
|
47
|
+
html_url: string;
|
|
48
|
+
default_branch: string;
|
|
49
|
+
language: string | null;
|
|
50
|
+
stargazers_count: number;
|
|
51
|
+
forks_count: number;
|
|
52
|
+
open_issues_count: number;
|
|
53
|
+
updated_at: string;
|
|
54
|
+
},
|
|
55
|
+
) => ({
|
|
56
|
+
name: repo.name,
|
|
57
|
+
fullName: repo.full_name,
|
|
58
|
+
description: repo.description || null,
|
|
59
|
+
isPrivate: repo.private,
|
|
60
|
+
url: repo.html_url,
|
|
61
|
+
defaultBranch: repo.default_branch,
|
|
62
|
+
language: repo.language,
|
|
63
|
+
stars: repo.stargazers_count,
|
|
64
|
+
forks: repo.forks_count,
|
|
65
|
+
openIssues: repo.open_issues_count,
|
|
66
|
+
updatedAt: repo.updated_at,
|
|
67
|
+
})),
|
|
68
|
+
count: repos.length,
|
|
69
|
+
message: `Found ${repos.length} repository(s).`,
|
|
70
|
+
};
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof Error && error.message.includes("not connected")) {
|
|
73
|
+
return {
|
|
74
|
+
error: "GitHub not connected. Please connect your GitHub account.",
|
|
75
|
+
connectUrl: "/api/auth/github",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Callback
|
|
3
|
+
*
|
|
4
|
+
* Handles the OAuth callback from GitHub, exchanges code for tokens,
|
|
5
|
+
* and stores them securely.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { tokenStore } from "../../../../../lib/token-store.ts";
|
|
9
|
+
import { githubOAuthProvider } from "../../../../../lib/github-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>GitHub 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("github_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}${githubOAuthProvider.callbackPath}`;
|
|
56
|
+
|
|
57
|
+
// Exchange code for tokens
|
|
58
|
+
const response = await fetch(githubOAuthProvider.tokenUrl, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
62
|
+
Accept: "application/json",
|
|
63
|
+
},
|
|
64
|
+
body: new URLSearchParams({
|
|
65
|
+
client_id: githubOAuthProvider.clientId,
|
|
66
|
+
client_secret: githubOAuthProvider.clientSecret,
|
|
67
|
+
code,
|
|
68
|
+
redirect_uri: redirectUri,
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const data = await response.json();
|
|
73
|
+
|
|
74
|
+
if (data.error) {
|
|
75
|
+
throw new Error(data.error_description || data.error);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Get actual user ID from session in production
|
|
79
|
+
const userId = "current-user";
|
|
80
|
+
|
|
81
|
+
// Store tokens
|
|
82
|
+
await tokenStore.setToken(userId, "github", {
|
|
83
|
+
accessToken: data.access_token,
|
|
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>GitHub 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;">✓ GitHub Connected!</h1>
|
|
99
|
+
<p style="color: #666;">Your GitHub account 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": "github_oauth_state=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error("GitHub 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>GitHub Connection Failed</h1>
|
|
121
|
+
<p style="color: #666;">Unable to complete GitHub authorization. Please try again.</p>
|
|
122
|
+
<a href="/api/auth/github" 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
|
+
* GitHub OAuth Initiation
|
|
3
|
+
*
|
|
4
|
+
* Redirects to GitHub OAuth consent screen
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getAuthorizationUrl } from "../../../../lib/oauth.ts";
|
|
8
|
+
import { githubOAuthProvider } from "../../../../lib/github-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}${githubOAuthProvider.callbackPath}`;
|
|
19
|
+
const authUrl = getAuthorizationUrl(githubOAuthProvider, state, redirectUri);
|
|
20
|
+
|
|
21
|
+
// Set state cookie and redirect to GitHub
|
|
22
|
+
return new Response(null, {
|
|
23
|
+
status: 302,
|
|
24
|
+
headers: {
|
|
25
|
+
Location: authUrl,
|
|
26
|
+
"Set-Cookie": `github_oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|