@openparachute/hub 0.7.4 → 0.7.5-rc.2

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/src/hub-server.ts CHANGED
@@ -239,6 +239,8 @@ import { CONFIG_DIR, SERVICES_MANIFEST_PATH } from "./config.ts";
239
239
  import { applyCorsHeaders, corsPreflightResponse, isCorsAllowedRoute } from "./cors.ts";
240
240
  import { ensureCsrfToken } from "./csrf.ts";
241
241
  import { readExposeState } from "./expose-state.ts";
242
+ import { notifySurfacePushed } from "./git-notify.ts";
243
+ import { handleGitTransport } from "./git-transport.ts";
242
244
  import { HUB_DEFAULT_PORT, HUB_SVC, clearHubPort, writeHubPort } from "./hub-control.ts";
243
245
  import {
244
246
  classifyDbError,
@@ -1212,6 +1214,12 @@ export interface HubFetchDeps {
1212
1214
  * the doc reflect `parachute vault create` etc. without re-running expose.
1213
1215
  */
1214
1216
  manifestPath?: string;
1217
+ /**
1218
+ * Directory holding the per-surface bare repos for the git-transport endpoint
1219
+ * (`/git/<name>/*` → `<gitRoot>/<name>.git`). Tests point this at a tmpdir;
1220
+ * production defaults to `<CONFIG_DIR>/hub/git`.
1221
+ */
1222
+ gitRoot?: string;
1215
1223
  /**
1216
1224
  * Path to `connections.json` (the Connections store, P5). Tests point this
1217
1225
  * at a tmpdir; production defaults to `<CONFIG_DIR>/connections.json`.
@@ -1983,6 +1991,7 @@ export function hubFetch(
1983
1991
  const getDb = deps?.getDb;
1984
1992
  const configuredIssuer = deps?.issuer;
1985
1993
  const manifestPath = deps?.manifestPath ?? SERVICES_MANIFEST_PATH;
1994
+ const gitRoot = deps?.gitRoot ?? join(CONFIG_DIR, "hub", "git");
1986
1995
  const spaDistDir = deps?.spaDistDir ?? defaultSpaDistDir();
1987
1996
  const loopbackPort = deps?.loopbackPort;
1988
1997
  const loadExposeHubOrigin =
@@ -3801,6 +3810,39 @@ export function hubFetch(
3801
3810
  return serveSpa(spaDistDir, pathname, "/admin");
3802
3811
  }
3803
3812
 
3813
+ // /git/<name>/* — hub-authenticated git smart-HTTP transport (Surface
3814
+ // Git Transport, Phase 0a). Placed BEFORE the generic services.json
3815
+ // proxy so a `/git/` route is never shadowed by a module mount. The
3816
+ // endpoint is AUTH-gated, not LAYER-gated: it's reachable from any
3817
+ // exposure layer because the hub JWT (validated against the multi-origin
3818
+ // iss-set) is the gate. It NEVER builds or executes the pushed tree — the
3819
+ // hub only receives + stores bytes (the RCE-bearing build is surface-host's
3820
+ // sandboxed job, Phase 0b). See src/git-transport.ts.
3821
+ if (pathname.startsWith("/git/")) {
3822
+ if (!getDb) return new Response("not found", { status: 404 });
3823
+ const db = getDb();
3824
+ const issuer = oauthDeps(req).issuer;
3825
+ return handleGitTransport(req, {
3826
+ db,
3827
+ gitRoot,
3828
+ knownIssuers: () => oauthDeps(req).hubBoundOrigins(),
3829
+ peerAddr,
3830
+ // Deploy hand-off (Phase 0b §5 step 5): on a successful push, notify
3831
+ // the surface module over HTTP + a hub JWT so it pulls + builds +
3832
+ // serves. NEVER a shell-out that builds the pushed tree — the hub
3833
+ // only sends the authenticated signal (git-notify.ts). Fire-and-
3834
+ // forget; a notify failure is logged, never surfaced to the pusher.
3835
+ onPushed: async (name) => {
3836
+ await notifySurfacePushed(name, {
3837
+ db,
3838
+ issuer: issuer ?? `http://127.0.0.1:${loopbackPort ?? 1939}`,
3839
+ resolveModuleOrigin: makeResolveModuleOrigin(manifestPath),
3840
+ cloneBaseOrigin: `http://127.0.0.1:${loopbackPort ?? 1939}`,
3841
+ });
3842
+ },
3843
+ });
3844
+ }
3845
+
3804
3846
  // Generic services.json-driven dispatch for non-vault modules. Reaches
3805
3847
  // here only after every hub-owned prefix above has had its turn — so
3806
3848
  // `/`, `/admin/*`, `/oauth/*`, `/.well-known/*`, `/hub/*`, `/vault/*`,
@@ -425,6 +425,7 @@ function oauthErrorRedirect(
425
425
  const OPTIONAL_MODULE_SCOPES: ReadonlyArray<readonly [prefix: string, short: string]> = [
426
426
  ["scribe:", "scribe"],
427
427
  ["agent:", "agent"],
428
+ ["surface:", "surface"],
428
429
  ];
429
430
 
430
431
  /**
@@ -46,12 +46,13 @@ export const SCOPE_EXPLANATIONS: Record<string, ScopeExplanation> = {
46
46
  "Read and write everything, plus admin: config & settings, triggers & automation, GitHub backup, and minting access tokens.",
47
47
  level: "admin",
48
48
  },
49
- // Optional-module scopes (scribe / agent). These are in FIRST_PARTY_SCOPES
50
- // (= Object.keys(this map)) but the modules may not be installed — so they're
51
- // GATED in `OPTIONAL_MODULE_SCOPES` (oauth-handlers.ts) and only advertised in
52
- // `scopes_supported` when the service is in services.json. If you add scopes
53
- // for another optional module here, add a matching gate there too, or a
54
- // vault-only hub will over-advertise them (the bug behind hub#489).
49
+ // Optional-module scopes (scribe / agent / surface). These are in
50
+ // FIRST_PARTY_SCOPES (= Object.keys(this map)) but the modules may not be
51
+ // installed — so they're GATED in `OPTIONAL_MODULE_SCOPES` (oauth-handlers.ts)
52
+ // and only advertised in `scopes_supported` when the service is in
53
+ // services.json. If you add scopes for another optional module here, add a
54
+ // matching gate there too, or a vault-only hub will over-advertise them (the
55
+ // bug behind hub#489).
55
56
  "scribe:transcribe": {
56
57
  label: "Send audio to Scribe for transcription.",
57
58
  level: "write",
@@ -64,6 +65,20 @@ export const SCOPE_EXPLANATIONS: Record<string, ScopeExplanation> = {
64
65
  label: "Post messages to your Agent.",
65
66
  level: "send",
66
67
  },
68
+ // Surface Git Transport scopes (surface-host). `surface:read` = clone/fetch a
69
+ // surface's hub-hosted git repo; `surface:write` = push to it. Named forms
70
+ // (`surface:<name>:<verb>`) collapse to these via the 3→2-segment rule in
71
+ // `isKnownScope`. surface-host's module.json declares them too; listing them
72
+ // here makes them first-party (mintable + a consent label) even on a hub
73
+ // where surface-host isn't installed.
74
+ "surface:read": {
75
+ label: "Clone and fetch a surface's source (its hub-hosted git repo).",
76
+ level: "read",
77
+ },
78
+ "surface:write": {
79
+ label: "Push to a surface's source (its hub-hosted git repo).",
80
+ level: "write",
81
+ },
67
82
  "hub:admin": {
68
83
  label: "Manage hub identity (user accounts, signing keys, registered OAuth clients).",
69
84
  level: "admin",