| 1 | // Stand-ins for identity, repos and events, so the packages service runs |
| 2 | // on its own with `wrangler dev` (see packages.jsonc beside it). |
| 3 | // |
| 4 | // - Identity knows one person, `dev`, an owner of the workspace `acme`, |
| 5 | // whose token is DEV_TOKEN, a member, `mo` (MEMBER_TOKEN), and one |
| 6 | // outsider, `bo` (OUTSIDER_TOKEN). |
| 7 | // - Repos knows two repositories of acme: `web` (private) and `site` |
| 8 | // (public). |
| 9 | // - Repos also keeps a small git store for `lib` (rep_lib), a private PHP |
| 10 | // library: tags v1.0.0 and v1.1.0, branches main and feature, for the |
| 11 | // Composer registry (refs, raw_file, raw_blobs, list_files, all_ids). |
| 12 | // Signing in with the password `g1t_push` (any request with Basic |
| 13 | // credentials) tags v1.2.0 on a new commit, as a push would; then call |
| 14 | // `sync_composer` as the git.push event would. |
| 15 | // - Events takes every event and audit entry and logs them. |
| 16 | // - Billing says acme is free, with FREE_PRIVATE_BYTES of private package |
| 17 | // storage and 10 GB public. |
| 18 | |
| 19 | const REPOS = { |
| 20 | web: { isPrivate: true }, |
| 21 | site: { isPrivate: false }, |
| 22 | lib: { isPrivate: true }, |
| 23 | }; |
| 24 | |
| 25 | // The library's commits: each its files. Hashes are made up, 40 hex each. |
| 26 | const composerJson = JSON.stringify( |
| 27 | { |
| 28 | name: "acme/lib", |
| 29 | description: "Greets people", |
| 30 | type: "library", |
| 31 | license: "MIT", |
| 32 | require: { php: ">=8.1" }, |
| 33 | autoload: { "psr-4": { "Acme\\Lib\\": "src/" } }, |
| 34 | "require-dev": { "phpunit/phpunit": "^11" }, |
| 35 | }, |
| 36 | null, |
| 37 | 2, |
| 38 | ); |
| 39 | const greeter = (body) => `<?php\n\nnamespace Acme\\Lib;\n\nfinal class Greeter\n{\n${body}\n}\n`; |
| 40 | const COMMITS = { |
| 41 | ["1".repeat(40)]: { |
| 42 | "composer.json": composerJson, |
| 43 | "src/Greeter.php": greeter(' public function hello(string $name): string { return "Hello, $name!"; }'), |
| 44 | "README.md": "# acme/lib\n\nGreets people.\n", |
| 45 | ".gitattributes": "/tests export-ignore\n", |
| 46 | "tests/GreeterTest.php": "<?php // not in the archive\n", |
| 47 | }, |
| 48 | ["2".repeat(40)]: { |
| 49 | "composer.json": composerJson, |
| 50 | "src/Greeter.php": greeter( |
| 51 | ' public function hello(string $name): string { return "Hello, $name!"; }\n public function bye(string $name): string { return "Bye, $name."; }', |
| 52 | ), |
| 53 | "README.md": "# acme/lib\n\nGreets people, and says goodbye.\n", |
| 54 | ".gitattributes": "/tests export-ignore\n", |
| 55 | "tests/GreeterTest.php": "<?php // not in the archive\n", |
| 56 | }, |
| 57 | ["3".repeat(40)]: { "composer.json": composerJson, "src/Greeter.php": greeter(" // a feature in progress") }, |
| 58 | ["4".repeat(40)]: { |
| 59 | "composer.json": composerJson, |
| 60 | "src/Greeter.php": greeter( |
| 61 | ' public function hello(string $name): string { return "Hi, $name!"; }\n public function bye(string $name): string { return "Bye, $name."; }', |
| 62 | ), |
| 63 | "README.md": "# acme/lib\n\nv1.2.\n", |
| 64 | }, |
| 65 | }; |
| 66 | const REFS = [ |
| 67 | { name: "refs/heads/main", commit: "2".repeat(40) }, |
| 68 | { name: "refs/heads/feature", commit: "3".repeat(40) }, |
| 69 | { name: "refs/tags/v1.0.0", commit: "1".repeat(40) }, |
| 70 | { name: "refs/tags/v1.1.0", commit: "2".repeat(40) }, |
| 71 | ]; |
| 72 | const blobHash = (commit, path) => `${commit.slice(0, 8)}${Buffer.from(path).toString("hex")}`; |
| 73 | const base64 = (text) => Buffer.from(text).toString("base64"); |
| 74 | function blob(hash) { |
| 75 | for (const [commit, files] of Object.entries(COMMITS)) { |
| 76 | for (const [path, text] of Object.entries(files)) if (blobHash(commit, path) === hash) return text; |
| 77 | } |
| 78 | return null; |
| 79 | } |
| 80 | const libRepo = () => ({ |
| 81 | id: "rep_lib", |
| 82 | namespace: "acme", |
| 83 | name: "lib", |
| 84 | description: null, |
| 85 | isPrivate: true, |
| 86 | ownerId: "usr_dev", |
| 87 | defaultBranch: "main", |
| 88 | forkOf: null, |
| 89 | createdAt: "2026-10-01T00:00:00.000Z", |
| 90 | }); |
| 91 | |
| 92 | function push() { |
| 93 | if (!REFS.some((r) => r.name === "refs/tags/v1.2.0")) { |
| 94 | REFS.push({ name: "refs/tags/v1.2.0", commit: "4".repeat(40) }); |
| 95 | REFS[0].commit = "4".repeat(40); |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | function user(env, secret) { |
| 100 | if (secret === "g1t_push") { |
| 101 | push(); |
| 102 | return null; |
| 103 | } |
| 104 | if (secret === env.DEV_TOKEN) { |
| 105 | return { |
| 106 | id: "usr_dev", |
| 107 | username: "dev", |
| 108 | kind: "user", |
| 109 | verified: true, |
| 110 | workspaces: [{ slug: "acme", role: "owner" }], |
| 111 | token: { token_id: "tok_dev" }, |
| 112 | }; |
| 113 | } |
| 114 | if (secret === env.MEMBER_TOKEN) { |
| 115 | return { |
| 116 | id: "usr_mo", |
| 117 | username: "mo", |
| 118 | kind: "user", |
| 119 | verified: true, |
| 120 | workspaces: [{ slug: "acme", role: "member" }], |
| 121 | token: { token_id: "tok_mo" }, |
| 122 | }; |
| 123 | } |
| 124 | if (secret === env.OUTSIDER_TOKEN) { |
| 125 | return { id: "usr_bo", username: "bo", kind: "user", verified: true, workspaces: [], token: { token_id: "tok_bo" } }; |
| 126 | } |
| 127 | return null; |
| 128 | } |
| 129 | |
| 130 | export default { |
| 131 | async fetch(request, env) { |
| 132 | const method = new URL(request.url).pathname.replace(/^\/rpc\//, ""); |
| 133 | const args = await request.json().catch(() => ({})); |
| 134 | const json = (value) => Response.json(value); |
| 135 | |
| 136 | switch (method) { |
| 137 | case "refs": |
| 138 | return json(args.repoId === "rep_lib" ? { repo: libRepo(), refs: REFS } : null); |
| 139 | case "raw_file": { |
| 140 | const commit = REFS.find((r) => r.name === args.ref || r.name === `refs/heads/${args.ref}`)?.commit ?? args.ref; |
| 141 | const text = args.repoId === "rep_lib" ? COMMITS[commit]?.[args.path] : undefined; |
| 142 | return json(text === undefined ? null : { size: text.length, data: base64(text) }); |
| 143 | } |
| 144 | case "list_files": { |
| 145 | const files = args.repoId === "rep_lib" ? COMMITS[args.ref] : undefined; |
| 146 | return json({ |
| 147 | commit: files ? args.ref : null, |
| 148 | files: Object.keys(files ?? {}).map((path) => ({ path, hash: blobHash(args.ref, path) })), |
| 149 | truncated: false, |
| 150 | }); |
| 151 | } |
| 152 | case "raw_blobs": |
| 153 | return json( |
| 154 | (args.hashes ?? []).map((hash) => { |
| 155 | const text = blob(hash); |
| 156 | return { hash, size: text?.length ?? 0, data: text === null ? null : base64(text) }; |
| 157 | }), |
| 158 | ); |
| 159 | case "all_ids": |
| 160 | return json(args.after ? { ids: [], next: null } : { ids: ["rep_web", "rep_lib"], next: null }); |
| 161 | case "user_for_git_credentials": |
| 162 | return json(user(env, args.secret)); |
| 163 | case "get": { |
| 164 | const repo = args.path?.namespace === "acme" ? REPOS[args.path.name] : undefined; |
| 165 | if (!repo) return json({ ok: false, error: { code: "not_found", message: "Repository not found." } }); |
| 166 | return json({ |
| 167 | ok: true, |
| 168 | value: { |
| 169 | id: `rep_${args.path.name}`, |
| 170 | namespace: "acme", |
| 171 | name: args.path.name, |
| 172 | description: null, |
| 173 | isPrivate: repo.isPrivate, |
| 174 | ownerId: "usr_dev", |
| 175 | defaultBranch: "main", |
| 176 | forkOf: null, |
| 177 | createdAt: "2026-10-01T00:00:00.000Z", |
| 178 | }, |
| 179 | }); |
| 180 | } |
| 181 | case "publish": |
| 182 | for (const event of args.events ?? []) console.log(`event ${event.type} ${JSON.stringify(event.data)}`); |
| 183 | return json(null); |
| 184 | case "entitlements": |
| 185 | return json({ |
| 186 | workspace: args.workspace, |
| 187 | plan: "free", |
| 188 | hasPlan: false, |
| 189 | packagePublicFreeBytes: 10_000_000_000, |
| 190 | packagePrivateFreeBytes: Number(env.FREE_PRIVATE_BYTES), |
| 191 | }); |
| 192 | case "audit_record": |
| 193 | for (const entry of args.entries ?? []) console.log(`audit ${entry.action} by ${entry.actor} on ${entry.path}`); |
| 194 | return json((args.entries ?? []).length); |
| 195 | default: |
| 196 | return new Response(`stub: no method ${method}`, { status: 404 }); |
| 197 | } |
| 198 | }, |
| 199 | }; |