@ggui-ai/registry-core 0.7.0 → 0.9.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.
Files changed (45) hide show
  1. package/dist/impls/memory-bundle-storage.d.ts.map +1 -1
  2. package/dist/impls/memory-bundle-storage.js +25 -22
  3. package/dist/impls/memory-registry-storage.d.ts.map +1 -1
  4. package/dist/impls/memory-registry-storage.js +10 -0
  5. package/dist/index.d.ts +4 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +4 -1
  8. package/dist/interfaces/bundle-storage.d.ts +55 -31
  9. package/dist/interfaces/bundle-storage.d.ts.map +1 -1
  10. package/dist/interfaces/registry-storage.d.ts +24 -2
  11. package/dist/interfaces/registry-storage.d.ts.map +1 -1
  12. package/dist/mcp-tool-filters.d.ts +23 -0
  13. package/dist/mcp-tool-filters.d.ts.map +1 -0
  14. package/dist/mcp-tool-filters.js +14 -0
  15. package/dist/ops/compile.d.ts.map +1 -1
  16. package/dist/ops/compile.js +19 -1
  17. package/dist/ops/conformance.d.ts +8 -5
  18. package/dist/ops/conformance.d.ts.map +1 -1
  19. package/dist/ops/conformance.js +35 -7
  20. package/dist/ops/list-versions.d.ts +19 -10
  21. package/dist/ops/list-versions.d.ts.map +1 -1
  22. package/dist/ops/list-versions.js +43 -8
  23. package/dist/ops/private-read-authz.d.ts +88 -0
  24. package/dist/ops/private-read-authz.d.ts.map +1 -0
  25. package/dist/ops/private-read-authz.js +58 -0
  26. package/dist/ops/publish.d.ts +30 -0
  27. package/dist/ops/publish.d.ts.map +1 -1
  28. package/dist/ops/publish.js +163 -29
  29. package/dist/ops/read.d.ts +28 -6
  30. package/dist/ops/read.d.ts.map +1 -1
  31. package/dist/ops/read.js +47 -7
  32. package/dist/ops/search.d.ts +18 -3
  33. package/dist/ops/search.d.ts.map +1 -1
  34. package/dist/ops/search.js +54 -2
  35. package/dist/testing/bundle-storage-contract.d.ts.map +1 -1
  36. package/dist/testing/bundle-storage-contract.js +61 -27
  37. package/dist/testing/registry-storage-contract.d.ts.map +1 -1
  38. package/dist/testing/registry-storage-contract.js +148 -1
  39. package/dist/types.d.ts +115 -16
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/types.js +30 -14
  42. package/dist/utils/lazy-import.d.ts +17 -0
  43. package/dist/utils/lazy-import.d.ts.map +1 -0
  44. package/dist/utils/lazy-import.js +27 -0
  45. package/package.json +11 -5
@@ -5,14 +5,34 @@
5
5
  *
6
6
  * Flow:
7
7
  * 1. Point-read the version row via {@link RegistryStorage.getArtifactVersion}.
8
- * 2. Enforce row `visibility`: `private` → require an {@link AuthnContext}.
8
+ * 2. Enforce row `visibility`: `private` → the caller must satisfy
9
+ * {@link canReadPrivateArtifact} (publisher or scope owner).
10
+ * Anyone else receives a response byte-identical to a true
11
+ * not-found — no status, code, or message distinguishes "exists
12
+ * but you may not read it" from "does not exist".
9
13
  * 3. Yanked → 410 with the manifest still in the body (audit-friendly).
14
+ * The private gate fires FIRST — an unauthorized caller never sees
15
+ * the 410 body.
10
16
  * 4. For blueprints, resolve the {@link CompiledBlobRow} via
11
17
  * `row.compiledDigest` (two-layer content-addressed storage). A
12
18
  * missing blob when the pointer is set is a critical storage
13
19
  * inconsistency — the op returns 500 rather than silently
14
20
  * omitting `compiledBytes`.
15
- * 5. Project the row + blob into the {@link ReadPkgResponse} wire shape.
21
+ * 5. Project the row + blob into the {@link ReadPkgResponse} wire shape,
22
+ * including `scopeVerification`/`verifiedDomain`.
23
+ *
24
+ * Scope-owner reads (2026-08-10, MCP discovery §2): every read — public
25
+ * or private — now costs exactly ONE memoized {@link RegistryStorage.getScopeOwner}
26
+ * point-read via {@link createScopeOwnerResolver}, shared between the
27
+ * private-read gate (when applicable) and the `scopeVerification` /
28
+ * `verifiedDomain` wire projection. This deliberately SUPERSEDES the
29
+ * prior "public reads cost zero extra storage reads" guarantee from
30
+ * the private-read-ownership work ({@link ./private-read-authz.js}) —
31
+ * verification surfacing needs the scope row on every read, public
32
+ * included, so the old zero-reads pin was retired in favor of a
33
+ * one-read invariant instead. A failed or missing scope-owner read
34
+ * degrades to absent `scopeVerification`/`verifiedDomain` (never
35
+ * defaulted to `'unverified'`) and never fails the read itself.
16
36
  */
17
37
  import type { ReadErrorBody, ReadPkgResponse } from '../types.js';
18
38
  import type { AuthnContext } from '../interfaces/authn.js';
@@ -29,9 +49,11 @@ export interface ReadArtifactInput {
29
49
  export interface ReadArtifactDeps {
30
50
  readonly storage: RegistryStorage;
31
51
  /**
32
- * Optional — when undefined, the op rejects `private` rows with 403.
33
- * Cloud transport extracts from the JWT authorizer; OSS extracts from
34
- * the bearer authn middleware.
52
+ * Optional — the verified caller context, produced by the
53
+ * transport's own credential verification. When the requested row
54
+ * is `private`, the op admits only the row's publisher or the
55
+ * artifact's scope owner ({@link canReadPrivateArtifact}); every
56
+ * other caller — anonymous included — gets the not-found shape.
35
57
  */
36
58
  readonly authn?: AuthnContext;
37
59
  }
@@ -45,7 +67,7 @@ export type ReadArtifactResult = {
45
67
  readonly body: ReadPkgResponse;
46
68
  } | {
47
69
  readonly ok: false;
48
- readonly status: 400 | 403 | 404 | 500;
70
+ readonly status: 400 | 404 | 500;
49
71
  readonly body: ReadErrorBody;
50
72
  };
51
73
  export declare function readArtifact(input: ReadArtifactInput, deps: ReadArtifactDeps): Promise<ReadArtifactResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/ops/read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAGV,aAAa,EAEb,eAAe,EAChB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAEzE,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;CAC/B;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;CAAE,GAC3E;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;CAAE,GAC5E;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;CAC9B,CAAC;AAEN,wBAAsB,YAAY,CAChC,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,kBAAkB,CAAC,CAkE7B"}
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/ops/read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,KAAK,EAGV,aAAa,EAEb,eAAe,EAEhB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAMzE,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;CAC/B;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;CAAE,GAC3E;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;CAAE,GAC5E;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;CAC9B,CAAC;AAEN,wBAAsB,YAAY,CAChC,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,kBAAkB,CAAC,CA0E7B"}
package/dist/ops/read.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { canReadPrivateArtifact, createScopeOwnerResolver, } from './private-read-authz.js';
1
2
  export async function readArtifact(input, deps) {
2
3
  if (typeof input.artifactId !== 'string' || input.artifactId.length === 0) {
3
4
  return errorResult(400, 'invalid_request', 'missing artifactId');
@@ -5,18 +6,33 @@ export async function readArtifact(input, deps) {
5
6
  if (typeof input.version !== 'string' || input.version.length === 0) {
6
7
  return errorResult(400, 'invalid_request', 'missing version');
7
8
  }
9
+ // Scope-owner resolver — memoized, so the private-read gate and the
10
+ // verification surfacing share at most ONE storage read. Fail-closed:
11
+ // a storage fault resolves null, which denies on the private gate and
12
+ // omits the verification fields (state unknown is not 'unverified').
13
+ const resolveScopeOwner = createScopeOwnerResolver(deps.storage, input.artifactId);
8
14
  let row;
9
15
  try {
10
16
  row = await deps.storage.getArtifactVersion(input.artifactId, input.version);
11
17
  }
12
18
  catch (err) {
13
- return errorResult(500, 'server_error', `failed to read package: ${err instanceof Error ? err.message : String(err)}`);
19
+ logStorageFailure('read version row', input.artifactId, err);
20
+ return errorResult(500, 'server_error', 'failed to read package');
14
21
  }
15
22
  if (row === null) {
16
- return errorResult(404, 'not_found', `package ${input.artifactId}@${input.version} not found`);
23
+ return notFoundResult(input);
17
24
  }
18
- if (row.visibility === 'private' && deps.authn === undefined) {
19
- return errorResult(403, 'forbidden', 'private package requires authentication');
25
+ if (row.visibility === 'private') {
26
+ // Ownership gate — publisher or scope owner only. The
27
+ // unauthorized response is produced by the SAME function as a
28
+ // true miss so the two are structurally indistinguishable. The
29
+ // resolver is fail-closed: a storage fault during the owner
30
+ // lookup denies (logged server-side) rather than erroring — a
31
+ // 500 only private rows could trigger would leak existence.
32
+ const allowed = await canReadPrivateArtifact(deps.authn, row, resolveScopeOwner);
33
+ if (!allowed) {
34
+ return notFoundResult(input);
35
+ }
20
36
  }
21
37
  // Two-layer resolution — blueprint rows carry a pointer into the
22
38
  // compiled-blob table. Yanked rows still resolve so the 410
@@ -28,7 +44,8 @@ export async function readArtifact(input, deps) {
28
44
  compiledBlob = await deps.storage.getCompiledBlob(row.compiledDigest);
29
45
  }
30
46
  catch (err) {
31
- return errorResult(500, 'server_error', `failed to read compiled blob for compiledDigest=${row.compiledDigest}: ${err instanceof Error ? err.message : String(err)}`);
47
+ logStorageFailure('read compiled blob', input.artifactId, err);
48
+ return errorResult(500, 'server_error', `failed to read compiled blob for compiledDigest=${row.compiledDigest}`);
32
49
  }
33
50
  if (compiledBlob === null) {
34
51
  // CRITICAL — version row points at a digest with no blob.
@@ -36,13 +53,14 @@ export async function readArtifact(input, deps) {
36
53
  return errorResult(500, 'server_error', `version row points at compiledDigest=${row.compiledDigest} but no compiled-blob row exists`);
37
54
  }
38
55
  }
39
- const body = rowToResponse(row, compiledBlob);
56
+ const scopeOwner = await resolveScopeOwner();
57
+ const body = rowToResponse(row, compiledBlob, scopeOwner);
40
58
  if (row.yanked === true) {
41
59
  return { ok: false, status: 410, body };
42
60
  }
43
61
  return { ok: true, status: 200, body };
44
62
  }
45
- function rowToResponse(row, compiledBlob) {
63
+ function rowToResponse(row, compiledBlob, scopeOwner) {
46
64
  return {
47
65
  manifest: row.manifest,
48
66
  bundleUrl: row.bundleUrl,
@@ -53,8 +71,30 @@ function rowToResponse(row, compiledBlob) {
53
71
  authorPublicKey: row.authorPublicKey,
54
72
  publishedAt: row.publishedAt,
55
73
  publishedBy: row.publishedBy,
74
+ scopeVerification: scopeOwner?.verification,
75
+ verifiedDomain: scopeOwner?.verification === 'verified' ? scopeOwner.verifiedDomain : undefined,
56
76
  };
57
77
  }
78
+ /**
79
+ * Structured server-side failure log. Raw storage error text stays
80
+ * OUT of wire bodies (it can carry backend identifiers a caller has
81
+ * no business seeing); this log line is the operator's copy.
82
+ */
83
+ function logStorageFailure(operation, artifactId, err) {
84
+ // eslint-disable-next-line no-console -- server-side operator signal; the wire stays generic
85
+ console.error(`registry read: failed to ${operation}`, {
86
+ artifactId,
87
+ error: err instanceof Error ? err.message : String(err),
88
+ });
89
+ }
90
+ /**
91
+ * The one not-found projection — used for BOTH a true miss and an
92
+ * unauthorized private read, so the two responses cannot drift apart
93
+ * (drift would reintroduce the existence signal).
94
+ */
95
+ function notFoundResult(input) {
96
+ return errorResult(404, 'not_found', `package ${input.artifactId}@${input.version} not found`);
97
+ }
58
98
  function errorResult(status, error, message) {
59
99
  return { ok: false, status, body: { error, message } };
60
100
  }
@@ -2,12 +2,15 @@
2
2
  * `searchArtifacts` — pure op for `GET /search?…`. Delegates the
3
3
  * post-fetch filter + cursor to {@link RegistryStorage.scanArtifacts} so
4
4
  * every storage impl shares the single semantic — AND-composition over
5
- * q/kind/hook/tag/author.
5
+ * q/kind/hook/tag/author/tool/server.
6
6
  *
7
7
  * Visibility: only `visibility: "public"` rows are exposed. Without that
8
8
  * filter the public `/search` route would leak private artifact IDs.
9
- * Impls SHOULD apply the visibility filter inside `scanArtifacts`; the
10
- * op verifies defensively after the page returns.
9
+ * Impls MUST apply the visibility filter inside `scanArtifacts` — the
10
+ * normative obligation on {@link RegistryStorage.scanArtifacts} (#474);
11
+ * this op's post-fetch check is defense-in-depth ONLY, never the sole
12
+ * gate (a private row that reaches the storage-layer page would
13
+ * consume a `limit` slot before this filter ever discards it).
11
14
  */
12
15
  import type { SearchErrorBody, SearchResponse } from '../types.js';
13
16
  import type { RegistryStorage } from '../interfaces/registry-storage.js';
@@ -31,6 +34,18 @@ export interface SearchArtifactsInput {
31
34
  * globally-correct newest-first order across the full cursor chain.
32
35
  */
33
36
  readonly sort?: string;
37
+ /**
38
+ * Exact MCP tool-name filter — matches artifacts whose bindings
39
+ * include this tool (declared under any server, or none). Must match
40
+ * `^[A-Za-z0-9_.-]{1,128}$`; anything else is a 400.
41
+ */
42
+ readonly tool?: string;
43
+ /**
44
+ * Exact MCP server-name filter — matches artifacts whose bindings
45
+ * declare this server; bindings without a server never match. Same
46
+ * charset rule as `tool`.
47
+ */
48
+ readonly server?: string;
34
49
  }
35
50
  export interface SearchArtifactsDeps {
36
51
  readonly storage: RegistryStorage;
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/ops/search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAIV,eAAe,EACf,cAAc,EAGf,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAKzE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;CACnC;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GAC1E;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC;AAEvF,wBAAsB,eAAe,CACnC,KAAK,EAAE,oBAAoB,EAC3B,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,qBAAqB,CAAC,CAsFhC"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/ops/search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAKV,eAAe,EACf,cAAc,EAGf,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAOzE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;CACnC;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GAC1E;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC;AAEvF,wBAAsB,eAAe,CACnC,KAAK,EAAE,oBAAoB,EAC3B,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,qBAAqB,CAAC,CAwHhC"}
@@ -1,4 +1,6 @@
1
1
  import { SEARCH_SORT_OPTIONS } from '../types.js';
2
+ import { artifactScope } from './private-read-authz.js';
3
+ import { MCP_TOOL_BINDING_NAME_RE } from '@ggui-ai/artifact-manifest';
2
4
  const DEFAULT_LIMIT = 50;
3
5
  const MAX_LIMIT = 200;
4
6
  export async function searchArtifacts(input, deps) {
@@ -45,12 +47,36 @@ export async function searchArtifacts(input, deps) {
45
47
  }
46
48
  sort = input.sort;
47
49
  }
50
+ const tool = nonEmpty(input.tool);
51
+ if (tool !== undefined && !MCP_TOOL_BINDING_NAME_RE.test(tool)) {
52
+ return {
53
+ ok: false,
54
+ status: 400,
55
+ body: {
56
+ error: 'invalid_request',
57
+ message: '`tool` must match ^[A-Za-z0-9_.-]{1,128}$',
58
+ },
59
+ };
60
+ }
61
+ const server = nonEmpty(input.server);
62
+ if (server !== undefined && !MCP_TOOL_BINDING_NAME_RE.test(server)) {
63
+ return {
64
+ ok: false,
65
+ status: 400,
66
+ body: {
67
+ error: 'invalid_request',
68
+ message: '`server` must match ^[A-Za-z0-9_.-]{1,128}$',
69
+ },
70
+ };
71
+ }
48
72
  const filter = {
49
73
  q: nonEmpty(input.q),
50
74
  kind,
51
75
  hook: nonEmpty(input.hook),
52
76
  tag: nonEmpty(input.tag),
53
77
  author: nonEmpty(input.author),
78
+ tool,
79
+ server,
54
80
  limit,
55
81
  cursor: nonEmpty(input.cursor),
56
82
  order: sort,
@@ -72,11 +98,16 @@ export async function searchArtifacts(input, deps) {
72
98
  continue;
73
99
  visibleRows.push(row);
74
100
  }
101
+ // Scope-verification surfacing — ONE owner read per unique scope on
102
+ // the page, batched. A failed or empty read leaves the fields ABSENT
103
+ // (state unknown is not 'unverified'): the wire only carries a label
104
+ // the ownership row actually asserted.
105
+ const ownersByScope = await readScopeOwners(deps.storage, visibleRows);
75
106
  // `sort=recent` ordering is served by the storage layer (the filter
76
107
  // carries `order: 'recent'`); rows arrive newest-first and are
77
108
  // emitted verbatim — no re-sort here, so page order and cursor order
78
109
  // cannot diverge.
79
- const results = visibleRows.map(rowToEntry);
110
+ const results = visibleRows.map((row) => rowToEntry(row, ownersByScope.get(artifactScope(row.artifactId)) ?? null));
80
111
  return {
81
112
  ok: true,
82
113
  status: 200,
@@ -86,7 +117,24 @@ export async function searchArtifacts(input, deps) {
86
117
  function isSearchSort(value) {
87
118
  return SEARCH_SORT_OPTIONS.includes(value);
88
119
  }
89
- function rowToEntry(row) {
120
+ async function readScopeOwners(storage, rows) {
121
+ const scopes = [...new Set(rows.map((row) => artifactScope(row.artifactId)))];
122
+ const entries = await Promise.all(scopes.map(async (scope) => {
123
+ try {
124
+ return [scope, await storage.getScopeOwner(scope)];
125
+ }
126
+ catch (err) {
127
+ // eslint-disable-next-line no-console -- server-side operator signal; the wire simply omits the fields
128
+ console.error('registry search: scope-owner lookup failed; omitting verification fields', {
129
+ scope,
130
+ error: err instanceof Error ? err.message : String(err),
131
+ });
132
+ return [scope, null];
133
+ }
134
+ }));
135
+ return new Map(entries);
136
+ }
137
+ function rowToEntry(row, scopeOwner) {
90
138
  return {
91
139
  artifactId: row.artifactId,
92
140
  latestVersion: row.latestVersion,
@@ -94,6 +142,10 @@ function rowToEntry(row) {
94
142
  description: row.description,
95
143
  tags: row.tags,
96
144
  publishedAt: row.publishedAt,
145
+ mcpTools: row.mcpTools,
146
+ mcpToolsSource: row.mcpToolsSource,
147
+ scopeVerification: scopeOwner?.verification,
148
+ verifiedDomain: scopeOwner?.verification === 'verified' ? scopeOwner.verifiedDomain : undefined,
97
149
  };
98
150
  }
99
151
  function nonEmpty(s) {
@@ -1 +1 @@
1
- {"version":3,"file":"bundle-storage-contract.d.ts","sourceRoot":"","sources":["../../src/testing/bundle-storage-contract.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AA0DrE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,aAAa,GAAG,IAAI,CA6H5E"}
1
+ {"version":3,"file":"bundle-storage-contract.d.ts","sourceRoot":"","sources":["../../src/testing/bundle-storage-contract.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AA0DrE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,aAAa,GAAG,IAAI,CA8K5E"}
@@ -69,35 +69,69 @@ export function bundleStorageContract(makeStorage) {
69
69
  describe('bundle', () => {
70
70
  it('returns null on miss', async () => {
71
71
  const s = makeStorage();
72
- expect(await s.getBundle('@test', 'foo', '0.1.0')).toBe(null);
72
+ expect(await s.getBundle('@test', 'foo', '0.1.0', 'public')).toBe(null);
73
73
  });
74
74
  it('round-trips bundle bytes', async () => {
75
75
  const s = makeStorage();
76
76
  const bytes = new Uint8Array([0xff, 0x00, 0x10, 0x20]);
77
- await s.putBundle('@test', 'foo', '0.1.0', bytes);
78
- const fetched = await s.getBundle('@test', 'foo', '0.1.0');
77
+ await s.putBundle('@test', 'foo', '0.1.0', 'public', bytes);
78
+ const fetched = await s.getBundle('@test', 'foo', '0.1.0', 'public');
79
79
  expect(fetched).toEqual(bytes);
80
80
  });
81
81
  it('isolates bundles by (scope, name, version)', async () => {
82
82
  const s = makeStorage();
83
- await s.putBundle('@a', 'pkg', '0.1.0', new Uint8Array([1]));
84
- await s.putBundle('@a', 'pkg', '0.2.0', new Uint8Array([2]));
85
- await s.putBundle('@b', 'pkg', '0.1.0', new Uint8Array([3]));
86
- expect((await s.getBundle('@a', 'pkg', '0.1.0'))?.[0]).toBe(1);
87
- expect((await s.getBundle('@a', 'pkg', '0.2.0'))?.[0]).toBe(2);
88
- expect((await s.getBundle('@b', 'pkg', '0.1.0'))?.[0]).toBe(3);
83
+ await s.putBundle('@a', 'pkg', '0.1.0', 'public', new Uint8Array([1]));
84
+ await s.putBundle('@a', 'pkg', '0.2.0', 'public', new Uint8Array([2]));
85
+ await s.putBundle('@b', 'pkg', '0.1.0', 'public', new Uint8Array([3]));
86
+ expect((await s.getBundle('@a', 'pkg', '0.1.0', 'public'))?.[0]).toBe(1);
87
+ expect((await s.getBundle('@a', 'pkg', '0.2.0', 'public'))?.[0]).toBe(2);
88
+ expect((await s.getBundle('@b', 'pkg', '0.1.0', 'public'))?.[0]).toBe(3);
89
+ });
90
+ });
91
+ describe('visibility key split (H1)', () => {
92
+ it('a private-written bundle is invisible to a public read of the same triple', async () => {
93
+ const s = makeStorage();
94
+ await s.putBundle('@acme', 'secret', '1.0.0', 'private', new Uint8Array([7]));
95
+ expect(await s.getBundle('@acme', 'secret', '1.0.0', 'public')).toBe(null);
96
+ expect((await s.getBundle('@acme', 'secret', '1.0.0', 'private'))?.[0]).toBe(7);
97
+ });
98
+ it('a public-written bundle is invisible to a private read of the same triple', async () => {
99
+ const s = makeStorage();
100
+ await s.putBundle('@acme', 'open', '1.0.0', 'public', new Uint8Array([9]));
101
+ expect(await s.getBundle('@acme', 'open', '1.0.0', 'private')).toBe(null);
102
+ expect((await s.getBundle('@acme', 'open', '1.0.0', 'public'))?.[0]).toBe(9);
103
+ });
104
+ it('splits signatures and manifests by visibility too', async () => {
105
+ const s = makeStorage();
106
+ await s.putSignature('@acme', 'secret', '1.0.0', 'private', stubSignature());
107
+ await s.putManifest('@acme', 'secret', '1.0.0', 'private', stubManifest());
108
+ expect(await s.getSignature('@acme', 'secret', '1.0.0', 'public')).toBe(null);
109
+ expect(await s.getManifest('@acme', 'secret', '1.0.0', 'public')).toBe(null);
110
+ expect(await s.getSignature('@acme', 'secret', '1.0.0', 'private')).not.toBe(null);
111
+ expect(await s.getManifest('@acme', 'secret', '1.0.0', 'private')).not.toBe(null);
112
+ });
113
+ it('embeds the visibility segment in every composed URL', () => {
114
+ const s = makeStorage();
115
+ for (const compose of [
116
+ s.bundleUrl.bind(s),
117
+ s.signatureUrl.bind(s),
118
+ s.manifestUrl.bind(s),
119
+ ]) {
120
+ expect(compose('@test', 'foo', '0.1.0', 'public')).toContain('/bundles/public/');
121
+ expect(compose('@test', 'foo', '0.1.0', 'private')).toContain('/bundles/private/');
122
+ }
89
123
  });
90
124
  });
91
125
  describe('signature', () => {
92
126
  it('returns null on miss', async () => {
93
127
  const s = makeStorage();
94
- expect(await s.getSignature('@test', 'foo', '0.1.0')).toBe(null);
128
+ expect(await s.getSignature('@test', 'foo', '0.1.0', 'public')).toBe(null);
95
129
  });
96
130
  it('round-trips an Ed25519 signature envelope', async () => {
97
131
  const s = makeStorage();
98
132
  const sig = stubSignature();
99
- await s.putSignature('@test', 'foo', '0.1.0', sig);
100
- const fetched = await s.getSignature('@test', 'foo', '0.1.0');
133
+ await s.putSignature('@test', 'foo', '0.1.0', 'private', sig);
134
+ const fetched = await s.getSignature('@test', 'foo', '0.1.0', 'private');
101
135
  expect(fetched).toEqual(sig);
102
136
  });
103
137
  // Pin the sigstore round-trip through the storage layer. Guards
@@ -110,8 +144,8 @@ export function bundleStorageContract(makeStorage) {
110
144
  it('round-trips a sigstore signature envelope including the embedded cosign bundle', async () => {
111
145
  const s = makeStorage();
112
146
  const sig = stubSigstoreSignature();
113
- await s.putSignature('@test', 'foo', '0.1.0', sig);
114
- const fetched = await s.getSignature('@test', 'foo', '0.1.0');
147
+ await s.putSignature('@test', 'foo', '0.1.0', 'public', sig);
148
+ const fetched = await s.getSignature('@test', 'foo', '0.1.0', 'public');
115
149
  expect(fetched).toEqual(sig);
116
150
  // Specifically pin the `algorithm` discriminator + the
117
151
  // `bundle` field — these are sigstore-only fields the prior
@@ -135,41 +169,41 @@ export function bundleStorageContract(makeStorage) {
135
169
  // case is covered by the round-trip tests; this test pins the
136
170
  // null-on-miss contract specifically — without it, a regression
137
171
  // that silently returns a stub object would slip through.
138
- const fetched = await s.getSignature('@no-such-scope', 'no-such-name', '0.0.0');
172
+ const fetched = await s.getSignature('@no-such-scope', 'no-such-name', '0.0.0', 'public');
139
173
  expect(fetched).toBe(null);
140
174
  });
141
175
  });
142
176
  describe('manifest', () => {
143
177
  it('returns null on miss', async () => {
144
178
  const s = makeStorage();
145
- expect(await s.getManifest('@test', 'foo', '0.1.0')).toBe(null);
179
+ expect(await s.getManifest('@test', 'foo', '0.1.0', 'public')).toBe(null);
146
180
  });
147
181
  it('round-trips the manifest', async () => {
148
182
  const s = makeStorage();
149
183
  const m = stubManifest();
150
- await s.putManifest('@test', 'foo', '0.1.0', m);
151
- const fetched = await s.getManifest('@test', 'foo', '0.1.0');
184
+ await s.putManifest('@test', 'foo', '0.1.0', 'public', m);
185
+ const fetched = await s.getManifest('@test', 'foo', '0.1.0', 'public');
152
186
  expect(fetched).toEqual(m);
153
187
  });
154
188
  });
155
189
  describe('URL composition', () => {
156
190
  it('produces distinct URLs for bundle / sig / manifest', async () => {
157
191
  const s = makeStorage();
158
- const b = s.bundleUrl('@test', 'foo', '0.1.0');
159
- const sig = s.signatureUrl('@test', 'foo', '0.1.0');
160
- const man = s.manifestUrl('@test', 'foo', '0.1.0');
192
+ const b = s.bundleUrl('@test', 'foo', '0.1.0', 'public');
193
+ const sig = s.signatureUrl('@test', 'foo', '0.1.0', 'public');
194
+ const man = s.manifestUrl('@test', 'foo', '0.1.0', 'public');
161
195
  expect(b).not.toBe(sig);
162
196
  expect(b).not.toBe(man);
163
197
  expect(sig).not.toBe(man);
164
198
  });
165
199
  it('matches the URL returned from put*', async () => {
166
200
  const s = makeStorage();
167
- const putBundleUrl = await s.putBundle('@test', 'foo', '0.1.0', new Uint8Array([1]));
168
- expect(putBundleUrl).toBe(s.bundleUrl('@test', 'foo', '0.1.0'));
169
- const putSigUrl = await s.putSignature('@test', 'foo', '0.1.0', stubSignature());
170
- expect(putSigUrl).toBe(s.signatureUrl('@test', 'foo', '0.1.0'));
171
- const putManUrl = await s.putManifest('@test', 'foo', '0.1.0', stubManifest());
172
- expect(putManUrl).toBe(s.manifestUrl('@test', 'foo', '0.1.0'));
201
+ const putBundleUrl = await s.putBundle('@test', 'foo', '0.1.0', 'private', new Uint8Array([1]));
202
+ expect(putBundleUrl).toBe(s.bundleUrl('@test', 'foo', '0.1.0', 'private'));
203
+ const putSigUrl = await s.putSignature('@test', 'foo', '0.1.0', 'private', stubSignature());
204
+ expect(putSigUrl).toBe(s.signatureUrl('@test', 'foo', '0.1.0', 'private'));
205
+ const putManUrl = await s.putManifest('@test', 'foo', '0.1.0', 'private', stubManifest());
206
+ expect(putManUrl).toBe(s.manifestUrl('@test', 'foo', '0.1.0', 'private'));
173
207
  });
174
208
  });
175
209
  });
@@ -1 +1 @@
1
- {"version":3,"file":"registry-storage-contract.d.ts","sourceRoot":"","sources":["../../src/testing/registry-storage-contract.ts"],"names":[],"mappings":"AAkBA,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,mCAAmC,CAAC;AAqI3C,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,CAkqBhF"}
1
+ {"version":3,"file":"registry-storage-contract.d.ts","sourceRoot":"","sources":["../../src/testing/registry-storage-contract.ts"],"names":[],"mappings":"AAkBA,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,mCAAmC,CAAC;AAqI3C,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,CA01BhF"}