Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Initial g1t: services, event bus, intents and attempts | 1 | import { WorkerEntrypoint } from "cloudflare:workers"; |
| 2 | ||
| 3 | import { | |
| 4 | type BlobView, | |
| 5 | type Commit, | |
| 6 | type CreateRepoInput, | |
| 7 | type EventsApi, | |
| 8 | type GitAccess, | |
| 9 | type GitService, | |
| API and MCP server, Rust identity service, registration, site redesign | 10 | type ServiceBinding, |
| Initial g1t: services, event bus, intents and attempts | 11 | type NewEvent, |
| 12 | type Repo, | |
| 13 | type RepoPath, | |
| 14 | type ReposApi, | |
| 15 | type Result, | |
| 16 | type TreeView, | |
| 17 | type User, | |
| 18 | type Viewer, | |
| Email verification, password reset, and Git for AI scale positioning | 19 | UNVERIFIED, |
| Initial g1t: services, event bus, intents and attempts | 20 | fail, |
| API and MCP server, Rust identity service, registration, site redesign | 21 | identityClient, |
| Initial g1t: services, event bus, intents and attempts | 22 | isValidNamespace, |
| 23 | isValidRepoName, | |
| 24 | newId, | |
| 25 | ok, | |
| 26 | } from "@g1t/contracts"; | |
| 27 | ||
| 28 | import { ArtifactsGitStore } from "./artifacts-git-store"; | |
| 29 | import { handleGitHttp } from "./git-http"; | |
| 30 | import type { GitStore } from "./git-store"; | |
| 31 | import { | |
| 32 | RepoRegistry, | |
| 33 | canRead, | |
| 34 | canWrite, | |
| 35 | storeKey, | |
| 36 | } from "./registry"; | |
| 37 | ||
| 38 | export interface ReposEnv { | |
| 39 | DB: D1Database; | |
| 40 | ARTIFACTS: Artifacts; | |
| API and MCP server, Rust identity service, registration, site redesign | 41 | IDENTITY: ServiceBinding; |
| Initial g1t: services, event bus, intents and attempts | 42 | EVENTS: EventsApi; |
| 43 | } | |
| 44 | ||
| 45 | /** Namespace that holds every attempt's fork: `attempts/<attempt id>`. */ | |
| 46 | const ATTEMPTS_NAMESPACE = "attempts"; | |
| 47 | const MAX_TEXT_BYTES = 512 * 1024; | |
| 48 | const README = /^readme(\.(md|markdown|txt))?$/i; | |
| 49 | const SOURCE = "repos"; | |
| 50 | ||
| 51 | const NOT_FOUND = fail("not_found", "Repository not found."); | |
| 52 | ||
| 53 | /** Decoded text, or null when the blob is too large or looks binary. */ | |
| 54 | async function blobText(blob: Blob): Promise<string | null> { | |
| 55 | if (blob.size > MAX_TEXT_BYTES) return null; | |
| 56 | const bytes = new Uint8Array(await blob.arrayBuffer()); | |
| 57 | if (bytes.includes(0)) return null; | |
| 58 | return new TextDecoder().decode(bytes); | |
| 59 | } | |
| 60 | ||
| 61 | export default class ReposService | |
| 62 | extends WorkerEntrypoint<ReposEnv> | |
| 63 | implements ReposApi | |
| 64 | { | |
| 65 | private readonly registry = new RepoRegistry(this.env.DB); | |
| 66 | private readonly store: GitStore = new ArtifactsGitStore(this.env.ARTIFACTS); | |
| 67 | ||
| 68 | /** Resolves a repo the viewer may read; private repos look missing. */ | |
| 69 | private async readable(path: RepoPath, viewer: Viewer): Promise<Repo | null> { | |
| 70 | const repo = await this.registry.byPath(path); | |
| 71 | return repo && canRead(repo, viewer) ? repo : null; | |
| 72 | } | |
| 73 | ||
| 74 | async get(path: RepoPath, viewer: Viewer): Promise<Result<Repo>> { | |
| 75 | const repo = await this.readable(path, viewer); | |
| 76 | return repo ? ok(repo) : NOT_FOUND; | |
| 77 | } | |
| 78 | ||
| 79 | async getById(id: string, viewer: Viewer): Promise<Result<Repo>> { | |
| 80 | const repo = await this.registry.byId(id); | |
| 81 | return repo && canRead(repo, viewer) ? ok(repo) : NOT_FOUND; | |
| 82 | } | |
| 83 | ||
| 84 | async list( | |
| 85 | viewer: Viewer, | |
| 86 | options: { query?: string; namespace?: string } = {}, | |
| 87 | ): Promise<Repo[]> { | |
| 88 | return this.registry.list(viewer, options); | |
| 89 | } | |
| 90 | ||
| 91 | async create(owner: User, input: CreateRepoInput): Promise<Result<Repo>> { | |
| Email verification, password reset, and Git for AI scale positioning | 92 | if (!owner.verified) return UNVERIFIED; |
| Initial g1t: services, event bus, intents and attempts | 93 | const name = input.name.trim().toLowerCase(); |
| 94 | if (!isValidRepoName(name)) { | |
| 95 | return fail("invalid", "Use letters, digits, dots, hyphens and underscores only."); | |
| 96 | } | |
| 97 | if (!isValidNamespace(owner.username)) { | |
| 98 | return fail("invalid", "This account cannot own repositories."); | |
| 99 | } | |
| 100 | const path = { namespace: owner.username, name }; | |
| 101 | if (await this.registry.byPath(path)) { | |
| 102 | return fail("conflict", "You already have a repository with that name."); | |
| 103 | } | |
| 104 | const repo: Repo = { | |
| 105 | id: newId("rep"), | |
| 106 | ...path, | |
| 107 | description: input.description?.trim() || null, | |
| 108 | isPrivate: input.isPrivate ?? false, | |
| 109 | ownerId: owner.id, | |
| 110 | defaultBranch: "main", | |
| 111 | forkOf: null, | |
| 112 | createdAt: Date.now(), | |
| 113 | }; | |
| 114 | await this.store.create(storeKey(repo), { | |
| 115 | description: repo.description ?? undefined, | |
| 116 | defaultBranch: repo.defaultBranch, | |
| 117 | }); | |
| 118 | await this.registry.insert(repo); | |
| 119 | await this.publish({ | |
| 120 | type: "repo.created", | |
| 121 | source: SOURCE, | |
| 122 | repoId: repo.id, | |
| 123 | actor: owner.id, | |
| 124 | data: { | |
| 125 | repoId: repo.id, | |
| 126 | namespace: repo.namespace, | |
| 127 | name: repo.name, | |
| 128 | isPrivate: repo.isPrivate, | |
| 129 | }, | |
| 130 | }); | |
| 131 | return ok(repo); | |
| 132 | } | |
| 133 | ||
| 134 | async tree( | |
| 135 | path: RepoPath, | |
| 136 | viewer: Viewer, | |
| 137 | ref: string | null, | |
| 138 | treePath: string, | |
| 139 | ): Promise<Result<TreeView>> { | |
| 140 | const repo = await this.readable(path, viewer); | |
| 141 | if (!repo) return NOT_FOUND; | |
| 142 | const key = storeKey(repo); | |
| 143 | const resolvedRef = ref ?? repo.defaultBranch; | |
| 144 | const base = { repo, ref: resolvedRef, path: treePath }; | |
| 145 | ||
| 146 | const [head] = await this.store.log(key, resolvedRef, 1); | |
| 147 | if (!head) { | |
| 148 | // An unknown ref is an error; a repo with no commits is just empty. | |
| 149 | if (ref) return fail("not_found", "No such branch, tag or commit."); | |
| 150 | return ok({ ...base, head: null, entries: [], readme: null }); | |
| 151 | } | |
| 152 | ||
| 153 | let entries = await this.store.readTree(key, head.treeHash); | |
| 154 | for (const segment of treePath.split("/").filter(Boolean)) { | |
| 155 | const next = entries?.find( | |
| 156 | (entry) => entry.name === segment && entry.kind === "tree", | |
| 157 | ); | |
| 158 | if (!next) return fail("not_found", "No such directory."); | |
| 159 | entries = await this.store.readTree(key, next.hash); | |
| 160 | } | |
| 161 | if (!entries) return fail("not_found", "No such directory."); | |
| 162 | entries.sort( | |
| 163 | (a, b) => | |
| 164 | Number(b.kind === "tree") - Number(a.kind === "tree") || | |
| 165 | a.name.localeCompare(b.name), | |
| 166 | ); | |
| 167 | ||
| 168 | const readmeEntry = entries.find( | |
| 169 | (entry) => entry.kind === "blob" && README.test(entry.name), | |
| 170 | ); | |
| 171 | const readmeBlob = readmeEntry | |
| 172 | ? await this.store.readBlob(key, readmeEntry.hash) | |
| 173 | : null; | |
| 174 | const readme = | |
| 175 | readmeEntry && readmeBlob | |
| 176 | ? { name: readmeEntry.name, text: await blobText(readmeBlob) } | |
| 177 | : null; | |
| 178 | return ok({ ...base, head, entries, readme }); | |
| 179 | } | |
| 180 | ||
| 181 | async blob( | |
| 182 | path: RepoPath, | |
| 183 | viewer: Viewer, | |
| 184 | ref: string, | |
| 185 | filePath: string, | |
| 186 | ): Promise<Result<BlobView>> { | |
| 187 | const repo = await this.readable(path, viewer); | |
| 188 | if (!repo) return NOT_FOUND; | |
| 189 | const blob = filePath | |
| 190 | ? await this.store.readFile(storeKey(repo), ref, filePath) | |
| 191 | : null; | |
| 192 | if (!blob) return fail("not_found", "No such file."); | |
| 193 | return ok({ | |
| 194 | repo, | |
| 195 | ref, | |
| 196 | path: filePath, | |
| 197 | size: blob.size, | |
| 198 | text: await blobText(blob), | |
| 199 | }); | |
| 200 | } | |
| 201 | ||
| 202 | async log( | |
| 203 | path: RepoPath, | |
| 204 | viewer: Viewer, | |
| 205 | ref: string | null, | |
| 206 | limit: number, | |
| 207 | ): Promise<Result<Commit[]>> { | |
| 208 | const repo = await this.readable(path, viewer); | |
| 209 | if (!repo) return NOT_FOUND; | |
| 210 | return ok( | |
| 211 | await this.store.log(storeKey(repo), ref ?? repo.defaultBranch, limit), | |
| 212 | ); | |
| 213 | } | |
| 214 | ||
| 215 | async forkForAttempt( | |
| 216 | sourceId: string, | |
| 217 | attemptId: string, | |
| 218 | actor: User, | |
| 219 | ): Promise<Result<Repo>> { | |
| 220 | const source = await this.registry.byId(sourceId); | |
| 221 | if (!source || !canRead(source, actor)) return NOT_FOUND; | |
| 222 | const fork: Repo = { | |
| 223 | id: newId("rep"), | |
| 224 | namespace: ATTEMPTS_NAMESPACE, | |
| 225 | name: attemptId, | |
| 226 | description: null, | |
| 227 | // A fork is exactly as visible as the repo it came from. | |
| 228 | isPrivate: source.isPrivate, | |
| 229 | ownerId: actor.id, | |
| 230 | defaultBranch: source.defaultBranch, | |
| 231 | forkOf: source.id, | |
| 232 | createdAt: Date.now(), | |
| 233 | }; | |
| 234 | await this.store.fork(storeKey(source), storeKey(fork)); | |
| 235 | await this.registry.insert(fork); | |
| 236 | await this.publish({ | |
| 237 | type: "repo.forked", | |
| 238 | source: SOURCE, | |
| 239 | repoId: source.id, | |
| 240 | actor: actor.id, | |
| 241 | data: { repoId: fork.id, sourceRepoId: source.id, attemptId }, | |
| 242 | }); | |
| 243 | return ok(fork); | |
| 244 | } | |
| 245 | ||
| 246 | async gitAccess( | |
| 247 | path: RepoPath, | |
| 248 | viewer: Viewer, | |
| 249 | service: GitService, | |
| 250 | ): Promise<Result<GitAccess>> { | |
| 251 | const write = service === "git-receive-pack"; | |
| Email verification, password reset, and Git for AI scale positioning | 252 | if (write && viewer && !viewer.verified) return UNVERIFIED; |
| Initial g1t: services, event bus, intents and attempts | 253 | let repo = await this.registry.byPath(path); |
| 254 | if (!repo) { | |
| 255 | // Push to create, in the pusher's own namespace only. | |
| 256 | if (!write || !viewer || viewer.username !== path.namespace.toLowerCase()) { | |
| 257 | return this.denied(viewer); | |
| 258 | } | |
| 259 | const created = await this.create(viewer, { name: path.name }); | |
| 260 | if (!created.ok) return created; | |
| 261 | repo = created.value; | |
| 262 | } else if (write ? !canWrite(repo, viewer) : !canRead(repo, viewer)) { | |
| 263 | return this.denied(viewer); | |
| 264 | } | |
| 265 | return ok(await this.store.access(storeKey(repo), write ? "write" : "read")); | |
| 266 | } | |
| 267 | ||
| 268 | /** | |
| 269 | * Anonymous callers are asked to authenticate whether or not the repo | |
| 270 | * exists, so private repos cannot be told apart from missing ones. | |
| 271 | */ | |
| 272 | private denied(viewer: Viewer): Result<never> { | |
| 273 | return viewer | |
| 274 | ? NOT_FOUND | |
| 275 | : fail("unauthenticated", "Authentication required."); | |
| 276 | } | |
| 277 | ||
| 278 | private async publish(event: NewEvent): Promise<void> { | |
| 279 | await this.env.EVENTS.publish([event]); | |
| 280 | } | |
| 281 | ||
| 282 | /** Git over HTTPS. */ | |
| 283 | async fetch(request: Request): Promise<Response> { | |
| API and MCP server, Rust identity service, registration, site redesign | 284 | const response = await handleGitHttp(request, identityClient(this.env.IDENTITY), this, (path) => |
| Initial g1t: services, event bus, intents and attempts | 285 | this.ctx.waitUntil(this.pushed(path)), |
| 286 | ); | |
| 287 | return response ?? new Response("Not found\n", { status: 404 }); | |
| 288 | } | |
| 289 | ||
| 290 | /** | |
| 291 | * Publishes `git.push` after a push has gone through the git front end. | |
| 292 | * Artifacts' own push subscriptions are per repository, which does not fit | |
| 293 | * a repo per attempt, so the front end reports pushes itself. | |
| 294 | */ | |
| 295 | private async pushed(path: RepoPath): Promise<void> { | |
| 296 | const repo = await this.registry.byPath(path); | |
| 297 | if (!repo) return; | |
| 298 | const [head] = await this.store.log(storeKey(repo), repo.defaultBranch, 1); | |
| 299 | if (!head) return; | |
| 300 | await this.publish({ | |
| 301 | type: "git.push", | |
| 302 | source: SOURCE, | |
| 303 | repoId: repo.id, | |
| 304 | actor: null, | |
| 305 | data: { | |
| 306 | repoId: repo.id, | |
| 307 | ref: `refs/heads/${repo.defaultBranch}`, | |
| 308 | after: head.hash, | |
| 309 | }, | |
| 310 | }); | |
| 311 | } | |
| 312 | } |