@volter/twin-github 0.1.2 → 2.0.0

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 +230 -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 +101 -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 +159 -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 +223 -95
  161. package/src/github-git-http.ts +47 -83
  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 +47 -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 +186 -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,255 @@
1
+ // GitHub's users family: a user by login or account id, the authenticated user and their profile,
2
+ // the follow graph, emails, SSH and GPG keys, blocks, and the activity streams scoped to a user.
3
+ // The twin models one follow actor, the authenticated `octocat`.
4
+ import { me } from "./activity.js";
5
+ import { createHash } from 'node:crypto';
6
+ import { githubActivityEvents, githubNotFound, paginate, seedUser, toGithubOrg, toGithubRepo, toGithubUser, toGithubUserEmail, toGithubUserGpgKey, toGithubUserSshKey, toSimpleUser, twinUserId, } from "../github-twin.js";
7
+ import { asView, gh, nextSeq, put, send } from "./shared.js";
8
+ /** GET /users/:login/events[/public] and /users/:login/received_events[/public]: events = activity the
9
+ * user PERFORMED (actor); received_events = activity in repos the user OWNS. Both filter the SAME
10
+ * modeled stream. */
11
+ const userEvents = (received, only) => async (ctx) => {
12
+ const { seg, query, state } = gh(ctx);
13
+ const login = decodeURIComponent(seg[1]);
14
+ const all = githubActivityEvents(state());
15
+ let items = !received
16
+ ? all.filter((e) => e.actor.login === login)
17
+ : all.filter((e) => String(e.repo.name ?? '').split('/')[0] === login);
18
+ if (only === 'public')
19
+ items = items.filter((e) => e.public === true);
20
+ return ctx.reply(paginate(items, query));
21
+ };
22
+ export function usersSemantics() {
23
+ const usersGetByUsername = async (ctx) => {
24
+ const { seg, state } = gh(ctx);
25
+ const login = decodeURIComponent(seg[1]);
26
+ return ctx.reply(toGithubUser(login === 'octocat' ? { ...seedUser('octocat'), ...state().userProfile, login } : seedUser(login)));
27
+ };
28
+ const reposListForAuthenticatedUser = async (ctx) => {
29
+ const { caller, query, state } = gh(ctx);
30
+ const me = caller ?? 'octocat';
31
+ const params = new URLSearchParams(query ?? '');
32
+ const affiliation = new Set((params.get('affiliation') ?? 'owner,collaborator,organization_member').split(',').map((a) => a.trim()));
33
+ const visibility = params.get('visibility') ?? 'all';
34
+ const st = state();
35
+ const memberOf = new Set(st.orgMembers.filter((m) => m.login === me && (m.state ?? 'active') === 'active').map((m) => m.org));
36
+ const collaborating = new Set(st.collaborators.filter((c) => c.login === me).map((c) => c.repository));
37
+ const mine = st.repos.filter((r) => ((affiliation.has('owner') && r.owner === me) || (affiliation.has('collaborator') && collaborating.has(r.full_name)) || (affiliation.has('organization_member') && memberOf.has(r.owner)))
38
+ && (visibility === 'all' || (visibility === 'private') === (r.private === true)));
39
+ return ctx.reply(paginate([...mine].sort((a, b) => a.full_name.localeCompare(b.full_name)), query).map((r) => asView('repository', toGithubRepo(r))));
40
+ };
41
+ const orgsListForAuthenticatedUser = async (ctx) => {
42
+ const { caller, query, state } = gh(ctx);
43
+ const me = caller ?? 'octocat';
44
+ const st = state();
45
+ const logins = new Set(st.orgMembers.filter((m) => m.login === me && (m.state ?? 'active') === 'active').map((m) => m.org));
46
+ const SIMPLE = ['login', 'id', 'node_id', 'url', 'repos_url', 'events_url', 'hooks_url', 'issues_url', 'members_url', 'public_members_url', 'avatar_url', 'description'];
47
+ const simple = (o) => Object.fromEntries(SIMPLE.filter((k) => k in o).map((k) => [k, o[k]]));
48
+ return ctx.reply(paginate(st.orgs.filter((o) => logins.has(o.login)), query).map((o) => simple(toGithubOrg(o))));
49
+ };
50
+ const reposListForUser = async (ctx) => {
51
+ const { seg, query, state } = gh(ctx);
52
+ const login = decodeURIComponent(seg[1]);
53
+ return ctx.reply(paginate(state().repos.filter((r) => r.owner === login), query).map(toGithubRepo));
54
+ };
55
+ const usersListFollowersForUser = async (ctx) => {
56
+ const { seg, query, state } = gh(ctx);
57
+ const login = decodeURIComponent(seg[1]);
58
+ return ctx.reply(paginate(state().followEdges.filter((e) => e.to === login).map((e) => asView('simple-user', toGithubUser(seedUser(e.from)))), query));
59
+ };
60
+ const usersListFollowingForUser = async (ctx) => {
61
+ const { seg, query, state } = gh(ctx);
62
+ const login = decodeURIComponent(seg[1]);
63
+ return ctx.reply(paginate(state().followEdges.filter((e) => e.from === login).map((e) => asView('simple-user', toGithubUser(seedUser(e.to)))), query));
64
+ };
65
+ const usersCheckFollowingForUser = async (ctx) => {
66
+ const { seg, state } = gh(ctx);
67
+ const [login, other] = [decodeURIComponent(seg[1]), decodeURIComponent(seg[3])];
68
+ return send(ctx, state().followEdges.some((e) => e.from === login && e.to === other) ? { status: 204, body: undefined } : githubNotFound());
69
+ };
70
+ const usersGetById = async (ctx) => {
71
+ const { seg, state } = gh(ctx);
72
+ if (!/^\d+$/.test(seg[1]))
73
+ return send(ctx, githubNotFound());
74
+ const st = state();
75
+ const known = new Set(['octocat', ...st.orgMembers.map((m) => m.login), ...st.oauthGrants.map((g) => g.login), ...st.prs.map((p) => p.user_login ?? '')].filter((l) => !!l));
76
+ const login = [...known].find((l) => twinUserId(l) === Number(seg[1]));
77
+ return send(ctx, login ? { status: 200, body: { ...toSimpleUser(login), id: twinUserId(login) } } : githubNotFound());
78
+ };
79
+ const usersListFollowedByAuthenticatedUser = async (ctx) => {
80
+ const { query, state } = gh(ctx);
81
+ return ctx.reply(paginate(state().followEdges.filter((e) => e.from === me(ctx)).map((e) => asView('simple-user', toGithubUser(seedUser(e.to)))), query));
82
+ };
83
+ const usersCheckPersonIsFollowedByAuthenticated = async (ctx) => {
84
+ const { seg, state } = gh(ctx);
85
+ return send(ctx, state().followEdges.some((e) => e.from === me(ctx) && e.to === decodeURIComponent(seg[2])) ? { status: 204, body: undefined } : githubNotFound());
86
+ };
87
+ const usersListFollowersForAuthenticatedUser = async (ctx) => {
88
+ const { query, state } = gh(ctx);
89
+ return ctx.reply(paginate(state().followEdges.filter((e) => e.to === me(ctx)).map((e) => asView('simple-user', toGithubUser(seedUser(e.from)))), query));
90
+ };
91
+ const usersListEmailsForAuthenticatedUser = async (ctx) => {
92
+ const { query, state } = gh(ctx);
93
+ return ctx.reply(paginate(state().userEmails, query).map(toGithubUserEmail));
94
+ };
95
+ const usersListPublicEmailsForAuthenticatedUser = async (ctx) => {
96
+ const { query, state } = gh(ctx);
97
+ return ctx.reply(paginate(state().userEmails.filter((e) => e.visibility === 'public'), query).map(toGithubUserEmail));
98
+ };
99
+ const usersListPublicSshKeysForAuthenticatedUser = async (ctx) => {
100
+ const { query, state } = gh(ctx);
101
+ return ctx.reply(paginate(state().userSshKeys, query).map(toGithubUserSshKey));
102
+ };
103
+ const usersGetPublicSshKeyForAuthenticatedUser = async (ctx) => {
104
+ const { seg, state } = gh(ctx);
105
+ const k = state().userSshKeys.find((x) => x.id === Number(decodeURIComponent(seg[2])));
106
+ return send(ctx, k ? { status: 200, body: toGithubUserSshKey(k) } : githubNotFound());
107
+ };
108
+ const usersListGpgKeysForAuthenticatedUser = async (ctx) => {
109
+ const { query, state } = gh(ctx);
110
+ return ctx.reply(paginate(state().userGpgKeys, query).map(toGithubUserGpgKey));
111
+ };
112
+ const usersGetGpgKeyForAuthenticatedUser = async (ctx) => {
113
+ const { seg, state } = gh(ctx);
114
+ const k = state().userGpgKeys.find((x) => x.id === Number(decodeURIComponent(seg[2])));
115
+ return send(ctx, k ? { status: 200, body: toGithubUserGpgKey(k) } : githubNotFound());
116
+ };
117
+ const usersListBlockedByAuthenticatedUser = async (ctx) => {
118
+ const { query, state } = gh(ctx);
119
+ return ctx.reply(paginate([...state().blockedUsers].sort().map((l) => asView('simple-user', toGithubUser(seedUser(l)))), query));
120
+ };
121
+ const usersCheckBlocked = async (ctx) => {
122
+ const { seg, state } = gh(ctx);
123
+ return send(ctx, state().blockedUsers.has(decodeURIComponent(seg[2])) ? { status: 204, body: undefined } : githubNotFound());
124
+ };
125
+ const usersGetAuthenticated = async (ctx) => {
126
+ const { caller, state } = gh(ctx);
127
+ if (caller)
128
+ return ctx.reply(toGithubUser(seedUser(caller)));
129
+ return ctx.reply(toGithubUser({ ...seedUser('octocat'), ...state().userProfile, login: 'octocat' }));
130
+ };
131
+ const usersUpdateAuthenticated = async (ctx) => {
132
+ const { body, state } = gh(ctx);
133
+ const fields = {};
134
+ for (const k of ['name', 'bio', 'company', 'blog', 'location', 'email'])
135
+ if (body[k] !== undefined)
136
+ fields[k] = body[k];
137
+ await put(ctx, { operation: 'user_profile.update', subjectType: 'user_profile', subjectId: 'user_profile:octocat', fields });
138
+ return ctx.reply(toGithubUser({ ...seedUser('octocat'), ...state().userProfile, login: 'octocat' }));
139
+ };
140
+ const usersCreatePublicSshKeyForAuthenticatedUser = async (ctx) => {
141
+ const { body, occurredAt, state } = gh(ctx);
142
+ if (!body.key)
143
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'PublicKey', field: 'key', code: 'missing_field' }] }, 422);
144
+ const id = nextSeq(ctx, 'sshkey');
145
+ await put(ctx, { operation: 'user_ssh_key.create', subjectType: 'user_ssh_key', subjectId: `sshkey:${id}`, fields: { key: String(body.key), title: String(body.title ?? ''), created_at: occurredAt } });
146
+ return ctx.reply(toGithubUserSshKey(state().userSshKeys.find((x) => x.id === id)), 201);
147
+ };
148
+ const usersDeletePublicSshKeyForAuthenticatedUser = async (ctx) => {
149
+ const { seg, state } = gh(ctx);
150
+ const id = Number(decodeURIComponent(seg[2]));
151
+ if (!state().userSshKeys.find((k) => k.id === id))
152
+ return send(ctx, githubNotFound());
153
+ await put(ctx, { operation: 'user_ssh_key.delete', subjectType: 'user_ssh_key', subjectId: `sshkey:${id}`, fields: { deleted: true } });
154
+ return send(ctx, { status: 204, body: undefined });
155
+ };
156
+ const usersCreateGpgKeyForAuthenticatedUser = async (ctx) => {
157
+ const { body, occurredAt, state } = gh(ctx);
158
+ if (!body.armored_public_key)
159
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'GpgKey', field: 'armored_public_key', code: 'missing_field' }] }, 422);
160
+ const id = nextSeq(ctx, 'gpgkey');
161
+ const raw = String(body.armored_public_key);
162
+ const keyId = createHash('sha256').update(raw).digest('hex').slice(0, 16).toUpperCase();
163
+ await put(ctx, { operation: 'user_gpg_key.create', subjectType: 'user_gpg_key', subjectId: `gpgkey:${id}`, fields: { key_id: keyId, raw_key: raw, created_at: occurredAt } });
164
+ return ctx.reply(toGithubUserGpgKey(state().userGpgKeys.find((x) => x.id === id)), 201);
165
+ };
166
+ const usersDeleteGpgKeyForAuthenticatedUser = async (ctx) => {
167
+ const { seg, state } = gh(ctx);
168
+ const id = Number(decodeURIComponent(seg[2]));
169
+ if (!state().userGpgKeys.find((k) => k.id === id))
170
+ return send(ctx, githubNotFound());
171
+ await put(ctx, { operation: 'user_gpg_key.delete', subjectType: 'user_gpg_key', subjectId: `gpgkey:${id}`, fields: { deleted: true } });
172
+ return send(ctx, { status: 204, body: undefined });
173
+ };
174
+ return {
175
+ // the twin's own account (octocat) answers with the profile its owner edited (PATCH /user)
176
+ 'users/get-by-username': usersGetByUsername,
177
+ // the signed-in person's repositories, by GitHub's affiliation (owner, collaborator, organization_member;
178
+ // all three by default) and visibility (all, public, private), sorted by full_name
179
+ 'repos/list-for-authenticated-user': reposListForAuthenticatedUser,
180
+ // the organizations the signed-in person is an active member of, in GitHub's simple organization shape
181
+ 'orgs/list-for-authenticated-user': orgsListForAuthenticatedUser,
182
+ 'repos/list-for-user': reposListForUser,
183
+ // who follows :login, and whom :login follows
184
+ 'users/list-followers-for-user': usersListFollowersForUser,
185
+ 'users/list-following-for-user': usersListFollowingForUser,
186
+ // 204 if :login follows :other, else 404
187
+ 'users/check-following-for-user': usersCheckFollowingForUser,
188
+ 'activity/list-events-for-authenticated-user': userEvents(false, undefined),
189
+ 'activity/list-public-events-for-user': userEvents(false, 'public'),
190
+ 'activity/list-received-events-for-user': userEvents(true, undefined),
191
+ 'activity/list-received-public-events-for-user': userEvents(true, 'public'),
192
+ // an account by its id, among the accounts the twin knows (its own octocat, members, token holders, authors)
193
+ 'users/get-by-id': usersGetById,
194
+ 'users/list-followed-by-authenticated-user': usersListFollowedByAuthenticatedUser,
195
+ 'users/check-person-is-followed-by-authenticated': usersCheckPersonIsFollowedByAuthenticated,
196
+ // the authenticated user's own followers
197
+ 'users/list-followers-for-authenticated-user': usersListFollowersForAuthenticatedUser,
198
+ 'users/list-emails-for-authenticated-user': usersListEmailsForAuthenticatedUser,
199
+ 'users/list-public-emails-for-authenticated-user': usersListPublicEmailsForAuthenticatedUser,
200
+ 'users/list-public-ssh-keys-for-authenticated-user': usersListPublicSshKeysForAuthenticatedUser,
201
+ 'users/get-public-ssh-key-for-authenticated-user': usersGetPublicSshKeyForAuthenticatedUser,
202
+ 'users/list-gpg-keys-for-authenticated-user': usersListGpgKeysForAuthenticatedUser,
203
+ 'users/get-gpg-key-for-authenticated-user': usersGetGpgKeyForAuthenticatedUser,
204
+ 'users/list-blocked-by-authenticated-user': usersListBlockedByAuthenticatedUser,
205
+ // "Check if a user is blocked by the authenticated user" → 204 blocked / 404 not.
206
+ 'users/check-blocked': usersCheckBlocked,
207
+ // the token's own user when the twin's user door minted it; else the deterministic octocat
208
+ 'users/get-authenticated': usersGetAuthenticated,
209
+ 'users/update-authenticated': usersUpdateAuthenticated,
210
+ 'users/add-email-for-authenticated-user': async (ctx) => emails(ctx, false),
211
+ 'users/delete-email-for-authenticated-user': async (ctx) => emails(ctx, true),
212
+ 'users/create-public-ssh-key-for-authenticated-user': usersCreatePublicSshKeyForAuthenticatedUser,
213
+ 'users/delete-public-ssh-key-for-authenticated-user': usersDeletePublicSshKeyForAuthenticatedUser,
214
+ 'users/create-gpg-key-for-authenticated-user': usersCreateGpgKeyForAuthenticatedUser,
215
+ 'users/delete-gpg-key-for-authenticated-user': usersDeleteGpgKeyForAuthenticatedUser,
216
+ // block / unblock answer 204
217
+ 'users/block': async (ctx) => block(ctx, true),
218
+ 'users/unblock': async (ctx) => block(ctx, false),
219
+ // follow / unfollow answer 204
220
+ 'users/follow': async (ctx) => follow(ctx, true),
221
+ 'users/unfollow': async (ctx) => follow(ctx, false),
222
+ };
223
+ }
224
+ /** POST /user/emails adds, DELETE removes; both take `{emails}` or a bare array. The first address a
225
+ * user adds is primary and public. */
226
+ async function emails(ctx, remove) {
227
+ const { body, state } = gh(ctx);
228
+ const list = Array.isArray(body.emails) ? body.emails.map(String) : Array.isArray(body) ? body.map(String) : [];
229
+ if (!list.length)
230
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'Email', field: 'emails', code: 'missing_field' }] }, 422);
231
+ const st = state();
232
+ for (const email of list) {
233
+ const subjectId = `useremail:${email}`;
234
+ if (remove) {
235
+ await put(ctx, { operation: 'user_email.delete', subjectType: 'user_email', subjectId, fields: { deleted: true, email } });
236
+ }
237
+ else {
238
+ const isFirst = st.userEmails.length === 0 && email === list[0];
239
+ await put(ctx, { operation: 'user_email.upsert', subjectType: 'user_email', subjectId, fields: { email, primary: isFirst, verified: true, visibility: isFirst ? 'public' : 'private' } });
240
+ }
241
+ }
242
+ if (remove)
243
+ return send(ctx, { status: 204, body: undefined });
244
+ return ctx.reply(state().userEmails.map(toGithubUserEmail), 201);
245
+ }
246
+ async function block(ctx, blocking) {
247
+ const login = decodeURIComponent(gh(ctx).seg[2]);
248
+ await put(ctx, { operation: blocking ? 'user_block.create' : 'user_block.delete', subjectType: 'user_block', subjectId: `block:${login}`, fields: blocking ? { login } : { login, deleted: true } });
249
+ return send(ctx, { status: 204, body: undefined });
250
+ }
251
+ async function follow(ctx, following) {
252
+ const login = decodeURIComponent(gh(ctx).seg[2]);
253
+ await put(ctx, { operation: following ? 'user_following.add' : 'user_following.remove', subjectType: 'user_following', subjectId: `following:${me(ctx)}:${login}`, fields: { login, following } });
254
+ return send(ctx, { status: 204, body: undefined });
255
+ }
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare function webhooksSemantics(): Record<string, Semantics>;
@@ -0,0 +1,107 @@
1
+ import { githubNotFound, hookConfigOverlaps, paginate, toGithubWebhook, toGithubWebhookConfig, validateGithubWebhook } from "../github-twin.js";
2
+ import { handleGithubWebhookRequest, hookLastResponse, pingGithubHook } from "../github-events.js";
3
+ import { gh, nextSeq, put, send } from "./shared.js";
4
+ const hookOf = (ctx) => {
5
+ const { repo, seg, state } = gh(ctx);
6
+ const id = Number(decodeURIComponent(seg[4]));
7
+ return { id, hook: state().webhooks.find((h) => h.repository === repo && h.id === id) };
8
+ };
9
+ /** PATCH the hook (events, active, config; an omitted secret is removed) or only its config (a
10
+ * secret is kept unless named), refused when the result would duplicate another hook. */
11
+ const updateHook = (configOnly) => async (ctx) => {
12
+ const { id, hook } = hookOf(ctx);
13
+ if (!hook)
14
+ return send(ctx, githubNotFound());
15
+ const { repo, body, occurredAt, state } = gh(ctx);
16
+ const error = validateGithubWebhook(body, configOnly, false);
17
+ if (error)
18
+ return send(ctx, error);
19
+ const cfg = (configOnly ? body : (body.config ?? {}));
20
+ const response = await ctx.atomically(() => {
21
+ const hooks = state().webhooks;
22
+ const current = hooks.find((h) => h.repository === repo && h.id === id);
23
+ if (!current)
24
+ return { value: githubNotFound() };
25
+ const fields = { repository: repo, updated_at: occurredAt };
26
+ if (!configOnly) {
27
+ if (body.active !== undefined)
28
+ fields.active = body.active;
29
+ if (body.events !== undefined || body.add_events !== undefined || body.remove_events !== undefined) {
30
+ const events = [...(body.events ?? current.events), ...(body.add_events ?? [])];
31
+ const removed = new Set(body.remove_events ?? []);
32
+ fields.events = [...new Set(events)].filter((event) => !removed.has(event));
33
+ }
34
+ fields.secret = cfg.secret ?? '';
35
+ }
36
+ if (cfg.url !== undefined)
37
+ fields.url = cfg.url;
38
+ if (cfg.content_type !== undefined)
39
+ fields.content_type = cfg.content_type;
40
+ if (cfg.secret !== undefined)
41
+ fields.secret = cfg.secret;
42
+ const next = { ...current, ...fields };
43
+ if (hookConfigOverlaps(hooks, next, id))
44
+ return { value: { status: 422, body: { message: 'Hook already exists on this repository' } } };
45
+ return { value: { status: 200, body: configOnly ? toGithubWebhookConfig(next) : toGithubWebhook(next) }, write: { resource: 'webhook', id: `webhook:${id}`, fields, operation: 'webhook.update' } };
46
+ });
47
+ return send(ctx, response);
48
+ };
49
+ /** The delivery doors, answered by the delivery pipeline. */
50
+ const deliveries = async (ctx) => (await handleGithubWebhookRequest(ctx.call.request, { ...(ctx.root !== undefined ? { root: ctx.root } : {}) })) ?? send(ctx, githubNotFound());
51
+ export function webhooksSemantics() {
52
+ const reposListWebhooks = async (ctx) => {
53
+ const { repo, query, state } = gh(ctx);
54
+ return ctx.reply(paginate(state().webhooks.filter((h) => h.repository === repo), query).map((h) => ({ ...toGithubWebhook(h), last_response: hookLastResponse(h.repository, h.id, ctx.root) })));
55
+ };
56
+ const reposGetWebhook = async (ctx) => {
57
+ const { hook } = hookOf(ctx);
58
+ return send(ctx, hook ? { status: 200, body: { ...toGithubWebhook(hook), last_response: hookLastResponse(hook.repository, hook.id, ctx.root) } } : githubNotFound());
59
+ };
60
+ const reposGetWebhookConfigForRepo = async (ctx) => {
61
+ const { hook } = hookOf(ctx);
62
+ return send(ctx, hook ? { status: 200, body: toGithubWebhookConfig(hook) } : githubNotFound());
63
+ };
64
+ const reposCreateWebhook = async (ctx) => {
65
+ const { repo, body, occurredAt, root, state } = gh(ctx);
66
+ const error = validateGithubWebhook(body, false, true);
67
+ if (error)
68
+ return send(ctx, error);
69
+ const cfg = (body.config ?? {});
70
+ const events = body.events ?? ['push'];
71
+ const fields = { repository: repo, active: body.active !== false, events, url: cfg.url, content_type: cfg.content_type ?? 'form', ...(cfg.secret !== undefined ? { secret: cfg.secret } : {}), created_at: occurredAt, updated_at: occurredAt };
72
+ const hook = await ctx.atomically(() => {
73
+ if (hookConfigOverlaps(state().webhooks, fields))
74
+ return { value: undefined };
75
+ const id = nextSeq(ctx, 'webhook');
76
+ return { value: { id, name: 'web', ...fields }, write: { resource: 'webhook', id: `webhook:${id}`, fields, operation: 'webhook.create' } };
77
+ });
78
+ if (hook === undefined)
79
+ return ctx.reply({ message: 'Hook already exists on this repository' }, 422);
80
+ // GitHub pings a hook it creates; a delivery failure does not undo the hook
81
+ try {
82
+ await pingGithubHook(repo, hook.id, { occurredAt, ...(root !== undefined ? { root } : {}) });
83
+ }
84
+ catch { /* fire-and-forget */ }
85
+ return ctx.reply(toGithubWebhook(hook), 201);
86
+ };
87
+ const reposDeleteWebhook = async (ctx) => {
88
+ const { id, hook } = hookOf(ctx);
89
+ if (!hook)
90
+ return send(ctx, githubNotFound());
91
+ await put(ctx, { operation: 'webhook.delete', subjectType: 'webhook', subjectId: `webhook:${id}`, fields: { deleted: true, repository: gh(ctx).repo } });
92
+ return send(ctx, { status: 204, body: undefined });
93
+ };
94
+ return {
95
+ 'repos/list-webhooks': reposListWebhooks,
96
+ 'repos/get-webhook': reposGetWebhook,
97
+ 'repos/get-webhook-config-for-repo': reposGetWebhookConfigForRepo,
98
+ 'repos/create-webhook': reposCreateWebhook,
99
+ 'repos/update-webhook': updateHook(false),
100
+ 'repos/update-webhook-config-for-repo': updateHook(true),
101
+ 'repos/delete-webhook': reposDeleteWebhook,
102
+ 'repos/list-webhook-deliveries': deliveries,
103
+ 'repos/get-webhook-delivery': deliveries,
104
+ 'repos/redeliver-webhook-delivery': deliveries,
105
+ 'repos/ping-webhook': deliveries,
106
+ };
107
+ }
@@ -0,0 +1,56 @@
1
+ # github-a11y-reference.pr-list.json — provenance (TWIN-51 / H5, dev/02)
2
+
3
+ Real github.com accessibility-tree (role/name) reference for the PR-list a11y-congruence
4
+ BONUS check (`packages/world-tooling/src/a11ySnapshot.ts`, `diffA11yShape`). Consumed by
5
+ `packages/twin/github/src/github-a11y-snapshot.uitest.ts`, which diffs it SHAPE-ONLY against a
6
+ live capture of the mirror's own PR-list for the same seeded scenario
7
+ (`github-journey.uitest.ts:58-86`) and PRINTS the result — this check never gates (see
8
+ `a11yBonusOutcome`, hardcoded zero).
9
+
10
+ ## Adjudication — no user-authorized session required
11
+
12
+ TWIN-51's dev/01 evidence left a tracker comment asserting dev/02 "requires a user-authorized
13
+ session." That premise is FALSE for the scenario actually being compared: a repo-scoped
14
+ **public** pull-request list. `https://github.com/<owner>/<repo>/pulls` is a public page —
15
+ reachable, and its accessibility tree capturable, with **no login at all**. This fixture is
16
+ the proof: it was captured logged out. (Superseding tracker comment recorded on
17
+ `.volter/tracker/markdown/TWIN-51.md`, dated 2026-07-09.)
18
+
19
+ ## Capture facts
20
+
21
+ - **Source URL:** `https://github.com/microsoft/vscode/pulls` (a large, permanently-public,
22
+ high-traffic open-source repo's PR list — stable choice of target; overridable via
23
+ `GITHUB_A11Y_PUBLIC_REPO_URL`)
24
+ - **Capture date:** 2026-07-09
25
+ - **Session:** logged OUT — no auth, no storage-state, no cookies seeded. Verified live at
26
+ capture time: `GET https://github.com/microsoft/vscode/pulls` → **HTTP 200**, and the
27
+ response's own `logged_in` cookie read back as `"no"` (github.com's own logged-out/in
28
+ signal). The script (`scripts/capture/github-a11y.ts`) asserts both of these and refuses
29
+ to write output otherwise.
30
+ - **Tool:** Playwright `chromium` (headless), `page.locator(selector).ariaSnapshot()`, driven
31
+ by `scripts/capture/github-a11y.ts` (no-auth default mode).
32
+ - **Captured region:** `[aria-label="Issues"][role="group"]` — the PR-row list region on the
33
+ repo's `/pulls` page (a real `<div role="group" aria-label="Issues">` wrapping just the PR
34
+ rows), excluding the repo header/nav/filter-toolbar chrome above it, which is real-github
35
+ page furniture and not part of the seeded PR-list SCENARIO the mirror side reproduces.
36
+ - **Scrub rule** (same doctrine as `scripts/capture/slack.ts`): every accessible `name` is
37
+ redacted to the literal string `"x"`; `role` and the tree's shape (`children`) are
38
+ preserved byte-for-byte. **No real PR titles, usernames, avatars, comment counts, or any
39
+ other page content is committed** — only the structural role tree, which is all
40
+ `diffA11yShape` ever compares.
41
+
42
+ ## Shape summary (post-scrub)
43
+
44
+ 265 total nodes: 1 `group` root, 121 `link`, 121 `/url` (Playwright's own href-line
45
+ pseudo-role, one per link), 22 `img` (contributor avatars). 1447 lines of scrubbed JSON.
46
+
47
+ ## Regenerating
48
+
49
+ ```
50
+ bun run scripts/capture/github-a11y.ts \
51
+ > packages/twin/github/test-fixtures/github-a11y-reference.pr-list.json
52
+ ```
53
+
54
+ (No env var needed — no-auth is the default. `GITHUB_A11Y_PUBLIC_REPO_URL` overrides the
55
+ target repo; `GITHUB_A11Y_STORAGE_STATE` switches to the opt-in authed cross-repo-dashboard
56
+ mode instead, unrelated to this fixture.)