@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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function matchesMcpToolFilters(bindings, filters) {
|
|
2
|
+
const { tool, server } = filters;
|
|
3
|
+
if (tool === undefined && server === undefined)
|
|
4
|
+
return true;
|
|
5
|
+
if (bindings === undefined || bindings.length === 0)
|
|
6
|
+
return false;
|
|
7
|
+
return bindings.some((binding) => {
|
|
8
|
+
if (tool !== undefined && binding.tool !== tool)
|
|
9
|
+
return false;
|
|
10
|
+
if (server !== undefined && binding.server !== server)
|
|
11
|
+
return false;
|
|
12
|
+
return true;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `deleteAuthorKey` — pure op for `DELETE /author-keys/{keyId}`.
|
|
3
|
+
*
|
|
4
|
+
* Hard-deletes the `(subject, keyId)` row under the VERIFIED caller
|
|
5
|
+
* identity. Like {@link listAuthorKeys} there is no anonymous branch —
|
|
6
|
+
* transports MUST 401 before calling this op when the bearer
|
|
7
|
+
* credential is missing or invalid.
|
|
8
|
+
*
|
|
9
|
+
* Semantics (see {@link DeleteAuthorKeyResponseBody}):
|
|
10
|
+
*
|
|
11
|
+
* - Idempotent: an absent keyId is a 200 with `deleted: false`, not
|
|
12
|
+
* an error — retries and double-clicks converge.
|
|
13
|
+
* - Subject-scoped: the delete only ever addresses the caller's own
|
|
14
|
+
* partition. A keyId registered by ANOTHER subject is untouched
|
|
15
|
+
* and the response is byte-identical to the plain-absent case, so
|
|
16
|
+
* key existence never leaks across accounts.
|
|
17
|
+
* - Forward-only: removal blocks FUTURE publishes signed with this
|
|
18
|
+
* key. Versions already published stay valid — their signing key
|
|
19
|
+
* is pinned per version at publish time, which this delete never
|
|
20
|
+
* touches.
|
|
21
|
+
*/
|
|
22
|
+
import type { DeleteAuthorKeyErrorBody, DeleteAuthorKeyResponseBody } from '../types.js';
|
|
23
|
+
import type { AuthnContext } from '../interfaces/authn.js';
|
|
24
|
+
import type { RegistryStorage } from '../interfaces/registry-storage.js';
|
|
25
|
+
export interface DeleteAuthorKeyInput {
|
|
26
|
+
/** The keyId path segment — `derivePublicKeyId` output, non-empty. */
|
|
27
|
+
readonly keyId: string;
|
|
28
|
+
}
|
|
29
|
+
export interface DeleteAuthorKeyDeps {
|
|
30
|
+
readonly storage: RegistryStorage;
|
|
31
|
+
/** REQUIRED — the verified caller identity. No anonymous branch. */
|
|
32
|
+
readonly authn: AuthnContext;
|
|
33
|
+
}
|
|
34
|
+
export type DeleteAuthorKeyResult = {
|
|
35
|
+
readonly ok: true;
|
|
36
|
+
readonly status: 200;
|
|
37
|
+
readonly body: DeleteAuthorKeyResponseBody;
|
|
38
|
+
} | {
|
|
39
|
+
readonly ok: false;
|
|
40
|
+
readonly status: 400 | 500;
|
|
41
|
+
readonly body: DeleteAuthorKeyErrorBody;
|
|
42
|
+
};
|
|
43
|
+
export declare function deleteAuthorKey(input: DeleteAuthorKeyInput, deps: DeleteAuthorKeyDeps): Promise<DeleteAuthorKeyResult>;
|
|
44
|
+
export type { DeleteAuthorKeyErrorBody, DeleteAuthorKeyResponseBody };
|
|
45
|
+
//# sourceMappingURL=delete-author-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delete-author-key.d.ts","sourceRoot":"","sources":["../../src/ops/delete-author-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EACV,wBAAwB,EACxB,2BAA2B,EAC5B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAEzE,MAAM,WAAW,oBAAoB;IACnC,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;CAC9B;AAED,MAAM,MAAM,qBAAqB,GAC7B;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;CAC5C,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;CACzC,CAAC;AAEN,wBAAsB,eAAe,CACnC,KAAK,EAAE,oBAAoB,EAC3B,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA+BhC;AAID,YAAY,EAAE,wBAAwB,EAAE,2BAA2B,EAAE,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export async function deleteAuthorKey(input, deps) {
|
|
2
|
+
if (typeof input.keyId !== 'string' || input.keyId.length === 0) {
|
|
3
|
+
return {
|
|
4
|
+
ok: false,
|
|
5
|
+
status: 400,
|
|
6
|
+
body: {
|
|
7
|
+
error: 'invalid_request',
|
|
8
|
+
message: '`keyId` path segment is required',
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
let deleted;
|
|
13
|
+
try {
|
|
14
|
+
deleted = await deps.storage.deleteAuthorKey(deps.authn.subject, input.keyId);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
status: 500,
|
|
20
|
+
body: {
|
|
21
|
+
error: 'server_error',
|
|
22
|
+
message: `failed to delete author key: ${err instanceof Error ? err.message : String(err)}`,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
ok: true,
|
|
28
|
+
status: 200,
|
|
29
|
+
body: { keyId: input.keyId, deleted },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `listAuthorKeys` — pure op for `GET /author-keys`.
|
|
3
|
+
*
|
|
4
|
+
* Lists every signing key registered under the VERIFIED caller
|
|
5
|
+
* identity. Unlike the read/list-versions ops there is no anonymous
|
|
6
|
+
* branch: "my keys" has no public projection, so `deps.authn` is
|
|
7
|
+
* required — transports MUST 401 before calling this op when the
|
|
8
|
+
* bearer credential is missing or invalid.
|
|
9
|
+
*
|
|
10
|
+
* Ordering: newest first by `createdAt` (ISO strings compare
|
|
11
|
+
* lexicographically === chronologically), rows without `createdAt`
|
|
12
|
+
* (registered before the display enrichment) last, `keyId` tie-break —
|
|
13
|
+
* deterministic across storage impls whose list order is arbitrary.
|
|
14
|
+
*/
|
|
15
|
+
import type { ListAuthorKeysErrorBody, ListAuthorKeysResponseBody } from '../types.js';
|
|
16
|
+
import type { AuthnContext } from '../interfaces/authn.js';
|
|
17
|
+
import type { RegistryStorage } from '../interfaces/registry-storage.js';
|
|
18
|
+
export interface ListAuthorKeysDeps {
|
|
19
|
+
readonly storage: RegistryStorage;
|
|
20
|
+
/** REQUIRED — the verified caller identity. No anonymous branch. */
|
|
21
|
+
readonly authn: AuthnContext;
|
|
22
|
+
}
|
|
23
|
+
export type ListAuthorKeysResult = {
|
|
24
|
+
readonly ok: true;
|
|
25
|
+
readonly status: 200;
|
|
26
|
+
readonly body: ListAuthorKeysResponseBody;
|
|
27
|
+
} | {
|
|
28
|
+
readonly ok: false;
|
|
29
|
+
readonly status: 500;
|
|
30
|
+
readonly body: ListAuthorKeysErrorBody;
|
|
31
|
+
};
|
|
32
|
+
export declare function listAuthorKeys(deps: ListAuthorKeysDeps): Promise<ListAuthorKeysResult>;
|
|
33
|
+
export type { ListAuthorKeysErrorBody, ListAuthorKeysResponseBody };
|
|
34
|
+
//# sourceMappingURL=list-author-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-author-keys.d.ts","sourceRoot":"","sources":["../../src/ops/list-author-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAGV,uBAAuB,EACvB,0BAA0B,EAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAEzE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;CAC9B;AAED,MAAM,MAAM,oBAAoB,GAC5B;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,0BAA0B,CAAC;CAC3C,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;CACxC,CAAC;AAEN,wBAAsB,cAAc,CAClC,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,oBAAoB,CAAC,CAuB/B;AAyBD,YAAY,EAAE,uBAAuB,EAAE,0BAA0B,EAAE,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export async function listAuthorKeys(deps) {
|
|
2
|
+
const subject = deps.authn.subject;
|
|
3
|
+
let rows;
|
|
4
|
+
try {
|
|
5
|
+
rows = await deps.storage.listAuthorKeys(subject);
|
|
6
|
+
}
|
|
7
|
+
catch (err) {
|
|
8
|
+
return {
|
|
9
|
+
ok: false,
|
|
10
|
+
status: 500,
|
|
11
|
+
body: {
|
|
12
|
+
error: 'server_error',
|
|
13
|
+
message: `failed to list author keys: ${err instanceof Error ? err.message : String(err)}`,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const sorted = [...rows].sort(compareNewestFirst);
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
status: 200,
|
|
21
|
+
body: { subject, keys: sorted.map(rowToEntry) },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function compareNewestFirst(a, b) {
|
|
25
|
+
if (a.createdAt !== undefined && b.createdAt !== undefined) {
|
|
26
|
+
const byCreated = b.createdAt.localeCompare(a.createdAt);
|
|
27
|
+
if (byCreated !== 0)
|
|
28
|
+
return byCreated;
|
|
29
|
+
}
|
|
30
|
+
else if (a.createdAt !== undefined) {
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
else if (b.createdAt !== undefined) {
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
return a.keyId.localeCompare(b.keyId);
|
|
37
|
+
}
|
|
38
|
+
function rowToEntry(row) {
|
|
39
|
+
return {
|
|
40
|
+
keyId: row.keyId,
|
|
41
|
+
publicKeyBase64: row.publicKeyBase64,
|
|
42
|
+
...(row.createdAt !== undefined ? { createdAt: row.createdAt } : {}),
|
|
43
|
+
...(row.label !== undefined ? { label: row.label } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -7,12 +7,20 @@
|
|
|
7
7
|
* 1. Point-read the metadata row via {@link RegistryStorage.getArtifactMetadata}.
|
|
8
8
|
* Missing metadata → 404 (`not_found`).
|
|
9
9
|
* 2. Fetch all version rows via {@link RegistryStorage.listArtifactVersions}.
|
|
10
|
-
* 3. Filter out private rows
|
|
11
|
-
* as the read op
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* 3. Filter out private rows the caller cannot read — the same
|
|
11
|
+
* ownership rule as the read op ({@link canReadPrivateArtifact}:
|
|
12
|
+
* publisher or scope owner). Unreadable rows are FILTERED, never
|
|
13
|
+
* errored: an unauthorized caller (anonymous or authenticated)
|
|
14
|
+
* gets the readable subset. When NOTHING is readable, the
|
|
15
|
+
* response is the SAME 404 `not_found` as a true miss — a 200
|
|
16
|
+
* `versions: []` would differ from the miss shape and that
|
|
17
|
+
* difference is an existence oracle (cf. GitHub's
|
|
18
|
+
* 404-on-private-repo behaviour). The scope-owner lookup is lazy,
|
|
19
|
+
* memoized, and fail-closed ({@link createScopeOwnerResolver}):
|
|
20
|
+
* at most one {@link RegistryStorage.getScopeOwner} call per
|
|
21
|
+
* request; none when every row is public, the caller published
|
|
22
|
+
* every private row, or the caller is anonymous; a lookup fault
|
|
23
|
+
* denies instead of erroring.
|
|
16
24
|
* 4. Sort by semver DESC so the latest version is first. Yanked rows
|
|
17
25
|
* are NOT filtered — they stay in the list with `yanked: true`
|
|
18
26
|
* so the UI can show "this version was yanked".
|
|
@@ -38,10 +46,11 @@ export interface ListArtifactVersionsInput {
|
|
|
38
46
|
export interface ListArtifactVersionsDeps {
|
|
39
47
|
readonly storage: RegistryStorage;
|
|
40
48
|
/**
|
|
41
|
-
* Optional —
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
49
|
+
* Optional — the verified caller context, produced by the
|
|
50
|
+
* transport's own credential verification. Private versions appear
|
|
51
|
+
* in the response only for the caller who published them or the
|
|
52
|
+
* owner of the artifact's scope ({@link canReadPrivateArtifact});
|
|
53
|
+
* every other caller — anonymous included — gets the public subset.
|
|
45
54
|
*/
|
|
46
55
|
readonly authn?: AuthnContext;
|
|
47
56
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-versions.d.ts","sourceRoot":"","sources":["../../src/ops/list-versions.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"list-versions.d.ts","sourceRoot":"","sources":["../../src/ops/list-versions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,KAAK,EAEV,qBAAqB,EAErB,oBAAoB,EAErB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAOzE,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;CAC/B;AAED,MAAM,MAAM,0BAA0B,GAClC;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;CACrC,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;CACtC,CAAC;AAEN,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,yBAAyB,EAChC,IAAI,EAAE,wBAAwB,GAC7B,OAAO,CAAC,0BAA0B,CAAC,CAsErC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compareSemver } from '../utils/semver.js';
|
|
2
|
+
import { canReadPrivateArtifact, createScopeOwnerResolver, } from './private-read-authz.js';
|
|
2
3
|
export async function listArtifactVersions(input, deps) {
|
|
3
4
|
if (typeof input.artifactId !== 'string' || input.artifactId.length === 0) {
|
|
4
5
|
return errorResult(400, 'invalid_request', 'missing artifactId');
|
|
@@ -12,10 +13,11 @@ export async function listArtifactVersions(input, deps) {
|
|
|
12
13
|
metadataExists = metadata !== null;
|
|
13
14
|
}
|
|
14
15
|
catch (err) {
|
|
15
|
-
|
|
16
|
+
logStorageFailure('read metadata', input.artifactId, err);
|
|
17
|
+
return errorResult(500, 'server_error', 'failed to list versions');
|
|
16
18
|
}
|
|
17
19
|
if (!metadataExists) {
|
|
18
|
-
return
|
|
20
|
+
return notFoundResult(input);
|
|
19
21
|
}
|
|
20
22
|
// Step 2 — fetch all version rows.
|
|
21
23
|
let rows;
|
|
@@ -23,18 +25,31 @@ export async function listArtifactVersions(input, deps) {
|
|
|
23
25
|
rows = await deps.storage.listArtifactVersions(input.artifactId);
|
|
24
26
|
}
|
|
25
27
|
catch (err) {
|
|
26
|
-
|
|
28
|
+
logStorageFailure('list version rows', input.artifactId, err);
|
|
29
|
+
return errorResult(500, 'server_error', 'failed to list versions');
|
|
27
30
|
}
|
|
28
|
-
// Step 3 —
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
31
|
+
// Step 3 — ownership filter. A private row stays in the list only
|
|
32
|
+
// when the caller may read it under the shared rule (publisher or
|
|
33
|
+
// scope owner). All rows share one artifactId, hence one scope —
|
|
34
|
+
// the shared resolver memoizes the lazy owner lookup (one
|
|
35
|
+
// getScopeOwner call at most; none for public rows / anonymous
|
|
36
|
+
// callers / publisher-only matches) and fails CLOSED on a storage
|
|
37
|
+
// fault (deny + server-side log, never a distinctive error status).
|
|
38
|
+
const getScopeOwner = createScopeOwnerResolver(deps.storage, input.artifactId);
|
|
32
39
|
const visibleRows = [];
|
|
33
40
|
for (const row of rows) {
|
|
34
|
-
if (row.visibility === 'private' &&
|
|
41
|
+
if (row.visibility === 'private' &&
|
|
42
|
+
!(await canReadPrivateArtifact(deps.authn, row, getScopeOwner))) {
|
|
35
43
|
continue;
|
|
44
|
+
}
|
|
36
45
|
visibleRows.push(row);
|
|
37
46
|
}
|
|
47
|
+
// No visible versions ⇒ answer exactly as a true miss. Emitting a
|
|
48
|
+
// 200 `versions: []` here would differ from the miss shape, and
|
|
49
|
+
// that difference is an existence oracle for private artifacts.
|
|
50
|
+
if (visibleRows.length === 0) {
|
|
51
|
+
return notFoundResult(input);
|
|
52
|
+
}
|
|
38
53
|
// Step 4 — semver DESC sort. `compareSemver(a, b)` returns -1/0/1
|
|
39
54
|
// matching ascending order; flip the sign for DESC.
|
|
40
55
|
const sorted = [...visibleRows].sort((a, b) => -compareSemver(a.version, b.version));
|
|
@@ -55,6 +70,26 @@ function rowToEntry(row) {
|
|
|
55
70
|
visibility: row.visibility,
|
|
56
71
|
};
|
|
57
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The one not-found projection — used for a true miss AND an artifact
|
|
75
|
+
* with no versions visible to the caller, so the two responses cannot
|
|
76
|
+
* drift apart (drift would reintroduce the existence signal).
|
|
77
|
+
*/
|
|
78
|
+
function notFoundResult(input) {
|
|
79
|
+
return errorResult(404, 'not_found', `no such artifact: ${input.artifactId}`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Structured server-side failure log. Raw storage error text stays
|
|
83
|
+
* OUT of wire bodies (it can carry backend identifiers a caller has
|
|
84
|
+
* no business seeing); this log line is the operator's copy.
|
|
85
|
+
*/
|
|
86
|
+
function logStorageFailure(operation, artifactId, err) {
|
|
87
|
+
// eslint-disable-next-line no-console -- server-side operator signal; the wire stays generic
|
|
88
|
+
console.error(`registry list-versions: failed to ${operation}`, {
|
|
89
|
+
artifactId,
|
|
90
|
+
error: err instanceof Error ? err.message : String(err),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
58
93
|
function errorResult(status, error, message) {
|
|
59
94
|
return { ok: false, status, body: { error, message } };
|
|
60
95
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single ownership rule for private-row reads.
|
|
3
|
+
*
|
|
4
|
+
* A `visibility: 'private'` row is readable when the verified caller
|
|
5
|
+
* is the row's publisher (`authn.subject === row.publishedBy`) OR the
|
|
6
|
+
* owner of the artifact's scope (`authn.subject === ScopeOwnerRow.
|
|
7
|
+
* ownerSubject`). Everyone else — including anonymous callers — MUST
|
|
8
|
+
* receive a response indistinguishable from "no such artifact" so a
|
|
9
|
+
* probe can never confirm a private artifact exists.
|
|
10
|
+
*
|
|
11
|
+
* Every read-path consumer (the read op, the list-versions op, any
|
|
12
|
+
* transport route that serves private artifact content — bundle and
|
|
13
|
+
* signature byte routes included) MUST authorize through this one
|
|
14
|
+
* predicate. A second hand-rolled comparison is how the rule forks;
|
|
15
|
+
* import this instead. Note the byte-route obligation explicitly:
|
|
16
|
+
* metadata gating alone is insufficient while bundle URLs serve
|
|
17
|
+
* private bytes anonymously — a deployment MUST place its private
|
|
18
|
+
* bundle storage behind a route that applies this same predicate
|
|
19
|
+
* (the hosted deployment's public/private storage-prefix split does
|
|
20
|
+
* exactly that).
|
|
21
|
+
*
|
|
22
|
+
* ## Protocol & Contract Bar
|
|
23
|
+
*
|
|
24
|
+
* **Parties:** read-path ops and transport routes are the callers;
|
|
25
|
+
* the deployment's {@link RegistryStorage.getScopeOwner} backs the
|
|
26
|
+
* lazy scope-owner lookup.
|
|
27
|
+
*
|
|
28
|
+
* **Obligations:** callers MUST pass the verified caller context (or
|
|
29
|
+
* `undefined` for anonymous) — never a caller-supplied subject. The
|
|
30
|
+
* `getScopeOwner` thunk MUST resolve the ownership row for the
|
|
31
|
+
* artifact's scope (see {@link artifactScope}); it is invoked at most
|
|
32
|
+
* once, and ONLY when the caller is authenticated and not the
|
|
33
|
+
* publisher — the hot public path and the publisher fast path incur
|
|
34
|
+
* zero extra storage reads. Callers SHOULD build the thunk via
|
|
35
|
+
* {@link createScopeOwnerResolver}, which adds memoization and the
|
|
36
|
+
* fail-closed error posture.
|
|
37
|
+
*
|
|
38
|
+
* **Failure mode:** {@link createScopeOwnerResolver} resolves a
|
|
39
|
+
* storage failure to `null` (unclaimed) — the owner arm DENIES, the
|
|
40
|
+
* caller answers with its ordinary miss shape, and the fault is
|
|
41
|
+
* logged server-side. Failing open would serve the private row;
|
|
42
|
+
* failing loud (a 5xx only private rows can trigger) would leak
|
|
43
|
+
* existence. A hand-built thunk that rejects propagates to the
|
|
44
|
+
* caller instead — prefer the resolver.
|
|
45
|
+
*
|
|
46
|
+
* **Observable violation:** a private row served to a subject that is
|
|
47
|
+
* neither its `publishedBy` nor its scope's `ownerSubject` — pinned
|
|
48
|
+
* by the op suites (`read.test.ts`, `list-versions.test.ts`) and the
|
|
49
|
+
* server parity suite.
|
|
50
|
+
*/
|
|
51
|
+
import type { AuthnContext } from '../interfaces/authn.js';
|
|
52
|
+
import type { RegistryStorage } from '../interfaces/registry-storage.js';
|
|
53
|
+
import type { ArtifactVersionRow, ScopeOwnerRow } from '../types.js';
|
|
54
|
+
/**
|
|
55
|
+
* Decide whether `authn` may read a private row.
|
|
56
|
+
*
|
|
57
|
+
* @param authn verified caller context; `undefined` for anonymous.
|
|
58
|
+
* @param row the private row under decision (only `publishedBy` is
|
|
59
|
+
* consulted — pass the full row or a narrowed projection).
|
|
60
|
+
* @param getScopeOwner lazy scope-owner lookup for the artifact's
|
|
61
|
+
* scope. Called at most once, only when the publisher fast path
|
|
62
|
+
* misses. Build it with {@link createScopeOwnerResolver} for
|
|
63
|
+
* memoization + fail-closed semantics.
|
|
64
|
+
*/
|
|
65
|
+
export declare function canReadPrivateArtifact(authn: AuthnContext | undefined, row: Pick<ArtifactVersionRow, 'publishedBy'>, getScopeOwner: () => Promise<ScopeOwnerRow | null>): Promise<boolean>;
|
|
66
|
+
/**
|
|
67
|
+
* Build the memoized, fail-closed `getScopeOwner` thunk for
|
|
68
|
+
* {@link canReadPrivateArtifact}.
|
|
69
|
+
*
|
|
70
|
+
* - **Lazy** — no storage read happens until the owner arm is
|
|
71
|
+
* actually needed (public rows, anonymous callers, and publisher
|
|
72
|
+
* matches never trigger one).
|
|
73
|
+
* - **Memoized** — a caller checking many rows of one artifact
|
|
74
|
+
* (the list op) pays for at most one lookup.
|
|
75
|
+
* - **Fail-closed** — a storage failure resolves to `null`
|
|
76
|
+
* (unclaimed ⇒ deny) and is logged server-side with structure;
|
|
77
|
+
* the raw error never reaches a wire body. See the failure-mode
|
|
78
|
+
* section above for why neither fail-open nor fail-loud is
|
|
79
|
+
* acceptable here.
|
|
80
|
+
*/
|
|
81
|
+
export declare function createScopeOwnerResolver(storage: Pick<RegistryStorage, 'getScopeOwner'>, artifactId: string): () => Promise<ScopeOwnerRow | null>;
|
|
82
|
+
/**
|
|
83
|
+
* Extract the scope segment (leading `@` included) from a canonical
|
|
84
|
+
* `<@scope>/<name>` artifactId — the key for
|
|
85
|
+
* {@link RegistryStorage.getScopeOwner}.
|
|
86
|
+
*/
|
|
87
|
+
export declare function artifactScope(artifactId: string): string;
|
|
88
|
+
//# sourceMappingURL=private-read-authz.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"private-read-authz.d.ts","sourceRoot":"","sources":["../../src/ops/private-read-authz.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAErE;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,YAAY,GAAG,SAAS,EAC/B,GAAG,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,EAC5C,aAAa,EAAE,MAAM,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,GACjD,OAAO,CAAC,OAAO,CAAC,CAKlB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,eAAe,CAAC,EAC/C,UAAU,EAAE,MAAM,GACjB,MAAM,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAiBrC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGxD"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether `authn` may read a private row.
|
|
3
|
+
*
|
|
4
|
+
* @param authn verified caller context; `undefined` for anonymous.
|
|
5
|
+
* @param row the private row under decision (only `publishedBy` is
|
|
6
|
+
* consulted — pass the full row or a narrowed projection).
|
|
7
|
+
* @param getScopeOwner lazy scope-owner lookup for the artifact's
|
|
8
|
+
* scope. Called at most once, only when the publisher fast path
|
|
9
|
+
* misses. Build it with {@link createScopeOwnerResolver} for
|
|
10
|
+
* memoization + fail-closed semantics.
|
|
11
|
+
*/
|
|
12
|
+
export async function canReadPrivateArtifact(authn, row, getScopeOwner) {
|
|
13
|
+
if (authn === undefined)
|
|
14
|
+
return false;
|
|
15
|
+
if (authn.subject === row.publishedBy)
|
|
16
|
+
return true;
|
|
17
|
+
const owner = await getScopeOwner();
|
|
18
|
+
return owner !== null && owner.ownerSubject === authn.subject;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build the memoized, fail-closed `getScopeOwner` thunk for
|
|
22
|
+
* {@link canReadPrivateArtifact}.
|
|
23
|
+
*
|
|
24
|
+
* - **Lazy** — no storage read happens until the owner arm is
|
|
25
|
+
* actually needed (public rows, anonymous callers, and publisher
|
|
26
|
+
* matches never trigger one).
|
|
27
|
+
* - **Memoized** — a caller checking many rows of one artifact
|
|
28
|
+
* (the list op) pays for at most one lookup.
|
|
29
|
+
* - **Fail-closed** — a storage failure resolves to `null`
|
|
30
|
+
* (unclaimed ⇒ deny) and is logged server-side with structure;
|
|
31
|
+
* the raw error never reaches a wire body. See the failure-mode
|
|
32
|
+
* section above for why neither fail-open nor fail-loud is
|
|
33
|
+
* acceptable here.
|
|
34
|
+
*/
|
|
35
|
+
export function createScopeOwnerResolver(storage, artifactId) {
|
|
36
|
+
let lookup;
|
|
37
|
+
return () => {
|
|
38
|
+
lookup ??= storage.getScopeOwner(artifactScope(artifactId)).catch((err) => {
|
|
39
|
+
// eslint-disable-next-line no-console -- server-side operator signal; the wire stays opaque
|
|
40
|
+
console.error('registry: scope-owner lookup failed; treating scope as unclaimed (fail closed)', {
|
|
41
|
+
artifactId,
|
|
42
|
+
scope: artifactScope(artifactId),
|
|
43
|
+
error: err instanceof Error ? err.message : String(err),
|
|
44
|
+
});
|
|
45
|
+
return null;
|
|
46
|
+
});
|
|
47
|
+
return lookup;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extract the scope segment (leading `@` included) from a canonical
|
|
52
|
+
* `<@scope>/<name>` artifactId — the key for
|
|
53
|
+
* {@link RegistryStorage.getScopeOwner}.
|
|
54
|
+
*/
|
|
55
|
+
export function artifactScope(artifactId) {
|
|
56
|
+
const slash = artifactId.indexOf('/');
|
|
57
|
+
return slash === -1 ? artifactId : artifactId.slice(0, slash);
|
|
58
|
+
}
|
package/dist/ops/publish.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type GadgetSignature } from '@ggui-ai/gadget-signing';
|
|
1
|
+
import { type GadgetSignature, type VerifyBundleSigstoreInput } from '@ggui-ai/gadget-signing';
|
|
2
2
|
import type { PublishErrorBody, PublishResponseBody } from '../types.js';
|
|
3
3
|
import type { BlueprintProbeRunner } from './conformance.js';
|
|
4
4
|
import type { AuthnContext } from '../interfaces/authn.js';
|
|
@@ -12,6 +12,22 @@ import type { RegistryStorage } from '../interfaces/registry-storage.js';
|
|
|
12
12
|
* against an OSS-mirrored registry without re-bundling).
|
|
13
13
|
*/
|
|
14
14
|
export declare const MAX_BUNDLE_BYTES: number;
|
|
15
|
+
/**
|
|
16
|
+
* Scopes no publish may CLAIM — well-known names whose squatting would
|
|
17
|
+
* mislead installers about who authored an artifact. The default
|
|
18
|
+
* covers first-party names plus obvious squat-bait; a deployment
|
|
19
|
+
* REPLACES the whole list via
|
|
20
|
+
* {@link PublishArtifactDeps.reservedScopes} (spread this constant to
|
|
21
|
+
* extend it instead).
|
|
22
|
+
*
|
|
23
|
+
* Reserved scopes block first-publish CLAIMS only — not owned
|
|
24
|
+
* publishes. A registry operator who wants artifacts under a reserved
|
|
25
|
+
* name seeds its ownership row out-of-band via
|
|
26
|
+
* {@link RegistryStorage.updateScopeOwner}; once the row exists, the
|
|
27
|
+
* normal owner rule applies (the seeded owner publishes, everyone else
|
|
28
|
+
* gets `scope_forbidden`) with no change to this list.
|
|
29
|
+
*/
|
|
30
|
+
export declare const RESERVED_SCOPES: readonly string[];
|
|
15
31
|
export interface PublishArtifactInput {
|
|
16
32
|
/** Unvalidated request body — the op parses + validates. */
|
|
17
33
|
readonly manifest: unknown;
|
|
@@ -44,6 +60,13 @@ export interface PublishArtifactDeps {
|
|
|
44
60
|
* `http://` for `localhost`/`127.0.0.1`.
|
|
45
61
|
*/
|
|
46
62
|
readonly registryHostname: string;
|
|
63
|
+
/**
|
|
64
|
+
* Scopes no publish may claim — REPLACES {@link RESERVED_SCOPES}
|
|
65
|
+
* when set (spread the constant to extend instead:
|
|
66
|
+
* `[...RESERVED_SCOPES, '@my-brand']`). Exact-match against
|
|
67
|
+
* `manifest.scope`, leading `@` included.
|
|
68
|
+
*/
|
|
69
|
+
readonly reservedScopes?: readonly string[];
|
|
47
70
|
/**
|
|
48
71
|
* Optional runtime probe for blueprint manifests. Static gates
|
|
49
72
|
* always run; the probe additionally compiles + renders the
|
|
@@ -56,7 +79,51 @@ export interface PublishArtifactDeps {
|
|
|
56
79
|
* HTTP endpoint that should not pay for react-dom.
|
|
57
80
|
*/
|
|
58
81
|
readonly blueprintProbe?: BlueprintProbeRunner;
|
|
82
|
+
/**
|
|
83
|
+
* Optional TUF trust-root tuning forwarded verbatim to
|
|
84
|
+
* sigstore-cosign signature verification (see
|
|
85
|
+
* {@link VerifyBundleSigstoreInput}). `tufCachePath` points the
|
|
86
|
+
* verifier's trust-root cache at a writable directory — serverless
|
|
87
|
+
* runtimes typically only allow writes under `/tmp`.
|
|
88
|
+
* `tufForceCache` reuses valid cached TUF metadata without a remote
|
|
89
|
+
* refresh. `tufMirrorURL` + `tufRootPath` select an alternative TUF
|
|
90
|
+
* repository (private sigstore deployments; hermetic tests) — the
|
|
91
|
+
* trust root is THE knob that decides which signing infrastructure
|
|
92
|
+
* this registry's verifier trusts. Ed25519 verification never
|
|
93
|
+
* consults any of these.
|
|
94
|
+
*/
|
|
95
|
+
readonly sigstoreTuf?: Pick<VerifyBundleSigstoreInput, 'tufCachePath' | 'tufForceCache' | 'tufMirrorURL' | 'tufRootPath'>;
|
|
96
|
+
/**
|
|
97
|
+
* F4 identity binding — resolve the VERIFIED email address of the
|
|
98
|
+
* publishing account identified by `subject` (the same value as
|
|
99
|
+
* {@link AuthnContext.subject}). Return `undefined` when the account
|
|
100
|
+
* has no verified email; the resolver MUST NOT return an address the
|
|
101
|
+
* deployment's identity layer has not verified, because the publish
|
|
102
|
+
* gate authorizes signer identities against it.
|
|
103
|
+
*
|
|
104
|
+
* Consumed only on sigstore-signed publishes into a scope whose
|
|
105
|
+
* ownership row carries NO `sanAllowlist`: the bundle's certificate
|
|
106
|
+
* SAN must then equal the resolved email (compared
|
|
107
|
+
* case-insensitively). Scopes WITH an allowlist never consult the
|
|
108
|
+
* resolver — the allowlist is the stricter, operator-managed rule.
|
|
109
|
+
*
|
|
110
|
+
* OPTIONAL, and honestly so: a deployment that does not wire a
|
|
111
|
+
* resolver enforces publisher identity ONLY through per-scope
|
|
112
|
+
* allowlists ({@link ScopeOwnerRow.sanAllowlist}); scopes without
|
|
113
|
+
* one accept any identity a valid sigstore bundle proves. Wire a
|
|
114
|
+
* resolver backed by your identity provider to bind default
|
|
115
|
+
* publishes to account emails.
|
|
116
|
+
*/
|
|
117
|
+
readonly verifiedEmailResolver?: VerifiedEmailResolver;
|
|
59
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Deployment-provided lookup from an authenticated caller subject to
|
|
121
|
+
* that account's VERIFIED email address. See
|
|
122
|
+
* {@link PublishArtifactDeps.verifiedEmailResolver} for the contract
|
|
123
|
+
* (parties, obligations, and the fail-closed posture the publish gate
|
|
124
|
+
* layers on top).
|
|
125
|
+
*/
|
|
126
|
+
export type VerifiedEmailResolver = (subject: string) => Promise<string | undefined>;
|
|
60
127
|
export type PublishArtifactResult = {
|
|
61
128
|
readonly ok: true;
|
|
62
129
|
readonly status: 201;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../src/ops/publish.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../src/ops/publish.ts"],"names":[],"mappings":"AAwDA,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,yBAAyB,EAC/B,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EAIV,gBAAgB,EAEhB,mBAAmB,EAEpB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,oBAAoB,EAErB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAMzE;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,QAAkB,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAc5C,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,8FAA8F;IAC9F,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;CACrC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CACzB,yBAAyB,EACzB,cAAc,GAAG,eAAe,GAAG,cAAc,GAAG,aAAa,CAClE,CAAC;IACF;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;CACxD;AAED;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAClC,OAAO,EAAE,MAAM,KACZ,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjC,MAAM,MAAM,qBAAqB,GAC7B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAA;CAAE,GAC/E;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;CACjC,CAAC;AAEN,wBAAsB,eAAe,CACnC,KAAK,EAAE,oBAAoB,EAC3B,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,qBAAqB,CAAC,CAupBhC"}
|