@ggui-ai/registry-core 0.1.0-rc.1

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.
Files changed (60) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +76 -0
  3. package/dist/impls/memory-bundle-storage.d.ts +12 -0
  4. package/dist/impls/memory-bundle-storage.d.ts.map +1 -0
  5. package/dist/impls/memory-bundle-storage.js +40 -0
  6. package/dist/impls/memory-registry-storage.d.ts +3 -0
  7. package/dist/impls/memory-registry-storage.d.ts.map +1 -0
  8. package/dist/impls/memory-registry-storage.js +154 -0
  9. package/dist/index.d.ts +26 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +26 -0
  12. package/dist/interfaces/authn.d.ts +51 -0
  13. package/dist/interfaces/authn.d.ts.map +1 -0
  14. package/dist/interfaces/authn.js +1 -0
  15. package/dist/interfaces/bundle-storage.d.ts +79 -0
  16. package/dist/interfaces/bundle-storage.d.ts.map +1 -0
  17. package/dist/interfaces/bundle-storage.js +1 -0
  18. package/dist/interfaces/registry-storage.d.ts +204 -0
  19. package/dist/interfaces/registry-storage.d.ts.map +1 -0
  20. package/dist/interfaces/registry-storage.js +19 -0
  21. package/dist/ops/compile.d.ts +48 -0
  22. package/dist/ops/compile.d.ts.map +1 -0
  23. package/dist/ops/compile.js +163 -0
  24. package/dist/ops/conformance.d.ts +154 -0
  25. package/dist/ops/conformance.d.ts.map +1 -0
  26. package/dist/ops/conformance.js +487 -0
  27. package/dist/ops/list-versions.d.ts +58 -0
  28. package/dist/ops/list-versions.d.ts.map +1 -0
  29. package/dist/ops/list-versions.js +60 -0
  30. package/dist/ops/publish.d.ts +70 -0
  31. package/dist/ops/publish.d.ts.map +1 -0
  32. package/dist/ops/publish.js +417 -0
  33. package/dist/ops/read.d.ts +52 -0
  34. package/dist/ops/read.d.ts.map +1 -0
  35. package/dist/ops/read.js +60 -0
  36. package/dist/ops/register-author-key.d.ts +22 -0
  37. package/dist/ops/register-author-key.d.ts.map +1 -0
  38. package/dist/ops/register-author-key.js +171 -0
  39. package/dist/ops/search.d.ts +50 -0
  40. package/dist/ops/search.d.ts.map +1 -0
  41. package/dist/ops/search.js +106 -0
  42. package/dist/testing/bundle-storage-contract.d.ts +3 -0
  43. package/dist/testing/bundle-storage-contract.d.ts.map +1 -0
  44. package/dist/testing/bundle-storage-contract.js +176 -0
  45. package/dist/testing/index.d.ts +10 -0
  46. package/dist/testing/index.d.ts.map +1 -0
  47. package/dist/testing/index.js +9 -0
  48. package/dist/testing/registry-storage-contract.d.ts +3 -0
  49. package/dist/testing/registry-storage-contract.d.ts.map +1 -0
  50. package/dist/testing/registry-storage-contract.js +392 -0
  51. package/dist/types.d.ts +482 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +110 -0
  54. package/dist/utils/base64.d.ts +15 -0
  55. package/dist/utils/base64.d.ts.map +1 -0
  56. package/dist/utils/base64.js +35 -0
  57. package/dist/utils/semver.d.ts +13 -0
  58. package/dist/utils/semver.d.ts.map +1 -0
  59. package/dist/utils/semver.js +69 -0
  60. package/package.json +85 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * `listArtifactVersions` — pure op for `GET /pkg/{scope}/{name}`.
3
+ * Mirrors {@link readArtifact}'s structure — same authn injection
4
+ * point, same storage seam, same discriminated-union result.
5
+ *
6
+ * Flow:
7
+ * 1. Point-read the metadata row via {@link RegistryStorage.getArtifactMetadata}.
8
+ * Missing metadata → 404 (`not_found`).
9
+ * 2. Fetch all version rows via {@link RegistryStorage.listArtifactVersions}.
10
+ * 3. Filter out private rows for unauthenticated callers — same gate
11
+ * as the read op's `visibility === 'private' && authn === undefined`
12
+ * branch. We do NOT 403 on a fully-private artifact for an unauthed
13
+ * caller; we 200 with `versions: []` so the wire response doesn't
14
+ * leak the existence of a private artifact (cf. GitHub's 404-on-
15
+ * private-repo behaviour).
16
+ * 4. Sort by semver DESC so the latest version is first. Yanked rows
17
+ * are NOT filtered — they stay in the list with `yanked: true`
18
+ * so the UI can show "this version was yanked".
19
+ * 5. Project the row → {@link VersionListEntry} shape (drop heavy
20
+ * fields: manifest, compiledDigest, manifestSig).
21
+ *
22
+ * **Why not gate on metadata.visibility?** A private artifact might
23
+ * have a public version (visibility lives on every version row
24
+ * independently). Authn-gating at the metadata level would over-block.
25
+ * The version-by-version filter in step 3 is the precise gate.
26
+ */
27
+ import type { ListVersionsErrorBody, ListVersionsResponse } from '../types.js';
28
+ import type { AuthnContext } from '../interfaces/authn.js';
29
+ import type { RegistryStorage } from '../interfaces/registry-storage.js';
30
+ export interface ListArtifactVersionsInput {
31
+ /**
32
+ * `<scope>/<name>` — the install identifier. Scope MUST start with `@`.
33
+ * Cloud API Gateway path params drop the leading `@`; the cloud
34
+ * Lambda shell re-prepends before calling this op.
35
+ */
36
+ readonly artifactId: string;
37
+ }
38
+ export interface ListArtifactVersionsDeps {
39
+ readonly storage: RegistryStorage;
40
+ /**
41
+ * Optional — when undefined, the op filters `private` versions OUT
42
+ * of the response. Authenticated callers see every version they own
43
+ * (the storage layer's row-level visibility filter is the source of
44
+ * truth; this op just honours it).
45
+ */
46
+ readonly authn?: AuthnContext;
47
+ }
48
+ export type ListArtifactVersionsResult = {
49
+ readonly ok: true;
50
+ readonly status: 200;
51
+ readonly body: ListVersionsResponse;
52
+ } | {
53
+ readonly ok: false;
54
+ readonly status: 400 | 404 | 500;
55
+ readonly body: ListVersionsErrorBody;
56
+ };
57
+ export declare function listArtifactVersions(input: ListArtifactVersionsInput, deps: ListArtifactVersionsDeps): Promise<ListArtifactVersionsResult>;
58
+ //# sourceMappingURL=list-versions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-versions.d.ts","sourceRoot":"","sources":["../../src/ops/list-versions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;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;AAGzE,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;;;;;OAKG;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,CA6DrC"}
@@ -0,0 +1,60 @@
1
+ import { compareSemver } from '../utils/semver.js';
2
+ export async function listArtifactVersions(input, deps) {
3
+ if (typeof input.artifactId !== 'string' || input.artifactId.length === 0) {
4
+ return errorResult(400, 'invalid_request', 'missing artifactId');
5
+ }
6
+ // Step 1 — metadata existence check. A missing metadata row is the
7
+ // "no such artifact" signal. We rely on the publish op writing a
8
+ // metadata row on every successful publish (idempotent upsert).
9
+ let metadataExists = false;
10
+ try {
11
+ const metadata = await deps.storage.getArtifactMetadata(input.artifactId);
12
+ metadataExists = metadata !== null;
13
+ }
14
+ catch (err) {
15
+ return errorResult(500, 'server_error', `failed to read metadata: ${err instanceof Error ? err.message : String(err)}`);
16
+ }
17
+ if (!metadataExists) {
18
+ return errorResult(404, 'not_found', `no such artifact: ${input.artifactId}`);
19
+ }
20
+ // Step 2 — fetch all version rows.
21
+ let rows;
22
+ try {
23
+ rows = await deps.storage.listArtifactVersions(input.artifactId);
24
+ }
25
+ catch (err) {
26
+ return errorResult(500, 'server_error', `failed to list versions: ${err instanceof Error ? err.message : String(err)}`);
27
+ }
28
+ // Step 3 — visibility filter. Unauthed callers see only public rows;
29
+ // authed callers see everything (finer org-membership gating is a
30
+ // future concern — for now, "authed" === "can see your own private
31
+ // rows" is captured by the verified caller subject being non-null).
32
+ const visibleRows = [];
33
+ for (const row of rows) {
34
+ if (row.visibility === 'private' && deps.authn === undefined)
35
+ continue;
36
+ visibleRows.push(row);
37
+ }
38
+ // Step 4 — semver DESC sort. `compareSemver(a, b)` returns -1/0/1
39
+ // matching ascending order; flip the sign for DESC.
40
+ const sorted = [...visibleRows].sort((a, b) => -compareSemver(a.version, b.version));
41
+ // Step 5 — project to the lightweight wire shape.
42
+ const versions = sorted.map(rowToEntry);
43
+ return {
44
+ ok: true,
45
+ status: 200,
46
+ body: { artifactId: input.artifactId, versions },
47
+ };
48
+ }
49
+ function rowToEntry(row) {
50
+ return {
51
+ version: row.version,
52
+ publishedAt: row.publishedAt,
53
+ yanked: row.yanked === true,
54
+ kind: row.kind,
55
+ visibility: row.visibility,
56
+ };
57
+ }
58
+ function errorResult(status, error, message) {
59
+ return { ok: false, status, body: { error, message } };
60
+ }
@@ -0,0 +1,70 @@
1
+ import { type GadgetSignature } from '@ggui-ai/gadget-signing';
2
+ import type { PublishErrorBody, PublishResponseBody } from '../types.js';
3
+ import type { BlueprintProbeRunner } from './conformance.js';
4
+ import type { AuthnContext } from '../interfaces/authn.js';
5
+ import type { BundleStorage } from '../interfaces/bundle-storage.js';
6
+ import type { RegistryStorage } from '../interfaces/registry-storage.js';
7
+ /**
8
+ * Hard cap on the base64-DECODED bundle byte length. 5 MiB matches the
9
+ * apigwv2 Lambda integration payload ceiling (6 MB) minus headroom for
10
+ * the manifest + signature + JSON envelope. OSS server enforces the
11
+ * same ceiling for parity (a cloud-published bundle MUST be installable
12
+ * against an OSS-mirrored registry without re-bundling).
13
+ */
14
+ export declare const MAX_BUNDLE_BYTES: number;
15
+ export interface PublishArtifactInput {
16
+ /** Unvalidated request body — the op parses + validates. */
17
+ readonly manifest: unknown;
18
+ /** Base64-encoded gadget bundle bytes. Required if `kind === 'gadget'`. */
19
+ readonly bundle?: string;
20
+ /** Base64 SHA-384 digest of the decoded bundle bytes. Client-supplied; the op re-verifies. */
21
+ readonly bundleSha384?: string;
22
+ /**
23
+ * Author signature. Discriminated union over `algorithm`:
24
+ * - `'ed25519'` → private-gadget path, AuthorKeys-rooted
25
+ * trust chain.
26
+ * - `'sigstore-cosign'` → public-gadget path, Fulcio cert +
27
+ * Rekor inclusion trust chain.
28
+ *
29
+ * The op dispatches on `algorithm` after the signature-shape guard.
30
+ */
31
+ readonly signature: GadgetSignature;
32
+ }
33
+ export interface PublishArtifactDeps {
34
+ readonly storage: RegistryStorage;
35
+ readonly bundleStorage: BundleStorage;
36
+ readonly authn: AuthnContext;
37
+ readonly clock: () => Date;
38
+ /**
39
+ * Hostname the install CLI talks to — embedded into the success
40
+ * response's `installCommand`. Cloud: API Gateway custom domain (e.g.
41
+ * `dev.registry.sandbox.ggui.ai`). OSS: `localhost:9001` etc.
42
+ *
43
+ * No protocol prefix — the op composes `https://` for cloud and
44
+ * `http://` for `localhost`/`127.0.0.1`.
45
+ */
46
+ readonly registryHostname: string;
47
+ /**
48
+ * Optional runtime probe for blueprint manifests. Static gates
49
+ * always run; the probe additionally compiles + renders the
50
+ * blueprint default export with the manifest's fixtureProps in a
51
+ * sandboxed Node `vm` + React server-renderer.
52
+ *
53
+ * Wire from `@ggui-ai/blueprint-probe` in OSS and cloud Lambda
54
+ * environments where the runtime probe is desired. Leaving it
55
+ * unset skips the probe — useful for the standalone conformance
56
+ * HTTP endpoint that should not pay for react-dom.
57
+ */
58
+ readonly blueprintProbe?: BlueprintProbeRunner;
59
+ }
60
+ export type PublishArtifactResult = {
61
+ readonly ok: true;
62
+ readonly status: 201;
63
+ readonly body: PublishResponseBody;
64
+ } | {
65
+ readonly ok: false;
66
+ readonly status: 400 | 401 | 403 | 409 | 413 | 500;
67
+ readonly body: PublishErrorBody;
68
+ };
69
+ export declare function publishArtifact(input: PublishArtifactInput, deps: PublishArtifactDeps): Promise<PublishArtifactResult>;
70
+ //# sourceMappingURL=publish.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../src/ops/publish.ts"],"names":[],"mappings":"AA2BA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EAIV,gBAAgB,EAEhB,mBAAmB,EACpB,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,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;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;CAChD;AAED,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,CA0XhC"}
@@ -0,0 +1,417 @@
1
+ /**
2
+ * `publishArtifact` — pure op for `POST /publish`.
3
+ *
4
+ * Flow (short-circuits on first failure):
5
+ *
6
+ * 1. Validate {@link AuthnContext} present (transport enforces auth
7
+ * before calling; this is defense-in-depth).
8
+ * 2. Parse + validate the manifest via `parseArtifactManifest`.
9
+ * 3. Decode + size-check the bundle (gadgets only).
10
+ * 4. Recompute SHA-384 of the bundle bytes; compare to client claim.
11
+ * 5. Re-run the conformance gate ({@link checkConformance}).
12
+ * 6. Verify the Ed25519 author signature — look up the
13
+ * {@link AuthorKeyRow} by `(subject, keyId)`, base64-decode the
14
+ * stored public key, call `verifyBundleEd25519`.
15
+ * 7. Insert the {@link ArtifactVersionRow} via
16
+ * {@link RegistryStorage.putArtifactVersionIfAbsent}. Re-publish
17
+ * (collision) returns 409 `version_exists`.
18
+ * 8. Upload the bundle + signature + manifest via
19
+ * {@link BundleStorage}.
20
+ * 9. Upsert the {@link ArtifactsMetadataRow} — only rewrites
21
+ * `latestVersion` when the new version is the highest semver.
22
+ * 10. Return 201 with the wire-locked {@link PublishResponseBody}.
23
+ */
24
+ import { parseArtifactManifest, } from '@ggui-ai/artifact-manifest';
25
+ import { canonicalJson, isGadgetSignature, verifyBundleEd25519, verifyBundleSigstore, } from '@ggui-ai/gadget-signing';
26
+ import { bundleHostScheme } from '@ggui-ai/protocol';
27
+ import { ZodError } from 'zod';
28
+ import { ARTIFACTS_METADATA_SK } from '../types.js';
29
+ import { safeBase64Decode, sha384Base64 } from '../utils/base64.js';
30
+ import { compareSemver } from '../utils/semver.js';
31
+ import { compileBlueprint } from './compile.js';
32
+ import { checkConformance } from './conformance.js';
33
+ /**
34
+ * Hard cap on the base64-DECODED bundle byte length. 5 MiB matches the
35
+ * apigwv2 Lambda integration payload ceiling (6 MB) minus headroom for
36
+ * the manifest + signature + JSON envelope. OSS server enforces the
37
+ * same ceiling for parity (a cloud-published bundle MUST be installable
38
+ * against an OSS-mirrored registry without re-bundling).
39
+ */
40
+ export const MAX_BUNDLE_BYTES = 5 * 1024 * 1024;
41
+ export async function publishArtifact(input, deps) {
42
+ // 1. Authn — transport enforces before calling, but defensive read.
43
+ if (typeof deps.authn.subject !== 'string' || deps.authn.subject.length === 0) {
44
+ return error(401, 'unauthorized', 'request is missing a verified caller subject');
45
+ }
46
+ // 1b. Signature shape — transport pre-decoded the JSON envelope; we
47
+ // still validate the structured object before the cryptographic
48
+ // path. The discriminated guard accepts both Ed25519 (private) and
49
+ // sigstore-cosign (public) shapes; the verify-time dispatch on
50
+ // `signature.algorithm` selects the correct trust chain.
51
+ if (!isGadgetSignature(input.signature)) {
52
+ return error(400, 'signature_invalid', 'request body is missing or malformed `signature` (expected Ed25519Signature or SigstoreSignature shape)');
53
+ }
54
+ // 2. Manifest schema
55
+ let manifest;
56
+ try {
57
+ manifest = parseArtifactManifest(input.manifest);
58
+ }
59
+ catch (err) {
60
+ if (err instanceof ZodError) {
61
+ const first = err.issues[0];
62
+ const path = (first?.path ?? []).map((seg) => typeof seg === 'symbol' ? String(seg) : seg);
63
+ return error(400, 'manifest_invalid', first?.message ?? 'manifest failed schema validation', { path, issues: err.issues });
64
+ }
65
+ return error(400, 'manifest_invalid', `manifest failed validation: ${errorMessage(err)}`);
66
+ }
67
+ const artifactId = `${manifest.scope}/${manifest.name}`;
68
+ const version = manifest.version;
69
+ // 3. Bundle decode + size (gadgets only)
70
+ let bundleBytes;
71
+ if (manifest.kind === 'gadget') {
72
+ if (input.bundle === undefined || input.bundle.length === 0) {
73
+ return error(400, 'bundle_required', 'gadget publish requires a base64-encoded `bundle` field — blueprint manifests carry source inline, but gadgets ship compiled bytes');
74
+ }
75
+ const decoded = safeBase64Decode(input.bundle);
76
+ if (decoded === undefined) {
77
+ return error(400, 'manifest_invalid', '`bundle` field is not valid base64');
78
+ }
79
+ if (decoded.byteLength > MAX_BUNDLE_BYTES) {
80
+ return error(413, 'bundle_too_large', `bundle is ${decoded.byteLength} bytes; maximum is ${MAX_BUNDLE_BYTES} bytes (${MAX_BUNDLE_BYTES / (1024 * 1024)} MiB)`);
81
+ }
82
+ bundleBytes = decoded;
83
+ // 4. Hash check
84
+ if (input.bundleSha384 === undefined || input.bundleSha384.length === 0) {
85
+ return error(400, 'bundle_hash_mismatch', 'gadget publish requires `bundleSha384` (base64 SHA-384 digest of the decoded bundle bytes)');
86
+ }
87
+ const recomputed = sha384Base64(bundleBytes);
88
+ if (recomputed !== input.bundleSha384) {
89
+ return error(400, 'bundle_hash_mismatch', 'server-computed SHA-384 of the bundle does not match the client-supplied `bundleSha384`', { expected: recomputed, received: input.bundleSha384 });
90
+ }
91
+ }
92
+ // 5. Conformance gate
93
+ const conformanceBundleText = bundleBytes === undefined
94
+ ? undefined
95
+ : Buffer.from(bundleBytes.buffer, bundleBytes.byteOffset, bundleBytes.byteLength).toString('utf8');
96
+ const conformanceResult = checkConformance({
97
+ manifest,
98
+ bundle: conformanceBundleText,
99
+ });
100
+ if (!conformanceResult.ok) {
101
+ return conformanceFailureResponse(conformanceResult.errors);
102
+ }
103
+ // 5b. Blueprint runtime probe — opt-in via deps.blueprintProbe.
104
+ // Static gates already accepted the manifest above; the probe runs
105
+ // only for blueprints and only when the caller wired a runner.
106
+ // Probe failures surface through the same `conformance_failed`
107
+ // envelope so wire consumers don't branch on a separate error code.
108
+ if (manifest.kind === 'blueprint' && deps.blueprintProbe !== undefined) {
109
+ const probeResult = await deps.blueprintProbe.probe(manifest);
110
+ if (!probeResult.ok) {
111
+ return conformanceFailureResponse(probeResult.errors);
112
+ }
113
+ }
114
+ // 6. Signature verification — dispatch on the locked discriminator.
115
+ //
116
+ // `authorPublicKey` is the publisher identity persisted on the
117
+ // version row (`ArtifactVersionRow.authorPublicKey`). For Ed25519
118
+ // it's the base64 32-byte public key; for sigstore-cosign it's
119
+ // the leaf-cert PEM extracted from the bundle.
120
+ const bytesForSignature = bundleBytes ?? new TextEncoder().encode(canonicalJson(manifest));
121
+ let authorPublicKey;
122
+ if (input.signature.algorithm === 'ed25519') {
123
+ // Private gadgets — AuthorKeys-rooted trust chain.
124
+ const authorKeyRow = await deps.storage.getAuthorKey(deps.authn.subject, input.signature.publicKeyId);
125
+ if (authorKeyRow === null) {
126
+ return error(403, 'unknown_key', `no registered AuthorKeys row for publisher \`${deps.authn.subject}\` + keyId \`${input.signature.publicKeyId}\` — run \`ggui keygen\` and register the public key before publishing`);
127
+ }
128
+ const publicKeyBytes = safeBase64Decode(authorKeyRow.publicKeyBase64);
129
+ if (publicKeyBytes === undefined) {
130
+ return error(500, 'internal', 'author key row is malformed');
131
+ }
132
+ const verifyResult = await verifyBundleEd25519({
133
+ bundleBytes: bytesForSignature,
134
+ signature: input.signature,
135
+ publicKey: publicKeyBytes,
136
+ });
137
+ if (!verifyResult.valid) {
138
+ return error(400, 'signature_invalid', verifyResult.reason);
139
+ }
140
+ authorPublicKey = authorKeyRow.publicKeyBase64;
141
+ }
142
+ else {
143
+ // Public gadgets — sigstore (Fulcio + Rekor) trust chain.
144
+ // Identity claim: trust ANY valid OIDC identity at publish-time —
145
+ // the publisher is already authenticated by the transport layer
146
+ // ahead of this op. Install-time consumers CAN tighten via
147
+ // `--verify-identity <pattern>` (CLI install flag); that's a
148
+ // separate trust decision controlled by the install operator, not
149
+ // the publisher.
150
+ const verifyResult = await verifyBundleSigstore({
151
+ bundleBytes: bytesForSignature,
152
+ signature: input.signature,
153
+ });
154
+ if (!verifyResult.valid) {
155
+ return error(400, 'signature_invalid', verifyResult.reason);
156
+ }
157
+ // Persist the leaf cert PEM on the version row so install
158
+ // consumers can render the signer identity. Extracted inline from
159
+ // the bundle's `verificationMaterial.x509CertificateChain.certificates[0]`
160
+ // for now — TODO: replace with `extractSigstoreLeafCertPem(signature)`
161
+ // from `@ggui-ai/gadget-signing` so the parsing logic lives next to
162
+ // the bundle-format knowledge (single source of truth for the
163
+ // cosign-bundle shape).
164
+ const leafCertPem = extractSigstoreLeafCertPemInline(input.signature);
165
+ if (leafCertPem === undefined) {
166
+ return error(400, 'signature_invalid', 'sigstore verify succeeded but bundle is missing `verificationMaterial.x509CertificateChain.certificates[0].rawBytes` — cannot pin author identity on the version row');
167
+ }
168
+ authorPublicKey = leafCertPem;
169
+ }
170
+ // 6b. Blueprint compile boundary (TSX → JS).
171
+ // Blueprints compile at publish time; the canonical compiled JS bytes
172
+ // are stored content-addressed in {@link CompiledBlobRow} and the
173
+ // version row only carries the digest pointer. Two-layer storage
174
+ // gives dedup (byte-identical compiled output reuses one blob row)
175
+ // and federation-ready content-addressable identity.
176
+ //
177
+ // Compile failure surfaces through the same `conformance_failed`
178
+ // wire envelope as the static gates — same shape, same code
179
+ // (`blueprint_compile_error`). The static gate already runs a
180
+ // best-effort `transformSync` to catch the same class of errors;
181
+ // this is the load-bearing run (its output is what's persisted).
182
+ let compiledDigest;
183
+ let compiledBlobToWrite;
184
+ if (manifest.kind === 'blueprint') {
185
+ const compileResult = compileBlueprint(manifest.source);
186
+ if (!compileResult.ok) {
187
+ return conformanceFailureResponse(compileResult.errors.map((e) => ({
188
+ code: 'blueprint_compile_error',
189
+ message: e.message,
190
+ ...(e.location !== undefined ? { detail: { location: e.location } } : {}),
191
+ })));
192
+ }
193
+ compiledDigest = compileResult.compiledDigest;
194
+ compiledBlobToWrite = {
195
+ compiledDigest: compileResult.compiledDigest,
196
+ compiledBytes: compileResult.compiledBytes,
197
+ compiledSize: compileResult.compiledSize,
198
+ refCount: 1,
199
+ // Persist the publisher's signature envelope inline on the blob
200
+ // row so a hosted install path can re-verify without a second
201
+ // fetch. Stored as a JSON-encoded `GadgetSignature` discriminated
202
+ // union; the install path runs `JSON.parse` + `isGadgetSignature`
203
+ // before dispatching to verifyBundleEd25519 / verifyBundleSigstore.
204
+ manifestSig: JSON.stringify(input.signature),
205
+ // compiledSig still reserved — a registry-rooted sigstore
206
+ // signature over the compiled bytes is a future enhancement.
207
+ createdAt: deps.clock().toISOString(),
208
+ };
209
+ }
210
+ // 7. Atomic commit.
211
+ //
212
+ // For blueprints — version row + compiled-blob row land under one
213
+ // logical transaction via `commitVersionAndBlob`. The DDB impl uses
214
+ // `TransactWriteItems` so the all-or-nothing guarantee holds at the
215
+ // service level; memory + filesystem impls get atomicity from the
216
+ // single-threaded JS event loop.
217
+ //
218
+ // For gadgets — no compiled-blob row, so we still use the simple
219
+ // conditional version-row insert. (Adding a synthetic blob row for
220
+ // gadgets just to share the code path would widen the schema; the
221
+ // discriminator on `kind` is cleaner.)
222
+ //
223
+ // Either way: the version-row conditional `attribute_not_exists` is
224
+ // the load-bearing concurrency primitive. On conflict, return 409
225
+ // `version_exists` immediately — the publisher's idempotent retry path.
226
+ const nowIso = deps.clock().toISOString();
227
+ const sriHash = bundleBytes === undefined ? undefined : `sha384-${sha384Base64(bundleBytes)}`;
228
+ const bundleUrl = bundleBytes === undefined
229
+ ? undefined
230
+ : deps.bundleStorage.bundleUrl(manifest.scope, manifest.name, version);
231
+ const signatureUrl = bundleBytes === undefined
232
+ ? undefined
233
+ : deps.bundleStorage.signatureUrl(manifest.scope, manifest.name, version);
234
+ const versionRow = {
235
+ artifactId,
236
+ version,
237
+ manifest,
238
+ kind: manifest.kind,
239
+ visibility: manifest.visibility,
240
+ bundleUrl,
241
+ bundleSri: sriHash,
242
+ signatureUrl,
243
+ ...(compiledDigest !== undefined ? { compiledDigest } : {}),
244
+ authorPublicKey,
245
+ publishedAt: nowIso,
246
+ publishedBy: deps.authn.subject,
247
+ };
248
+ if (compiledBlobToWrite !== undefined) {
249
+ // Blueprint path — atomic two-row commit.
250
+ let commitResult;
251
+ try {
252
+ commitResult = await deps.storage.commitVersionAndBlob(versionRow, compiledBlobToWrite);
253
+ }
254
+ catch (err) {
255
+ return error(500, 'internal', `failed to commit version + compiled-blob rows for ${artifactId}@${version}: ${errorMessage(err)}`);
256
+ }
257
+ if (!commitResult.ok) {
258
+ return error(409, 'version_exists', `${artifactId}@${version} is already published — versions are immutable. Publish a new version instead.`);
259
+ }
260
+ }
261
+ else {
262
+ // Gadget path — single conditional version-row insert.
263
+ const insertResult = await deps.storage.putArtifactVersionIfAbsent(versionRow);
264
+ if (!insertResult.ok) {
265
+ return error(409, 'version_exists', `${artifactId}@${version} is already published — versions are immutable. Publish a new version instead.`);
266
+ }
267
+ }
268
+ // 8. Upload bundle + signature + manifest
269
+ let manifestUrl;
270
+ try {
271
+ if (bundleBytes !== undefined) {
272
+ await deps.bundleStorage.putBundle(manifest.scope, manifest.name, version, bundleBytes);
273
+ await deps.bundleStorage.putSignature(manifest.scope, manifest.name, version, input.signature);
274
+ }
275
+ manifestUrl = await deps.bundleStorage.putManifest(manifest.scope, manifest.name, version, manifest);
276
+ }
277
+ catch (err) {
278
+ return error(500, 'internal', `failed to upload artifact: ${errorMessage(err)}`);
279
+ }
280
+ // 9. Upsert Plugins metadata row.
281
+ // Only rewrite `latestVersion` when the new version is the highest
282
+ // semver. Race-condition note: between the get and the put, another
283
+ // concurrent publisher could update the row. We accept rare
284
+ // last-writer-wins on `latestVersion` — per-version row INSERT
285
+ // remains strongly conditional, which is the load-bearing invariant.
286
+ const existing = await deps.storage.getArtifactMetadata(artifactId);
287
+ const shouldUpdateLatest = existing === null || compareSemver(version, existing.latestVersion) > 0;
288
+ if (shouldUpdateLatest) {
289
+ // Denormalized search field — a gadget package's primary (first)
290
+ // export name. The package may export several hooks/components;
291
+ // the manifest's `exports[]` is the source of truth.
292
+ const primaryExport = manifest.kind === 'gadget' ? manifest.exports[0] : undefined;
293
+ const metadataRow = {
294
+ artifactId,
295
+ sk: ARTIFACTS_METADATA_SK,
296
+ kind: manifest.kind,
297
+ latestVersion: version,
298
+ description: manifest.description,
299
+ tags: manifest.tags,
300
+ visibility: manifest.visibility,
301
+ hook: primaryExport === undefined
302
+ ? undefined
303
+ : 'hook' in primaryExport
304
+ ? primaryExport.hook
305
+ : primaryExport.component,
306
+ authorName: manifest.author?.name,
307
+ publishedAt: nowIso,
308
+ publishedBy: deps.authn.subject,
309
+ };
310
+ try {
311
+ await deps.storage.putArtifactMetadata(metadataRow);
312
+ }
313
+ catch (err) {
314
+ return error(500, 'internal', `failed to write Plugins metadata row: ${errorMessage(err)}`);
315
+ }
316
+ }
317
+ // 10. Success
318
+ return {
319
+ ok: true,
320
+ status: 201,
321
+ body: {
322
+ artifactId,
323
+ version,
324
+ manifestUrl,
325
+ bundleUrl,
326
+ signatureUrl,
327
+ installCommand: buildInstallCommand(artifactId, version, deps.registryHostname, manifest.kind),
328
+ },
329
+ };
330
+ }
331
+ /** Compose the install command. Loopback hosts use http://; everything else https://. */
332
+ function buildInstallCommand(artifactId, version, registryHostname, kind) {
333
+ // Loopback hosts get `http://`. Symmetric with the push-time
334
+ // `resolveGadgetUrls` resolver in `@ggui-ai/mcp-server-handlers` —
335
+ // install + render MUST agree on scheme for local-dev / sandbox
336
+ // workflows, otherwise the iframe blocks mixed content.
337
+ return `ggui ${kind} install ${artifactId}@${version} --registry=${bundleHostScheme(registryHostname)}://${registryHostname}`;
338
+ }
339
+ function errorMessage(err) {
340
+ if (err instanceof Error)
341
+ return err.message;
342
+ return String(err);
343
+ }
344
+ function error(status, code, message, detail) {
345
+ return {
346
+ ok: false,
347
+ status,
348
+ body: detail === undefined ? { error: code, message } : { error: code, message, detail },
349
+ };
350
+ }
351
+ /**
352
+ * Extract the Fulcio leaf cert's base64 raw bytes from a serialized
353
+ * cosign bundle (per `@sigstore/bundle` v0.3 spec). Returns `undefined`
354
+ * if the bundle is malformed or missing the cert chain.
355
+ *
356
+ * Inline today — TODO: pull this into `@ggui-ai/gadget-signing` as
357
+ * `extractSigstoreLeafCertPem` so the bundle-format knowledge sits
358
+ * next to the verify impl. The install CLI carries an identical
359
+ * helper today; both should collapse to the single canonical export.
360
+ */
361
+ function extractSigstoreLeafCertPemInline(signature) {
362
+ let parsed;
363
+ try {
364
+ parsed = JSON.parse(signature.bundle);
365
+ }
366
+ catch {
367
+ return undefined;
368
+ }
369
+ if (parsed === null || typeof parsed !== 'object')
370
+ return undefined;
371
+ const verificationMaterial = parsed
372
+ .verificationMaterial;
373
+ if (verificationMaterial === null || typeof verificationMaterial !== 'object') {
374
+ return undefined;
375
+ }
376
+ const chain = verificationMaterial
377
+ .x509CertificateChain;
378
+ if (chain === null || typeof chain !== 'object')
379
+ return undefined;
380
+ const certificates = chain.certificates;
381
+ if (!Array.isArray(certificates) || certificates.length === 0)
382
+ return undefined;
383
+ const leaf = certificates[0];
384
+ if (leaf === null || typeof leaf !== 'object')
385
+ return undefined;
386
+ const rawBytes = leaf.rawBytes;
387
+ if (typeof rawBytes !== 'string' || rawBytes.length === 0)
388
+ return undefined;
389
+ return rawBytes;
390
+ }
391
+ /**
392
+ * Translate a {@link ConformanceResponseBody} error list into a 400
393
+ * `PublishArtifactResult`. Used by both the static-gate failure path
394
+ * and the runtime-probe failure path so the wire shape is identical
395
+ * regardless of which gate caught the issue.
396
+ *
397
+ * The first failure's sub-discriminator is hoisted to the wire body's
398
+ * TOP level (alongside `error: 'conformance_failed'`) so callers can
399
+ * branch without parsing nested JSON. The full error list stays in
400
+ * `detail.errors` for verbose rendering.
401
+ */
402
+ function conformanceFailureResponse(errors) {
403
+ const conformanceFailureCode = errors[0]?.code;
404
+ const body = conformanceFailureCode === undefined
405
+ ? {
406
+ error: 'conformance_failed',
407
+ message: 'submission failed the registry conformance gate',
408
+ detail: { errors },
409
+ }
410
+ : {
411
+ error: 'conformance_failed',
412
+ message: 'submission failed the registry conformance gate',
413
+ conformanceFailureCode,
414
+ detail: { errors },
415
+ };
416
+ return { ok: false, status: 400, body };
417
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `readArtifact` — pure op for `GET /pkg/{scope}/{name}/{version}`.
3
+ * A shared op that both this server and the hosted registry project
4
+ * through their respective transport shells.
5
+ *
6
+ * Flow:
7
+ * 1. Point-read the version row via {@link RegistryStorage.getArtifactVersion}.
8
+ * 2. Enforce row `visibility`: `private` → require an {@link AuthnContext}.
9
+ * 3. Yanked → 410 with the manifest still in the body (audit-friendly).
10
+ * 4. For blueprints, resolve the {@link CompiledBlobRow} via
11
+ * `row.compiledDigest` (two-layer content-addressed storage). A
12
+ * missing blob when the pointer is set is a critical storage
13
+ * inconsistency — the op returns 500 rather than silently
14
+ * omitting `compiledBytes`.
15
+ * 5. Project the row + blob into the {@link ReadPkgResponse} wire shape.
16
+ */
17
+ import type { ReadErrorBody, ReadPkgResponse } from '../types.js';
18
+ import type { AuthnContext } from '../interfaces/authn.js';
19
+ import type { RegistryStorage } from '../interfaces/registry-storage.js';
20
+ export interface ReadArtifactInput {
21
+ /**
22
+ * `<scope>/<name>` — the install identifier. Scope MUST start with `@`.
23
+ * Cloud API Gateway path params drop the leading `@`; the cloud
24
+ * Lambda shell re-prepends before calling this op.
25
+ */
26
+ readonly artifactId: string;
27
+ readonly version: string;
28
+ }
29
+ export interface ReadArtifactDeps {
30
+ readonly storage: RegistryStorage;
31
+ /**
32
+ * Optional — when undefined, the op rejects `private` rows with 403.
33
+ * Cloud transport extracts from the JWT authorizer; OSS extracts from
34
+ * the bearer authn middleware.
35
+ */
36
+ readonly authn?: AuthnContext;
37
+ }
38
+ export type ReadArtifactResult = {
39
+ readonly ok: true;
40
+ readonly status: 200;
41
+ readonly body: ReadPkgResponse;
42
+ } | {
43
+ readonly ok: false;
44
+ readonly status: 410;
45
+ readonly body: ReadPkgResponse;
46
+ } | {
47
+ readonly ok: false;
48
+ readonly status: 400 | 403 | 404 | 500;
49
+ readonly body: ReadErrorBody;
50
+ };
51
+ export declare function readArtifact(input: ReadArtifactInput, deps: ReadArtifactDeps): Promise<ReadArtifactResult>;
52
+ //# sourceMappingURL=read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/ops/read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAGV,aAAa,EAEb,eAAe,EAChB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAEzE,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;;;;OAIG;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,GAAG,GAAG,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;CAC9B,CAAC;AAEN,wBAAsB,YAAY,CAChC,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,kBAAkB,CAAC,CAkE7B"}