@ggui-ai/registry-server 0.7.0 → 0.8.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 CHANGED
@@ -42,7 +42,7 @@ ggui gadget publish --auth=bearer --token=$GGUI_REGISTRY_TOKEN \
42
42
  | GET | `/bundles/:scope/:name/:version/bundle.js.sig` | none | Serve the signature envelope (immutable cache) |
43
43
  | GET | `/bundles/:scope/:name/:version/manifest.json` | none | Serve the manifest verbatim (immutable cache) |
44
44
 
45
- \* Reads of `visibility: 'private'` rows require bearer; unauthenticated callers see only public rows.
45
+ \* Reads of `visibility: 'private'` rows are ownership-gated: a private version is served only to the verified caller who published it or who owns the artifact's scope. Every other caller — unauthenticated included — receives the same `not_found` response as a genuinely missing artifact, and the version list filters unreadable versions (answering `not_found` when none are visible), so responses never reveal that a private artifact exists.
46
46
 
47
47
  ## Storage modes
48
48
 
@@ -60,7 +60,7 @@ This server is **MVP-scoped** for self-hosters who want the marketplace surface
60
60
  - Rate limiting (use a reverse proxy)
61
61
  - TLS termination (use a reverse proxy)
62
62
  - Backup automation (the filesystem mode is single-machine)
63
- - Per-org private artifact scoping (treat `visibility: 'private'` as a label, not enforcement)
63
+ - Per-org private artifact scoping (enforcement is per-subject — publisher or scope owner; org-membership admission is not implemented)
64
64
  - Sigstore transparency log integration
65
65
 
66
66
  If you need any of the above, build your own transport on top of [`@ggui-ai/registry-core`](https://github.com/ggui-ai/ggui/tree/main/packages/registry-core).
@@ -1 +1 @@
1
- {"version":3,"file":"filesystem-bundle-storage.d.ts","sourceRoot":"","sources":["../src/filesystem-bundle-storage.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE5D,MAAM,WAAW,8BAA8B;IAC7C,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,8BAA8B,GACtC,aAAa,CA+Ef"}
1
+ {"version":3,"file":"filesystem-bundle-storage.d.ts","sourceRoot":"","sources":["../src/filesystem-bundle-storage.ts"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,wBAAwB,CAAC;AAExE,MAAM,WAAW,8BAA8B;IAC7C,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,8BAA8B,GACtC,aAAa,CA0Ff"}
@@ -1,17 +1,20 @@
1
1
  /**
2
2
  * Filesystem-backed {@link BundleStorage} for the OSS registry server.
3
3
  *
4
- * Blob layout:
4
+ * Blob layout (H1 visibility split — placement is the access-control
5
+ * boundary):
5
6
  *
6
- * <root>/bundles/<scope>/<name>/<version>/bundle.js
7
- * <root>/bundles/<scope>/<name>/<version>/bundle.js.sig
8
- * <root>/bundles/<scope>/<name>/<version>/manifest.json
7
+ * <root>/bundles/public/<scope>/<name>/<version>/bundle.js
8
+ * <root>/bundles/public/<scope>/<name>/<version>/bundle.js.sig
9
+ * <root>/bundles/public/<scope>/<name>/<version>/manifest.json
10
+ * <root>/bundles/private/<scope>/<name>/<version>/{...}
9
11
  *
10
12
  * Scope is written verbatim (`@my-org/...`) because the leading `@`
11
13
  * is filename-safe on every supported OS. The URL composition mirrors
12
- * the same path so the public URL the install CLI follows resolves
13
- * directly against the file (the hono server's `/bundles/*` route
14
- * streams the file).
14
+ * the same path so the URL the install CLI follows resolves directly
15
+ * against the file: the hono server serves `/bundles/public/*`
16
+ * anonymously and `/bundles/private/*` through its authenticated
17
+ * bundle route.
15
18
  *
16
19
  * ## Path-traversal defense
17
20
  *
@@ -40,31 +43,39 @@ import { isGadgetSignature } from '@ggui-ai/gadget-signing';
40
43
  export function createFilesystemBundleStorage(options) {
41
44
  const bundlesRoot = join(options.root, 'bundles');
42
45
  const bundleHost = stripTrailingSlash(options.bundleHost);
43
- const dirFor = (scope, name, version) => {
46
+ const dirFor = (scope, name, version, visibility) => {
47
+ // `visibility` is a closed union ('public' | 'private') — no
48
+ // traversal surface; the free-form triple still gets the check.
44
49
  rejectTraversal(scope, 'scope');
45
50
  rejectTraversal(name, 'name');
46
51
  rejectTraversal(version, 'version');
47
- return join(bundlesRoot, scope, name, version);
52
+ return join(bundlesRoot, visibility, scope, name, version);
53
+ };
54
+ const urlFor = (scope, name, version, visibility, file) => {
55
+ rejectTraversal(scope, 'scope');
56
+ rejectTraversal(name, 'name');
57
+ rejectTraversal(version, 'version');
58
+ return `${bundleHost}/bundles/${visibility}/${scope}/${name}/${version}/${file}`;
48
59
  };
49
60
  return {
50
- async putBundle(scope, name, version, bytes) {
51
- const dir = dirFor(scope, name, version);
61
+ async putBundle(scope, name, version, visibility, bytes) {
62
+ const dir = dirFor(scope, name, version, visibility);
52
63
  await mkdir(dir, { recursive: true });
53
64
  await writeFile(join(dir, 'bundle.js'), bytes);
54
- return this.bundleUrl(scope, name, version);
65
+ return this.bundleUrl(scope, name, version, visibility);
55
66
  },
56
- async getBundle(scope, name, version) {
57
- const path = join(dirFor(scope, name, version), 'bundle.js');
67
+ async getBundle(scope, name, version, visibility) {
68
+ const path = join(dirFor(scope, name, version, visibility), 'bundle.js');
58
69
  return readFileOrNull(path);
59
70
  },
60
- async putSignature(scope, name, version, signature) {
61
- const dir = dirFor(scope, name, version);
71
+ async putSignature(scope, name, version, visibility, signature) {
72
+ const dir = dirFor(scope, name, version, visibility);
62
73
  await mkdir(dir, { recursive: true });
63
74
  await writeFile(join(dir, 'bundle.js.sig'), JSON.stringify(signature, null, 2), 'utf8');
64
- return this.signatureUrl(scope, name, version);
75
+ return this.signatureUrl(scope, name, version, visibility);
65
76
  },
66
- async getSignature(scope, name, version) {
67
- const path = join(dirFor(scope, name, version), 'bundle.js.sig');
77
+ async getSignature(scope, name, version, visibility) {
78
+ const path = join(dirFor(scope, name, version, visibility), 'bundle.js.sig');
68
79
  const text = await readTextOrNull(path);
69
80
  if (text === null)
70
81
  return null;
@@ -78,34 +89,25 @@ export function createFilesystemBundleStorage(options) {
78
89
  return null;
79
90
  return parsed;
80
91
  },
81
- async putManifest(scope, name, version, manifest) {
82
- const dir = dirFor(scope, name, version);
92
+ async putManifest(scope, name, version, visibility, manifest) {
93
+ const dir = dirFor(scope, name, version, visibility);
83
94
  await mkdir(dir, { recursive: true });
84
95
  await writeFile(join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
85
- return this.manifestUrl(scope, name, version);
96
+ return this.manifestUrl(scope, name, version, visibility);
86
97
  },
87
- async getManifest(scope, name, version) {
88
- const path = join(dirFor(scope, name, version), 'manifest.json');
98
+ async getManifest(scope, name, version, visibility) {
99
+ const path = join(dirFor(scope, name, version, visibility), 'manifest.json');
89
100
  const text = await readTextOrNull(path);
90
101
  return text === null ? null : JSON.parse(text);
91
102
  },
92
- bundleUrl(scope, name, version) {
93
- rejectTraversal(scope, 'scope');
94
- rejectTraversal(name, 'name');
95
- rejectTraversal(version, 'version');
96
- return `${bundleHost}/bundles/${scope}/${name}/${version}/bundle.js`;
103
+ bundleUrl(scope, name, version, visibility) {
104
+ return urlFor(scope, name, version, visibility, 'bundle.js');
97
105
  },
98
- signatureUrl(scope, name, version) {
99
- rejectTraversal(scope, 'scope');
100
- rejectTraversal(name, 'name');
101
- rejectTraversal(version, 'version');
102
- return `${bundleHost}/bundles/${scope}/${name}/${version}/bundle.js.sig`;
106
+ signatureUrl(scope, name, version, visibility) {
107
+ return urlFor(scope, name, version, visibility, 'bundle.js.sig');
103
108
  },
104
- manifestUrl(scope, name, version) {
105
- rejectTraversal(scope, 'scope');
106
- rejectTraversal(name, 'name');
107
- rejectTraversal(version, 'version');
108
- return `${bundleHost}/bundles/${scope}/${name}/${version}/manifest.json`;
109
+ manifestUrl(scope, name, version, visibility) {
110
+ return urlFor(scope, name, version, visibility, 'manifest.json');
109
111
  },
110
112
  };
111
113
  }
@@ -1 +1 @@
1
- {"version":3,"file":"filesystem-registry-storage.d.ts","sourceRoot":"","sources":["../src/filesystem-registry-storage.ts"],"names":[],"mappings":"AAoEA,OAAO,EAOL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAKhC,MAAM,WAAW,gCAAgC;IAC/C,iFAAiF;IACjF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,gCAAgC,GACxC,eAAe,CA2SjB"}
1
+ {"version":3,"file":"filesystem-registry-storage.d.ts","sourceRoot":"","sources":["../src/filesystem-registry-storage.ts"],"names":[],"mappings":"AAoEA,OAAO,EAQL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAKhC,MAAM,WAAW,gCAAgC;IAC/C,iFAAiF;IACjF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,gCAAgC,GACxC,eAAe,CA2SjB"}
@@ -66,7 +66,7 @@
66
66
  */
67
67
  import { mkdir, readFile, readdir, unlink, writeFile } from 'node:fs/promises';
68
68
  import { dirname, join } from 'node:path';
69
- import { AuthorKeyAlreadyExistsError, } from '@ggui-ai/registry-core';
69
+ import { AuthorKeyAlreadyExistsError, matchesMcpToolFilters, } from '@ggui-ai/registry-core';
70
70
  const DEFAULT_LIMIT = 50;
71
71
  const MAX_LIMIT = 200;
72
72
  export function createFilesystemRegistryStorage(options) {
@@ -448,13 +448,17 @@ function parseCursor(cursor) {
448
448
  * Mirrors {@link inMemoryRegistryStorage}'s filter — same
449
449
  * AND-composition, same case-insensitive substring match on q, same
450
450
  * publisher-sub OR author-name match. Kept inline so the filesystem
451
- * impl doesn't depend on a private helper from registry-core.
451
+ * impl doesn't depend on a private helper from registry-core; the
452
+ * tool/server dimensions delegate to the PUBLIC shared predicate
453
+ * `matchesMcpToolFilters` so the reference semantics cannot drift.
452
454
  */
453
455
  function rowMatchesFilter(row, q) {
454
456
  if (q.kind !== undefined && row.kind !== q.kind)
455
457
  return false;
456
458
  if (q.hook !== undefined && row.hook !== q.hook)
457
459
  return false;
460
+ if (!matchesMcpToolFilters(row.mcpTools, q))
461
+ return false;
458
462
  if (q.tag !== undefined) {
459
463
  if (row.tags === undefined || !row.tags.includes(q.tag))
460
464
  return false;
package/dist/index.d.ts CHANGED
@@ -46,6 +46,13 @@ export interface CreateRegistryServerOptions {
46
46
  * {@link RegistryAppOptions.sigstoreTuf}).
47
47
  */
48
48
  readonly sigstoreTuf?: PublishArtifactDeps['sigstoreTuf'];
49
+ /**
50
+ * Optional verified-email lookup for the publish gate's identity
51
+ * binding. Forwarded to `createRegistryApp` (see
52
+ * {@link RegistryAppOptions.verifiedEmailResolver} for the default
53
+ * allowlist-only posture when unset).
54
+ */
55
+ readonly verifiedEmailResolver?: PublishArtifactDeps['verifiedEmailResolver'];
49
56
  }
50
57
  export interface RegistryServerHandle {
51
58
  /** The port the server is actually bound to. Resolves after {@link start}. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,eAAe,EAChB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,iBAAiB,EAAE,KAAK,WAAW,EAAE,KAAK,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACvG,OAAO,EACL,6BAA6B,EAC7B,KAAK,8BAA8B,GACpC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,+BAA+B,EAC/B,KAAK,gCAAgC,GACtC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEzE,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAC,aAAa,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,oBAAoB;IACnC,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,2BAA2B,GACnC,oBAAoB,CAoDtB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,eAAe,EAChB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,iBAAiB,EAAE,KAAK,WAAW,EAAE,KAAK,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACvG,OAAO,EACL,6BAA6B,EAC7B,KAAK,8BAA8B,GACpC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,+BAA+B,EAC/B,KAAK,gCAAgC,GACtC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEzE,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAC,aAAa,CAAC,CAAC;IAC1D;;;;;OAKG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,mBAAmB,CAAC,uBAAuB,CAAC,CAAC;CAC/E;AAED,MAAM,WAAW,oBAAoB;IACnC,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,2BAA2B,GACnC,oBAAoB,CAuDtB"}
package/dist/index.js CHANGED
@@ -37,6 +37,9 @@ export function createRegistryServer(options) {
37
37
  ...(options.sigstoreTuf !== undefined
38
38
  ? { sigstoreTuf: options.sigstoreTuf }
39
39
  : {}),
40
+ ...(options.verifiedEmailResolver !== undefined
41
+ ? { verifiedEmailResolver: options.verifiedEmailResolver }
42
+ : {}),
40
43
  });
41
44
  const host = options.host ?? '0.0.0.0';
42
45
  const requestedPort = options.port ?? 9001;
package/dist/server.d.ts CHANGED
@@ -10,21 +10,40 @@
10
10
  * GET /author-keys → listAuthorKeys (bearer-gated)
11
11
  * DELETE /author-keys/:keyId → deleteAuthorKey (bearer-gated)
12
12
  * POST /conformance/check → checkConformance (pre-flight gate)
13
- * GET /bundles/:scope/:name/:version/bundle.js
14
- * GET /bundles/:scope/:name/:version/bundle.js.sig
15
- * GET /bundles/:scope/:name/:version/manifest.json
13
+ * GET /bundles/public/:scope/:name/:version/bundle.js (anonymous)
14
+ * GET /bundles/public/:scope/:name/:version/bundle.js.sig (anonymous)
15
+ * GET /bundles/public/:scope/:name/:version/manifest.json (anonymous)
16
+ * GET /bundles/private/:scope/:name/:version/bundle.js (bearer-gated)
17
+ * GET /bundles/private/:scope/:name/:version/bundle.js.sig (bearer-gated)
18
+ * GET /bundles/private/:scope/:name/:version/manifest.json (bearer-gated)
16
19
  *
17
20
  * Bundle / signature / manifest serves go through {@link BundleStorage}
18
21
  * directly so the operator can plug a different storage backend
19
22
  * (memory for tests; filesystem for self-hosting) without rewriting
20
- * the route table. The server always emits
21
- * `Cache-Control: public, max-age=31536000, immutable` on these
22
- * routes — bundles are SRI-pinned + immutable post-publish.
23
+ * the route table.
24
+ *
25
+ * H1 visibility split: blob placement mirrors the manifest's
26
+ * `visibility` (`bundles/public/…` vs `bundles/private/…`), and the
27
+ * ROUTE decides the auth posture — the public prefix serves
28
+ * anonymously; the private prefix requires a bearer AND authorizes the
29
+ * caller against the artifact's publisher or the scope owner (403
30
+ * otherwise; unknown triples answer 404). A blob is only findable
31
+ * through the prefix it was published under, so a private artifact can
32
+ * never be fetched via the anonymous route.
33
+ *
34
+ * Cache headers: public-prefix responses emit
35
+ * `Cache-Control: public, max-age=31536000, immutable` (bundles are
36
+ * SRI-pinned + immutable post-publish); private-prefix responses stay
37
+ * immutable but non-shared-cacheable
38
+ * (`Cache-Control: private, max-age=31536000, immutable`).
23
39
  *
24
40
  * CORS: permissive on public-read routes (`/search`, `/pkg/*`,
25
- * `/bundles/*`, `/conformance/check`); strict on `/publish` (same-origin
26
- * or no Origin header — the install CLI runs server-to-server, not
27
- * browser-side).
41
+ * `/bundles/public/*`, `/conformance/check`); strict on `/publish`
42
+ * (same-origin or no Origin header — the install CLI runs
43
+ * server-to-server, not browser-side) AND on `/bundles/private/*`
44
+ * (per-caller authorized content — no ACAO, no permissive preflight;
45
+ * browser consumers of private bundles go through presigned URLs
46
+ * rather than cross-origin credentialed fetches).
28
47
  */
29
48
  import { Hono } from 'hono';
30
49
  import { type BlueprintProbeRunner, type PublishArtifactDeps, type BundleStorage, type RegistryStorage } from '@ggui-ai/registry-core';
@@ -53,6 +72,16 @@ export interface RegistryAppOptions {
53
72
  * the sigstore public-good trust root.
54
73
  */
55
74
  readonly sigstoreTuf?: PublishArtifactDeps['sigstoreTuf'];
75
+ /**
76
+ * Optional verified-email lookup for the publish gate's F4 identity
77
+ * binding — see {@link PublishArtifactDeps.verifiedEmailResolver}
78
+ * for the contract. Unset (the default here): publisher identity is
79
+ * enforced only on scopes whose ownership row carries a
80
+ * `sanAllowlist`; scopes without one accept any identity a valid
81
+ * sigstore bundle proves. Wire a resolver backed by your identity
82
+ * layer to bind default publishes to account emails.
83
+ */
84
+ readonly verifiedEmailResolver?: PublishArtifactDeps['verifiedEmailResolver'];
56
85
  }
57
86
  /**
58
87
  * Build the hono app. Does NOT bind a port — callers (the CLI, the
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAQL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EAGxB,KAAK,aAAa,EAIlB,KAAK,eAAe,EACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIrD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,6EAA6E;IAC7E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAC,aAAa,CAAC,CAAC;CAC3D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAkSnE"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAUL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EAGxB,KAAK,aAAa,EAIlB,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AASrD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,6EAA6E;IAC7E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAC,aAAa,CAAC,CAAC;IAC1D;;;;;;;;OAQG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,mBAAmB,CAAC,uBAAuB,CAAC,CAAC;CAC/E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAyXnE"}
package/dist/server.js CHANGED
@@ -10,25 +10,49 @@
10
10
  * GET /author-keys → listAuthorKeys (bearer-gated)
11
11
  * DELETE /author-keys/:keyId → deleteAuthorKey (bearer-gated)
12
12
  * POST /conformance/check → checkConformance (pre-flight gate)
13
- * GET /bundles/:scope/:name/:version/bundle.js
14
- * GET /bundles/:scope/:name/:version/bundle.js.sig
15
- * GET /bundles/:scope/:name/:version/manifest.json
13
+ * GET /bundles/public/:scope/:name/:version/bundle.js (anonymous)
14
+ * GET /bundles/public/:scope/:name/:version/bundle.js.sig (anonymous)
15
+ * GET /bundles/public/:scope/:name/:version/manifest.json (anonymous)
16
+ * GET /bundles/private/:scope/:name/:version/bundle.js (bearer-gated)
17
+ * GET /bundles/private/:scope/:name/:version/bundle.js.sig (bearer-gated)
18
+ * GET /bundles/private/:scope/:name/:version/manifest.json (bearer-gated)
16
19
  *
17
20
  * Bundle / signature / manifest serves go through {@link BundleStorage}
18
21
  * directly so the operator can plug a different storage backend
19
22
  * (memory for tests; filesystem for self-hosting) without rewriting
20
- * the route table. The server always emits
21
- * `Cache-Control: public, max-age=31536000, immutable` on these
22
- * routes — bundles are SRI-pinned + immutable post-publish.
23
+ * the route table.
24
+ *
25
+ * H1 visibility split: blob placement mirrors the manifest's
26
+ * `visibility` (`bundles/public/…` vs `bundles/private/…`), and the
27
+ * ROUTE decides the auth posture — the public prefix serves
28
+ * anonymously; the private prefix requires a bearer AND authorizes the
29
+ * caller against the artifact's publisher or the scope owner (403
30
+ * otherwise; unknown triples answer 404). A blob is only findable
31
+ * through the prefix it was published under, so a private artifact can
32
+ * never be fetched via the anonymous route.
33
+ *
34
+ * Cache headers: public-prefix responses emit
35
+ * `Cache-Control: public, max-age=31536000, immutable` (bundles are
36
+ * SRI-pinned + immutable post-publish); private-prefix responses stay
37
+ * immutable but non-shared-cacheable
38
+ * (`Cache-Control: private, max-age=31536000, immutable`).
23
39
  *
24
40
  * CORS: permissive on public-read routes (`/search`, `/pkg/*`,
25
- * `/bundles/*`, `/conformance/check`); strict on `/publish` (same-origin
26
- * or no Origin header — the install CLI runs server-to-server, not
27
- * browser-side).
41
+ * `/bundles/public/*`, `/conformance/check`); strict on `/publish`
42
+ * (same-origin or no Origin header — the install CLI runs
43
+ * server-to-server, not browser-side) AND on `/bundles/private/*`
44
+ * (per-caller authorized content — no ACAO, no permissive preflight;
45
+ * browser consumers of private bundles go through presigned URLs
46
+ * rather than cross-origin credentialed fetches).
28
47
  */
29
48
  import { Hono } from 'hono';
30
- import { checkConformance, deleteAuthorKey, unauthorizedErrorBody, listArtifactVersions, listAuthorKeys, publishArtifact, registerAuthorKey, readArtifact, searchArtifacts, } from '@ggui-ai/registry-core';
49
+ import { canReadPrivateArtifact, checkConformance, createScopeOwnerResolver, deleteAuthorKey, unauthorizedErrorBody, listArtifactVersions, listAuthorKeys, publishArtifact, registerAuthorKey, readArtifact, searchArtifacts, } from '@ggui-ai/registry-core';
31
50
  const IMMUTABLE_CACHE_HEADER = 'public, max-age=31536000, immutable';
51
+ /**
52
+ * Private-prefix responses are immutable too, but MUST NOT land in
53
+ * shared caches — the authorization decision is per-caller.
54
+ */
55
+ const PRIVATE_IMMUTABLE_CACHE_HEADER = 'private, max-age=31536000, immutable';
32
56
  /**
33
57
  * Build the hono app. Does NOT bind a port — callers (the CLI, the
34
58
  * `createRegistryServer` factory, programmatic embeds) decide how to
@@ -40,18 +64,24 @@ export function createRegistryApp(options) {
40
64
  const clock = options.clock ?? (() => new Date());
41
65
  const blueprintProbe = options.blueprintProbe;
42
66
  const sigstoreTuf = options.sigstoreTuf;
67
+ const verifiedEmailResolver = options.verifiedEmailResolver;
43
68
  const app = new Hono();
44
69
  // Shared bearer gate for every authed route — ONE verification call
45
70
  // shape, ONE 401 body (`unauthorizedErrorBody`, exported beside the
46
71
  // registry-core error enums so every transport of this wire contract
47
72
  // agrees).
48
73
  const verifyBearer = (c) => authn.verify(c.req.header('authorization'));
49
- // CORS — permissive read; strict for /publish. Adds ACAO headers
50
- // AFTER the route runs so they overlay on the route's response.
74
+ // CORS — permissive read; strict for /publish AND the private
75
+ // bundle prefix (review finding 5). Private bundles are per-caller
76
+ // authorized content: no cross-origin page has any business reading
77
+ // them, and the browser consumers that need private bundle bytes go
78
+ // through presigned URLs, not cross-origin credentialed fetches.
79
+ // Adds ACAO headers AFTER the route runs so they overlay on the
80
+ // route's response.
51
81
  app.use('*', async (c, next) => {
52
82
  const path = c.req.path;
53
- const isPublishRoute = path === '/publish';
54
- if (c.req.method === 'OPTIONS' && !isPublishRoute) {
83
+ const isCorsExcluded = path === '/publish' || path.startsWith('/bundles/private/');
84
+ if (c.req.method === 'OPTIONS' && !isCorsExcluded) {
55
85
  // Preflight short-circuit for read routes. DELETE is listed for
56
86
  // the author-key management route — a browser console revokes
57
87
  // keys cross-origin ([E]).
@@ -65,7 +95,7 @@ export function createRegistryApp(options) {
65
95
  });
66
96
  }
67
97
  await next();
68
- if (!isPublishRoute) {
98
+ if (!isCorsExcluded) {
69
99
  c.res.headers.set('Access-Control-Allow-Origin', '*');
70
100
  c.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
71
101
  c.res.headers.set('Access-Control-Allow-Headers', 'content-type, authorization');
@@ -85,6 +115,8 @@ export function createRegistryApp(options) {
85
115
  limit: q.limit,
86
116
  cursor: q.cursor,
87
117
  sort: q.sort,
118
+ tool: q.tool,
119
+ server: q.server,
88
120
  }, { storage });
89
121
  if (!result.ok) {
90
122
  return c.json(result.body, result.status);
@@ -170,6 +202,7 @@ export function createRegistryApp(options) {
170
202
  registryHostname,
171
203
  ...(blueprintProbe !== undefined ? { blueprintProbe } : {}),
172
204
  ...(sigstoreTuf !== undefined ? { sigstoreTuf } : {}),
205
+ ...(verifiedEmailResolver !== undefined ? { verifiedEmailResolver } : {}),
173
206
  });
174
207
  return c.json(result.body, result.status);
175
208
  });
@@ -253,32 +286,106 @@ export function createRegistryApp(options) {
253
286
  // conformance handler's wire shape.
254
287
  return c.json(result, 200);
255
288
  });
256
- // ── /bundles/:scope/:name/:version/bundle.js ─────────────────────────
257
- app.get('/bundles/:scope/:name/:version/bundle.js', async (c) => {
289
+ // ── /bundles/public/… — anonymous, CDN-parity serving ────────────────
290
+ app.get('/bundles/public/:scope/:name/:version/bundle.js', async (c) => {
291
+ const { scope, name, version } = c.req.param();
292
+ return serveBundle(c, bundleStorage, scope, name, version, 'public');
293
+ });
294
+ app.get('/bundles/public/:scope/:name/:version/bundle.js.sig', async (c) => {
295
+ const { scope, name, version } = c.req.param();
296
+ return serveSignature(c, bundleStorage, scope, name, version, 'public');
297
+ });
298
+ app.get('/bundles/public/:scope/:name/:version/manifest.json', async (c) => {
299
+ const { scope, name, version } = c.req.param();
300
+ return serveManifest(c, bundleStorage, scope, name, version, 'public');
301
+ });
302
+ // ── /bundles/private/… — authenticated + ownership-authorized ────────
303
+ // Auth rule (H1): a valid bearer is required (401 — uniform, fires
304
+ // before any storage read, so it can't distinguish triples); then
305
+ // authorization goes through H2's shared `canReadPrivateArtifact`
306
+ // predicate (publisher OR scope owner; owner lookup lazy + memoized
307
+ // + fail-closed via `createScopeOwnerResolver`). Unknown triples,
308
+ // rows that are not private (their blobs live on the public prefix),
309
+ // AND authorization denials all answer the SAME opaque 404 — a
310
+ // distinguishable denial is an existence oracle for private
311
+ // artifacts. Storage faults answer the documented `server_error`
312
+ // envelope with NO raw error text (fail closed, log server-side).
313
+ const privateBundleGate = async (c, rawScope, name, version) => {
314
+ const verified = authn.verify(c.req.header('authorization'));
315
+ if (verified === null) {
316
+ return c.json({
317
+ error: 'unauthorized',
318
+ message: 'private bundles require a valid bearer token in the Authorization header',
319
+ }, 401);
320
+ }
321
+ const scope = rawScope.startsWith('@') ? rawScope : `@${rawScope}`;
322
+ const artifactId = `${scope}/${name}`;
323
+ let row;
324
+ try {
325
+ row = await storage.getArtifactVersion(artifactId, version);
326
+ }
327
+ catch (err) {
328
+ // Fail closed with the documented envelope — the raw fault text
329
+ // stays server-side (a wire-visible internals string is both a
330
+ // leak and a fingerprint).
331
+ // eslint-disable-next-line no-console -- server-side operator signal; the wire stays opaque
332
+ console.error('registry: private bundle gate storage read failed', {
333
+ artifactId,
334
+ version,
335
+ error: err instanceof Error ? err.message : String(err),
336
+ });
337
+ return c.json(errorBody('server_error', 'internal error'), 500);
338
+ }
339
+ if (row === null || row.visibility !== 'private') {
340
+ return c.json(errorBody('not_found', 'bundle not found'), 404);
341
+ }
342
+ // `createScopeOwnerResolver` already resolves owner-lookup faults
343
+ // to null (deny) with its own server-side log, so no try/catch is
344
+ // needed around the predicate.
345
+ const authorized = await canReadPrivateArtifact(verified, row, createScopeOwnerResolver(storage, artifactId));
346
+ if (!authorized) {
347
+ return c.json(errorBody('not_found', 'bundle not found'), 404);
348
+ }
349
+ return null;
350
+ };
351
+ app.get('/bundles/private/:scope/:name/:version/bundle.js', async (c) => {
258
352
  const { scope, name, version } = c.req.param();
259
- return serveBundle(c, bundleStorage, scope, name, version);
353
+ const denied = await privateBundleGate(c, scope, name, version);
354
+ if (denied !== null)
355
+ return denied;
356
+ return serveBundle(c, bundleStorage, scope, name, version, 'private');
260
357
  });
261
- // ── /bundles/:scope/:name/:version/bundle.js.sig ─────────────────────
262
- app.get('/bundles/:scope/:name/:version/bundle.js.sig', async (c) => {
358
+ app.get('/bundles/private/:scope/:name/:version/bundle.js.sig', async (c) => {
263
359
  const { scope, name, version } = c.req.param();
264
- return serveSignature(c, bundleStorage, scope, name, version);
360
+ const denied = await privateBundleGate(c, scope, name, version);
361
+ if (denied !== null)
362
+ return denied;
363
+ return serveSignature(c, bundleStorage, scope, name, version, 'private');
265
364
  });
266
- // ── /bundles/:scope/:name/:version/manifest.json ─────────────────────
267
- app.get('/bundles/:scope/:name/:version/manifest.json', async (c) => {
365
+ app.get('/bundles/private/:scope/:name/:version/manifest.json', async (c) => {
268
366
  const { scope, name, version } = c.req.param();
269
- return serveManifest(c, bundleStorage, scope, name, version);
367
+ const denied = await privateBundleGate(c, scope, name, version);
368
+ if (denied !== null)
369
+ return denied;
370
+ return serveManifest(c, bundleStorage, scope, name, version, 'private');
270
371
  });
271
372
  return app;
272
373
  }
374
+ /** Cache header for the given prefix — see the module docstring. */
375
+ function cacheHeaderFor(visibility) {
376
+ return visibility === 'public'
377
+ ? IMMUTABLE_CACHE_HEADER
378
+ : PRIVATE_IMMUTABLE_CACHE_HEADER;
379
+ }
273
380
  /**
274
381
  * Serve the bundle bytes. `application/javascript` MIME with the
275
- * immutable cache header. 404 on miss.
382
+ * visibility-appropriate immutable cache header. 404 on miss.
276
383
  */
277
- async function serveBundle(c, bundleStorage, rawScope, name, version) {
384
+ async function serveBundle(c, bundleStorage, rawScope, name, version, visibility) {
278
385
  const scope = rawScope.startsWith('@') ? rawScope : `@${rawScope}`;
279
386
  let bytes;
280
387
  try {
281
- bytes = await bundleStorage.getBundle(scope, name, version);
388
+ bytes = await bundleStorage.getBundle(scope, name, version, visibility);
282
389
  }
283
390
  catch (err) {
284
391
  return c.json(errorBody('server_error', errorMessage(err)), 500);
@@ -297,15 +404,15 @@ async function serveBundle(c, bundleStorage, rawScope, name, version) {
297
404
  status: 200,
298
405
  headers: {
299
406
  'Content-Type': 'application/javascript; charset=utf-8',
300
- 'Cache-Control': IMMUTABLE_CACHE_HEADER,
407
+ 'Cache-Control': cacheHeaderFor(visibility),
301
408
  },
302
409
  });
303
410
  }
304
- async function serveSignature(c, bundleStorage, rawScope, name, version) {
411
+ async function serveSignature(c, bundleStorage, rawScope, name, version, visibility) {
305
412
  const scope = rawScope.startsWith('@') ? rawScope : `@${rawScope}`;
306
413
  let sig;
307
414
  try {
308
- sig = await bundleStorage.getSignature(scope, name, version);
415
+ sig = await bundleStorage.getSignature(scope, name, version, visibility);
309
416
  }
310
417
  catch (err) {
311
418
  return c.json(errorBody('server_error', errorMessage(err)), 500);
@@ -313,14 +420,14 @@ async function serveSignature(c, bundleStorage, rawScope, name, version) {
313
420
  if (sig === null) {
314
421
  return c.json(errorBody('not_found', 'signature not found'), 404);
315
422
  }
316
- c.header('Cache-Control', IMMUTABLE_CACHE_HEADER);
423
+ c.header('Cache-Control', cacheHeaderFor(visibility));
317
424
  return c.json(sig, 200);
318
425
  }
319
- async function serveManifest(c, bundleStorage, rawScope, name, version) {
426
+ async function serveManifest(c, bundleStorage, rawScope, name, version, visibility) {
320
427
  const scope = rawScope.startsWith('@') ? rawScope : `@${rawScope}`;
321
428
  let manifest;
322
429
  try {
323
- manifest = await bundleStorage.getManifest(scope, name, version);
430
+ manifest = await bundleStorage.getManifest(scope, name, version, visibility);
324
431
  }
325
432
  catch (err) {
326
433
  return c.json(errorBody('server_error', errorMessage(err)), 500);
@@ -328,7 +435,7 @@ async function serveManifest(c, bundleStorage, rawScope, name, version) {
328
435
  if (manifest === null) {
329
436
  return c.json(errorBody('not_found', 'manifest not found'), 404);
330
437
  }
331
- c.header('Cache-Control', IMMUTABLE_CACHE_HEADER);
438
+ c.header('Cache-Control', cacheHeaderFor(visibility));
332
439
  return c.json(manifest, 200);
333
440
  }
334
441
  function errorBody(error, message) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/registry-server",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Self-hostable HTTP server for the ggui marketplace registry. Wraps @ggui-ai/registry-core with hono, filesystem storage, and bearer-token auth. Run via npx for local dev, CI, and on-prem deployments.",
5
5
  "keywords": [
6
6
  "ggui",
@@ -31,9 +31,9 @@
31
31
  "dependencies": {
32
32
  "@hono/node-server": "^2.0.12",
33
33
  "hono": "^4.12.8",
34
- "@ggui-ai/artifact-manifest": "0.7.0",
35
- "@ggui-ai/gadget-signing": "0.7.0",
36
- "@ggui-ai/registry-core": "0.7.0"
34
+ "@ggui-ai/artifact-manifest": "0.8.0",
35
+ "@ggui-ai/gadget-signing": "0.8.0",
36
+ "@ggui-ai/registry-core": "0.8.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@sigstore/mock": "~0.12.1",