CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | /**
* Identify the pusher on a git-receive-pack request.
*
* Smart-HTTP push doesn't ride the cookie/session middleware (git CLI
* doesn't keep cookies). It sends `Authorization: Basic <b64(user:secret)>`
* or `Authorization: Bearer <token>`. We accept three secret shapes:
*
* - `glc_*` — personal access token (Block C2)
* - `glct_*` — OAuth access token (Block B6)
* - `ghi_*` — installation token for an app-bot (Block H2). The
* token resolves to the synthetic users row created by
* `createApp(...)` (`<slug>[bot]` username). Legacy bots
* created before the synthetic-user back-fill landed
* fail soft and resolve as anonymous.
*
* Best-effort only: returns null (anonymous) on any failure. The caller
* must decide what anonymous can do (current policy: anonymous can
* push to non-protected refs; protected refs always require auth).
*/
import { eq } from "drizzle-orm";
import { db } from "../db";
import {
users,
apiTokens,
oauthAccessTokens,
appInstallTokens,
appInstallations,
appBots,
} from "../db/schema";
import { sha256Hex } from "./oauth";
export type ResolvedPusher = {
userId: string;
username: string;
source: "pat" | "oauth" | "install_token";
};
/** Decode a `Basic` auth header → `{user, secret}` or null on malformed. */
export function decodeBasicAuth(
header: string | null | undefined
): { user: string; secret: string } | null {
if (!header) return null;
const m = /^\s*Basic\s+(.+)$/i.exec(header);
if (!m) return null;
let decoded: string;
try {
decoded = Buffer.from(m[1].trim(), "base64").toString("utf8");
} catch {
return null;
}
const colon = decoded.indexOf(":");
if (colon < 0) return null;
return {
user: decoded.slice(0, colon),
secret: decoded.slice(colon + 1),
};
}
/** Decode a `Bearer` auth header → token string or null. */
export function decodeBearerAuth(
header: string | null | undefined
): string | null {
if (!header) return null;
const m = /^\s*Bearer\s+(.+)$/i.exec(header);
if (!m) return null;
const tok = m[1].trim();
return tok || null;
}
async function resolveByPat(token: string): Promise<ResolvedPusher | null> {
if (!token.startsWith("glc_")) return null;
try {
const hash = await sha256Hex(token);
const [row] = await db
.select()
.from(apiTokens)
.where(eq(apiTokens.tokenHash, hash))
.limit(1);
if (!row) return null;
if (row.expiresAt && new Date(row.expiresAt) < new Date()) return null;
const [u] = await db
.select({ id: users.id, username: users.username })
.from(users)
.where(eq(users.id, row.userId))
.limit(1);
if (!u) return null;
return { userId: u.id, username: u.username, source: "pat" };
} catch {
return null;
}
}
async function resolveByOauth(token: string): Promise<ResolvedPusher | null> {
if (!token.startsWith("glct_")) return null;
try {
const hash = await sha256Hex(token);
const [row] = await db
.select()
.from(oauthAccessTokens)
.where(eq(oauthAccessTokens.accessTokenHash, hash))
.limit(1);
if (!row) return null;
if (row.revokedAt) return null;
if (new Date(row.expiresAt) < new Date()) return null;
const [u] = await db
.select({ id: users.id, username: users.username })
.from(users)
.where(eq(users.id, row.userId))
.limit(1);
if (!u) return null;
return { userId: u.id, username: u.username, source: "oauth" };
} catch {
return null;
}
}
async function resolveByInstallToken(
token: string
): Promise<ResolvedPusher | null> {
if (!token.startsWith("ghi_")) return null;
try {
const hash = await sha256Hex(token);
// Look up the install token + its installation + the app's bot
// username in one round-trip via the join shape.
const [row] = await db
.select({
tokenId: appInstallTokens.id,
revokedAt: appInstallTokens.revokedAt,
expiresAt: appInstallTokens.expiresAt,
suspendedAt: appInstallations.suspendedAt,
uninstalledAt: appInstallations.uninstalledAt,
botUsername: appBots.username,
})
.from(appInstallTokens)
.innerJoin(
appInstallations,
eq(appInstallTokens.installationId, appInstallations.id)
)
.innerJoin(appBots, eq(appBots.appId, appInstallations.appId))
.where(eq(appInstallTokens.tokenHash, hash))
.limit(1);
if (!row) return null;
if (row.revokedAt) return null;
if (row.expiresAt && new Date(row.expiresAt) < new Date()) return null;
if (row.suspendedAt) return null;
if (row.uninstalledAt) return null;
// Find the synthetic users row that createApp inserts for the bot.
// Legacy bots (created before the back-fill landed) won't have one;
// those resolve as anonymous, which fails closed on every protected
// ref but doesn't break public-repo writes.
const [u] = await db
.select({ id: users.id, username: users.username })
.from(users)
.where(eq(users.username, row.botUsername))
.limit(1);
if (!u) return null;
return { userId: u.id, username: u.username, source: "install_token" };
} catch {
return null;
}
}
/**
* Resolve the pusher from an Authorization header. Tries Bearer (PAT,
* OAuth, or install token) then Basic (where the secret in the password
* field can also be any of the three). git CLI sends
* `credential.helper` output as username + password; users typically
* paste the token as the password.
*/
export async function resolvePusher(
authHeader: string | null | undefined
): Promise<ResolvedPusher | null> {
if (!authHeader) return null;
const bearer = decodeBearerAuth(authHeader);
if (bearer) {
return (
(await resolveByPat(bearer)) ||
(await resolveByOauth(bearer)) ||
(await resolveByInstallToken(bearer))
);
}
const basic = decodeBasicAuth(authHeader);
if (basic) {
const secret = basic.secret;
return (
(await resolveByPat(secret)) ||
(await resolveByOauth(secret)) ||
(await resolveByInstallToken(secret))
);
}
return null;
}
|