@ggui-ai/registry-core 0.7.0 → 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 +3 -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 +11 -0
- 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/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 +162 -28
- 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 +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.map +1 -1
- package/dist/testing/registry-storage-contract.js +122 -1
- package/dist/types.d.ts +115 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +30 -14
- package/package.json +11 -5
|
@@ -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
|
@@ -93,7 +93,37 @@ export interface PublishArtifactDeps {
|
|
|
93
93
|
* consults any of these.
|
|
94
94
|
*/
|
|
95
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;
|
|
96
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>;
|
|
97
127
|
export type PublishArtifactResult = {
|
|
98
128
|
readonly ok: true;
|
|
99
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"}
|
package/dist/ops/publish.js
CHANGED
|
@@ -18,11 +18,21 @@
|
|
|
18
18
|
* unclaimed scope is claimed for the caller via the atomic
|
|
19
19
|
* {@link RegistryStorage.claimScope} (first-writer-wins; a lost
|
|
20
20
|
* race re-reads and re-applies the owner check). NOTE — the
|
|
21
|
-
* claim is durable
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* claim is durable when a LATER gate (bundle, conformance,
|
|
22
|
+
* crypto verify) fails this publish: by then the caller passed
|
|
23
|
+
* every policy gate, so the claim records legitimate intent, a
|
|
24
|
+
* failed-publish claim stays re-usable by the same caller, and
|
|
25
|
+
* an unverified claim remains reclaimable by the registry
|
|
26
|
+
* operator. Deliberately the simplest correct behavior.
|
|
27
|
+
* 2d. Bind the publisher identity (F4, sigstore-signed publishes
|
|
28
|
+
* only): the bundle certificate's SAN must be on the scope's
|
|
29
|
+
* `sanAllowlist` when the ownership row carries one, else must
|
|
30
|
+
* equal the account's verified email when the deployment wires
|
|
31
|
+
* {@link PublishArtifactDeps.verifiedEmailResolver}. Neither
|
|
32
|
+
* configured ⇒ no identity rule (allowlist-only posture).
|
|
33
|
+
* Violations answer 403 `identity_mismatch`. On the UNCLAIMED
|
|
34
|
+
* path this gate runs BEFORE the claim — a rejected signer
|
|
35
|
+
* must not walk away owning the scope.
|
|
26
36
|
* 3. Decode + size-check the bundle (gadgets only).
|
|
27
37
|
* 4. Recompute SHA-384 of the bundle bytes; compare to client claim.
|
|
28
38
|
* 5. Re-run the conformance gate ({@link checkConformance}).
|
|
@@ -38,11 +48,11 @@
|
|
|
38
48
|
* `latestVersion` when the new version is the highest semver.
|
|
39
49
|
* 10. Return 201 with the wire-locked {@link PublishResponseBody}.
|
|
40
50
|
*/
|
|
41
|
-
import { manifestToRegistryEntry, parseArtifactManifest, } from '@ggui-ai/artifact-manifest';
|
|
42
|
-
import { canonicalJson, extractSigstoreLeafCertPem, isGadgetSignature, verifyBundleEd25519, verifyBundleSigstore, } from '@ggui-ai/gadget-signing';
|
|
51
|
+
import { manifestToRegistryEntry, parseArtifactManifest, resolveMcpToolBindings, } from '@ggui-ai/artifact-manifest';
|
|
52
|
+
import { canonicalJson, extractSigstoreLeafCertPem, extractSigstoreSANs, isGadgetSignature, verifyBundleEd25519, verifyBundleSigstore, } from '@ggui-ai/gadget-signing';
|
|
43
53
|
import { bundleHostScheme, strictGadgetDescriptorSchema } from '@ggui-ai/protocol';
|
|
44
54
|
import { ZodError } from 'zod';
|
|
45
|
-
import { ARTIFACTS_METADATA_SK } from '../types.js';
|
|
55
|
+
import { ARTIFACTS_METADATA_SK, SAN_ALLOWLIST_INVALID } from '../types.js';
|
|
46
56
|
import { safeBase64Decode, sha384Base64 } from '../utils/base64.js';
|
|
47
57
|
import { compareSemver } from '../utils/semver.js';
|
|
48
58
|
import { compileBlueprint } from './compile.js';
|
|
@@ -145,24 +155,127 @@ export async function publishArtifact(input, deps) {
|
|
|
145
155
|
// somebody else's row is now durable and the re-read decides whose
|
|
146
156
|
// scope this is.
|
|
147
157
|
//
|
|
148
|
-
// The claim is DURABLE
|
|
149
|
-
// (documented judgment call — see
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
158
|
+
// The claim is DURABLE when a LATER gate (bundle, conformance,
|
|
159
|
+
// crypto verify) fails this publish (documented judgment call — see
|
|
160
|
+
// the flow docstring): by then the caller has already passed every
|
|
161
|
+
// policy gate, so the claim records legitimate intent. The identity
|
|
162
|
+
// gate (2d) is different — it runs BEFORE the claim on the unclaimed
|
|
163
|
+
// path, because a signer the scope's identity rule rejects must not
|
|
164
|
+
// walk away owning the scope. That ordering costs nothing: a fresh
|
|
165
|
+
// claim can never carry a `sanAllowlist`, so only the deployment's
|
|
166
|
+
// default email rule can apply pre-claim. The claim still lands
|
|
167
|
+
// before cryptographic signature verification: moving it after would
|
|
168
|
+
// not stop a motivated squatter (any authenticated caller can
|
|
169
|
+
// produce a validly-signed private publish), so the real defenses
|
|
170
|
+
// against mass squatting are the operator reclaim flow, the audit
|
|
171
|
+
// trail, and (future) rate limiting.
|
|
156
172
|
const scopeForbiddenByOwner = () => error(403, 'scope_forbidden', `scope \`${manifest.scope}\` is owned by another publisher. Choose a scope you own — your first publish into an unclaimed scope claims it. If you hold the rights to this name (for example the matching domain or brand), the registry operator can verify that ownership and reclaim an unverified scope.`);
|
|
173
|
+
// 2d. Publisher-identity binding (F4) — sigstore-signed publishes
|
|
174
|
+
// only (returns `undefined` = pass for Ed25519). Binds the bundle's
|
|
175
|
+
// certificate identity (the Fulcio cert's SAN) to the ggui account
|
|
176
|
+
// that owns (or is claiming) the scope, so a valid-but-unrelated
|
|
177
|
+
// OIDC identity can no longer sign publishes into it. Two rules,
|
|
178
|
+
// strictly ordered:
|
|
179
|
+
//
|
|
180
|
+
// 1. ALLOWLIST — the ownership row carries a non-empty
|
|
181
|
+
// `sanAllowlist`: the SAN must be one of its literal entries
|
|
182
|
+
// (operator-managed; org/CI flexibility).
|
|
183
|
+
// 2. VERIFIED EMAIL (default) — no allowlist, and the deployment
|
|
184
|
+
// wires {@link PublishArtifactDeps.verifiedEmailResolver}: the
|
|
185
|
+
// SAN must equal the account's verified email
|
|
186
|
+
// (case-insensitive). An account without a verified email
|
|
187
|
+
// fails closed.
|
|
188
|
+
//
|
|
189
|
+
// No allowlist AND no resolver ⇒ no identity rule — the deployment
|
|
190
|
+
// enforces identity only through allowlists (see the resolver
|
|
191
|
+
// docstring for why that posture is documented rather than papered
|
|
192
|
+
// over).
|
|
193
|
+
//
|
|
194
|
+
// Invocation points (all before bundle decode or cryptographic
|
|
195
|
+
// verify — the SAN is a cheap local projection, so a forbidden
|
|
196
|
+
// identity never pays for, or leaks errors from, bundle work):
|
|
197
|
+
// - claimed scope: with the stored ownership row;
|
|
198
|
+
// - unclaimed scope: with `null` BEFORE the claim (fresh claims
|
|
199
|
+
// never carry an allowlist — email rule only);
|
|
200
|
+
// - lost claim race won by the same subject: re-run with the
|
|
201
|
+
// winner row, which MAY be operator-seeded with an allowlist.
|
|
202
|
+
// The projection is trustworthy in the reject direction
|
|
203
|
+
// unconditionally; in the accept direction it is paired with the
|
|
204
|
+
// REAL `verifyBundleSigstore` at step 6, which proves the same
|
|
205
|
+
// SAN-bearing cert is genuinely CA-issued and bound to the signed
|
|
206
|
+
// bytes (same parser both places — the projection cannot drift from
|
|
207
|
+
// what verification enforces).
|
|
208
|
+
//
|
|
209
|
+
// Error hygiene: messages name the rule that failed and MAY echo the
|
|
210
|
+
// caller's OWN certificate identity (they supplied it), but NEVER
|
|
211
|
+
// other identities — not allowlist entries, not the account email.
|
|
212
|
+
const checkPublisherIdentity = async (ownerRow) => {
|
|
213
|
+
if (input.signature.algorithm !== 'sigstore-cosign')
|
|
214
|
+
return undefined;
|
|
215
|
+
// Fail closed on corrupt policy data: a storage adapter projects a
|
|
216
|
+
// malformed allowlist column as SAN_ALLOWLIST_INVALID (see the
|
|
217
|
+
// ScopeOwnerRow docstring). Falling through to the email rule (or
|
|
218
|
+
// no rule) would let corruption silently WIDEN who may publish.
|
|
219
|
+
const rawAllowlist = ownerRow?.sanAllowlist;
|
|
220
|
+
if (rawAllowlist === SAN_ALLOWLIST_INVALID) {
|
|
221
|
+
return error(500, 'internal', `scope \`${manifest.scope}\` has a malformed publisher-identity allowlist in storage — refusing to fall back to a weaker identity rule. A registry operator must rewrite the scope's allowlist (set or clear it) before sigstore publishes into this scope can proceed.`);
|
|
222
|
+
}
|
|
223
|
+
// An empty allowlist behaves like an absent one — the allowlist
|
|
224
|
+
// rule applies only when at least one entry names a signer.
|
|
225
|
+
const sanAllowlist = rawAllowlist ?? [];
|
|
226
|
+
const resolver = deps.verifiedEmailResolver;
|
|
227
|
+
if (sanAllowlist.length === 0 && resolver === undefined)
|
|
228
|
+
return undefined;
|
|
229
|
+
// EVERY SAN on the certificate (a Fulcio cert can carry both a
|
|
230
|
+
// URI and an email SAN) — the rules below accept on ANY hit, so a
|
|
231
|
+
// both-SAN cert matches whichever identity the policy names.
|
|
232
|
+
const sans = extractSigstoreSANs(input.signature).map((san) => san.toLowerCase());
|
|
233
|
+
if (sans.length === 0) {
|
|
234
|
+
return error(403, 'identity_mismatch', `scope \`${manifest.scope}\` requires a bound publisher identity, but the sigstore bundle's certificate carries no subject identity (SAN) to check`);
|
|
235
|
+
}
|
|
236
|
+
const echoedSans = sans.map((san) => `\`${san}\``).join(', ');
|
|
237
|
+
if (sanAllowlist.length > 0) {
|
|
238
|
+
// ONE case rule for every identity comparison (mirrors the email
|
|
239
|
+
// rule below): operator tooling lowercase-normalizes entries at
|
|
240
|
+
// write, and the comparison is case-insensitive regardless so
|
|
241
|
+
// rows seeded by other paths behave identically.
|
|
242
|
+
const allowlistLower = sanAllowlist.map((entry) => entry.toLowerCase());
|
|
243
|
+
if (!sans.some((san) => allowlistLower.includes(san))) {
|
|
244
|
+
return error(403, 'identity_mismatch', `no certificate identity (${echoedSans}) is on the publisher-identity allowlist for scope \`${manifest.scope}\`. Sign with an allowlisted identity, or ask the registry operator to update the scope's allowlist.`);
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
if (resolver !== undefined) {
|
|
249
|
+
const verifiedEmail = await resolver(deps.authn.subject);
|
|
250
|
+
if (verifiedEmail === undefined) {
|
|
251
|
+
return error(403, 'identity_mismatch', `scope \`${manifest.scope}\` binds publishes to the account's verified email, but the publishing account has none. Verify your account email, or ask the registry operator to set a publisher-identity allowlist for the scope. If you verified your email just now, it can take a minute to propagate — retry shortly.`);
|
|
252
|
+
}
|
|
253
|
+
if (!sans.includes(verifiedEmail.toLowerCase())) {
|
|
254
|
+
return error(403, 'identity_mismatch', `no certificate identity (${echoedSans}) matches the publishing account's verified email. Sign with the OIDC identity of your account email, or ask the registry operator to add this identity to the scope's publisher allowlist.`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
};
|
|
157
259
|
const existingOwner = await deps.storage.getScopeOwner(manifest.scope);
|
|
158
260
|
if (existingOwner !== null && existingOwner.ownerSubject !== deps.authn.subject) {
|
|
159
261
|
return scopeForbiddenByOwner();
|
|
160
262
|
}
|
|
161
|
-
if (existingOwner
|
|
263
|
+
if (existingOwner !== null) {
|
|
264
|
+
const identityFailure = await checkPublisherIdentity(existingOwner);
|
|
265
|
+
if (identityFailure !== undefined)
|
|
266
|
+
return identityFailure;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
162
269
|
const reservedScopes = deps.reservedScopes ?? RESERVED_SCOPES;
|
|
163
270
|
if (reservedScopes.includes(manifest.scope)) {
|
|
164
271
|
return error(403, 'scope_forbidden', `scope \`${manifest.scope}\` is reserved on this registry and cannot be claimed by publishing. Choose a scope you own — your first publish into an unclaimed scope claims it.`);
|
|
165
272
|
}
|
|
273
|
+
// Identity BEFORE the claim (review r1 finding 2): a rejected
|
|
274
|
+
// signer must not walk away owning the scope. Fresh claims carry
|
|
275
|
+
// no allowlist, so this pre-claim run applies the email rule only.
|
|
276
|
+
const identityFailure = await checkPublisherIdentity(null);
|
|
277
|
+
if (identityFailure !== undefined)
|
|
278
|
+
return identityFailure;
|
|
166
279
|
const claim = await deps.storage.claimScope({
|
|
167
280
|
scope: manifest.scope,
|
|
168
281
|
ownerSubject: deps.authn.subject,
|
|
@@ -180,6 +293,12 @@ export async function publishArtifact(input, deps) {
|
|
|
180
293
|
if (winner.ownerSubject !== deps.authn.subject) {
|
|
181
294
|
return scopeForbiddenByOwner();
|
|
182
295
|
}
|
|
296
|
+
// The winner row may be operator-seeded WITH an allowlist the
|
|
297
|
+
// pre-claim run (against `null`) never saw — re-apply the gate
|
|
298
|
+
// against the durable row.
|
|
299
|
+
const raceIdentityFailure = await checkPublisherIdentity(winner);
|
|
300
|
+
if (raceIdentityFailure !== undefined)
|
|
301
|
+
return raceIdentityFailure;
|
|
183
302
|
}
|
|
184
303
|
}
|
|
185
304
|
// 3. Bundle decode + size (gadgets only)
|
|
@@ -282,12 +401,15 @@ export async function publishArtifact(input, deps) {
|
|
|
282
401
|
}
|
|
283
402
|
else {
|
|
284
403
|
// Public gadgets — sigstore (Fulcio + Rekor) trust chain.
|
|
285
|
-
// Identity claim:
|
|
286
|
-
// the
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// the
|
|
404
|
+
// Identity claim: gate 2d already bound the certificate's SAN to
|
|
405
|
+
// the scope's allowlist / the account's verified email (where
|
|
406
|
+
// configured — see the gate for the honest no-rule posture). This
|
|
407
|
+
// verify is the cryptographic half of that pairing: it proves the
|
|
408
|
+
// SAN-bearing cert is genuinely CA-issued, transparency-logged,
|
|
409
|
+
// and bound to the signed bytes. Install-time consumers can layer
|
|
410
|
+
// their own policy via `--verify-identity <pattern>` (CLI install
|
|
411
|
+
// flag) — a separate trust decision controlled by the install
|
|
412
|
+
// operator, not the publisher.
|
|
291
413
|
const verifyResult = await verifyBundleSigstore({
|
|
292
414
|
bundleBytes: bytesForSignature,
|
|
293
415
|
signature: input.signature,
|
|
@@ -364,12 +486,17 @@ export async function publishArtifact(input, deps) {
|
|
|
364
486
|
// `version_exists` immediately — the publisher's idempotent retry path.
|
|
365
487
|
const nowIso = deps.clock().toISOString();
|
|
366
488
|
const sriHash = bundleBytes === undefined ? undefined : `sha384-${sha384Base64(bundleBytes)}`;
|
|
489
|
+
// H1 prefix split — the manifest's visibility selects the blob
|
|
490
|
+
// placement (`bundles/public/…` vs `bundles/private/…`), and the
|
|
491
|
+
// persisted row URLs reflect that real location: public URLs stay
|
|
492
|
+
// CDN/static-servable, private URLs resolve only through the
|
|
493
|
+
// authenticated private-bundle route.
|
|
367
494
|
const bundleUrl = bundleBytes === undefined
|
|
368
495
|
? undefined
|
|
369
|
-
: deps.bundleStorage.bundleUrl(manifest.scope, manifest.name, version);
|
|
496
|
+
: deps.bundleStorage.bundleUrl(manifest.scope, manifest.name, version, manifest.visibility);
|
|
370
497
|
const signatureUrl = bundleBytes === undefined
|
|
371
498
|
? undefined
|
|
372
|
-
: deps.bundleStorage.signatureUrl(manifest.scope, manifest.name, version);
|
|
499
|
+
: deps.bundleStorage.signatureUrl(manifest.scope, manifest.name, version, manifest.visibility);
|
|
373
500
|
const versionRow = {
|
|
374
501
|
artifactId,
|
|
375
502
|
version,
|
|
@@ -408,10 +535,10 @@ export async function publishArtifact(input, deps) {
|
|
|
408
535
|
let manifestUrl;
|
|
409
536
|
try {
|
|
410
537
|
if (bundleBytes !== undefined) {
|
|
411
|
-
await deps.bundleStorage.putBundle(manifest.scope, manifest.name, version, bundleBytes);
|
|
412
|
-
await deps.bundleStorage.putSignature(manifest.scope, manifest.name, version, input.signature);
|
|
538
|
+
await deps.bundleStorage.putBundle(manifest.scope, manifest.name, version, manifest.visibility, bundleBytes);
|
|
539
|
+
await deps.bundleStorage.putSignature(manifest.scope, manifest.name, version, manifest.visibility, input.signature);
|
|
413
540
|
}
|
|
414
|
-
manifestUrl = await deps.bundleStorage.putManifest(manifest.scope, manifest.name, version, manifest);
|
|
541
|
+
manifestUrl = await deps.bundleStorage.putManifest(manifest.scope, manifest.name, version, manifest.visibility, manifest);
|
|
415
542
|
}
|
|
416
543
|
catch (err) {
|
|
417
544
|
return error(500, 'internal', `failed to upload artifact: ${errorMessage(err)}`);
|
|
@@ -429,6 +556,11 @@ export async function publishArtifact(input, deps) {
|
|
|
429
556
|
// export name. The package may export several hooks/components;
|
|
430
557
|
// the manifest's `exports[]` is the source of truth.
|
|
431
558
|
const primaryExport = manifest.kind === 'gadget' ? manifest.exports[0] : undefined;
|
|
559
|
+
// Denormalized search field — the effective MCP tool bindings
|
|
560
|
+
// (declared wins entirely; blueprints derive from their contract).
|
|
561
|
+
// Search metadata ONLY: never part of contract canonicalization,
|
|
562
|
+
// blueprintKey, or any cache identity.
|
|
563
|
+
const bindingResolution = resolveMcpToolBindings(manifest);
|
|
432
564
|
const metadataRow = {
|
|
433
565
|
artifactId,
|
|
434
566
|
sk: ARTIFACTS_METADATA_SK,
|
|
@@ -442,6 +574,8 @@ export async function publishArtifact(input, deps) {
|
|
|
442
574
|
: 'hook' in primaryExport
|
|
443
575
|
? primaryExport.hook
|
|
444
576
|
: primaryExport.component,
|
|
577
|
+
mcpTools: bindingResolution?.bindings,
|
|
578
|
+
mcpToolsSource: bindingResolution?.source,
|
|
445
579
|
authorName: manifest.author?.name,
|
|
446
580
|
publishedAt: nowIso,
|
|
447
581
|
publishedBy: deps.authn.subject,
|
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,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"}
|