@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.
- package/dist/impls/memory-bundle-storage.d.ts.map +1 -1
- package/dist/impls/memory-bundle-storage.js +25 -22
- package/dist/impls/memory-registry-storage.d.ts.map +1 -1
- package/dist/impls/memory-registry-storage.js +10 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/interfaces/bundle-storage.d.ts +55 -31
- package/dist/interfaces/bundle-storage.d.ts.map +1 -1
- package/dist/interfaces/registry-storage.d.ts +24 -2
- package/dist/interfaces/registry-storage.d.ts.map +1 -1
- package/dist/mcp-tool-filters.d.ts +23 -0
- package/dist/mcp-tool-filters.d.ts.map +1 -0
- package/dist/mcp-tool-filters.js +14 -0
- package/dist/ops/compile.d.ts.map +1 -1
- package/dist/ops/compile.js +19 -1
- package/dist/ops/conformance.d.ts +8 -5
- package/dist/ops/conformance.d.ts.map +1 -1
- package/dist/ops/conformance.js +35 -7
- package/dist/ops/list-versions.d.ts +19 -10
- package/dist/ops/list-versions.d.ts.map +1 -1
- package/dist/ops/list-versions.js +43 -8
- package/dist/ops/private-read-authz.d.ts +88 -0
- package/dist/ops/private-read-authz.d.ts.map +1 -0
- package/dist/ops/private-read-authz.js +58 -0
- package/dist/ops/publish.d.ts +30 -0
- package/dist/ops/publish.d.ts.map +1 -1
- package/dist/ops/publish.js +163 -29
- package/dist/ops/read.d.ts +28 -6
- package/dist/ops/read.d.ts.map +1 -1
- package/dist/ops/read.js +47 -7
- package/dist/ops/search.d.ts +18 -3
- package/dist/ops/search.d.ts.map +1 -1
- package/dist/ops/search.js +54 -2
- package/dist/testing/bundle-storage-contract.d.ts.map +1 -1
- package/dist/testing/bundle-storage-contract.js +61 -27
- package/dist/testing/registry-storage-contract.d.ts.map +1 -1
- package/dist/testing/registry-storage-contract.js +148 -1
- package/dist/types.d.ts +115 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +30 -14
- package/dist/utils/lazy-import.d.ts +17 -0
- package/dist/utils/lazy-import.d.ts.map +1 -0
- package/dist/utils/lazy-import.js +27 -0
- package/package.json +11 -5
package/dist/ops/read.d.ts
CHANGED
|
@@ -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` →
|
|
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 —
|
|
33
|
-
*
|
|
34
|
-
* the
|
|
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 |
|
|
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>;
|
package/dist/ops/read.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/ops/read.ts"],"names":[],"mappings":"AAAA
|
|
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
|
-
|
|
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
|
|
23
|
+
return notFoundResult(input);
|
|
17
24
|
}
|
|
18
|
-
if (row.visibility === 'private'
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/dist/ops/search.d.ts
CHANGED
|
@@ -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
|
|
10
|
-
*
|
|
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;
|
package/dist/ops/search.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/ops/search.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
package/dist/ops/search.js
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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"}
|