@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.
- package/LICENSE +201 -0
- package/README.md +76 -0
- package/dist/impls/memory-bundle-storage.d.ts +12 -0
- package/dist/impls/memory-bundle-storage.d.ts.map +1 -0
- package/dist/impls/memory-bundle-storage.js +40 -0
- package/dist/impls/memory-registry-storage.d.ts +3 -0
- package/dist/impls/memory-registry-storage.d.ts.map +1 -0
- package/dist/impls/memory-registry-storage.js +154 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/interfaces/authn.d.ts +51 -0
- package/dist/interfaces/authn.d.ts.map +1 -0
- package/dist/interfaces/authn.js +1 -0
- package/dist/interfaces/bundle-storage.d.ts +79 -0
- package/dist/interfaces/bundle-storage.d.ts.map +1 -0
- package/dist/interfaces/bundle-storage.js +1 -0
- package/dist/interfaces/registry-storage.d.ts +204 -0
- package/dist/interfaces/registry-storage.d.ts.map +1 -0
- package/dist/interfaces/registry-storage.js +19 -0
- package/dist/ops/compile.d.ts +48 -0
- package/dist/ops/compile.d.ts.map +1 -0
- package/dist/ops/compile.js +163 -0
- package/dist/ops/conformance.d.ts +154 -0
- package/dist/ops/conformance.d.ts.map +1 -0
- package/dist/ops/conformance.js +487 -0
- package/dist/ops/list-versions.d.ts +58 -0
- package/dist/ops/list-versions.d.ts.map +1 -0
- package/dist/ops/list-versions.js +60 -0
- package/dist/ops/publish.d.ts +70 -0
- package/dist/ops/publish.d.ts.map +1 -0
- package/dist/ops/publish.js +417 -0
- package/dist/ops/read.d.ts +52 -0
- package/dist/ops/read.d.ts.map +1 -0
- package/dist/ops/read.js +60 -0
- package/dist/ops/register-author-key.d.ts +22 -0
- package/dist/ops/register-author-key.d.ts.map +1 -0
- package/dist/ops/register-author-key.js +171 -0
- package/dist/ops/search.d.ts +50 -0
- package/dist/ops/search.d.ts.map +1 -0
- package/dist/ops/search.js +106 -0
- package/dist/testing/bundle-storage-contract.d.ts +3 -0
- package/dist/testing/bundle-storage-contract.d.ts.map +1 -0
- package/dist/testing/bundle-storage-contract.js +176 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +9 -0
- package/dist/testing/registry-storage-contract.d.ts +3 -0
- package/dist/testing/registry-storage-contract.d.ts.map +1 -0
- package/dist/testing/registry-storage-contract.js +392 -0
- package/dist/types.d.ts +482 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +110 -0
- package/dist/utils/base64.d.ts +15 -0
- package/dist/utils/base64.d.ts.map +1 -0
- package/dist/utils/base64.js +35 -0
- package/dist/utils/semver.d.ts +13 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +69 -0
- 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"}
|