Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

repo-create-gate.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

repo-create-gate.tsBlame82 lines · 1 contributor
c63b860Claude1/**
2 * Block P4 — shared "before create repo" gate.
3 *
4 * Pricing is fiction until creation paths actually enforce the plan's
5 * repoLimit. This module wraps `src/lib/billing.ts`'s pure helpers
6 * (`wouldExceedRepoLimit`, `getUserQuota`, `resetIfCycleExpired`) into a
7 * single decision call that every repo-create site shares.
8 *
9 * Fail-open: any error in the underlying helpers returns `{ ok: true }`
10 * so a Neon hiccup or billing-table outage never blocks legitimate
11 * users from creating repos. This is consistent with billing.ts's own
12 * fail-open posture (see invariants in BUILD_BIBLE §4.9).
13 */
14
15import {
16 getUserQuota,
17 resetIfCycleExpired,
18 wouldExceedRepoLimit,
19} from "./billing";
20
21export type RepoCreateGateResult =
22 | { ok: true }
23 | { ok: false; reason: string; upgradeUrl: string };
24
25/**
26 * Should this user be allowed to create another repo right now?
27 *
28 * Used by:
29 * - POST /new (web UI)
30 * - POST /api/v2/repos (REST API v2)
31 * - POST /import/github/repo (GitHub import)
32 *
33 * Caller patterns:
34 * - Web routes redirect to `?error=<reason>` on `ok: false`.
35 * - API routes return 402 Payment Required with `{error, upgrade_url}`.
36 */
37export async function checkRepoCreateAllowed(
38 userId: string
39): Promise<RepoCreateGateResult> {
40 try {
41 // Roll the monthly counter window forward if needed.
42 await resetIfCycleExpired(userId).catch(() => false);
43 if (await wouldExceedRepoLimit(userId)) {
44 const quota = await getUserQuota(userId);
45 const planName = quota.plan.name || "current plan";
46 const limit = quota.plan.repoLimit;
47 return {
48 ok: false,
49 reason: `Your ${planName} is limited to ${limit} repos. Upgrade for more.`,
50 upgradeUrl: "/pricing#upgrade",
51 };
52 }
53 return { ok: true };
54 } catch {
55 // Fail-open. Billing must never break the primary request path.
56 return { ok: true };
57 }
58}
59
60/** Render-friendly "X of Y repos used (Plan)" for the /new form header. */
61export async function getRepoCreateUsage(userId: string): Promise<{
62 used: number;
63 limit: number;
64 planName: string;
65 atLimit: boolean;
66} | null> {
67 try {
68 const quota = await getUserQuota(userId);
69 // Lazy import to avoid pulling repoCountForUser into the module's
70 // hot path — it's not exported, so we re-derive via the existing
71 // helper without re-implementing the count query.
72 const atLimit = await wouldExceedRepoLimit(userId);
73 return {
74 used: atLimit ? quota.plan.repoLimit : Math.max(0, quota.plan.repoLimit - 1),
75 limit: quota.plan.repoLimit,
76 planName: quota.plan.name || "Free",
77 atLimit,
78 };
79 } catch {
80 return null;
81 }
82}