@volter/twin-github 0.1.1 → 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.
- package/README.md +88 -36
- package/client/github-mirror.css +277 -319
- package/client/github-mirror.d.ts +418 -0
- package/client/github-mirror.js +485 -0
- package/client/github-mirror.tsx +153 -357
- package/client/pulls-rest.ts +159 -0
- package/client/pulls-workspace.tsx +841 -0
- package/dist/client/github-mirror.bundle.js +239 -0
- package/dist/client/github-mirror.css +916 -0
- package/dist/client/github-mirror.d.ts +418 -0
- package/dist/client/github-mirror.js +485 -0
- package/dist/client/github-mirror.tsx +1315 -0
- package/dist/client/pulls-rest.d.ts +42 -0
- package/dist/client/pulls-rest.js +140 -0
- package/dist/client/pulls-rest.ts +159 -0
- package/dist/client/pulls-workspace.bundle.js +22 -0
- package/dist/client/pulls-workspace.d.ts +114 -0
- package/dist/client/pulls-workspace.js +418 -0
- package/dist/client/pulls-workspace.tsx +841 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +40 -0
- package/dist/src/generated/graphql-sdl.gen.json +1 -0
- package/dist/src/generated/graphql.gen.json +1 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/github-budget.d.ts +69 -0
- package/dist/src/github-budget.js +172 -0
- package/dist/src/github-capabilities.d.ts +5 -0
- package/dist/src/github-capabilities.js +4468 -0
- package/dist/src/github-conformance.d.ts +43 -0
- package/dist/src/github-conformance.js +76 -0
- package/dist/src/github-connector.d.ts +307 -0
- package/dist/src/github-connector.js +1398 -0
- package/dist/src/github-events.d.ts +41 -0
- package/dist/src/github-events.js +230 -0
- package/dist/src/github-git-http.d.ts +49 -0
- package/dist/src/github-git-http.js +185 -0
- package/dist/src/github-git-plane.d.ts +114 -0
- package/dist/src/github-git-plane.js +407 -0
- package/dist/src/github-mirror-state.d.ts +2 -0
- package/dist/src/github-mirror-state.js +335 -0
- package/dist/src/github-mirror-ui.d.ts +20 -0
- package/dist/src/github-mirror-ui.js +101 -0
- package/dist/src/github-server.d.ts +14 -0
- package/dist/src/github-server.js +240 -0
- package/dist/src/github-shared.d.ts +12 -0
- package/dist/src/github-shared.js +21 -0
- package/dist/src/github-twin.d.ts +1411 -0
- package/dist/src/github-twin.js +4084 -0
- package/dist/src/github-ui-conformance.d.ts +4 -0
- package/dist/src/github-ui-conformance.js +105 -0
- package/dist/src/github-ui-structure.d.ts +18 -0
- package/dist/src/github-ui-structure.js +251 -0
- package/dist/src/graphql-wire.d.ts +22 -0
- package/dist/src/graphql-wire.js +89 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +101 -0
- package/dist/src/manifest.d.ts +8 -0
- package/dist/src/manifest.js +597 -0
- package/dist/src/npm-registry.d.ts +7 -0
- package/dist/src/npm-registry.js +47 -0
- package/dist/src/screens/app-installation.d.ts +3 -0
- package/dist/src/screens/app-installation.js +166 -0
- package/dist/src/screens/app-manifest.d.ts +3 -0
- package/dist/src/screens/app-manifest.js +81 -0
- package/dist/src/screens/oauth.d.ts +15 -0
- package/dist/src/screens/oauth.js +257 -0
- package/dist/src/screens/session.d.ts +8 -0
- package/dist/src/screens/session.js +159 -0
- package/dist/src/semantics/actions.d.ts +2 -0
- package/dist/src/semantics/actions.js +413 -0
- package/dist/src/semantics/activity.d.ts +4 -0
- package/dist/src/semantics/activity.js +161 -0
- package/dist/src/semantics/apps.d.ts +2 -0
- package/dist/src/semantics/apps.js +144 -0
- package/dist/src/semantics/branches.d.ts +2 -0
- package/dist/src/semantics/branches.js +136 -0
- package/dist/src/semantics/checks.d.ts +2 -0
- package/dist/src/semantics/checks.js +176 -0
- package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
- package/dist/src/semantics/code-scanning-upload.js +97 -0
- package/dist/src/semantics/codespaces.d.ts +2 -0
- package/dist/src/semantics/codespaces.js +58 -0
- package/dist/src/semantics/commits.d.ts +2 -0
- package/dist/src/semantics/commits.js +109 -0
- package/dist/src/semantics/contents.d.ts +2 -0
- package/dist/src/semantics/contents.js +131 -0
- package/dist/src/semantics/deployments.d.ts +2 -0
- package/dist/src/semantics/deployments.js +127 -0
- package/dist/src/semantics/gists.d.ts +2 -0
- package/dist/src/semantics/gists.js +86 -0
- package/dist/src/semantics/git.d.ts +2 -0
- package/dist/src/semantics/git.js +235 -0
- package/dist/src/semantics/graphql.d.ts +7 -0
- package/dist/src/semantics/graphql.js +512 -0
- package/dist/src/semantics/index.d.ts +5 -0
- package/dist/src/semantics/index.js +62 -0
- package/dist/src/semantics/issues.d.ts +2 -0
- package/dist/src/semantics/issues.js +456 -0
- package/dist/src/semantics/keys.d.ts +2 -0
- package/dist/src/semantics/keys.js +66 -0
- package/dist/src/semantics/labels.d.ts +2 -0
- package/dist/src/semantics/labels.js +100 -0
- package/dist/src/semantics/meta.d.ts +12 -0
- package/dist/src/semantics/meta.js +144 -0
- package/dist/src/semantics/notifications.d.ts +4 -0
- package/dist/src/semantics/notifications.js +62 -0
- package/dist/src/semantics/orgs.d.ts +2 -0
- package/dist/src/semantics/orgs.js +420 -0
- package/dist/src/semantics/packages.d.ts +2 -0
- package/dist/src/semantics/packages.js +55 -0
- package/dist/src/semantics/pages.d.ts +2 -0
- package/dist/src/semantics/pages.js +176 -0
- package/dist/src/semantics/projects.d.ts +2 -0
- package/dist/src/semantics/projects.js +239 -0
- package/dist/src/semantics/pulls.d.ts +2 -0
- package/dist/src/semantics/pulls.js +462 -0
- package/dist/src/semantics/push-reactions.d.ts +101 -0
- package/dist/src/semantics/push-reactions.js +513 -0
- package/dist/src/semantics/releases.d.ts +19 -0
- package/dist/src/semantics/releases.js +231 -0
- package/dist/src/semantics/repo-invitations.d.ts +2 -0
- package/dist/src/semantics/repo-invitations.js +98 -0
- package/dist/src/semantics/repos.d.ts +17 -0
- package/dist/src/semantics/repos.js +390 -0
- package/dist/src/semantics/rulesets.d.ts +2 -0
- package/dist/src/semantics/rulesets.js +111 -0
- package/dist/src/semantics/search.d.ts +2 -0
- package/dist/src/semantics/search.js +84 -0
- package/dist/src/semantics/security.d.ts +2 -0
- package/dist/src/semantics/security.js +177 -0
- package/dist/src/semantics/shared.d.ts +61 -0
- package/dist/src/semantics/shared.js +151 -0
- package/dist/src/semantics/users.d.ts +2 -0
- package/dist/src/semantics/users.js +255 -0
- package/dist/src/semantics/webhooks.d.ts +2 -0
- package/dist/src/semantics/webhooks.js +107 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
- package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
- package/dist/test-fixtures/github-comment-schema.json +702 -0
- package/dist/test-fixtures/github-known-deviations.json +94 -0
- package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
- package/dist/test-fixtures/github-openapi-operations.json +362 -0
- package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
- package/dist/test-fixtures/github-pull-schema.json +3601 -0
- package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
- package/dist/test-fixtures/github-review-schema.json +236 -0
- package/package.json +20 -11
- package/src/cli.ts +7 -6
- package/src/generated/graphql-sdl.gen.json +1 -0
- package/src/generated/graphql.gen.json +1 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/github-a11y-snapshot.uitest.ts +5 -5
- package/src/github-budget.ts +4 -4
- package/src/github-capabilities.ts +1993 -556
- package/src/github-conformance.ts +12 -7
- package/src/github-connector.ts +108 -96
- package/src/github-events.ts +223 -95
- package/src/github-git-http.ts +47 -83
- package/src/github-git-plane.ts +247 -385
- package/src/github-journey.uitest.ts +28 -44
- package/src/github-mirror-state.ts +23 -61
- package/src/github-mirror-ui.ts +16 -10
- package/src/github-server.ts +158 -60
- package/src/github-shared.ts +1 -1
- package/src/github-twin.ts +1525 -4494
- package/src/github-ui-conformance.ts +7 -8
- package/src/github-ui-structure.ts +19 -18
- package/src/graphql-wire.ts +115 -0
- package/src/index.ts +47 -7
- package/src/manifest.ts +605 -0
- package/src/npm-registry.ts +43 -0
- package/src/screens/app-installation.tsx +217 -0
- package/src/screens/app-manifest.tsx +97 -0
- package/src/screens/oauth.tsx +258 -0
- package/src/screens/session.tsx +186 -0
- package/src/semantics/actions.ts +395 -0
- package/src/semantics/activity.ts +171 -0
- package/src/semantics/apps.ts +133 -0
- package/src/semantics/branches.ts +133 -0
- package/src/semantics/checks.ts +163 -0
- package/src/semantics/code-scanning-upload.ts +92 -0
- package/src/semantics/codespaces.ts +58 -0
- package/src/semantics/commits.ts +109 -0
- package/src/semantics/contents.ts +112 -0
- package/src/semantics/deployments.ts +116 -0
- package/src/semantics/gists.ts +85 -0
- package/src/semantics/git.ts +226 -0
- package/src/semantics/graphql.ts +505 -0
- package/src/semantics/index.ts +68 -0
- package/src/semantics/issues.ts +434 -0
- package/src/semantics/keys.ts +66 -0
- package/src/semantics/labels.ts +91 -0
- package/src/semantics/meta.ts +139 -0
- package/src/semantics/notifications.ts +58 -0
- package/src/semantics/orgs.ts +383 -0
- package/src/semantics/packages.ts +59 -0
- package/src/semantics/pages.ts +154 -0
- package/src/semantics/projects.ts +246 -0
- package/src/semantics/pulls.ts +421 -0
- package/src/semantics/push-reactions.ts +471 -0
- package/src/semantics/releases.ts +213 -0
- package/src/semantics/repo-invitations.ts +112 -0
- package/src/semantics/repos.ts +373 -0
- package/src/semantics/rulesets.ts +107 -0
- package/src/semantics/search.ts +83 -0
- package/src/semantics/security.ts +153 -0
- package/src/semantics/shared.ts +181 -0
- package/src/semantics/users.ts +252 -0
- package/src/semantics/webhooks.ts +101 -0
- package/test-fixtures/github-known-deviations.json +7 -8
- package/test-fixtures/github-openapi-operations.json +46 -227
- 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,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.)
|