@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.
@@ -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
- const refusal = worldEgressRefusal(url);
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 fetch(url, { method: 'POST', headers, body, redirect: 'manual', signal: AbortSignal.timeout(10_000) });
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 ?? new Date().toISOString();
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
- const returnTo = (raw) => (raw && raw.startsWith('/') && !raw.startsWith('//') ? raw : '/');
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.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.0",
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.0"
66
+ "@volter/world-core": "2.0.1"
67
67
  },
68
68
  "devDependencies": {
69
- "@volter/world-core": "2.0.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",
@@ -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
- const refusal = worldEgressRefusal(url);
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 fetch(url, { method: 'POST', headers, body, redirect: 'manual', signal: AbortSignal.timeout(10_000) });
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
 
@@ -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 ?? new Date().toISOString();
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: '^/@' },
@@ -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
- const returnTo = (raw: string | undefined): string => (raw && raw.startsWith('/') && !raw.startsWith('//') ? raw : '/');
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({