@volter/twin-github 2.0.0 → 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.
- package/dist/src/github-events.js +5 -3
- package/dist/src/github-git-http.js +2 -2
- package/dist/src/index.js +3 -0
- package/dist/src/screens/session.js +13 -2
- package/package.json +4 -4
- package/src/github-events.ts +5 -3
- package/src/github-git-http.ts +2 -2
- package/src/index.ts +3 -0
- package/src/screens/session.tsx +9 -2
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
import { createHash, createHmac } from 'node:crypto';
|
|
4
4
|
import { applyTwinWrite, applyTwinWriteAtomic, ownFields, twinResources, worldNow } from '@volter/world-core';
|
|
5
5
|
import { GITHUB_WORLD_USER, githubApps, githubState, handleGithubRequest, toGithubWebhook } from "./github-twin.js";
|
|
6
|
+
import { appDestination, appFetch } from '@volter/world-core/app-route';
|
|
6
7
|
import { worldEgressRefusal } from '@volter/world-core/network-policy';
|
|
7
8
|
const TYPE = 'webhook_delivery';
|
|
8
9
|
export function signGithubPayload(secret, body) {
|
|
9
10
|
return `sha256=${createHmac('sha256', secret).update(body).digest('hex')}`;
|
|
10
11
|
}
|
|
11
12
|
const httpDelivery = async (url, _event, _payload, headers, body) => {
|
|
12
|
-
// the World's egress rule: a refused hook URL fails as a transport failure, recorded on the delivery
|
|
13
|
-
|
|
13
|
+
// the World's egress rule: a refused hook URL fails as a transport failure, recorded on the delivery; the
|
|
14
|
+
// application's own hostnames reach it inside the World. GitHub follows no redirect: the 3xx is the delivery's answer.
|
|
15
|
+
const refusal = appDestination(url) ? null : worldEgressRefusal(url);
|
|
14
16
|
if (refusal !== null)
|
|
15
17
|
throw new Error(refusal);
|
|
16
|
-
const response = await
|
|
18
|
+
const response = await appFetch(url, { method: 'POST', headers, body, redirect: 'manual', signal: AbortSignal.timeout(10_000) });
|
|
17
19
|
return { status: response.status, headers: Object.fromEntries(response.headers), body: await response.text() };
|
|
18
20
|
};
|
|
19
21
|
async function nativeObject(path, root) {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
//
|
|
31
31
|
// Offline + deterministic: everything here is the kernel's git library over the World's
|
|
32
32
|
// blob store (no network, no rate budget, no `git` binary).
|
|
33
|
-
import { applyTwinWrite, git } from '@volter/world-core';
|
|
33
|
+
import { applyTwinWrite, git, worldNow } from '@volter/world-core';
|
|
34
34
|
const { serveSmartHttp, serveSmartHttpPost } = git;
|
|
35
35
|
import { ensureBareRepo, hasBareRepo, listGitPlaneRefs, planeRepo } from "./github-git-plane.js";
|
|
36
36
|
import { closePrsForDeletedBranch, githubState, kernelAtomic, syncPrHeadsToBranchTip } from "./github-twin.js";
|
|
@@ -134,7 +134,7 @@ let reconcileSeq = 0; // counter: strictly increasing within the process; never
|
|
|
134
134
|
*/
|
|
135
135
|
export async function reconcileGitPlaneRefs(repo, before, opts = {}) {
|
|
136
136
|
const root = opts.root;
|
|
137
|
-
const occurredAt = opts.occurredAt ??
|
|
137
|
+
const occurredAt = opts.occurredAt ?? worldNow();
|
|
138
138
|
const after = await listGitPlaneRefs(repo, root);
|
|
139
139
|
const beforeBy = new Map(before.map((r) => [r.ref, r]));
|
|
140
140
|
const afterBy = new Map(after.map((r) => [r.ref, r]));
|
package/dist/src/index.js
CHANGED
|
@@ -91,6 +91,9 @@ export const pack = {
|
|
|
91
91
|
{ host: 'uploads.github.com', pathPattern: '^/repos/[^/]+/[^/]+/releases/\\d+/assets$' },
|
|
92
92
|
{ host: 'github.com', pathPattern: '^/[^/]+/[^/]+/(info/refs|git-upload-pack|git-receive-pack)$' },
|
|
93
93
|
{ host: 'github.com', pathPattern: '^/login/oauth/(authorize|access_token)$' },
|
|
94
|
+
// the sign-in the consent and the other pages send a visitor who is not signed in to, and its two-factor step
|
|
95
|
+
// (src/screens/session.tsx); without these a browser following the redirect leaves the World
|
|
96
|
+
{ host: 'github.com', pathPattern: '^/(login|session|sessions/two-factor(/app)?)$' },
|
|
94
97
|
{ host: 'github.com', pathPattern: '^/apps/[^/]+/installations/new$' },
|
|
95
98
|
{ host: 'github.com', pathPattern: '^/(organizations/[^/]+/)?settings/apps/new$' },
|
|
96
99
|
{ host: 'npm.pkg.github.com', pathPattern: '^/@' },
|
|
@@ -49,8 +49,19 @@ export function toSignIn(request, suggested, page) {
|
|
|
49
49
|
const url = new URL(request.url);
|
|
50
50
|
return new Response(null, { status: 302, headers: { location: `/login?return_to=${encodeURIComponent(page ?? url.pathname + url.search)}${suggested ? `&login=${encodeURIComponent(suggested)}` : ''}` } });
|
|
51
51
|
}
|
|
52
|
-
/** A return_to GitHub follows: a path on github.com, never another site.
|
|
53
|
-
|
|
52
|
+
/** A return_to GitHub follows: a path on github.com, never another site. Resolved as a browser resolves it (a backslash
|
|
53
|
+
* or a control character can turn `/\evil.test` into another origin), and kept only when it stays on github.com. */
|
|
54
|
+
function returnTo(raw) {
|
|
55
|
+
if (!raw || !raw.startsWith('/') || /[\u0000-\u001f\\]/.test(raw))
|
|
56
|
+
return '/';
|
|
57
|
+
try {
|
|
58
|
+
const url = new URL(raw, 'https://github.com');
|
|
59
|
+
return url.origin === 'https://github.com' ? url.pathname + url.search : '/';
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return '/';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
54
65
|
function page(fields, error) {
|
|
55
66
|
return flowPage({
|
|
56
67
|
title: 'Sign in to GitHub · GitHub',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-github",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Local GitHub twin — PR-evidence REST; your real `@octokit/rest` talks to it unmodified. Mirror, simulate, and fork. Built on @volter/world-core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -57,16 +57,16 @@
|
|
|
57
57
|
"postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@volter/world-ui": "0.1.
|
|
60
|
+
"@volter/world-ui": "0.1.1",
|
|
61
61
|
"react": "^19.2.7",
|
|
62
62
|
"react-dom": "^19.2.7",
|
|
63
63
|
"graphql": "16.14.2"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@volter/world-core": "2.0.
|
|
66
|
+
"@volter/world-core": "2.0.1"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@volter/world-core": "2.0.
|
|
69
|
+
"@volter/world-core": "2.0.1",
|
|
70
70
|
"@volter/world-tooling": "0.1.0",
|
|
71
71
|
"@octokit/rest": "^22.0.1",
|
|
72
72
|
"playwright": "^1.61.0",
|
package/src/github-events.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createHash, createHmac } from 'node:crypto';
|
|
|
4
4
|
import { applyTwinWrite, applyTwinWriteAtomic, ownFields, twinResources, worldNow } from '@volter/world-core';
|
|
5
5
|
import { GITHUB_WORLD_USER, githubApps, githubState, handleGithubRequest, toGithubWebhook } from './github-twin.ts';
|
|
6
6
|
import type { GithubWebhook as Hook, GithubWriteEvent } from './github-twin.ts';
|
|
7
|
+
import { appDestination, appFetch } from '@volter/world-core/app-route';
|
|
7
8
|
import { worldEgressRefusal } from '@volter/world-core/network-policy';
|
|
8
9
|
|
|
9
10
|
export type GithubWebhook = Record<string, unknown>;
|
|
@@ -23,10 +24,11 @@ export function signGithubPayload(secret: string, body: string): string {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const httpDelivery: GithubWebhookDelivery = async (url, _event, _payload, headers, body) => {
|
|
26
|
-
// the World's egress rule: a refused hook URL fails as a transport failure, recorded on the delivery
|
|
27
|
-
|
|
27
|
+
// the World's egress rule: a refused hook URL fails as a transport failure, recorded on the delivery; the
|
|
28
|
+
// application's own hostnames reach it inside the World. GitHub follows no redirect: the 3xx is the delivery's answer.
|
|
29
|
+
const refusal = appDestination(url) ? null : worldEgressRefusal(url);
|
|
28
30
|
if (refusal !== null) throw new Error(refusal);
|
|
29
|
-
const response = await
|
|
31
|
+
const response = await appFetch(url, { method: 'POST', headers, body, redirect: 'manual', signal: AbortSignal.timeout(10_000) });
|
|
30
32
|
return { status: response.status, headers: Object.fromEntries(response.headers), body: await response.text() };
|
|
31
33
|
};
|
|
32
34
|
|
package/src/github-git-http.ts
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
//
|
|
31
31
|
// Offline + deterministic: everything here is the kernel's git library over the World's
|
|
32
32
|
// blob store (no network, no rate budget, no `git` binary).
|
|
33
|
-
import { applyTwinWrite, git } from '@volter/world-core';
|
|
33
|
+
import { applyTwinWrite, git, worldNow } from '@volter/world-core';
|
|
34
34
|
const { serveSmartHttp, serveSmartHttpPost } = git;
|
|
35
35
|
import { ensureBareRepo, hasBareRepo, listGitPlaneRefs, planeRepo, type PlaneRef } from './github-git-plane.ts';
|
|
36
36
|
import { closePrsForDeletedBranch, githubState, kernelAtomic, syncPrHeadsToBranchTip, type GithubWriteEvent } from './github-twin.ts';
|
|
@@ -161,7 +161,7 @@ export async function reconcileGitPlaneRefs(
|
|
|
161
161
|
opts: { root?: string; occurredAt?: string } = {},
|
|
162
162
|
): Promise<GithubWriteEvent[]> {
|
|
163
163
|
const root = opts.root;
|
|
164
|
-
const occurredAt = opts.occurredAt ??
|
|
164
|
+
const occurredAt = opts.occurredAt ?? worldNow();
|
|
165
165
|
const after = await listGitPlaneRefs(repo, root);
|
|
166
166
|
const beforeBy = new Map(before.map((r) => [r.ref, r]));
|
|
167
167
|
const afterBy = new Map(after.map((r) => [r.ref, r]));
|
package/src/index.ts
CHANGED
|
@@ -118,6 +118,9 @@ export const pack: TwinPack = {
|
|
|
118
118
|
{ host: 'uploads.github.com', pathPattern: '^/repos/[^/]+/[^/]+/releases/\\d+/assets$' },
|
|
119
119
|
{ host: 'github.com', pathPattern: '^/[^/]+/[^/]+/(info/refs|git-upload-pack|git-receive-pack)$' },
|
|
120
120
|
{ host: 'github.com', pathPattern: '^/login/oauth/(authorize|access_token)$' },
|
|
121
|
+
// the sign-in the consent and the other pages send a visitor who is not signed in to, and its two-factor step
|
|
122
|
+
// (src/screens/session.tsx); without these a browser following the redirect leaves the World
|
|
123
|
+
{ host: 'github.com', pathPattern: '^/(login|session|sessions/two-factor(/app)?)$' },
|
|
121
124
|
{ host: 'github.com', pathPattern: '^/apps/[^/]+/installations/new$' },
|
|
122
125
|
{ host: 'github.com', pathPattern: '^/(organizations/[^/]+/)?settings/apps/new$' },
|
|
123
126
|
{ host: 'npm.pkg.github.com', pathPattern: '^/@' },
|
package/src/screens/session.tsx
CHANGED
|
@@ -53,8 +53,15 @@ export function toSignIn(request: Request, suggested?: string, page?: string): R
|
|
|
53
53
|
return new Response(null, { status: 302, headers: { location: `/login?return_to=${encodeURIComponent(page ?? url.pathname + url.search)}${suggested ? `&login=${encodeURIComponent(suggested)}` : ''}` } });
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/** A return_to GitHub follows: a path on github.com, never another site.
|
|
57
|
-
|
|
56
|
+
/** A return_to GitHub follows: a path on github.com, never another site. Resolved as a browser resolves it (a backslash
|
|
57
|
+
* or a control character can turn `/\evil.test` into another origin), and kept only when it stays on github.com. */
|
|
58
|
+
function returnTo(raw: string | undefined): string {
|
|
59
|
+
if (!raw || !raw.startsWith('/') || /[\u0000-\u001f\\]/.test(raw)) return '/';
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(raw, 'https://github.com');
|
|
62
|
+
return url.origin === 'https://github.com' ? url.pathname + url.search : '/';
|
|
63
|
+
} catch { return '/'; }
|
|
64
|
+
}
|
|
58
65
|
|
|
59
66
|
function page(fields: { return_to: string; login?: string }, error?: string): Response {
|
|
60
67
|
return flowPage({
|