@ggui-ai/registry-core 0.6.3 → 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/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 +47 -4
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/install-command.d.ts +25 -0
- package/dist/install-command.d.ts.map +1 -0
- package/dist/install-command.js +15 -0
- 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 +106 -1
- 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/delete-author-key.d.ts +45 -0
- package/dist/ops/delete-author-key.d.ts.map +1 -0
- package/dist/ops/delete-author-key.js +31 -0
- package/dist/ops/list-author-keys.d.ts +34 -0
- package/dist/ops/list-author-keys.d.ts.map +1 -0
- package/dist/ops/list-author-keys.js +45 -0
- 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 +68 -1
- package/dist/ops/publish.d.ts.map +1 -1
- package/dist/ops/publish.js +292 -16
- 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/register-author-key.d.ts +13 -0
- package/dist/ops/register-author-key.d.ts.map +1 -1
- package/dist/ops/register-author-key.js +42 -0
- package/dist/ops/search.d.ts +13 -1
- 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 +1 -1
- package/dist/testing/registry-storage-contract.d.ts.map +1 -1
- package/dist/testing/registry-storage-contract.js +351 -1
- package/dist/types.d.ts +257 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +79 -14
- package/package.json +20 -5
|
@@ -40,6 +40,8 @@ import { derivePublicKeyId } from '@ggui-ai/gadget-signing';
|
|
|
40
40
|
import { AuthorKeyAlreadyExistsError, } from '../interfaces/registry-storage.js';
|
|
41
41
|
import { safeBase64Decode } from '../utils/base64.js';
|
|
42
42
|
const ED25519_PUBLIC_KEY_BYTES = 32;
|
|
43
|
+
/** Display-name budget — a label is a short human hint, not a document. */
|
|
44
|
+
export const MAX_LABEL_LENGTH = 100;
|
|
43
45
|
export async function registerAuthorKey(input, deps) {
|
|
44
46
|
if (typeof input.publicKeyBase64 !== 'string' ||
|
|
45
47
|
input.publicKeyBase64.length === 0) {
|
|
@@ -73,12 +75,42 @@ export async function registerAuthorKey(input, deps) {
|
|
|
73
75
|
},
|
|
74
76
|
};
|
|
75
77
|
}
|
|
78
|
+
// Label ([E] enrichment) — defend with a typeof check even though the
|
|
79
|
+
// input type says string (transports hand over parsed JSON); trim;
|
|
80
|
+
// whitespace-only collapses to absent.
|
|
81
|
+
if (input.label !== undefined && typeof input.label !== 'string') {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
status: 400,
|
|
85
|
+
body: {
|
|
86
|
+
error: 'invalid_request',
|
|
87
|
+
message: '`label` must be a string',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const trimmedLabel = input.label?.trim();
|
|
92
|
+
const label = trimmedLabel !== undefined && trimmedLabel.length > 0
|
|
93
|
+
? trimmedLabel
|
|
94
|
+
: undefined;
|
|
95
|
+
if (label !== undefined && label.length > MAX_LABEL_LENGTH) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
status: 400,
|
|
99
|
+
body: {
|
|
100
|
+
error: 'invalid_request',
|
|
101
|
+
message: `\`label\` must be at most ${MAX_LABEL_LENGTH} characters (got ${label.length})`,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
76
105
|
const keyId = derivePublicKeyId(decoded);
|
|
77
106
|
const subject = deps.authn.subject;
|
|
107
|
+
const clock = deps.clock ?? (() => new Date());
|
|
78
108
|
const row = {
|
|
79
109
|
subject,
|
|
80
110
|
keyId,
|
|
81
111
|
publicKeyBase64: input.publicKeyBase64,
|
|
112
|
+
createdAt: clock().toISOString(),
|
|
113
|
+
...(label !== undefined ? { label } : {}),
|
|
82
114
|
};
|
|
83
115
|
// Atomic first-write attempt. The `ifNotExists` flag tells the
|
|
84
116
|
// storage adapter to use a conditional put (a DDB
|
|
@@ -103,6 +135,8 @@ export async function registerAuthorKey(input, deps) {
|
|
|
103
135
|
subject,
|
|
104
136
|
keyId,
|
|
105
137
|
publicKeyBase64: input.publicKeyBase64,
|
|
138
|
+
...(row.createdAt !== undefined ? { createdAt: row.createdAt } : {}),
|
|
139
|
+
...(row.label !== undefined ? { label: row.label } : {}),
|
|
106
140
|
},
|
|
107
141
|
};
|
|
108
142
|
}
|
|
@@ -150,6 +184,10 @@ export async function registerAuthorKey(input, deps) {
|
|
|
150
184
|
};
|
|
151
185
|
}
|
|
152
186
|
if (existing.publicKeyBase64 === input.publicKeyBase64) {
|
|
187
|
+
// Idempotent re-register echoes the EXISTING row untouched — a
|
|
188
|
+
// different label on a retry does NOT rewrite the stored one
|
|
189
|
+
// (relabeling is a deliberate future op, not a register side
|
|
190
|
+
// effect).
|
|
153
191
|
return {
|
|
154
192
|
ok: true,
|
|
155
193
|
status: 200,
|
|
@@ -157,6 +195,10 @@ export async function registerAuthorKey(input, deps) {
|
|
|
157
195
|
subject: existing.subject,
|
|
158
196
|
keyId: existing.keyId,
|
|
159
197
|
publicKeyBase64: existing.publicKeyBase64,
|
|
198
|
+
...(existing.createdAt !== undefined
|
|
199
|
+
? { createdAt: existing.createdAt }
|
|
200
|
+
: {}),
|
|
201
|
+
...(existing.label !== undefined ? { label: existing.label } : {}),
|
|
160
202
|
},
|
|
161
203
|
};
|
|
162
204
|
}
|
package/dist/ops/search.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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.
|
|
@@ -31,6 +31,18 @@ export interface SearchArtifactsInput {
|
|
|
31
31
|
* globally-correct newest-first order across the full cursor chain.
|
|
32
32
|
*/
|
|
33
33
|
readonly sort?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Exact MCP tool-name filter — matches artifacts whose bindings
|
|
36
|
+
* include this tool (declared under any server, or none). Must match
|
|
37
|
+
* `^[A-Za-z0-9_.-]{1,128}$`; anything else is a 400.
|
|
38
|
+
*/
|
|
39
|
+
readonly tool?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Exact MCP server-name filter — matches artifacts whose bindings
|
|
42
|
+
* declare this server; bindings without a server never match. Same
|
|
43
|
+
* charset rule as `tool`.
|
|
44
|
+
*/
|
|
45
|
+
readonly server?: string;
|
|
34
46
|
}
|
|
35
47
|
export interface SearchArtifactsDeps {
|
|
36
48
|
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;;;;;;;;;;GAUG;AACH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/ops/search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;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,3 +1,3 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RegistryStorage } from '../interfaces/registry-storage.js';
|
|
2
2
|
export declare function registryStorageContract(makeStorage: () => RegistryStorage): void;
|
|
3
3
|
//# sourceMappingURL=registry-storage-contract.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry-storage-contract.d.ts","sourceRoot":"","sources":["../../src/testing/registry-storage-contract.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,
|
|
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,CAgzBhF"}
|