@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.
@@ -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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"}
@@ -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>;
@@ -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, ne, or } from "drizzle-orm/sql/expressions";
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 use its Connect GitHub link again.", 403),
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 use its Connect GitHub link again.", 403);
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 changes to the app's repositories. Open your vibe and press Connect GitHub again to bring them in.", 200);
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
- const bindable = {
463
- env,
464
- deps,
465
- sql,
466
- userId,
467
- ownerHandle,
468
- appSlug,
469
- userToken,
470
- nowMs,
471
- claimConsent: isOwner || consent === undefined ? undefined : consent.id,
472
- };
473
- if (installationId !== undefined)
474
- return bindInstallation({ ...bindable, installationId });
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
- async function bindInstallation(input) {
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} a single connection ` +
555
- "can bind, so nothing has been connected. In GitHub, change the app's repository access to select fewer " +
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
- if (input.claimConsent !== undefined &&
559
- (await claimGithubConsent(sql, { id: input.claimConsent, userId, owner: ownerHandle, slug: appSlug, nowMs })) === false) {
560
- return consentUsedPage();
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 heldBy = new Map();
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
- heldBy.set(row.repoFullName, row.installedByUserId);
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
- return flowRedirect(`/vibe/${ownerHandle}/${appSlug}`);
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({