@volter/twin-github 0.1.2 → 2.0.1

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.
Files changed (215) hide show
  1. package/README.md +88 -36
  2. package/client/github-mirror.css +277 -319
  3. package/client/github-mirror.d.ts +418 -0
  4. package/client/github-mirror.js +485 -0
  5. package/client/github-mirror.tsx +153 -357
  6. package/client/pulls-rest.ts +159 -0
  7. package/client/pulls-workspace.tsx +841 -0
  8. package/dist/client/github-mirror.bundle.js +239 -0
  9. package/dist/client/github-mirror.css +916 -0
  10. package/dist/client/github-mirror.d.ts +418 -0
  11. package/dist/client/github-mirror.js +485 -0
  12. package/dist/client/github-mirror.tsx +1315 -0
  13. package/dist/client/pulls-rest.d.ts +42 -0
  14. package/dist/client/pulls-rest.js +140 -0
  15. package/dist/client/pulls-rest.ts +159 -0
  16. package/dist/client/pulls-workspace.bundle.js +22 -0
  17. package/dist/client/pulls-workspace.d.ts +114 -0
  18. package/dist/client/pulls-workspace.js +418 -0
  19. package/dist/client/pulls-workspace.tsx +841 -0
  20. package/dist/src/cli.d.ts +2 -0
  21. package/dist/src/cli.js +40 -0
  22. package/dist/src/generated/graphql-sdl.gen.json +1 -0
  23. package/dist/src/generated/graphql.gen.json +1 -0
  24. package/dist/src/generated/surface.gen.json +1 -0
  25. package/dist/src/generated/ui.gen.json +1 -0
  26. package/dist/src/github-budget.d.ts +69 -0
  27. package/dist/src/github-budget.js +172 -0
  28. package/dist/src/github-capabilities.d.ts +5 -0
  29. package/dist/src/github-capabilities.js +4468 -0
  30. package/dist/src/github-conformance.d.ts +43 -0
  31. package/dist/src/github-conformance.js +76 -0
  32. package/dist/src/github-connector.d.ts +307 -0
  33. package/dist/src/github-connector.js +1398 -0
  34. package/dist/src/github-events.d.ts +41 -0
  35. package/dist/src/github-events.js +232 -0
  36. package/dist/src/github-git-http.d.ts +49 -0
  37. package/dist/src/github-git-http.js +185 -0
  38. package/dist/src/github-git-plane.d.ts +114 -0
  39. package/dist/src/github-git-plane.js +407 -0
  40. package/dist/src/github-mirror-state.d.ts +2 -0
  41. package/dist/src/github-mirror-state.js +335 -0
  42. package/dist/src/github-mirror-ui.d.ts +20 -0
  43. package/dist/src/github-mirror-ui.js +101 -0
  44. package/dist/src/github-server.d.ts +14 -0
  45. package/dist/src/github-server.js +240 -0
  46. package/dist/src/github-shared.d.ts +12 -0
  47. package/dist/src/github-shared.js +21 -0
  48. package/dist/src/github-twin.d.ts +1411 -0
  49. package/dist/src/github-twin.js +4084 -0
  50. package/dist/src/github-ui-conformance.d.ts +4 -0
  51. package/dist/src/github-ui-conformance.js +105 -0
  52. package/dist/src/github-ui-structure.d.ts +18 -0
  53. package/dist/src/github-ui-structure.js +251 -0
  54. package/dist/src/graphql-wire.d.ts +22 -0
  55. package/dist/src/graphql-wire.js +89 -0
  56. package/dist/src/index.d.ts +17 -0
  57. package/dist/src/index.js +104 -0
  58. package/dist/src/manifest.d.ts +8 -0
  59. package/dist/src/manifest.js +597 -0
  60. package/dist/src/npm-registry.d.ts +7 -0
  61. package/dist/src/npm-registry.js +47 -0
  62. package/dist/src/screens/app-installation.d.ts +3 -0
  63. package/dist/src/screens/app-installation.js +166 -0
  64. package/dist/src/screens/app-manifest.d.ts +3 -0
  65. package/dist/src/screens/app-manifest.js +81 -0
  66. package/dist/src/screens/oauth.d.ts +15 -0
  67. package/dist/src/screens/oauth.js +257 -0
  68. package/dist/src/screens/session.d.ts +8 -0
  69. package/dist/src/screens/session.js +170 -0
  70. package/dist/src/semantics/actions.d.ts +2 -0
  71. package/dist/src/semantics/actions.js +413 -0
  72. package/dist/src/semantics/activity.d.ts +4 -0
  73. package/dist/src/semantics/activity.js +161 -0
  74. package/dist/src/semantics/apps.d.ts +2 -0
  75. package/dist/src/semantics/apps.js +144 -0
  76. package/dist/src/semantics/branches.d.ts +2 -0
  77. package/dist/src/semantics/branches.js +136 -0
  78. package/dist/src/semantics/checks.d.ts +2 -0
  79. package/dist/src/semantics/checks.js +176 -0
  80. package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
  81. package/dist/src/semantics/code-scanning-upload.js +97 -0
  82. package/dist/src/semantics/codespaces.d.ts +2 -0
  83. package/dist/src/semantics/codespaces.js +58 -0
  84. package/dist/src/semantics/commits.d.ts +2 -0
  85. package/dist/src/semantics/commits.js +109 -0
  86. package/dist/src/semantics/contents.d.ts +2 -0
  87. package/dist/src/semantics/contents.js +131 -0
  88. package/dist/src/semantics/deployments.d.ts +2 -0
  89. package/dist/src/semantics/deployments.js +127 -0
  90. package/dist/src/semantics/gists.d.ts +2 -0
  91. package/dist/src/semantics/gists.js +86 -0
  92. package/dist/src/semantics/git.d.ts +2 -0
  93. package/dist/src/semantics/git.js +235 -0
  94. package/dist/src/semantics/graphql.d.ts +7 -0
  95. package/dist/src/semantics/graphql.js +512 -0
  96. package/dist/src/semantics/index.d.ts +5 -0
  97. package/dist/src/semantics/index.js +62 -0
  98. package/dist/src/semantics/issues.d.ts +2 -0
  99. package/dist/src/semantics/issues.js +456 -0
  100. package/dist/src/semantics/keys.d.ts +2 -0
  101. package/dist/src/semantics/keys.js +66 -0
  102. package/dist/src/semantics/labels.d.ts +2 -0
  103. package/dist/src/semantics/labels.js +100 -0
  104. package/dist/src/semantics/meta.d.ts +12 -0
  105. package/dist/src/semantics/meta.js +144 -0
  106. package/dist/src/semantics/notifications.d.ts +4 -0
  107. package/dist/src/semantics/notifications.js +62 -0
  108. package/dist/src/semantics/orgs.d.ts +2 -0
  109. package/dist/src/semantics/orgs.js +420 -0
  110. package/dist/src/semantics/packages.d.ts +2 -0
  111. package/dist/src/semantics/packages.js +55 -0
  112. package/dist/src/semantics/pages.d.ts +2 -0
  113. package/dist/src/semantics/pages.js +176 -0
  114. package/dist/src/semantics/projects.d.ts +2 -0
  115. package/dist/src/semantics/projects.js +239 -0
  116. package/dist/src/semantics/pulls.d.ts +2 -0
  117. package/dist/src/semantics/pulls.js +462 -0
  118. package/dist/src/semantics/push-reactions.d.ts +101 -0
  119. package/dist/src/semantics/push-reactions.js +513 -0
  120. package/dist/src/semantics/releases.d.ts +19 -0
  121. package/dist/src/semantics/releases.js +231 -0
  122. package/dist/src/semantics/repo-invitations.d.ts +2 -0
  123. package/dist/src/semantics/repo-invitations.js +98 -0
  124. package/dist/src/semantics/repos.d.ts +17 -0
  125. package/dist/src/semantics/repos.js +390 -0
  126. package/dist/src/semantics/rulesets.d.ts +2 -0
  127. package/dist/src/semantics/rulesets.js +111 -0
  128. package/dist/src/semantics/search.d.ts +2 -0
  129. package/dist/src/semantics/search.js +84 -0
  130. package/dist/src/semantics/security.d.ts +2 -0
  131. package/dist/src/semantics/security.js +177 -0
  132. package/dist/src/semantics/shared.d.ts +61 -0
  133. package/dist/src/semantics/shared.js +151 -0
  134. package/dist/src/semantics/users.d.ts +2 -0
  135. package/dist/src/semantics/users.js +255 -0
  136. package/dist/src/semantics/webhooks.d.ts +2 -0
  137. package/dist/src/semantics/webhooks.js +107 -0
  138. package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
  139. package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
  140. package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
  141. package/dist/test-fixtures/github-comment-schema.json +702 -0
  142. package/dist/test-fixtures/github-known-deviations.json +94 -0
  143. package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
  144. package/dist/test-fixtures/github-openapi-operations.json +362 -0
  145. package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
  146. package/dist/test-fixtures/github-pull-schema.json +3601 -0
  147. package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
  148. package/dist/test-fixtures/github-review-schema.json +236 -0
  149. package/package.json +20 -11
  150. package/src/cli.ts +7 -6
  151. package/src/generated/graphql-sdl.gen.json +1 -0
  152. package/src/generated/graphql.gen.json +1 -0
  153. package/src/generated/surface.gen.json +1 -0
  154. package/src/generated/ui.gen.json +1 -0
  155. package/src/github-a11y-snapshot.uitest.ts +5 -5
  156. package/src/github-budget.ts +4 -4
  157. package/src/github-capabilities.ts +1993 -556
  158. package/src/github-conformance.ts +12 -7
  159. package/src/github-connector.ts +108 -96
  160. package/src/github-events.ts +225 -95
  161. package/src/github-git-http.ts +48 -84
  162. package/src/github-git-plane.ts +247 -385
  163. package/src/github-journey.uitest.ts +28 -44
  164. package/src/github-mirror-state.ts +23 -61
  165. package/src/github-mirror-ui.ts +16 -10
  166. package/src/github-server.ts +158 -60
  167. package/src/github-shared.ts +1 -1
  168. package/src/github-twin.ts +1525 -4494
  169. package/src/github-ui-conformance.ts +7 -8
  170. package/src/github-ui-structure.ts +19 -18
  171. package/src/graphql-wire.ts +115 -0
  172. package/src/index.ts +50 -7
  173. package/src/manifest.ts +605 -0
  174. package/src/npm-registry.ts +43 -0
  175. package/src/screens/app-installation.tsx +217 -0
  176. package/src/screens/app-manifest.tsx +97 -0
  177. package/src/screens/oauth.tsx +258 -0
  178. package/src/screens/session.tsx +193 -0
  179. package/src/semantics/actions.ts +395 -0
  180. package/src/semantics/activity.ts +171 -0
  181. package/src/semantics/apps.ts +133 -0
  182. package/src/semantics/branches.ts +133 -0
  183. package/src/semantics/checks.ts +163 -0
  184. package/src/semantics/code-scanning-upload.ts +92 -0
  185. package/src/semantics/codespaces.ts +58 -0
  186. package/src/semantics/commits.ts +109 -0
  187. package/src/semantics/contents.ts +112 -0
  188. package/src/semantics/deployments.ts +116 -0
  189. package/src/semantics/gists.ts +85 -0
  190. package/src/semantics/git.ts +226 -0
  191. package/src/semantics/graphql.ts +505 -0
  192. package/src/semantics/index.ts +68 -0
  193. package/src/semantics/issues.ts +434 -0
  194. package/src/semantics/keys.ts +66 -0
  195. package/src/semantics/labels.ts +91 -0
  196. package/src/semantics/meta.ts +139 -0
  197. package/src/semantics/notifications.ts +58 -0
  198. package/src/semantics/orgs.ts +383 -0
  199. package/src/semantics/packages.ts +59 -0
  200. package/src/semantics/pages.ts +154 -0
  201. package/src/semantics/projects.ts +246 -0
  202. package/src/semantics/pulls.ts +421 -0
  203. package/src/semantics/push-reactions.ts +471 -0
  204. package/src/semantics/releases.ts +213 -0
  205. package/src/semantics/repo-invitations.ts +112 -0
  206. package/src/semantics/repos.ts +373 -0
  207. package/src/semantics/rulesets.ts +107 -0
  208. package/src/semantics/search.ts +83 -0
  209. package/src/semantics/security.ts +153 -0
  210. package/src/semantics/shared.ts +181 -0
  211. package/src/semantics/users.ts +252 -0
  212. package/src/semantics/webhooks.ts +101 -0
  213. package/test-fixtures/github-known-deviations.json +7 -8
  214. package/test-fixtures/github-openapi-operations.json +46 -227
  215. package/src/github-graphql.ts +0 -398
@@ -0,0 +1,240 @@
1
+ // GitHub twin HTTP server — serve the real GitHub REST API over HTTP so the real
2
+ // `@octokit/rest` client (pointed at this baseUrl) works unmodified (R1). Reads
3
+ // fold observed evidence + action overlay; writes (simulator/fork) become local
4
+ // actions and fire GitHub webhooks (R17). Mirror mode rejects writes (R4).
5
+ // The git SMART-HTTP protocol (clone/fetch/push by an unmodified `git` CLI) is served
6
+ // FIRST — its routes (/:owner/:repo/info/refs, git-upload-pack, git-receive-pack) are
7
+ // binary pkt-line streams over the kernel's git library (github-git-http.ts); a push is
8
+ // announced as an API commit's is (semantics/shared.ts announce).
9
+ import { bindSemantics, compileSurface, coreFor, createDerivedFetch, crossCutting, matchOperation, semanticsContext, serveHttp, vendorError } from '@volter/world-core';
10
+ import surface from './generated/surface.gen.json' with { type: 'json' };
11
+ import { manifest, spanning } from "./manifest.js";
12
+ import { githubSemantics } from "./semantics/index.js";
13
+ import { githubGraphql } from "./semantics/graphql.js";
14
+ import { createGraphqlWire } from "./graphql-wire.js";
15
+ import { githubState, handleGithubDoor, revokedToken } from "./github-twin.js";
16
+ import { announce } from "./semantics/shared.js";
17
+ import { answerVendorErrors, RefusedWriteError } from '@volter/world-core';
18
+ import { worldNow, statefulTwinManifest } from '@volter/world-core';
19
+ import { createHash } from 'node:crypto';
20
+ import { githubOAuthFlow } from "./screens/oauth.js";
21
+ import { githubSessionFlow } from "./screens/session.js";
22
+ import { githubNpmRegistry } from "./npm-registry.js";
23
+ import { githubAppInstallationFlow } from "./screens/app-installation.js";
24
+ import { githubAppManifestFlow } from "./screens/app-manifest.js";
25
+ import { assetUploads } from "./semantics/releases.js";
26
+ import { handleGithubGitSmartHttp, parseGitSmartHttpPath } from "./github-git-http.js";
27
+ import { githubMirrorState } from "./github-mirror-state.js";
28
+ import { GITHUB_MIRROR_STORE } from "./github-shared.js";
29
+ // THE STORE DOOR (runtime contract R5c; mirror purity R3): named, deterministic projections
30
+ // over stored state, served at `GET /twin/store/<name>` and listed in the manifest as
31
+ // `stores` — the kernel adapter's door (`createTwinFetchFromHandler`), carried here by the
32
+ // pack's own fetch because github's wire exceeds the handler shape (the git smart-HTTP
33
+ // plane's binary pkt-line streams). GitHub's ONE store is `mirror`: everything the mirror
34
+ // console renders, projected from the folded state (`github-mirror-state.ts`), so the
35
+ // mirror reads twin state ONLY over this door — never by importing the handler. The
36
+ // door's shapes — the manifest's `stores` + `doors.store`, the 404 body for an unknown
37
+ // name — are the kernel door's, byte for byte.
38
+ const STORE_DOOR = '/twin/store/';
39
+ /**
40
+ * The whole GitHub serve path as a plain `(Request) => Response` — the `/twin` manifest, the
41
+ * git smart-HTTP bridge, REST reads and the write path with its webhook emission. NOTHING
42
+ * about it is port-bound, and `createGithubTwinServer` is one line of `Bun.serve` around it,
43
+ * so the standalone (R1) and any in-process lane execute the SAME bytes of serving code.
44
+ *
45
+ * The GIT PLANE is the KERNEL'S GIT LIBRARY (docs/contributing/architecture.md#the-engine-slot):
46
+ * one content-addressed object store per world over the blob seam, refs as world state, smart HTTP
47
+ * in pure JS — Bun and workerd alike, no `git` binary. Every plane touch on the REST read path is
48
+ * still existence-guarded (`hasBareRepo`), so a world with no repositories answers from the folded
49
+ * state alone, which is exactly what a pull-fed remote mirror serves (R14: `syncGithubFromRemote` folds PRs,
50
+ * reviews, comments and issues; none of that needs the plane).
51
+ */
52
+ /** A release asset's upload: its raw bytes and media type go to the upload operation beside a request with no body,
53
+ * since the operation reads them as the file (semantics/releases.ts). */
54
+ async function uploadingAsset(request, inner) {
55
+ const bytes = new Uint8Array(await request.arrayBuffer());
56
+ const type = request.headers.get('content-type');
57
+ const headers = new Headers(request.headers);
58
+ headers.delete('content-type');
59
+ headers.delete('content-length');
60
+ return assetUploads.run({ bytes, type }, () => inner(new Request(request.url, { method: request.method, headers })));
61
+ }
62
+ /** GitHub's refusal of a request body that is not JSON. */
63
+ function malformedJson() {
64
+ return Response.json({ message: 'invalid JSON body' }, { status: 400 });
65
+ }
66
+ export function createGithubTwinFetch(options) {
67
+ const readOnly = options.readOnly ?? false;
68
+ const stores = { [GITHUB_MIRROR_STORE]: () => githubMirrorState(options.root) };
69
+ const storeNames = Object.keys(stores).sort();
70
+ const now = options.clock ?? worldNow;
71
+ const scope = { ...(options.root !== undefined ? { root: options.root } : {}), ...(options.clock ? { clock: options.clock } : {}) };
72
+ // The twin's doors, in front of GitHub's API: discovery, the store door, the git plane and the stand-ins for acts
73
+ // GitHub does in its web UI (github-twin.ts handleGithubDoor).
74
+ const doors = async (request) => {
75
+ const url = new URL(request.url);
76
+ const cleanPath = url.pathname.replace(/\/+$/, '') || '/';
77
+ // GET /twin — the discovery manifest (education inside the twin). It EDUCATES the
78
+ // kernel door's way: the store names + the door's path ride on the manifest.
79
+ if (request.method === 'GET' && cleanPath === '/twin') {
80
+ const manifest = statefulTwinManifest({ vendor: 'github', twinOf: 'the GitHub REST pull-request API', stores: 'repositories, pull requests, reviews and comments' });
81
+ const doors = manifest.doors !== null && typeof manifest.doors === 'object' ? manifest.doors : {};
82
+ return Response.json({ ...manifest, stores: storeNames, doors: { ...doors, store: 'GET /twin/store/<name>' } });
83
+ }
84
+ // GET /twin/store/<name> — the store door (see STORE_DOOR above).
85
+ if (request.method === 'GET' && cleanPath.startsWith(STORE_DOOR)) {
86
+ const name = cleanPath.slice(STORE_DOOR.length);
87
+ const store = stores[name];
88
+ if (store === undefined)
89
+ return Response.json({ error: 'unknown store', store: name, stores: storeNames }, { status: 404 });
90
+ return Response.json(store());
91
+ }
92
+ // git smart-HTTP (binary protocol, not JSON). Route-parse BEFORE touching the body so ordinary REST POSTs are
93
+ // never double-buffered.
94
+ if ((request.method === 'GET' || request.method === 'POST') && parseGitSmartHttpPath(request.method, url.pathname)) {
95
+ const occurredAt = now();
96
+ const git = await handleGithubGitSmartHttp({
97
+ method: request.method,
98
+ pathname: url.pathname,
99
+ query: url.searchParams,
100
+ ...(request.method === 'POST' ? { body: new Uint8Array(await request.arrayBuffer()) } : {}),
101
+ ...(options.root !== undefined ? { root: options.root } : {}),
102
+ readOnly,
103
+ contentEncoding: request.headers.get('content-encoding') ?? '',
104
+ gitProtocol: request.headers.get('git-protocol') ?? '',
105
+ occurredAt,
106
+ });
107
+ if (git) {
108
+ // a push moves branches as an API commit does: GitHub's own processes read it (workflows, secret scanning,
109
+ // Dependabot, rulesets) and its webhooks are delivered (semantics/shared.ts announce)
110
+ if (git.webhooks.length) {
111
+ const ctx = await semanticsContext(manifest, new Request(request.url, { headers: request.headers }), GIT_OPERATION, { ...scope, clock: () => occurredAt });
112
+ await announce(ctx, git.webhooks);
113
+ }
114
+ return new Response(git.body, { status: git.status, headers: { 'content-type': git.contentType, 'cache-control': 'no-cache' } });
115
+ }
116
+ }
117
+ return readOnly ? undefined : handleGithubDoor(request, { ...scope, now });
118
+ };
119
+ // An archived repository is read-only (docs.github.com/repositories/archiving-a-github-repository/archiving-repositories/
120
+ // about-archiving-repositories): GitHub refuses a write to it, except the repository's own edit (which unarchives it),
121
+ // deleting it, and a person's own watch of it.
122
+ const cross = crossCutting(manifest, { readOnly, ...scope });
123
+ const readOnlyWhenArchived = (request, next) => {
124
+ if (request.method === 'GET' || request.method === 'HEAD')
125
+ return next();
126
+ const seg = new URL(request.url).pathname.split('/').filter(Boolean);
127
+ if (seg[0] !== 'repos' || !seg[2] || seg.length === 3 || (seg.length === 4 && seg[3] === 'subscription'))
128
+ return next();
129
+ if (!githubState(scope.root).repos.find((r) => r.full_name === `${seg[1]}/${seg[2]}`)?.archived)
130
+ return next();
131
+ return Promise.resolve(Response.json({ message: 'Repository was archived so is read-only.', documentation_url: 'https://docs.github.com/articles/about-archiving-repositories' }, { status: 403 }));
132
+ };
133
+ // a refused or failed write at the head (kernel head.ts) answers in the vendor's own error shape
134
+ // The derived dispatch owns the REST wire: semantics handlers, then the derived core; an operation neither
135
+ // serves answers GitHub's Not Found.
136
+ const inner = createDerivedFetch({
137
+ surface: githubSurface,
138
+ handlers: bindSemantics(manifest, githubSemantics({ readOnly }), scope),
139
+ core: coreFor(manifest, scope),
140
+ around: (call, next) => readOnlyWhenArchived(call.request, () => cross(call, next)),
141
+ gap: () => vendorError(manifest, manifest.notFound),
142
+ });
143
+ // GraphQL is a second wire over the same state: the vendor's schema and the pack's resolvers, which get
144
+ // the handler interface for their request (semanticsContext). A query selecting a field nothing models
145
+ // answers GitHub's undefinedField error; a read-only twin refuses a mutation as it refuses any write.
146
+ const graphqlSemantics = githubGraphql();
147
+ const graphqlWire = createGraphqlWire({
148
+ loadSdl: async () => (await import('./generated/graphql-sdl.gen.json', { with: { type: 'json' } })).default.sdl,
149
+ resolvers: graphqlSemantics.resolvers,
150
+ stored: graphqlSemantics.stored,
151
+ context: async (request) => ({ ctx: await semanticsContext(manifest, request, GRAPHQL_OPERATION, { ...scope, clock: now }) }),
152
+ unmodeled: ({ type, field }) => Response.json({ errors: [{ message: `Field '${field}' doesn't exist on type '${type}' in the twin's modeled GraphQL subset`, type: 'undefinedField' }] }),
153
+ });
154
+ const graphql = async (request) => {
155
+ if (readOnly) {
156
+ const { query } = (await request.clone().json().catch(() => ({})));
157
+ if (typeof query === 'string' && /^\s*mutation\b/.test(query))
158
+ return Response.json({ message: 'twin is read-only (mirror mode); start in simulator/fork to write' }, { status: 403 });
159
+ }
160
+ return graphqlWire(request);
161
+ };
162
+ // GitHub reads a write's body as JSON whatever its content type says (manifest.body.json), and
163
+ // answers 400 to one that does not parse.
164
+ const routes = compileSurface(githubSurface);
165
+ const rest = async (request) => {
166
+ const url = new URL(request.url);
167
+ const matched = readOnly || request.method === 'GET' || request.method === 'HEAD' ? undefined : matchOperation(routes, request.method, url.pathname, url.searchParams);
168
+ if (!matched)
169
+ return inner(request);
170
+ // a release asset's body is the file itself, posted to the release's upload_url on uploads.github.com, not to the
171
+ // API host (docs.github.com/rest/releases/assets#upload-a-release-asset). Where the documentation stops and the
172
+ // twin decides: api.github.com answers the path as a route it does not serve, GitHub's 404; any other host is the
173
+ // twin's own address (a World's mount, a local port), where the two GitHub hosts cannot be told apart.
174
+ if (matched.operation.id === 'repos/upload-release-asset') {
175
+ if (url.hostname === 'api.github.com')
176
+ return Response.json({ message: 'Not Found', documentation_url: 'https://docs.github.com/rest', status: '404' }, { status: 404 });
177
+ return uploadingAsset(request, inner);
178
+ }
179
+ const text = await request.clone().text();
180
+ if (!text)
181
+ return inner(request);
182
+ try {
183
+ JSON.parse(text);
184
+ }
185
+ catch {
186
+ return malformedJson();
187
+ }
188
+ return inner(request);
189
+ };
190
+ // github.com's OAuth web flow (a hosted flow: the consent screen and the code exchange) sits beside the API
191
+ // the sign-in every page sends a visitor to who is not signed in (src/screens/session.tsx)
192
+ const session = githubSessionFlow({ ...(options.root !== undefined ? { root: options.root } : {}), now });
193
+ const oauth = githubOAuthFlow({ ...(options.root !== undefined ? { root: options.root } : {}), now });
194
+ // and a GitHub App's installation page (src/screens/app-installation.tsx)
195
+ const appInstall = githubAppInstallationFlow({ ...(options.root !== undefined ? { root: options.root } : {}), now });
196
+ // and the page that registers a GitHub App from a manifest (src/screens/app-manifest.tsx)
197
+ const appManifest = githubAppManifestFlow({ ...(options.root !== undefined ? { root: options.root } : {}), now });
198
+ // and GitHub Packages' npm registry, where `npm publish` puts a package (src/npm-registry.ts)
199
+ const npmRegistry = githubNpmRegistry({ ...(options.root !== undefined ? { root: options.root } : {}), now });
200
+ const wires = async (request) => {
201
+ const path = new URL(request.url).pathname.replace(/\/+$/, '');
202
+ const flow = readOnly ? undefined : (await session(request)) ?? (await oauth(request)) ?? (await appInstall(request)) ?? (await appManifest(request)) ?? (await npmRegistry(request));
203
+ if (flow)
204
+ return flow;
205
+ return request.method === 'POST' && (path === '/graphql' || path === '/api/graphql') ? graphql(request) : rest(request);
206
+ };
207
+ const served = async (request) => (await doors(request)) ?? wires(request);
208
+ const vended = answerVendorErrors(served, (error) => Response.json({ message: error instanceof RefusedWriteError ? `Validation Failed: ${error.message}` : error.message, errors: [{ resource: 'Write', code: error instanceof RefusedWriteError ? 'refused' : 'failed', message: error.message }], documentation_url: 'https://docs.github.com/rest' }, { status: 422 }));
209
+ // Every answer carries GitHub's own headers for when and which request it was: Date on the World's clock (a response
210
+ // dated by the host's clock contradicts the World's time) and an x-github-request-id in GitHub's colon-separated hex
211
+ // form, derived from the instant, the request and a counter so the same request twice gets two ids.
212
+ let answered = 0;
213
+ // which part serves each spec operation (the derived dispatch's owners), for the pack's report
214
+ return Object.assign(async (request) => {
215
+ const token = (request.headers.get('authorization') ?? '').replace(/^(?:bearer|token)\s+/i, '').trim() || undefined;
216
+ const res = revokedToken(token, options.root) ? vendorError(manifest, { status: 401, message: 'Bad credentials' }) : await vended(request);
217
+ const at = now();
218
+ const headers = new Headers(res.headers);
219
+ headers.set('date', new Date(at).toUTCString());
220
+ if (!headers.has('x-github-request-id')) {
221
+ const h = createHash('sha256').update(`${at}:${request.method}:${request.url}:${answered++}`).digest('hex').toUpperCase();
222
+ headers.set('x-github-request-id', `${h.slice(0, 4)}:${h.slice(4, 9)}:${h.slice(9, 15)}:${h.slice(15, 21)}:${h.slice(21, 29)}`);
223
+ }
224
+ return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
225
+ }, { owners: inner.owners });
226
+ }
227
+ /** The GraphQL wire's requests, as the handler interface names them. */
228
+ const GRAPHQL_OPERATION = { id: 'graphql', method: 'POST', path: '/graphql', class: 'action' };
229
+ /** A git push, as the handler interface names it. */
230
+ const GIT_OPERATION = { id: 'git-receive-pack', method: 'POST', path: '/{owner}/{repo}.git/git-receive-pack', class: 'action' };
231
+ // The REST surface, with the path parameters GitHub lets hold slashes (manifest.ts `spanning`).
232
+ const githubSurface = { ...surface, spanning };
233
+ export async function createGithubTwinServer(options) {
234
+ const server = await serveHttp({
235
+ port: options.port ?? 0,
236
+ idleTimeout: 60,
237
+ fetch: createGithubTwinFetch(options),
238
+ });
239
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
240
+ }
@@ -0,0 +1,12 @@
1
+ /** The ONE named store the mirror renders from: `GET /twin/store/${GITHUB_MIRROR_STORE}`
2
+ * (runtime contract R5 — the store door — is the only way a mirror reads twin state). */
3
+ export declare const GITHUB_MIRROR_STORE = "mirror";
4
+ /** The store door's path, relative to the twin's base (the mirror host's origin). */
5
+ export declare const GITHUB_MIRROR_STORE_PATH = "/twin/store/mirror";
6
+ /** GitHub's web origin — the projection's `url` fields are real github.com links. */
7
+ export declare const GITHUB_WEB_ORIGIN = "https://github.com";
8
+ /** A pull request's web path (`/{owner}/{repo}/pull/{n}`) — the projection's `url` tail and
9
+ * the console's own route for an opened PR are the same grammar, real GitHub's. */
10
+ export declare function githubPullPath(repo: string, number: number | string): string;
11
+ /** An issue's web path (`/{owner}/{repo}/issues/{n}`), the same way. */
12
+ export declare function githubIssuePath(repo: string, number: number | string): string;
@@ -0,0 +1,21 @@
1
+ // GitHub SHARED helpers — pure shape functions both sides of the wire need: the twin's
2
+ // mirror-state projection (server side, `github-mirror-state.ts`) and the mirror's React
3
+ // client (`client/github-mirror.tsx`). Plain data in → plain data out; no state access,
4
+ // no `@volter/world-core`, no Bun/node imports — this module rides into the browser bundle
5
+ // unchanged.
6
+ /** The ONE named store the mirror renders from: `GET /twin/store/${GITHUB_MIRROR_STORE}`
7
+ * (runtime contract R5 — the store door — is the only way a mirror reads twin state). */
8
+ export const GITHUB_MIRROR_STORE = 'mirror';
9
+ /** The store door's path, relative to the twin's base (the mirror host's origin). */
10
+ export const GITHUB_MIRROR_STORE_PATH = `/twin/store/${GITHUB_MIRROR_STORE}`;
11
+ /** GitHub's web origin — the projection's `url` fields are real github.com links. */
12
+ export const GITHUB_WEB_ORIGIN = 'https://github.com';
13
+ /** A pull request's web path (`/{owner}/{repo}/pull/{n}`) — the projection's `url` tail and
14
+ * the console's own route for an opened PR are the same grammar, real GitHub's. */
15
+ export function githubPullPath(repo, number) {
16
+ return `/${repo}/pull/${number}`;
17
+ }
18
+ /** An issue's web path (`/{owner}/{repo}/issues/{n}`), the same way. */
19
+ export function githubIssuePath(repo, number) {
20
+ return `/${repo}/issues/${number}`;
21
+ }