@vibes.diy/api-svc 14.3.164 → 14.3.166
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/index.d.ts +1 -0
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/intern/backend-github-callback.js +16 -20
- package/intern/backend-github-callback.js.map +1 -1
- package/intern/github-app-auth.d.ts +6 -2
- package/intern/github-app-auth.js +35 -9
- package/intern/github-app-auth.js.map +1 -1
- package/intern/is-unique-violation.d.ts +1 -0
- package/intern/is-unique-violation.js +13 -0
- package/intern/is-unique-violation.js.map +1 -0
- package/intern/open-turn-sweep.js +1 -12
- package/intern/open-turn-sweep.js.map +1 -1
- package/package.json +13 -13
- package/public/github-install-pick.html +4 -4
- package/public/github-install-signin.html +1 -1
- package/public/github-repo-pick-item.html +7 -0
- package/public/github-repo-pick.html +28 -0
- package/public/github-repos.d.ts +3 -0
- package/public/github-repos.js +170 -0
- package/public/github-repos.js.map +1 -0
- package/public/github-setup.d.ts +33 -0
- package/public/github-setup.js +154 -64
- package/public/github-setup.js.map +1 -1
- package/public/github-templates.d.ts +1 -1
- package/public/github-templates.js +1 -1
- package/public/github-templates.js.map +1 -1
- package/public/github-webhook.d.ts +1 -0
- package/public/github-webhook.js +50 -15
- package/public/github-webhook.js.map +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { exception2Result, Result } from "@adviser/cement";
|
|
2
|
+
import { and, eq, inArray, ne, or } from "drizzle-orm/sql/expressions";
|
|
3
|
+
import { appExists, claimGithubConsent, consentUsedPage, GITHUB_REPO_PICK_PROVIDER, githubConsentUnspent, mayBindVibe, notProvisionedPage, notYourVibePage, ownsHandle, sessionTokenFrom, sqlFor, stateSecret, verifyGithubRepoPickState, } from "./github-setup.js";
|
|
4
|
+
import { flowRedirect, htmlPage } from "./github-templates.js";
|
|
5
|
+
import { GITHUB_REPO_MAX_VIBES } from "./github-webhook.js";
|
|
6
|
+
function pickUsedPage() {
|
|
7
|
+
return htmlPage("Choice already saved", "That page has already saved a choice of repositories. Open the app and press its Connect button again to change it.", 403);
|
|
8
|
+
}
|
|
9
|
+
function pickExpiredPage() {
|
|
10
|
+
return htmlPage("Link expired", "That choice of repositories is no longer valid. Open the app and press its Connect button again.", 400);
|
|
11
|
+
}
|
|
12
|
+
async function claimPick(sql, pick, nowMs) {
|
|
13
|
+
const t = sql.tables.connectionApprovals;
|
|
14
|
+
const claimed = await sql.db
|
|
15
|
+
.update(t)
|
|
16
|
+
.set({ consumedAt: new Date(nowMs).toISOString() })
|
|
17
|
+
.where(and(eq(t.id, pick.id), eq(t.consumedAt, ""), eq(t.userId, pick.userId), eq(t.provider, GITHUB_REPO_PICK_PROVIDER), eq(t.ownerHandle, pick.owner), eq(t.appSlug, pick.slug)))
|
|
18
|
+
.returning({ id: t.id });
|
|
19
|
+
return claimed.length > 0;
|
|
20
|
+
}
|
|
21
|
+
async function bindOne(input) {
|
|
22
|
+
const { sql, pick, repoFullName, accountLogin, now } = input;
|
|
23
|
+
const t = sql.tables.githubRepoRoutes;
|
|
24
|
+
const rApplied = await exception2Result(() => sql.db
|
|
25
|
+
.insert(t)
|
|
26
|
+
.values({
|
|
27
|
+
installationId: pick.installationId,
|
|
28
|
+
repoFullName,
|
|
29
|
+
ownerHandle: pick.owner,
|
|
30
|
+
appSlug: pick.slug,
|
|
31
|
+
accountLogin,
|
|
32
|
+
installedByUserId: pick.userId,
|
|
33
|
+
created: now,
|
|
34
|
+
updated: now,
|
|
35
|
+
removedAt: "",
|
|
36
|
+
})
|
|
37
|
+
.onConflictDoUpdate({
|
|
38
|
+
target: [t.installationId, t.repoFullName, t.ownerHandle, t.appSlug],
|
|
39
|
+
set: { accountLogin, installedByUserId: pick.userId, updated: now, removedAt: "" },
|
|
40
|
+
setWhere: or(eq(t.installedByUserId, pick.userId), ne(t.removedAt, "")),
|
|
41
|
+
})
|
|
42
|
+
.returning({ repoFullName: t.repoFullName }));
|
|
43
|
+
if (rApplied.isErr())
|
|
44
|
+
return Result.Err(rApplied.Err());
|
|
45
|
+
return Result.Ok(rApplied.Ok().length > 0 ? "bound" : "held-here");
|
|
46
|
+
}
|
|
47
|
+
async function inChunks(items, fn) {
|
|
48
|
+
const out = [];
|
|
49
|
+
for (let i = 0; i < items.length; i = i + IN_ARRAY_CHUNK) {
|
|
50
|
+
out.push(...(await fn(items.slice(i, i + IN_ARRAY_CHUNK))));
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
const IN_ARRAY_CHUNK = 50;
|
|
55
|
+
async function otherVibesFed(input) {
|
|
56
|
+
const t = input.sql.tables.githubRepoRoutes;
|
|
57
|
+
const rows = await input.sql.db
|
|
58
|
+
.select({ ownerHandle: t.ownerHandle, appSlug: t.appSlug })
|
|
59
|
+
.from(t)
|
|
60
|
+
.where(and(eq(t.installationId, input.pick.installationId), eq(t.repoFullName, input.repoFullName), eq(t.removedAt, "")))
|
|
61
|
+
.limit(GITHUB_REPO_MAX_VIBES + 1);
|
|
62
|
+
return rows.filter((r) => r.ownerHandle !== input.pick.owner || r.appSlug !== input.pick.slug).length;
|
|
63
|
+
}
|
|
64
|
+
export async function handleGithubRepos(request, env, deps) {
|
|
65
|
+
const signingKey = stateSecret(env);
|
|
66
|
+
if (signingKey === undefined)
|
|
67
|
+
return notProvisionedPage();
|
|
68
|
+
const rForm = await exception2Result(() => request.formData());
|
|
69
|
+
if (rForm.isErr())
|
|
70
|
+
return pickExpiredPage();
|
|
71
|
+
const form = rForm.Ok();
|
|
72
|
+
const rawState = form.get("state");
|
|
73
|
+
const nowMs = deps.nowMs ?? Date.now();
|
|
74
|
+
const pick = typeof rawState === "string" ? await verifyGithubRepoPickState(signingKey, rawState, nowMs) : undefined;
|
|
75
|
+
if (pick === undefined)
|
|
76
|
+
return pickExpiredPage();
|
|
77
|
+
const token = sessionTokenFrom(request);
|
|
78
|
+
const sessionUserId = token === undefined ? undefined : await deps.verifySessionToken(token);
|
|
79
|
+
if (sessionUserId !== undefined && sessionUserId !== pick.userId) {
|
|
80
|
+
console.error(`[github-repos] pick state for clerk user ${pick.userId} presented by ${sessionUserId}; refusing`);
|
|
81
|
+
return pickExpiredPage();
|
|
82
|
+
}
|
|
83
|
+
const userId = pick.userId;
|
|
84
|
+
const sql = sqlFor(env, deps);
|
|
85
|
+
const isOwner = await ownsHandle(sql, userId, pick.owner);
|
|
86
|
+
if (isOwner === false) {
|
|
87
|
+
if (pick.consentId === undefined ||
|
|
88
|
+
(await mayBindVibe(sql, { userId, owner: pick.owner, slug: pick.slug }, deps.githubDecl)) === false) {
|
|
89
|
+
return notYourVibePage();
|
|
90
|
+
}
|
|
91
|
+
if ((await githubConsentUnspent(sql, { id: pick.consentId, userId, owner: pick.owner, slug: pick.slug })) === false) {
|
|
92
|
+
return consentUsedPage();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if ((await appExists(sql, pick.owner, pick.slug)) === false) {
|
|
96
|
+
return htmlPage("Vibe not found", "That vibe does not exist. Publish it first, then connect the repository.", 404);
|
|
97
|
+
}
|
|
98
|
+
const offered = new Map(pick.repos.map((r) => [r.fullName, r]));
|
|
99
|
+
const ticked = new Set();
|
|
100
|
+
for (const value of form.getAll("repo")) {
|
|
101
|
+
if (typeof value !== "string")
|
|
102
|
+
continue;
|
|
103
|
+
const name = value.toLowerCase();
|
|
104
|
+
if (offered.has(name))
|
|
105
|
+
ticked.add(name);
|
|
106
|
+
}
|
|
107
|
+
const t = sql.tables.githubRepoRoutes;
|
|
108
|
+
const offeredNames = [...offered.keys()];
|
|
109
|
+
const removable = (await inChunks(offeredNames, (names) => sql.db
|
|
110
|
+
.select({ repoFullName: t.repoFullName, installedByUserId: t.installedByUserId })
|
|
111
|
+
.from(t)
|
|
112
|
+
.where(and(eq(t.installationId, pick.installationId), eq(t.ownerHandle, pick.owner), eq(t.appSlug, pick.slug), eq(t.removedAt, ""), inArray(t.repoFullName, [...names])))))
|
|
113
|
+
.filter((row) => ticked.has(row.repoFullName) === false && (isOwner || row.installedByUserId === userId))
|
|
114
|
+
.map((row) => row.repoFullName);
|
|
115
|
+
if (ticked.size === 0 && removable.length === 0) {
|
|
116
|
+
return htmlPage("No repositories chosen", "Tick at least one repository for this vibe, then save. Go back to choose.", 400);
|
|
117
|
+
}
|
|
118
|
+
if ((await claimPick(sql, pick, nowMs)) === false)
|
|
119
|
+
return pickUsedPage();
|
|
120
|
+
if (pick.consentId !== undefined &&
|
|
121
|
+
isOwner === false &&
|
|
122
|
+
(await claimGithubConsent(sql, { id: pick.consentId, userId, owner: pick.owner, slug: pick.slug, nowMs })) === false) {
|
|
123
|
+
return consentUsedPage();
|
|
124
|
+
}
|
|
125
|
+
const now = new Date(nowMs).toISOString();
|
|
126
|
+
await inChunks(removable, async (names) => {
|
|
127
|
+
await sql.db
|
|
128
|
+
.delete(t)
|
|
129
|
+
.where(and(eq(t.installationId, pick.installationId), eq(t.ownerHandle, pick.owner), eq(t.appSlug, pick.slug), eq(t.removedAt, ""), inArray(t.repoFullName, [...names]), ...(isOwner ? [] : [eq(t.installedByUserId, userId)])));
|
|
130
|
+
return [];
|
|
131
|
+
});
|
|
132
|
+
let bound = 0;
|
|
133
|
+
let heldHere = 0;
|
|
134
|
+
let atCapacity = 0;
|
|
135
|
+
for (const name of ticked) {
|
|
136
|
+
const repo = offered.get(name);
|
|
137
|
+
if (repo === undefined)
|
|
138
|
+
continue;
|
|
139
|
+
if ((await otherVibesFed({ sql, pick, repoFullName: name })) >= GITHUB_REPO_MAX_VIBES) {
|
|
140
|
+
atCapacity = atCapacity + 1;
|
|
141
|
+
console.warn(`[github-repos] installation ${pick.installationId} repo ${name} already feeds ${GITHUB_REPO_MAX_VIBES} vibes; ` +
|
|
142
|
+
`not adding ${pick.owner}/${pick.slug} (clerk user ${userId})`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const rOutcome = await bindOne({ sql, pick, repoFullName: name, accountLogin: repo.accountLogin, now });
|
|
146
|
+
if (rOutcome.isErr()) {
|
|
147
|
+
console.error(`[github-repos] bind of ${name} to ${pick.owner}/${pick.slug} failed:`, rOutcome.Err().message);
|
|
148
|
+
return htmlPage("Not saved", "We could not save your choice of repositories. Open the app and press its Connect button again.", 500);
|
|
149
|
+
}
|
|
150
|
+
switch (rOutcome.Ok()) {
|
|
151
|
+
case "bound":
|
|
152
|
+
bound = bound + 1;
|
|
153
|
+
break;
|
|
154
|
+
case "held-here":
|
|
155
|
+
heldHere = heldHere + 1;
|
|
156
|
+
console.warn(`[github-repos] installation ${pick.installationId} repo ${name} is connected to ${pick.owner}/${pick.slug} by ` +
|
|
157
|
+
`another clerk user; leaving it theirs (clerk user ${userId} asked)`);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (atCapacity > 0) {
|
|
162
|
+
return htmlPage("That repository is popular", `One repository can feed up to ${GITHUB_REPO_MAX_VIBES} vibes, and one you chose already feeds ${GITHUB_REPO_MAX_VIBES} vibes, so ` +
|
|
163
|
+
"it was not added here. The rest of your choice is saved.", 409);
|
|
164
|
+
}
|
|
165
|
+
if (ticked.size > 0 && bound === 0 && heldHere > 0 && removable.length === 0) {
|
|
166
|
+
return htmlPage("Already connected", "Those repositories are already connected to this vibe by someone else, so they stay theirs.", 409);
|
|
167
|
+
}
|
|
168
|
+
return flowRedirect(`/vibe/${pick.owner}/${pick.slug}`);
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=github-repos.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-repos.js","sourceRoot":"","sources":["../../jsr/public/github-repos.ts"],"names":[],"mappings":"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"}
|
package/public/github-setup.d.ts
CHANGED
|
@@ -80,7 +80,20 @@ export declare function recordGithubConsent(sql: GithubSetupSql, input: {
|
|
|
80
80
|
readonly slug: string;
|
|
81
81
|
readonly nowMs: number;
|
|
82
82
|
}): Promise<string>;
|
|
83
|
+
interface ConsentRowKey {
|
|
84
|
+
readonly id: string;
|
|
85
|
+
readonly userId: string;
|
|
86
|
+
readonly owner: string;
|
|
87
|
+
readonly slug: string;
|
|
88
|
+
}
|
|
89
|
+
export declare function githubConsentUnspent(sql: GithubSetupSql, key: ConsentRowKey): Promise<boolean>;
|
|
90
|
+
export declare function claimGithubConsent(sql: GithubSetupSql, key: ConsentRowKey & {
|
|
91
|
+
readonly nowMs: number;
|
|
92
|
+
}): Promise<boolean>;
|
|
93
|
+
export declare function notYourVibePage(): Response;
|
|
83
94
|
export declare const GITHUB_SETUP_PATH = "/github/setup";
|
|
95
|
+
export declare function notProvisionedPage(): Response;
|
|
96
|
+
export declare function consentUsedPage(): Response;
|
|
84
97
|
export type GithubAuthorizeUrl = {
|
|
85
98
|
readonly ok: true;
|
|
86
99
|
readonly url: string;
|
|
@@ -100,3 +113,23 @@ export type GithubOAuthReady = {
|
|
|
100
113
|
export declare function githubOAuthReady(env: CFEnv): GithubOAuthReady;
|
|
101
114
|
export declare function githubAuthorizeUrl(env: CFEnv, state: SetupOAuthState): Promise<GithubAuthorizeUrl>;
|
|
102
115
|
export declare function installUrlFor(htmlUrl: string, state: string): string;
|
|
116
|
+
export declare const GITHUB_REPOS_PATH = "/github/repos";
|
|
117
|
+
export declare const GITHUB_REPO_PICK_STATE_PREFIX = "r1.";
|
|
118
|
+
export declare const GITHUB_REPO_PICK_STATE_TTL_MS: number;
|
|
119
|
+
export declare const GITHUB_REPO_PICK_PROVIDER = "github-app-repos";
|
|
120
|
+
export interface PickedRepo {
|
|
121
|
+
readonly fullName: string;
|
|
122
|
+
readonly accountLogin: string;
|
|
123
|
+
}
|
|
124
|
+
export interface GithubRepoPickState {
|
|
125
|
+
readonly id: string;
|
|
126
|
+
readonly owner: string;
|
|
127
|
+
readonly slug: string;
|
|
128
|
+
readonly installationId: string;
|
|
129
|
+
readonly userId: string;
|
|
130
|
+
readonly repos: readonly PickedRepo[];
|
|
131
|
+
readonly consentId?: string;
|
|
132
|
+
readonly exp: number;
|
|
133
|
+
}
|
|
134
|
+
export declare function signGithubRepoPickState(secret: string, state: GithubRepoPickState): Promise<string>;
|
|
135
|
+
export declare function verifyGithubRepoPickState(secret: string, signed: string, nowMs: number): Promise<GithubRepoPickState | undefined>;
|
package/public/github-setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { exception2Result, BuildURI, TxtEnDecoderSingleton, URI } from "@adviser/cement";
|
|
2
2
|
import { cfDrizzle, createVibesApiTables, toDBFlavour } from "@vibes.diy/api-sql";
|
|
3
|
-
import { and, eq
|
|
3
|
+
import { and, eq } from "drizzle-orm/sql/expressions";
|
|
4
4
|
import { getAppHtmlUrl, getInstallationToken, listUserInstallationRepos, nextGithubPageUrl } from "../intern/github-app-auth.js";
|
|
5
5
|
import { timingSafeEqualString } from "./github-webhook.js";
|
|
6
6
|
import { isConnectState } from "./connections-oauth.js";
|
|
@@ -296,12 +296,12 @@ function consentRowMatches(sql, key) {
|
|
|
296
296
|
const t = sql.tables.connectionApprovals;
|
|
297
297
|
return and(eq(t.id, key.id), eq(t.consumedAt, ""), eq(t.userId, key.userId), eq(t.provider, GITHUB_BIND_CONSENT_PROVIDER), eq(t.ownerHandle, key.owner), eq(t.appSlug, key.slug));
|
|
298
298
|
}
|
|
299
|
-
async function githubConsentUnspent(sql, key) {
|
|
299
|
+
export async function githubConsentUnspent(sql, key) {
|
|
300
300
|
const t = sql.tables.connectionApprovals;
|
|
301
301
|
const rows = await sql.db.select({ id: t.id }).from(t).where(consentRowMatches(sql, key)).limit(1);
|
|
302
302
|
return rows.length > 0;
|
|
303
303
|
}
|
|
304
|
-
async function claimGithubConsent(sql, key) {
|
|
304
|
+
export async function claimGithubConsent(sql, key) {
|
|
305
305
|
const t = sql.tables.connectionApprovals;
|
|
306
306
|
const claimed = await sql.db
|
|
307
307
|
.update(t)
|
|
@@ -310,7 +310,7 @@ async function claimGithubConsent(sql, key) {
|
|
|
310
310
|
.returning({ id: t.id });
|
|
311
311
|
return claimed.length > 0;
|
|
312
312
|
}
|
|
313
|
-
function notYourVibePage() {
|
|
313
|
+
export function notYourVibePage() {
|
|
314
314
|
return htmlPage("Not your vibe", "You can only connect a repository to a vibe you own.", 403);
|
|
315
315
|
}
|
|
316
316
|
async function admitPhaseOne(input) {
|
|
@@ -337,7 +337,7 @@ async function admitPhaseOne(input) {
|
|
|
337
337
|
return {
|
|
338
338
|
ok: false,
|
|
339
339
|
refusal: htmlPage("Consent not confirmed", "This install did not come from a consent you confirmed on vibes.diy, or that consent has expired. Open the " +
|
|
340
|
-
"app and
|
|
340
|
+
"app and press its Connect button again.", 403),
|
|
341
341
|
};
|
|
342
342
|
}
|
|
343
343
|
if (!(await githubConsentUnspent(sql, { id: consent.id, userId, owner: consent.owner, slug: consent.slug }))) {
|
|
@@ -349,15 +349,15 @@ async function admitPhaseOne(input) {
|
|
|
349
349
|
return { ok: true, owner: consent.owner, slug: consent.slug, consentId: consent.id };
|
|
350
350
|
}
|
|
351
351
|
export const GITHUB_SETUP_PATH = "/github/setup";
|
|
352
|
-
function notProvisionedPage() {
|
|
352
|
+
export function notProvisionedPage() {
|
|
353
353
|
return htmlPage("Not available", "The GitHub connection is not fully provisioned on this environment: connecting a repository needs a GitHub " +
|
|
354
354
|
"authorization step that is not configured yet, so nothing has been connected.", 503);
|
|
355
355
|
}
|
|
356
356
|
function notYourInstallationPage() {
|
|
357
357
|
return htmlPage("Not your installation", "That GitHub installation is not one your GitHub account can access.", 403);
|
|
358
358
|
}
|
|
359
|
-
function consentUsedPage() {
|
|
360
|
-
return htmlPage("Consent already used", "That consent has already connected a repository. Open the app and
|
|
359
|
+
export function consentUsedPage() {
|
|
360
|
+
return htmlPage("Consent already used", "That consent has already connected a repository. Open the app and press its Connect button again.", 403);
|
|
361
361
|
}
|
|
362
362
|
export function githubOAuthReady(env) {
|
|
363
363
|
const signingKey = stateSecret(env);
|
|
@@ -400,7 +400,7 @@ async function phaseOneStartOAuth(input) {
|
|
|
400
400
|
return htmlPage("Invalid link", "This install link is malformed. Start the connection again from your vibe's settings.", 400);
|
|
401
401
|
}
|
|
402
402
|
if (state === "" && uri.getParam("setup_action") === "update") {
|
|
403
|
-
return htmlPage("Saved on GitHub", "GitHub saved your
|
|
403
|
+
return htmlPage("Saved on GitHub", "GitHub saved your repository choices. Open your vibe and press its Connect button again to bring them in.", 200);
|
|
404
404
|
}
|
|
405
405
|
const admitted = await admitPhaseOne({ env, deps, state, userId, nowMs });
|
|
406
406
|
if (admitted.ok === false)
|
|
@@ -459,19 +459,22 @@ async function phaseTwoBind(input) {
|
|
|
459
459
|
if (!(await appExists(sql, ownerHandle, appSlug))) {
|
|
460
460
|
return htmlPage("Vibe not found", "That vibe does not exist. Publish it first, then connect the repository.", 404);
|
|
461
461
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
462
|
+
if (installationId !== undefined) {
|
|
463
|
+
return offerRepositories({
|
|
464
|
+
env,
|
|
465
|
+
deps,
|
|
466
|
+
sql,
|
|
467
|
+
userId,
|
|
468
|
+
ownerHandle,
|
|
469
|
+
appSlug,
|
|
470
|
+
userToken,
|
|
471
|
+
nowMs,
|
|
472
|
+
signingKey,
|
|
473
|
+
isOwner,
|
|
474
|
+
installationId,
|
|
475
|
+
consentId: isOwner || consent === undefined ? undefined : consent.id,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
475
478
|
return chooseOrInstall({
|
|
476
479
|
env,
|
|
477
480
|
deps,
|
|
@@ -527,7 +530,76 @@ async function chooseOrInstall(input) {
|
|
|
527
530
|
const vibe = `${owner}/${slug}`;
|
|
528
531
|
return templatePage(deps.templates, { name: "github-install-pick.html", status: 200 }, { vibe, choices, installUrl, vibeHref: `/vibe/${vibe}` });
|
|
529
532
|
}
|
|
530
|
-
|
|
533
|
+
export const GITHUB_REPOS_PATH = "/github/repos";
|
|
534
|
+
export const GITHUB_REPO_PICK_STATE_PREFIX = "r1.";
|
|
535
|
+
export const GITHUB_REPO_PICK_STATE_TTL_MS = 10 * 60 * 1000;
|
|
536
|
+
export const GITHUB_REPO_PICK_PROVIDER = "github-app-repos";
|
|
537
|
+
export async function signGithubRepoPickState(secret, state) {
|
|
538
|
+
const blob = b64urlEncode(TxtEnDecoderSingleton().encode(JSON.stringify(state)));
|
|
539
|
+
const message = `${GITHUB_REPO_PICK_STATE_PREFIX}${blob}`;
|
|
540
|
+
return `${message}.${await hmacHex(secret, message)}`;
|
|
541
|
+
}
|
|
542
|
+
function pickedReposFrom(raw) {
|
|
543
|
+
if (!Array.isArray(raw))
|
|
544
|
+
return undefined;
|
|
545
|
+
const out = [];
|
|
546
|
+
for (const item of raw) {
|
|
547
|
+
if (typeof item !== "object" || item === null)
|
|
548
|
+
return undefined;
|
|
549
|
+
const repo = item;
|
|
550
|
+
if (typeof repo.fullName !== "string" || repo.fullName === "" || typeof repo.accountLogin !== "string")
|
|
551
|
+
return undefined;
|
|
552
|
+
out.push({ fullName: repo.fullName, accountLogin: repo.accountLogin });
|
|
553
|
+
}
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
export async function verifyGithubRepoPickState(secret, signed, nowMs) {
|
|
557
|
+
if (!signed.startsWith(GITHUB_REPO_PICK_STATE_PREFIX))
|
|
558
|
+
return undefined;
|
|
559
|
+
const dot = signed.lastIndexOf(".");
|
|
560
|
+
if (dot <= GITHUB_REPO_PICK_STATE_PREFIX.length)
|
|
561
|
+
return undefined;
|
|
562
|
+
const message = signed.slice(0, dot);
|
|
563
|
+
const provided = signed.slice(dot + 1);
|
|
564
|
+
const expected = await hmacHex(secret, message);
|
|
565
|
+
if (!timingSafeEqualString(provided, expected))
|
|
566
|
+
return undefined;
|
|
567
|
+
const json = b64urlDecode(message.slice(GITHUB_REPO_PICK_STATE_PREFIX.length));
|
|
568
|
+
if (json === undefined)
|
|
569
|
+
return undefined;
|
|
570
|
+
const rParsed = exception2Result(() => JSON.parse(json));
|
|
571
|
+
if (rParsed.isErr())
|
|
572
|
+
return undefined;
|
|
573
|
+
const parsed = rParsed.Ok();
|
|
574
|
+
if (typeof parsed.owner !== "string" || typeof parsed.slug !== "string")
|
|
575
|
+
return undefined;
|
|
576
|
+
if (typeof parsed.id !== "string" || parsed.id === "")
|
|
577
|
+
return undefined;
|
|
578
|
+
if (typeof parsed.userId !== "string" || parsed.userId === "")
|
|
579
|
+
return undefined;
|
|
580
|
+
if (typeof parsed.installationId !== "string" || parsed.installationId === "")
|
|
581
|
+
return undefined;
|
|
582
|
+
if (typeof parsed.exp !== "number" || nowMs >= parsed.exp)
|
|
583
|
+
return undefined;
|
|
584
|
+
if (parsed.consentId !== undefined && (typeof parsed.consentId !== "string" || parsed.consentId === ""))
|
|
585
|
+
return undefined;
|
|
586
|
+
const repos = pickedReposFrom(parsed.repos);
|
|
587
|
+
if (repos === undefined)
|
|
588
|
+
return undefined;
|
|
589
|
+
if (!GITHUB_SETUP_STATE_RE.test(`${parsed.owner}/${parsed.slug}`))
|
|
590
|
+
return undefined;
|
|
591
|
+
return {
|
|
592
|
+
id: parsed.id,
|
|
593
|
+
owner: parsed.owner,
|
|
594
|
+
slug: parsed.slug,
|
|
595
|
+
installationId: parsed.installationId,
|
|
596
|
+
userId: parsed.userId,
|
|
597
|
+
repos,
|
|
598
|
+
exp: parsed.exp,
|
|
599
|
+
...(parsed.consentId === undefined ? {} : { consentId: parsed.consentId }),
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
async function offerRepositories(input) {
|
|
531
603
|
const { env, deps, sql, userId, ownerHandle, appSlug, userToken, nowMs, installationId } = input;
|
|
532
604
|
const rToken = await getInstallationToken({
|
|
533
605
|
installationId,
|
|
@@ -551,55 +623,73 @@ async function bindInstallation(input) {
|
|
|
551
623
|
"organization owner to grant you access, or install the app on a repository you own.", 403);
|
|
552
624
|
}
|
|
553
625
|
if (repos.length > GITHUB_SETUP_MAX_REPOS) {
|
|
554
|
-
return htmlPage("Too many repositories", `That installation covers ${repos.length} repositories, more than the ${GITHUB_SETUP_MAX_REPOS}
|
|
555
|
-
"can
|
|
626
|
+
return htmlPage("Too many repositories", `That installation covers ${repos.length} repositories, more than the ${GITHUB_SETUP_MAX_REPOS} one connection ` +
|
|
627
|
+
"can list, so nothing has been connected. In GitHub, change the app's repository access to select fewer " +
|
|
556
628
|
"repositories and run the connection again.", 400);
|
|
557
629
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return
|
|
630
|
+
const writable = repos.filter((r) => r.canWrite);
|
|
631
|
+
if (writable.length === 0) {
|
|
632
|
+
return htmlPage("No repositories to connect", "Connecting a repository needs write access to it on GitHub, and your account can only read the repositories " +
|
|
633
|
+
"in that installation. Ask a maintainer for write access, or connect a repository you maintain.", 403);
|
|
634
|
+
}
|
|
635
|
+
if (deps.templates === undefined) {
|
|
636
|
+
return htmlPage("Not available", "This page could not be shown right now. Try again in a moment.", 503);
|
|
637
|
+
}
|
|
638
|
+
const rItem = await deps.templates("github-repo-pick-item.html");
|
|
639
|
+
if (rItem.isErr()) {
|
|
640
|
+
console.error("[github-setup] template github-repo-pick-item.html unreadable:", rItem.Err().message);
|
|
641
|
+
return htmlPage("Not available", "This page could not be shown right now. Try again in a moment.", 503);
|
|
561
642
|
}
|
|
562
643
|
const t = sql.tables.githubRepoRoutes;
|
|
563
|
-
const
|
|
644
|
+
const here = new Map();
|
|
564
645
|
for (const row of await sql.db
|
|
565
646
|
.select({ repoFullName: t.repoFullName, installedByUserId: t.installedByUserId })
|
|
566
647
|
.from(t)
|
|
567
|
-
.where(and(eq(t.installationId, installationId), eq(t.removedAt, "")))) {
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
const now = new Date().toISOString();
|
|
571
|
-
const bound = [];
|
|
572
|
-
for (const repo of repos) {
|
|
573
|
-
const applied = await sql.db
|
|
574
|
-
.insert(t)
|
|
575
|
-
.values({
|
|
576
|
-
installationId,
|
|
577
|
-
repoFullName: repo.fullName,
|
|
578
|
-
ownerHandle,
|
|
579
|
-
appSlug,
|
|
580
|
-
accountLogin: repo.accountLogin,
|
|
581
|
-
installedByUserId: userId,
|
|
582
|
-
created: now,
|
|
583
|
-
updated: now,
|
|
584
|
-
removedAt: "",
|
|
585
|
-
})
|
|
586
|
-
.onConflictDoUpdate({
|
|
587
|
-
target: [t.installationId, t.repoFullName],
|
|
588
|
-
set: { ownerHandle, appSlug, installedByUserId: userId, updated: now, removedAt: "" },
|
|
589
|
-
setWhere: or(eq(t.installedByUserId, userId), ne(t.removedAt, "")),
|
|
590
|
-
})
|
|
591
|
-
.returning({ repoFullName: t.repoFullName });
|
|
592
|
-
if (applied.length === 0) {
|
|
593
|
-
console.warn(`[github-setup] installation ${installationId} repo ${repo.fullName} is already connected by clerk user ` +
|
|
594
|
-
`${heldBy.get(repo.fullName) ?? "unknown"}; refusing to rebind it for clerk user ${userId}`);
|
|
595
|
-
continue;
|
|
596
|
-
}
|
|
597
|
-
bound.push(repo.fullName);
|
|
598
|
-
}
|
|
599
|
-
if (bound.length === 0) {
|
|
600
|
-
return htmlPage("Already connected", "Those repositories are already connected to another vibe. Whoever connected them has to disconnect them first.", 409);
|
|
648
|
+
.where(and(eq(t.installationId, installationId), eq(t.ownerHandle, ownerHandle), eq(t.appSlug, appSlug), eq(t.removedAt, "")))) {
|
|
649
|
+
here.set(row.repoFullName, row.installedByUserId);
|
|
601
650
|
}
|
|
602
|
-
|
|
651
|
+
const id = crypto.randomUUID();
|
|
652
|
+
await sql.db.insert(sql.tables.connectionApprovals).values({
|
|
653
|
+
id,
|
|
654
|
+
userId,
|
|
655
|
+
provider: GITHUB_REPO_PICK_PROVIDER,
|
|
656
|
+
ownerHandle,
|
|
657
|
+
appSlug,
|
|
658
|
+
created: new Date(nowMs).toISOString(),
|
|
659
|
+
consumedAt: "",
|
|
660
|
+
});
|
|
661
|
+
const state = await signGithubRepoPickState(input.signingKey, {
|
|
662
|
+
id,
|
|
663
|
+
owner: ownerHandle,
|
|
664
|
+
slug: appSlug,
|
|
665
|
+
installationId,
|
|
666
|
+
userId,
|
|
667
|
+
repos: writable.map((r) => ({ fullName: r.fullName, accountLogin: r.accountLogin })),
|
|
668
|
+
...(input.consentId === undefined ? {} : { consentId: input.consentId }),
|
|
669
|
+
exp: nowMs + GITHUB_REPO_PICK_STATE_TTL_MS,
|
|
670
|
+
});
|
|
671
|
+
const rows = fillFragments(rItem.Ok(), repos.map((repo) => {
|
|
672
|
+
const binder = here.get(repo.fullName);
|
|
673
|
+
const lockedForMe = binder !== undefined && binder !== userId && input.isOwner === false;
|
|
674
|
+
const note = (() => {
|
|
675
|
+
switch (true) {
|
|
676
|
+
case repo.canWrite === false:
|
|
677
|
+
return "needs write access on GitHub to connect";
|
|
678
|
+
case binder !== undefined && binder !== userId:
|
|
679
|
+
return "connected here by someone else";
|
|
680
|
+
default:
|
|
681
|
+
return "";
|
|
682
|
+
}
|
|
683
|
+
})();
|
|
684
|
+
return {
|
|
685
|
+
repo: repo.fullName,
|
|
686
|
+
checked: binder === undefined ? "" : "checked",
|
|
687
|
+
disabled: lockedForMe || repo.canWrite === false ? "disabled" : "",
|
|
688
|
+
note,
|
|
689
|
+
};
|
|
690
|
+
}));
|
|
691
|
+
const vibe = `${ownerHandle}/${appSlug}`;
|
|
692
|
+
return templatePage(deps.templates, { name: "github-repo-pick.html", status: 200 }, { vibe, rows, state, action: GITHUB_REPOS_PATH, vibeHref: `/vibe/${vibe}` });
|
|
603
693
|
}
|
|
604
694
|
async function phaseConnect(input) {
|
|
605
695
|
return runConnectCallback({
|