@ggui-ai/registry-server 0.6.3 → 0.7.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/authn/bearer.js +1 -1
- package/dist/filesystem-registry-storage.d.ts +1 -1
- package/dist/filesystem-registry-storage.d.ts.map +1 -1
- package/dist/filesystem-registry-storage.js +116 -5
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/server.d.ts +13 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +51 -17
- package/package.json +8 -4
package/dist/authn/bearer.js
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* emits 401 with `{ error: 'unauthorized', message: ... }`.
|
|
38
38
|
*
|
|
39
39
|
* **Observable violation:**
|
|
40
|
-
* - The integration test in `server.test.ts` exercises three negative
|
|
40
|
+
* - The integration test in `server.integration.test.ts` exercises three negative
|
|
41
41
|
* paths (no header, wrong scheme, bad token) and asserts identical
|
|
42
42
|
* 401 responses. Any divergence — a different status, a leaked
|
|
43
43
|
* token hint in the body — is a contract violation.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RegistryStorage } from '@ggui-ai/registry-core';
|
|
2
2
|
export interface FilesystemRegistryStorageOptions {
|
|
3
3
|
/** Absolute path to the state root. Must already exist or be `mkdir -p`-able. */
|
|
4
4
|
readonly root: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filesystem-registry-storage.d.ts","sourceRoot":"","sources":["../src/filesystem-registry-storage.ts"],"names":[],"mappings":"
|
|
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"}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* <root>/state/plugins/<encoded artifactId>.json → ArtifactsMetadataRow
|
|
6
6
|
* <root>/state/versions/<encoded artifactId>__<v>.json → ArtifactVersionRow
|
|
7
7
|
* <root>/state/author-keys/<subject>__<keyId>.json → AuthorKeyRow
|
|
8
|
+
* <root>/state/scope-owners/<encoded scope>.json → ScopeOwnerRow
|
|
8
9
|
*
|
|
9
10
|
* The `__` separator is chosen because npm pkg names + semvers never
|
|
10
11
|
* contain it (semver disallows underscores entirely; npm names disallow
|
|
@@ -19,6 +20,8 @@
|
|
|
19
20
|
* Mirrors DDB's `ConditionExpression: attribute_not_exists(...)`. The
|
|
20
21
|
* happy-path write is atomic at the inode level — partial writes on
|
|
21
22
|
* crash leave the file absent rather than half-populated.
|
|
23
|
+
* {@link claimScope} uses the same `wx` primitive for its first-writer-
|
|
24
|
+
* wins scope claim (`EEXIST` → `{ conflict: true }`).
|
|
22
25
|
*
|
|
23
26
|
* ## Path-traversal defense
|
|
24
27
|
*
|
|
@@ -61,8 +64,9 @@
|
|
|
61
64
|
* {@link registryStorageContract} suite against a fresh tmpdir per
|
|
62
65
|
* test. Plus path-traversal rejection tests local to this file.
|
|
63
66
|
*/
|
|
64
|
-
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
67
|
+
import { mkdir, readFile, readdir, unlink, writeFile } from 'node:fs/promises';
|
|
65
68
|
import { dirname, join } from 'node:path';
|
|
69
|
+
import { AuthorKeyAlreadyExistsError, } from '@ggui-ai/registry-core';
|
|
66
70
|
const DEFAULT_LIMIT = 50;
|
|
67
71
|
const MAX_LIMIT = 200;
|
|
68
72
|
export function createFilesystemRegistryStorage(options) {
|
|
@@ -74,6 +78,13 @@ export function createFilesystemRegistryStorage(options) {
|
|
|
74
78
|
// hex digest + `.json`. The digest itself is filename-safe (hex
|
|
75
79
|
// chars only), so no URL-encoding is needed.
|
|
76
80
|
const compiledBlobsDir = join(stateRoot, 'compiled-blobs');
|
|
81
|
+
// Scope-ownership rows — one file per claimed scope. The scope is
|
|
82
|
+
// URL-encoded like every other row key (`@` → `%40`).
|
|
83
|
+
const scopeOwnersDir = join(stateRoot, 'scope-owners');
|
|
84
|
+
const scopeOwnerPath = (scope) => {
|
|
85
|
+
rejectTraversal(scope, 'scope');
|
|
86
|
+
return join(scopeOwnersDir, `${encodeRowKey(scope)}.json`);
|
|
87
|
+
};
|
|
77
88
|
const compiledBlobPath = (compiledDigest) => {
|
|
78
89
|
rejectTraversal(compiledDigest, 'compiledDigest');
|
|
79
90
|
return join(compiledBlobsDir, `${compiledDigest}.json`);
|
|
@@ -84,7 +95,13 @@ export function createFilesystemRegistryStorage(options) {
|
|
|
84
95
|
return join(versionsDir, `${encodeRowKey(artifactId)}__${encodeRowKey(version)}.json`);
|
|
85
96
|
};
|
|
86
97
|
const authorKeyPath = (subject, keyId) => {
|
|
87
|
-
rejectTraversal
|
|
98
|
+
// NO rejectTraversal on `subject`: subjects are operator-defined
|
|
99
|
+
// free text (a '/' is legal), and `encodeRowKey` neutralizes every
|
|
100
|
+
// traversal-capable character ('/' → %2F) before the value ever
|
|
101
|
+
// becomes a filename component — the appended `__<keyId>.json`
|
|
102
|
+
// suffix additionally guarantees the segment can never be a bare
|
|
103
|
+
// '..'. keyIds keep the guard as defense-in-depth (base64url by
|
|
104
|
+
// derivation — '.', '/', '\\' are unrepresentable).
|
|
88
105
|
rejectTraversal(keyId, 'keyId');
|
|
89
106
|
return join(authorKeysDir, `${encodeRowKey(subject)}__${encodeRowKey(keyId)}.json`);
|
|
90
107
|
};
|
|
@@ -223,14 +240,94 @@ export function createFilesystemRegistryStorage(options) {
|
|
|
223
240
|
throw err;
|
|
224
241
|
}
|
|
225
242
|
},
|
|
243
|
+
async getScopeOwner(scope) {
|
|
244
|
+
return readJsonOrNull(scopeOwnerPath(scope));
|
|
245
|
+
},
|
|
246
|
+
async claimScope(row) {
|
|
247
|
+
// Atomic first-writer-wins via `wx` (open(2) O_EXCL) — mirrors
|
|
248
|
+
// the conditional-create semantics of a hosted database adapter.
|
|
249
|
+
// EEXIST maps to `{ conflict: true }` with the winner's file
|
|
250
|
+
// untouched.
|
|
251
|
+
await ensureDir(scopeOwnersDir);
|
|
252
|
+
try {
|
|
253
|
+
await writeFile(scopeOwnerPath(row.scope), JSON.stringify(row, null, 2), {
|
|
254
|
+
flag: 'wx',
|
|
255
|
+
encoding: 'utf8',
|
|
256
|
+
});
|
|
257
|
+
return { ok: true };
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
if (isErrnoException(err) && err.code === 'EEXIST') {
|
|
261
|
+
return { conflict: true };
|
|
262
|
+
}
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
async updateScopeOwner(row, expect) {
|
|
267
|
+
// Compare-and-set against the caller's read snapshot. Atomicity
|
|
268
|
+
// note: the read-compare-write sequence relies on the OSS
|
|
269
|
+
// server's single-process, single-writer posture (the same
|
|
270
|
+
// documented caveat as commitVersionAndBlob's blob leg) — the
|
|
271
|
+
// `wx` seed path below is additionally inode-atomic.
|
|
272
|
+
await ensureDir(scopeOwnersDir);
|
|
273
|
+
const path = scopeOwnerPath(row.scope);
|
|
274
|
+
if ('absent' in expect) {
|
|
275
|
+
// Seed path — O_EXCL create, EEXIST means a claim (or another
|
|
276
|
+
// seed) landed first.
|
|
277
|
+
try {
|
|
278
|
+
await writeFile(path, JSON.stringify(row, null, 2), {
|
|
279
|
+
flag: 'wx',
|
|
280
|
+
encoding: 'utf8',
|
|
281
|
+
});
|
|
282
|
+
return { ok: true };
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
if (isErrnoException(err) && err.code === 'EEXIST') {
|
|
286
|
+
return { conflict: true };
|
|
287
|
+
}
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const current = await readJsonOrNull(path);
|
|
292
|
+
if (current === null ||
|
|
293
|
+
current.ownerSubject !== expect.ownerSubject ||
|
|
294
|
+
current.verification !== expect.verification) {
|
|
295
|
+
return { conflict: true };
|
|
296
|
+
}
|
|
297
|
+
await writeJson(path, row);
|
|
298
|
+
return { ok: true };
|
|
299
|
+
},
|
|
226
300
|
async getAuthorKey(subject, keyId) {
|
|
227
301
|
return readJsonOrNull(authorKeyPath(subject, keyId));
|
|
228
302
|
},
|
|
229
|
-
async putAuthorKey(row) {
|
|
303
|
+
async putAuthorKey(row, options) {
|
|
230
304
|
await ensureDir(authorKeysDir);
|
|
231
|
-
|
|
305
|
+
const path = authorKeyPath(row.subject, row.keyId);
|
|
306
|
+
if (options?.ifNotExists === true) {
|
|
307
|
+
// O_EXCL conditional create — same `wx` primitive as
|
|
308
|
+
// putArtifactVersionIfAbsent/claimScope. EEXIST maps to the
|
|
309
|
+
// typed conflict the register op's TOCTOU close dispatches on.
|
|
310
|
+
try {
|
|
311
|
+
await writeFile(path, JSON.stringify(row, null, 2), {
|
|
312
|
+
flag: 'wx',
|
|
313
|
+
encoding: 'utf8',
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
if (isErrnoException(err) && err.code === 'EEXIST') {
|
|
319
|
+
throw new AuthorKeyAlreadyExistsError(row.subject, row.keyId);
|
|
320
|
+
}
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
await writeJson(path, row);
|
|
232
325
|
},
|
|
233
326
|
async listAuthorKeys(subject) {
|
|
327
|
+
// Filename-prefix candidate scan, then a row-field filter — the
|
|
328
|
+
// row's own `subject` is the unambiguous ownership signal
|
|
329
|
+
// (encodeRowKey leaves '_' unescaped, so a prefix alone could
|
|
330
|
+
// cross-match subjects containing '__').
|
|
234
331
|
const prefix = `${encodeRowKey(subject)}__`;
|
|
235
332
|
let entries;
|
|
236
333
|
try {
|
|
@@ -246,11 +343,25 @@ export function createFilesystemRegistryStorage(options) {
|
|
|
246
343
|
if (!entry.startsWith(prefix) || !entry.endsWith('.json'))
|
|
247
344
|
continue;
|
|
248
345
|
const row = await readJsonOrNull(join(authorKeysDir, entry));
|
|
249
|
-
if (row !== null)
|
|
346
|
+
if (row !== null && row.subject === subject)
|
|
250
347
|
matches.push(row);
|
|
251
348
|
}
|
|
252
349
|
return matches;
|
|
253
350
|
},
|
|
351
|
+
async deleteAuthorKey(subject, keyId) {
|
|
352
|
+
// ENOENT is the "row absent" signal (mirrors reads), mapped to
|
|
353
|
+
// the contract's `false`; anything else is a transport failure
|
|
354
|
+
// and throws.
|
|
355
|
+
try {
|
|
356
|
+
await unlink(authorKeyPath(subject, keyId));
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
if (isErrnoException(err) && err.code === 'ENOENT')
|
|
361
|
+
return false;
|
|
362
|
+
throw err;
|
|
363
|
+
}
|
|
364
|
+
},
|
|
254
365
|
};
|
|
255
366
|
}
|
|
256
367
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BlueprintProbeRunner, BundleStorage, RegistryStorage } from '@ggui-ai/registry-core';
|
|
1
|
+
import type { BlueprintProbeRunner, BundleStorage, PublishArtifactDeps, RegistryStorage } from '@ggui-ai/registry-core';
|
|
2
2
|
import type { BearerAuthn } from './authn/bearer.js';
|
|
3
3
|
export { createBearerAuthn, type BearerAuthn, type CreateBearerAuthnOptions } from './authn/bearer.js';
|
|
4
4
|
export { createFilesystemBundleStorage, type FilesystemBundleStorageOptions, } from './filesystem-bundle-storage.js';
|
|
@@ -40,6 +40,12 @@ export interface CreateRegistryServerOptions {
|
|
|
40
40
|
* e2e fixtures, not unauthenticated public publish endpoints.
|
|
41
41
|
*/
|
|
42
42
|
readonly blueprintProbe?: BlueprintProbeRunner;
|
|
43
|
+
/**
|
|
44
|
+
* Optional TUF trust-root tuning for sigstore-cosign publish
|
|
45
|
+
* verification. Forwarded to `createRegistryApp` (see
|
|
46
|
+
* {@link RegistryAppOptions.sigstoreTuf}).
|
|
47
|
+
*/
|
|
48
|
+
readonly sigstoreTuf?: PublishArtifactDeps['sigstoreTuf'];
|
|
43
49
|
}
|
|
44
50
|
export interface RegistryServerHandle {
|
|
45
51
|
/** The port the server is actually bound to. Resolves after {@link start}. */
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EACb,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;
|
|
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"}
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,9 @@ export function createRegistryServer(options) {
|
|
|
34
34
|
...(options.blueprintProbe !== undefined
|
|
35
35
|
? { blueprintProbe: options.blueprintProbe }
|
|
36
36
|
: {}),
|
|
37
|
+
...(options.sigstoreTuf !== undefined
|
|
38
|
+
? { sigstoreTuf: options.sigstoreTuf }
|
|
39
|
+
: {}),
|
|
37
40
|
});
|
|
38
41
|
const host = options.host ?? '0.0.0.0';
|
|
39
42
|
const requestedPort = options.port ?? 9001;
|
package/dist/server.d.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* GET /search → searchArtifacts
|
|
7
7
|
* GET /pkg/:scope/:name/:version → readArtifact
|
|
8
8
|
* POST /publish → publishArtifact (bearer-gated)
|
|
9
|
+
* POST /author-keys → registerAuthorKey (bearer-gated)
|
|
10
|
+
* GET /author-keys → listAuthorKeys (bearer-gated)
|
|
11
|
+
* DELETE /author-keys/:keyId → deleteAuthorKey (bearer-gated)
|
|
9
12
|
* POST /conformance/check → checkConformance (pre-flight gate)
|
|
10
13
|
* GET /bundles/:scope/:name/:version/bundle.js
|
|
11
14
|
* GET /bundles/:scope/:name/:version/bundle.js.sig
|
|
@@ -24,7 +27,7 @@
|
|
|
24
27
|
* browser-side).
|
|
25
28
|
*/
|
|
26
29
|
import { Hono } from 'hono';
|
|
27
|
-
import { type BlueprintProbeRunner, type BundleStorage, type RegistryStorage } from '@ggui-ai/registry-core';
|
|
30
|
+
import { type BlueprintProbeRunner, type PublishArtifactDeps, type BundleStorage, type RegistryStorage } from '@ggui-ai/registry-core';
|
|
28
31
|
import type { BearerAuthn } from './authn/bearer.js';
|
|
29
32
|
export interface RegistryAppOptions {
|
|
30
33
|
readonly storage: RegistryStorage;
|
|
@@ -41,6 +44,15 @@ export interface RegistryAppOptions {
|
|
|
41
44
|
* only the static gates from `checkConformance`.
|
|
42
45
|
*/
|
|
43
46
|
readonly blueprintProbe?: BlueprintProbeRunner;
|
|
47
|
+
/**
|
|
48
|
+
* Optional TUF trust-root tuning for sigstore-cosign publish
|
|
49
|
+
* verification, forwarded verbatim to the publish op. Point
|
|
50
|
+
* `tufMirrorURL` + `tufRootPath` at an alternative TUF repository
|
|
51
|
+
* (private sigstore deployments; hermetic tests); `tufCachePath` /
|
|
52
|
+
* `tufForceCache` control cache placement + warm reuse. Unset =
|
|
53
|
+
* the sigstore public-good trust root.
|
|
54
|
+
*/
|
|
55
|
+
readonly sigstoreTuf?: PublishArtifactDeps['sigstoreTuf'];
|
|
44
56
|
}
|
|
45
57
|
/**
|
|
46
58
|
* Build the hono app. Does NOT bind a port — callers (the CLI, the
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
package/dist/server.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* GET /search → searchArtifacts
|
|
7
7
|
* GET /pkg/:scope/:name/:version → readArtifact
|
|
8
8
|
* POST /publish → publishArtifact (bearer-gated)
|
|
9
|
+
* POST /author-keys → registerAuthorKey (bearer-gated)
|
|
10
|
+
* GET /author-keys → listAuthorKeys (bearer-gated)
|
|
11
|
+
* DELETE /author-keys/:keyId → deleteAuthorKey (bearer-gated)
|
|
9
12
|
* POST /conformance/check → checkConformance (pre-flight gate)
|
|
10
13
|
* GET /bundles/:scope/:name/:version/bundle.js
|
|
11
14
|
* GET /bundles/:scope/:name/:version/bundle.js.sig
|
|
@@ -24,7 +27,7 @@
|
|
|
24
27
|
* browser-side).
|
|
25
28
|
*/
|
|
26
29
|
import { Hono } from 'hono';
|
|
27
|
-
import { checkConformance, listArtifactVersions, publishArtifact, registerAuthorKey, readArtifact, searchArtifacts, } from '@ggui-ai/registry-core';
|
|
30
|
+
import { checkConformance, deleteAuthorKey, unauthorizedErrorBody, listArtifactVersions, listAuthorKeys, publishArtifact, registerAuthorKey, readArtifact, searchArtifacts, } from '@ggui-ai/registry-core';
|
|
28
31
|
const IMMUTABLE_CACHE_HEADER = 'public, max-age=31536000, immutable';
|
|
29
32
|
/**
|
|
30
33
|
* Build the hono app. Does NOT bind a port — callers (the CLI, the
|
|
@@ -36,19 +39,27 @@ export function createRegistryApp(options) {
|
|
|
36
39
|
const { storage, bundleStorage, authn, registryHostname } = options;
|
|
37
40
|
const clock = options.clock ?? (() => new Date());
|
|
38
41
|
const blueprintProbe = options.blueprintProbe;
|
|
42
|
+
const sigstoreTuf = options.sigstoreTuf;
|
|
39
43
|
const app = new Hono();
|
|
44
|
+
// Shared bearer gate for every authed route — ONE verification call
|
|
45
|
+
// shape, ONE 401 body (`unauthorizedErrorBody`, exported beside the
|
|
46
|
+
// registry-core error enums so every transport of this wire contract
|
|
47
|
+
// agrees).
|
|
48
|
+
const verifyBearer = (c) => authn.verify(c.req.header('authorization'));
|
|
40
49
|
// CORS — permissive read; strict for /publish. Adds ACAO headers
|
|
41
50
|
// AFTER the route runs so they overlay on the route's response.
|
|
42
51
|
app.use('*', async (c, next) => {
|
|
43
52
|
const path = c.req.path;
|
|
44
53
|
const isPublishRoute = path === '/publish';
|
|
45
54
|
if (c.req.method === 'OPTIONS' && !isPublishRoute) {
|
|
46
|
-
// Preflight short-circuit for read routes.
|
|
55
|
+
// Preflight short-circuit for read routes. DELETE is listed for
|
|
56
|
+
// the author-key management route — a browser console revokes
|
|
57
|
+
// keys cross-origin ([E]).
|
|
47
58
|
return new Response(null, {
|
|
48
59
|
status: 204,
|
|
49
60
|
headers: {
|
|
50
61
|
'Access-Control-Allow-Origin': '*',
|
|
51
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
62
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
52
63
|
'Access-Control-Allow-Headers': 'content-type, authorization',
|
|
53
64
|
},
|
|
54
65
|
});
|
|
@@ -56,7 +67,7 @@ export function createRegistryApp(options) {
|
|
|
56
67
|
await next();
|
|
57
68
|
if (!isPublishRoute) {
|
|
58
69
|
c.res.headers.set('Access-Control-Allow-Origin', '*');
|
|
59
|
-
c.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
70
|
+
c.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
60
71
|
c.res.headers.set('Access-Control-Allow-Headers', 'content-type, authorization');
|
|
61
72
|
}
|
|
62
73
|
});
|
|
@@ -135,13 +146,9 @@ export function createRegistryApp(options) {
|
|
|
135
146
|
// Defensive: don't echo. Browsers will reject without the
|
|
136
147
|
// ACAO header, which is the protection we want.
|
|
137
148
|
}
|
|
138
|
-
const
|
|
139
|
-
const verified = authn.verify(authHeader);
|
|
149
|
+
const verified = verifyBearer(c);
|
|
140
150
|
if (verified === null) {
|
|
141
|
-
return c.json(
|
|
142
|
-
error: 'unauthorized',
|
|
143
|
-
message: 'publish requires a valid bearer token in the Authorization header',
|
|
144
|
-
}, 401);
|
|
151
|
+
return c.json(unauthorizedErrorBody(), 401);
|
|
145
152
|
}
|
|
146
153
|
let body;
|
|
147
154
|
try {
|
|
@@ -162,6 +169,7 @@ export function createRegistryApp(options) {
|
|
|
162
169
|
clock,
|
|
163
170
|
registryHostname,
|
|
164
171
|
...(blueprintProbe !== undefined ? { blueprintProbe } : {}),
|
|
172
|
+
...(sigstoreTuf !== undefined ? { sigstoreTuf } : {}),
|
|
165
173
|
});
|
|
166
174
|
return c.json(result.body, result.status);
|
|
167
175
|
});
|
|
@@ -170,13 +178,9 @@ export function createRegistryApp(options) {
|
|
|
170
178
|
// bearer-authenticated subject's identity. Shares one wire contract
|
|
171
179
|
// with the hosted registry's author-keys endpoint.
|
|
172
180
|
app.post('/author-keys', async (c) => {
|
|
173
|
-
const
|
|
174
|
-
const verified = authn.verify(authHeader);
|
|
181
|
+
const verified = verifyBearer(c);
|
|
175
182
|
if (verified === null) {
|
|
176
|
-
return c.json(
|
|
177
|
-
error: 'unauthorized',
|
|
178
|
-
message: 'author-keys requires a valid bearer token in the Authorization header',
|
|
179
|
-
}, 401);
|
|
183
|
+
return c.json(unauthorizedErrorBody(), 401);
|
|
180
184
|
}
|
|
181
185
|
let rawBody;
|
|
182
186
|
try {
|
|
@@ -201,7 +205,37 @@ export function createRegistryApp(options) {
|
|
|
201
205
|
message: '`publicKeyBase64` must be a string',
|
|
202
206
|
}, 400);
|
|
203
207
|
}
|
|
204
|
-
const result = await registerAuthorKey({
|
|
208
|
+
const result = await registerAuthorKey({
|
|
209
|
+
publicKeyBase64: body.publicKeyBase64,
|
|
210
|
+
...(body.label !== undefined ? { label: body.label } : {}),
|
|
211
|
+
}, { storage, authn: verified, clock });
|
|
212
|
+
return c.json(result.body, result.status);
|
|
213
|
+
});
|
|
214
|
+
// ── GET /author-keys ─────────────────────────────────────────────────
|
|
215
|
+
// Lists the bearer-authenticated subject's registered signing keys.
|
|
216
|
+
// Bearer REQUIRED: "my keys" has no anonymous projection, so a
|
|
217
|
+
// missing/invalid token is a 401 — never an empty anonymous list.
|
|
218
|
+
app.get('/author-keys', async (c) => {
|
|
219
|
+
const verified = verifyBearer(c);
|
|
220
|
+
if (verified === null) {
|
|
221
|
+
return c.json(unauthorizedErrorBody(), 401);
|
|
222
|
+
}
|
|
223
|
+
const result = await listAuthorKeys({ storage, authn: verified });
|
|
224
|
+
return c.json(result.body, result.status);
|
|
225
|
+
});
|
|
226
|
+
// ── DELETE /author-keys/:keyId ───────────────────────────────────────
|
|
227
|
+
// Hard-deletes the caller's `(subject, keyId)` row. Idempotent
|
|
228
|
+
// (absent → 200 `deleted: false`) and subject-scoped — see the op's
|
|
229
|
+
// docstring. Bearer REQUIRED, same as the list route. keyIds are
|
|
230
|
+
// base64url (`derivePublicKeyId`) — URL-safe by construction, the
|
|
231
|
+
// path param is the keyId verbatim.
|
|
232
|
+
app.delete('/author-keys/:keyId', async (c) => {
|
|
233
|
+
const verified = verifyBearer(c);
|
|
234
|
+
if (verified === null) {
|
|
235
|
+
return c.json(unauthorizedErrorBody(), 401);
|
|
236
|
+
}
|
|
237
|
+
const { keyId } = c.req.param();
|
|
238
|
+
const result = await deleteAuthorKey({ keyId }, { storage, authn: verified });
|
|
205
239
|
return c.json(result.body, result.status);
|
|
206
240
|
});
|
|
207
241
|
// ── /conformance/check ───────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/registry-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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,12 +31,16 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@hono/node-server": "^2.0.12",
|
|
33
33
|
"hono": "^4.12.8",
|
|
34
|
-
"@ggui-ai/artifact-manifest": "0.
|
|
35
|
-
"@ggui-ai/gadget-signing": "0.
|
|
36
|
-
"@ggui-ai/registry-core": "0.
|
|
34
|
+
"@ggui-ai/artifact-manifest": "0.7.0",
|
|
35
|
+
"@ggui-ai/gadget-signing": "0.7.0",
|
|
36
|
+
"@ggui-ai/registry-core": "0.7.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
+
"@sigstore/mock": "~0.12.1",
|
|
40
|
+
"@sigstore/protobuf-specs": "^0.5.1",
|
|
41
|
+
"@tufjs/repo-mock": "4.0.1",
|
|
39
42
|
"@types/node": "^24.0.0",
|
|
43
|
+
"nock": "^13.5.6",
|
|
40
44
|
"tsx": "^4.19.0",
|
|
41
45
|
"typescript": "^5.0.0",
|
|
42
46
|
"vitest": "^3.2.6"
|