@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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `BundleStorage` — bundle + signature + manifest blob storage. The
|
|
3
|
+
* hosted registry backs this with S3 behind a CDN; the open-source
|
|
4
|
+
* server backs it with the filesystem under
|
|
5
|
+
* `<root>/bundles/<scope>/<name>/<version>/`. A memory impl is
|
|
6
|
+
* provided for tests.
|
|
7
|
+
*
|
|
8
|
+
* Each `put*` method returns the fully-qualified URL the consumer
|
|
9
|
+
* (iframe runtime, install CLI) can fetch. The URL prefix is
|
|
10
|
+
* determined by the impl's constructor — a CDN alias for the hosted
|
|
11
|
+
* registry, or `http://localhost:9001` (or whatever the server is
|
|
12
|
+
* bound to) for the open-source server.
|
|
13
|
+
*
|
|
14
|
+
* Bundles are immutable post-publish. Responses MUST emit
|
|
15
|
+
* `Cache-Control: public, max-age=31536000, immutable` — SRI integrity
|
|
16
|
+
* depends on it. The OSS server's bundle route sets this header
|
|
17
|
+
* explicitly; the cloud's CloudFront distribution sets it via its
|
|
18
|
+
* cache policy.
|
|
19
|
+
*
|
|
20
|
+
* ## Protocol & Contract Bar
|
|
21
|
+
*
|
|
22
|
+
* **Parties:**
|
|
23
|
+
* - Producer: {@link publishArtifact} writes bundle (gadgets),
|
|
24
|
+
* signature (gadgets), and manifest (always) on every publish.
|
|
25
|
+
* - Consumer: iframe-runtime (bundleUrl), install CLI (signatureUrl,
|
|
26
|
+
* manifestUrl), audit tooling (manifestUrl on yanked versions).
|
|
27
|
+
*
|
|
28
|
+
* **Obligations:**
|
|
29
|
+
* - `put*` methods MUST be idempotent — re-publishes of the same
|
|
30
|
+
* `(scope, name, version)` triple write identical bytes (the
|
|
31
|
+
* per-version row immutability invariant on {@link RegistryStorage}
|
|
32
|
+
* prevents true re-publishes; the bundle-storage layer needs no
|
|
33
|
+
* conflict semantics).
|
|
34
|
+
* - URL composition methods (`bundleUrl`, `signatureUrl`,
|
|
35
|
+
* `manifestUrl`) MUST be pure — no I/O, no async. Consumers cache
|
|
36
|
+
* URLs liberally.
|
|
37
|
+
*
|
|
38
|
+
* **Failure mode:**
|
|
39
|
+
* - Transport-level failures throw; the publish op wraps and returns
|
|
40
|
+
* 500.
|
|
41
|
+
* - Missing-blob reads return `null`; never throw.
|
|
42
|
+
*
|
|
43
|
+
* **Observable violation:**
|
|
44
|
+
* - Contract test {@link bundleStorageContract} covers: bundle
|
|
45
|
+
* round-trip preserves bytes; signature round-trip preserves the
|
|
46
|
+
* structured object; manifest round-trip preserves the full
|
|
47
|
+
* discriminated-union shape; URL methods compose without side effects;
|
|
48
|
+
* missing reads return null.
|
|
49
|
+
*/
|
|
50
|
+
import type { ArtifactManifest } from '@ggui-ai/artifact-manifest';
|
|
51
|
+
import type { GadgetSignature } from '@ggui-ai/gadget-signing';
|
|
52
|
+
export interface BundleStorage {
|
|
53
|
+
/** Write gadget bundle bytes. Returns the public URL. */
|
|
54
|
+
putBundle(scope: string, name: string, version: string, bytes: Uint8Array): Promise<string>;
|
|
55
|
+
/** Read gadget bundle bytes. `null` on miss. */
|
|
56
|
+
getBundle(scope: string, name: string, version: string): Promise<Uint8Array | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Write the signature envelope. Returns the public URL.
|
|
59
|
+
*
|
|
60
|
+
* The envelope is a {@link GadgetSignature} — a discriminated union
|
|
61
|
+
* over `algorithm` (`ed25519` for private gadgets, `sigstore-cosign`
|
|
62
|
+
* for public). Impls serialize via `JSON.stringify(signature)` and
|
|
63
|
+
* store the resulting bytes verbatim.
|
|
64
|
+
*/
|
|
65
|
+
putSignature(scope: string, name: string, version: string, signature: GadgetSignature): Promise<string>;
|
|
66
|
+
/** Read the signature envelope. `null` on miss. */
|
|
67
|
+
getSignature(scope: string, name: string, version: string): Promise<GadgetSignature | null>;
|
|
68
|
+
/** Write the manifest verbatim. Returns the public URL. */
|
|
69
|
+
putManifest(scope: string, name: string, version: string, manifest: ArtifactManifest): Promise<string>;
|
|
70
|
+
/** Read the manifest. `null` on miss. */
|
|
71
|
+
getManifest(scope: string, name: string, version: string): Promise<ArtifactManifest | null>;
|
|
72
|
+
/** Compose the public bundle URL. No I/O. */
|
|
73
|
+
bundleUrl(scope: string, name: string, version: string): string;
|
|
74
|
+
/** Compose the public signature URL. No I/O. */
|
|
75
|
+
signatureUrl(scope: string, name: string, version: string): string;
|
|
76
|
+
/** Compose the public manifest URL. No I/O. */
|
|
77
|
+
manifestUrl(scope: string, name: string, version: string): string;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=bundle-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-storage.d.ts","sourceRoot":"","sources":["../../src/interfaces/bundle-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5F,gDAAgD;IAChD,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAEpF;;;;;;;OAOG;IACH,YAAY,CACV,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,eAAe,GACzB,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,mDAAmD;IACnD,YAAY,CACV,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IAEnC,2DAA2D;IAC3D,WAAW,CACT,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,yCAAyC;IACzC,WAAW,CACT,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAEpC,6CAA6C;IAC7C,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAChE,gDAAgD;IAChD,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACnE,+CAA+C;IAC/C,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CACnE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RegistryStorage` — the per-row persistence seam for the marketplace
|
|
3
|
+
* registry. The interface mirrors a three-table DynamoDB shape
|
|
4
|
+
* (artifacts + artifact-versions + author-keys) so a hosted DynamoDB
|
|
5
|
+
* adapter can be a structural pass-through. Memory + filesystem impls
|
|
6
|
+
* back the open-source server and unit tests.
|
|
7
|
+
*
|
|
8
|
+
* The shape follows a single-interface / multiple-impls / contract-test
|
|
9
|
+
* pattern, with a Protocol & Contract Bar docstring.
|
|
10
|
+
*
|
|
11
|
+
* The umbrella noun is `artifact`: the registry stores BOTH gadgets
|
|
12
|
+
* AND blueprints, and `kind` discriminates.
|
|
13
|
+
*
|
|
14
|
+
* ## Protocol & Contract Bar
|
|
15
|
+
*
|
|
16
|
+
* **Parties:**
|
|
17
|
+
* - Producer / writer: {@link publishArtifact} — writes
|
|
18
|
+
* {@link ArtifactsMetadataRow} on every publish (upserting
|
|
19
|
+
* `latestVersion`) and writes {@link ArtifactVersionRow} once per
|
|
20
|
+
* version via {@link putArtifactVersionIfAbsent}.
|
|
21
|
+
* - Reader: {@link readArtifact}, {@link searchArtifacts},
|
|
22
|
+
* {@link publishArtifact} (signature verification path).
|
|
23
|
+
*
|
|
24
|
+
* **Obligations:**
|
|
25
|
+
* - {@link putArtifactVersionIfAbsent} MUST atomically reject when a row
|
|
26
|
+
* exists for `(artifactId, version)`. Mirrors DDB's
|
|
27
|
+
* `ConditionExpression: attribute_not_exists(...)`. Implementations
|
|
28
|
+
* MUST NOT overwrite on conflict — per-version immutability is a
|
|
29
|
+
* load-bearing registry invariant.
|
|
30
|
+
* - {@link getArtifactMetadata} / {@link getArtifactVersion} MUST return
|
|
31
|
+
* exactly what was last written. `null` on miss; throw on transport
|
|
32
|
+
* failure (caller decides retry).
|
|
33
|
+
* - {@link scanArtifacts} returns rows in arbitrary order (cloud DDB
|
|
34
|
+
* Scan order; memory insertion order; filesystem directory order).
|
|
35
|
+
* Consumers MUST treat ordering as non-deterministic.
|
|
36
|
+
*
|
|
37
|
+
* **Failure mode:**
|
|
38
|
+
* - Transport-level failures (DDB throttle, disk full) throw.
|
|
39
|
+
* {@link publishArtifact} wraps and returns 500 with `internal`
|
|
40
|
+
* error code.
|
|
41
|
+
* - Missing rows return `null`; never throw.
|
|
42
|
+
*
|
|
43
|
+
* **Observable violation:**
|
|
44
|
+
* - Contract test {@link registryStorageContract} covers:
|
|
45
|
+
* round-trip preservation, idempotent {@link putArtifactMetadata},
|
|
46
|
+
* `putArtifactVersionIfAbsent` rejects on collision, missing returns
|
|
47
|
+
* null, `listAuthorKeys` returns only keys for the queried subject.
|
|
48
|
+
*/
|
|
49
|
+
import type { ArtifactScanFilter, ArtifactVersionRow, ArtifactsMetadataRow, AuthorKeyRow, CompiledBlobRow } from '../types.js';
|
|
50
|
+
/**
|
|
51
|
+
* Optional flags for {@link RegistryStorage.putAuthorKey}.
|
|
52
|
+
*
|
|
53
|
+
* `ifNotExists` — when `true`, the write MUST be conditional on no
|
|
54
|
+
* existing row for `(subject, keyId)`. On conflict (a concurrent first
|
|
55
|
+
* write landed between the caller's check and this put), implementations
|
|
56
|
+
* MUST throw {@link AuthorKeyAlreadyExistsError}. Used by
|
|
57
|
+
* {@link registerAuthorKey} to close the TOCTOU window between its
|
|
58
|
+
* idempotency read and the put.
|
|
59
|
+
*/
|
|
60
|
+
export interface PutAuthorKeyOptions {
|
|
61
|
+
readonly ifNotExists?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Thrown by {@link RegistryStorage.putAuthorKey} when the caller passed
|
|
65
|
+
* `ifNotExists: true` AND a row already exists for `(subject, keyId)`.
|
|
66
|
+
* Cloud DDB adapter surfaces `ConditionalCheckFailedException` as this
|
|
67
|
+
* type; in-memory impl mirrors the contract synchronously.
|
|
68
|
+
*
|
|
69
|
+
* Callers re-read via {@link RegistryStorage.getAuthorKey} and dispatch
|
|
70
|
+
* same-publicKey → 200, different-publicKey → 409.
|
|
71
|
+
*/
|
|
72
|
+
export declare class AuthorKeyAlreadyExistsError extends Error {
|
|
73
|
+
readonly subject: string;
|
|
74
|
+
readonly keyId: string;
|
|
75
|
+
constructor(subject: string, keyId: string);
|
|
76
|
+
}
|
|
77
|
+
export interface RegistryStorage {
|
|
78
|
+
getArtifactMetadata(artifactId: string): Promise<ArtifactsMetadataRow | null>;
|
|
79
|
+
putArtifactMetadata(row: ArtifactsMetadataRow): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Scan the metadata-row family with paginated cursor + post-fetch
|
|
82
|
+
* filter. The filter is applied per-row; impls MAY push it down
|
|
83
|
+
* (cloud GSI when available) or run it in-memory after a wide scan.
|
|
84
|
+
* `limit` is treated as a per-page ceiling; consumers may iterate
|
|
85
|
+
* via `nextCursor` for multi-page reads.
|
|
86
|
+
*/
|
|
87
|
+
scanArtifacts(filter: ArtifactScanFilter): Promise<{
|
|
88
|
+
readonly rows: readonly ArtifactsMetadataRow[];
|
|
89
|
+
readonly nextCursor?: string;
|
|
90
|
+
}>;
|
|
91
|
+
getArtifactVersion(artifactId: string, version: string): Promise<ArtifactVersionRow | null>;
|
|
92
|
+
/**
|
|
93
|
+
* List every version row for `artifactId`. Returns in arbitrary order
|
|
94
|
+
* (DDB Query order by SK = version, lexicographic; memory map
|
|
95
|
+
* insertion order; filesystem directory order). Callers MUST sort
|
|
96
|
+
* by semver themselves if they need ordering.
|
|
97
|
+
*
|
|
98
|
+
* Backs the `GET /pkg/:scope/:name` list-versions route.
|
|
99
|
+
*
|
|
100
|
+
* **Cost note.** A DynamoDB impl uses `Query` with `PK = artifactId`,
|
|
101
|
+
* which is the cheapest possible per-artifact lookup (no Scan, no
|
|
102
|
+
* GSI). Memory + filesystem impls do a full table walk filtered by
|
|
103
|
+
* artifactId — adequate for bounded row counts.
|
|
104
|
+
*
|
|
105
|
+
* **Returns:** empty array when no versions exist (NOT null) — the
|
|
106
|
+
* "metadata-row present but no version rows" state should never
|
|
107
|
+
* happen post-publish but is technically representable; the empty
|
|
108
|
+
* array keeps the type narrow.
|
|
109
|
+
*/
|
|
110
|
+
listArtifactVersions(artifactId: string): Promise<readonly ArtifactVersionRow[]>;
|
|
111
|
+
/**
|
|
112
|
+
* Atomically conditional put — succeeds only when no row exists for
|
|
113
|
+
* `(artifactId, version)`. The single load-bearing concurrency primitive
|
|
114
|
+
* in the registry; consumers MUST NOT pre-check with
|
|
115
|
+
* {@link getArtifactVersion} + put (race-prone).
|
|
116
|
+
*/
|
|
117
|
+
putArtifactVersionIfAbsent(row: ArtifactVersionRow): Promise<{
|
|
118
|
+
ok: true;
|
|
119
|
+
} | {
|
|
120
|
+
ok: false;
|
|
121
|
+
reason: 'version_exists';
|
|
122
|
+
}>;
|
|
123
|
+
yankArtifactVersion(artifactId: string, version: string): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Fetch the compiled-bytes row for `compiledDigest`. Returns `null` on miss.
|
|
126
|
+
*
|
|
127
|
+
* Used by:
|
|
128
|
+
* - Read op — projecting `compiledBytes` into
|
|
129
|
+
* {@link ReadPkgResponse} alongside the version row.
|
|
130
|
+
* - Install path — two-layer resolution of
|
|
131
|
+
* `ArtifactVersionRow.compiledDigest` → `CompiledBlobRow`.
|
|
132
|
+
* A missing blob when the version row's pointer is set is a
|
|
133
|
+
* CRITICAL inconsistency the caller surfaces, not silently
|
|
134
|
+
* fallback.
|
|
135
|
+
* - Tests / fixtures that pre-seed blob rows without a paired
|
|
136
|
+
* publish.
|
|
137
|
+
*/
|
|
138
|
+
getCompiledBlob(compiledDigest: string): Promise<CompiledBlobRow | null>;
|
|
139
|
+
/**
|
|
140
|
+
* Atomically commit BOTH the version row AND the compiled-blob row
|
|
141
|
+
* under a single logical transaction. A single transaction avoids
|
|
142
|
+
* the "dangling pointer" failure mode where a blob write fails
|
|
143
|
+
* after the version row is already durable.
|
|
144
|
+
*
|
|
145
|
+
* **Two paths**, dispatched by the storage impl:
|
|
146
|
+
*
|
|
147
|
+
* - **New-blob path** — `blobRow.compiledDigest` is not yet present.
|
|
148
|
+
* Both rows are PUT-INSERTed under one transaction with
|
|
149
|
+
* `attribute_not_exists` conditions on each. Returns
|
|
150
|
+
* `{ ok: true, mode: 'new-blob' }`.
|
|
151
|
+
* - **Dedup path** — `blobRow.compiledDigest` already has a row.
|
|
152
|
+
* The version row is PUT-INSERTed AND the existing blob row's
|
|
153
|
+
* `refCount` is incremented under one transaction. Returns
|
|
154
|
+
* `{ ok: true, mode: 'dedup' }`.
|
|
155
|
+
*
|
|
156
|
+
* **Failure mode:**
|
|
157
|
+
*
|
|
158
|
+
* - Version-row conflict — `(artifactId, version)` already exists.
|
|
159
|
+
* NEITHER row mutates. Returns `{ ok: false, reason: 'version_exists' }`.
|
|
160
|
+
* The publisher's idempotent-retry path.
|
|
161
|
+
* - Transport-level failures (DDB throttle, disk full, transaction
|
|
162
|
+
* conflict not attributable to either conditional) throw —
|
|
163
|
+
* {@link publishArtifact} wraps and returns 500.
|
|
164
|
+
*
|
|
165
|
+
* **Atomicity guarantee:**
|
|
166
|
+
*
|
|
167
|
+
* - Memory + filesystem impls: single-threaded JS event loop —
|
|
168
|
+
* the function awaits both writes before returning, no other
|
|
169
|
+
* awaitable interleaves.
|
|
170
|
+
* - Cloud DDB impl: `TransactWriteItems` is all-or-nothing at the
|
|
171
|
+
* service level. The new-blob path issues one transaction; the
|
|
172
|
+
* dedup path may retry once if the optimistic new-blob path
|
|
173
|
+
* fails because the digest landed between read-and-write.
|
|
174
|
+
*
|
|
175
|
+
* **Why a single transaction matters (refCount double-increment on
|
|
176
|
+
* retry)**: a sequenced write path could increment refCount, then
|
|
177
|
+
* crash before persisting the version row, then on retry increment
|
|
178
|
+
* again. With TransactWriteItems both mutations either land together
|
|
179
|
+
* or not at all — refCount can only grow when a version row also
|
|
180
|
+
* lands.
|
|
181
|
+
*
|
|
182
|
+
* **Invariant preserved:** once a version row is durable, the
|
|
183
|
+
* `(artifactId, version)` tuple cannot be re-used — semver
|
|
184
|
+
* immutability holds.
|
|
185
|
+
*/
|
|
186
|
+
commitVersionAndBlob(versionRow: ArtifactVersionRow, blobRow: CompiledBlobRow): Promise<{
|
|
187
|
+
ok: true;
|
|
188
|
+
mode: 'new-blob' | 'dedup';
|
|
189
|
+
} | {
|
|
190
|
+
ok: false;
|
|
191
|
+
reason: 'version_exists';
|
|
192
|
+
}>;
|
|
193
|
+
getAuthorKey(subject: string, keyId: string): Promise<AuthorKeyRow | null>;
|
|
194
|
+
/**
|
|
195
|
+
* Write an AuthorKey row. With `options.ifNotExists === true`, the
|
|
196
|
+
* write is conditional on no existing row for `(subject, keyId)`;
|
|
197
|
+
* on conflict, implementations MUST throw
|
|
198
|
+
* {@link AuthorKeyAlreadyExistsError}. Default (no options) is an
|
|
199
|
+
* unconditional upsert.
|
|
200
|
+
*/
|
|
201
|
+
putAuthorKey(row: AuthorKeyRow, options?: PutAuthorKeyOptions): Promise<void>;
|
|
202
|
+
listAuthorKeys(subject: string): Promise<readonly AuthorKeyRow[]>;
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=registry-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-storage.d.ts","sourceRoot":"","sources":["../../src/interfaces/registry-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,EACZ,eAAe,EAChB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;GASG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBACX,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAQ3C;AAED,MAAM,WAAW,eAAe;IAE9B,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IAC9E,mBAAmB,CAAC,GAAG,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D;;;;;;OAMG;IACH,aAAa,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC;QACjD,QAAQ,CAAC,IAAI,EAAE,SAAS,oBAAoB,EAAE,CAAC;QAC/C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;KAC9B,CAAC,CAAC;IAGH,kBAAkB,CAChB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IACtC;;;;;;;;;;;;;;;;;OAiBG;IACH,oBAAoB,CAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAC;IAC1C;;;;;OAKG;IACH,0BAA0B,CACxB,GAAG,EAAE,kBAAkB,GACtB,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,gBAAgB,CAAA;KAAE,CAAC,CAAC;IACnE,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAGxE;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8CG;IACH,oBAAoB,CAClB,UAAU,EAAE,kBAAkB,EAC9B,OAAO,EAAE,eAAe,GACvB,OAAO,CACN;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAA;KAAE,GACxC;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,gBAAgB,CAAA;KAAE,CAC1C,CAAC;IAGF,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC3E;;;;;;OAMG;IACH,YAAY,CACV,GAAG,EAAE,YAAY,EACjB,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,CAAC;CACnE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by {@link RegistryStorage.putAuthorKey} when the caller passed
|
|
3
|
+
* `ifNotExists: true` AND a row already exists for `(subject, keyId)`.
|
|
4
|
+
* Cloud DDB adapter surfaces `ConditionalCheckFailedException` as this
|
|
5
|
+
* type; in-memory impl mirrors the contract synchronously.
|
|
6
|
+
*
|
|
7
|
+
* Callers re-read via {@link RegistryStorage.getAuthorKey} and dispatch
|
|
8
|
+
* same-publicKey → 200, different-publicKey → 409.
|
|
9
|
+
*/
|
|
10
|
+
export class AuthorKeyAlreadyExistsError extends Error {
|
|
11
|
+
subject;
|
|
12
|
+
keyId;
|
|
13
|
+
constructor(subject, keyId) {
|
|
14
|
+
super(`AuthorKey row already exists for (subject=${subject}, keyId=${keyId})`);
|
|
15
|
+
this.name = 'AuthorKeyAlreadyExistsError';
|
|
16
|
+
this.subject = subject;
|
|
17
|
+
this.keyId = keyId;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Compile output — base64-encoded bytes, hex digest, decoded size. */
|
|
2
|
+
export interface CompileBlueprintOk {
|
|
3
|
+
readonly ok: true;
|
|
4
|
+
readonly compiledBytes: string;
|
|
5
|
+
readonly compiledDigest: string;
|
|
6
|
+
readonly compiledSize: number;
|
|
7
|
+
}
|
|
8
|
+
/** Compile failure — structured esbuild diagnostics for wire surfacing. */
|
|
9
|
+
export interface CompileBlueprintErr {
|
|
10
|
+
readonly ok: false;
|
|
11
|
+
readonly errors: ReadonlyArray<{
|
|
12
|
+
readonly message: string;
|
|
13
|
+
readonly location?: {
|
|
14
|
+
readonly line?: number;
|
|
15
|
+
readonly column?: number;
|
|
16
|
+
};
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export type CompileBlueprintResult = CompileBlueprintOk | CompileBlueprintErr;
|
|
20
|
+
/**
|
|
21
|
+
* Always-allowed imports on the SOURCE side. Mirrors the conformance
|
|
22
|
+
* gate's `BLUEPRINT_ALLOWED_IMPORTS` to keep the wire-contract aligned;
|
|
23
|
+
* any addition here MUST land in the conformance gate at the same
|
|
24
|
+
* commit (and vice versa).
|
|
25
|
+
*/
|
|
26
|
+
export declare const BLUEPRINT_EXTERNAL_MODULES: readonly string[];
|
|
27
|
+
/**
|
|
28
|
+
* Hex SHA-256 of `compiledBytes` (base64-decoded). Pure function; the
|
|
29
|
+
* deterministic-compile contract is `compile(source) → sha256(bytes)`.
|
|
30
|
+
*
|
|
31
|
+
* Exported for callers that want to recompute the digest without
|
|
32
|
+
* re-compiling (e.g. install-time defense-in-depth).
|
|
33
|
+
*/
|
|
34
|
+
export declare function compiledDigestHex(compiledBytes: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Compile a blueprint's TSX `source` into canonical compiled JS bytes.
|
|
37
|
+
*
|
|
38
|
+
* Sync at call site — esbuild's `transformSync` is used to keep the
|
|
39
|
+
* publish op's transaction boundaries clean (one async hop into
|
|
40
|
+
* compile, one async hop into storage). The transform is itself fast
|
|
41
|
+
* (<10ms for typical blueprints).
|
|
42
|
+
*
|
|
43
|
+
* @returns Discriminated-union result. Caller projects errors onto
|
|
44
|
+
* the conformance-failure wire envelope; success is a
|
|
45
|
+
* base64 string + hex digest + decoded byte count.
|
|
46
|
+
*/
|
|
47
|
+
export declare function compileBlueprint(source: string): CompileBlueprintResult;
|
|
48
|
+
//# sourceMappingURL=compile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/ops/compile.ts"],"names":[],"mappings":"AAsDA,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,2EAA2E;AAC3E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;QAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC1E,CAAC,CAAC;CACJ;AAED,MAAM,MAAM,sBAAsB,GAAG,kBAAkB,GAAG,mBAAmB,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,EAAE,SAAS,MAAM,EAKtD,CAAC;AAEH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,sBAAsB,CA6CvE"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `compileBlueprint` — the TSX → JS compile boundary.
|
|
3
|
+
*
|
|
4
|
+
* Pure transform from a blueprint's `manifest.source` (TSX text) to
|
|
5
|
+
* compiled JS bytes + content digest. Called by the publish op before
|
|
6
|
+
* writing the {@link CompiledBlobRow} + {@link ArtifactVersionRow}.
|
|
7
|
+
* NO I/O — esbuild's synchronous transform API is used so this op
|
|
8
|
+
* stays at the seam where reads and writes are sequenced by the
|
|
9
|
+
* caller's transaction.
|
|
10
|
+
*
|
|
11
|
+
* **Determinism.** The compile config below is a fixed settings
|
|
12
|
+
* matrix. Changing any field is a `compiledDigest` mass-invalidation
|
|
13
|
+
* — every previously-published version produces a different digest.
|
|
14
|
+
* Any change to this matrix requires a migration plan, because the
|
|
15
|
+
* matcher's cross-app cache sharing and federation keys depend on
|
|
16
|
+
* stable digests across all federating registries.
|
|
17
|
+
*
|
|
18
|
+
* format: 'esm' — required (the iframe runtime + matcher
|
|
19
|
+
* consume ES modules; no CJS path).
|
|
20
|
+
* target: 'es2022' — pinned to the lowest engine the
|
|
21
|
+
* protocol supports; rebumping is a
|
|
22
|
+
* minor protocol version bump.
|
|
23
|
+
* bundle: true — single-file output; the matcher writes
|
|
24
|
+
* the bytes directly into componentCode.
|
|
25
|
+
* minify: false — readability matters more than size at
|
|
26
|
+
* this scale (~10s of KB per blueprint).
|
|
27
|
+
* external: ['react', — host-provided modules at iframe-render
|
|
28
|
+
* 'react-dom', time. The conformance gate enforces
|
|
29
|
+
* 'react/jsx-runtime', the same allow-list on the SOURCE
|
|
30
|
+
* '@ggui-ai/gadgets'] side so we never compile a blueprint
|
|
31
|
+
* whose imports don't survive bundling.
|
|
32
|
+
* loader.tsx: 'tsx' — input is JSX/TSX text.
|
|
33
|
+
* treeShaking: false — preserve all imports so the conformance
|
|
34
|
+
* gate's import-walk matches what's in
|
|
35
|
+
* the compiled output.
|
|
36
|
+
*
|
|
37
|
+
* **esbuild version pin.** `packages/registry-core/package.json` pins
|
|
38
|
+
* `esbuild` to a single minor — the digest is sensitive to esbuild's
|
|
39
|
+
* own version bumps. Bumping esbuild is a coordinated re-publish
|
|
40
|
+
* exercise; the policy lives in the migration doc.
|
|
41
|
+
*
|
|
42
|
+
* **Failure mode.** Compile errors surface as a typed result
|
|
43
|
+
* `{ ok: false }` with structured `errors` from esbuild. The publish
|
|
44
|
+
* op projects them onto the conformance-failure wire envelope
|
|
45
|
+
* (`blueprint_compile_error`). The static-gates conformance op
|
|
46
|
+
* already runs a more permissive `esbuild.transformSync` to surface
|
|
47
|
+
* the same code at conformance-check time; this compile op is the
|
|
48
|
+
* load-bearing one (its OUTPUT is what's stored), but the two are
|
|
49
|
+
* deliberately separate so a `POST /conformance/check` dry-run
|
|
50
|
+
* remains cheap (no bundle resolution).
|
|
51
|
+
*/
|
|
52
|
+
import * as esbuild from 'esbuild';
|
|
53
|
+
import { createHash } from 'node:crypto';
|
|
54
|
+
/**
|
|
55
|
+
* Always-allowed imports on the SOURCE side. Mirrors the conformance
|
|
56
|
+
* gate's `BLUEPRINT_ALLOWED_IMPORTS` to keep the wire-contract aligned;
|
|
57
|
+
* any addition here MUST land in the conformance gate at the same
|
|
58
|
+
* commit (and vice versa).
|
|
59
|
+
*/
|
|
60
|
+
export const BLUEPRINT_EXTERNAL_MODULES = Object.freeze([
|
|
61
|
+
'react',
|
|
62
|
+
'react-dom',
|
|
63
|
+
'react/jsx-runtime',
|
|
64
|
+
'@ggui-ai/gadgets',
|
|
65
|
+
]);
|
|
66
|
+
/**
|
|
67
|
+
* Hex SHA-256 of `compiledBytes` (base64-decoded). Pure function; the
|
|
68
|
+
* deterministic-compile contract is `compile(source) → sha256(bytes)`.
|
|
69
|
+
*
|
|
70
|
+
* Exported for callers that want to recompute the digest without
|
|
71
|
+
* re-compiling (e.g. install-time defense-in-depth).
|
|
72
|
+
*/
|
|
73
|
+
export function compiledDigestHex(compiledBytes) {
|
|
74
|
+
return createHash('sha256').update(Buffer.from(compiledBytes, 'base64')).digest('hex');
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Compile a blueprint's TSX `source` into canonical compiled JS bytes.
|
|
78
|
+
*
|
|
79
|
+
* Sync at call site — esbuild's `transformSync` is used to keep the
|
|
80
|
+
* publish op's transaction boundaries clean (one async hop into
|
|
81
|
+
* compile, one async hop into storage). The transform is itself fast
|
|
82
|
+
* (<10ms for typical blueprints).
|
|
83
|
+
*
|
|
84
|
+
* @returns Discriminated-union result. Caller projects errors onto
|
|
85
|
+
* the conformance-failure wire envelope; success is a
|
|
86
|
+
* base64 string + hex digest + decoded byte count.
|
|
87
|
+
*/
|
|
88
|
+
export function compileBlueprint(source) {
|
|
89
|
+
try {
|
|
90
|
+
// esbuild.buildSync would require a virtual-fs layer to handle
|
|
91
|
+
// imports; we use transformSync which compiles a single text input
|
|
92
|
+
// and emits a single text output. Tree-shaking is OFF and externals
|
|
93
|
+
// are not stripped (preserved as ESM import statements). This
|
|
94
|
+
// matches what the runtime + matcher consume.
|
|
95
|
+
const result = esbuild.transformSync(source, {
|
|
96
|
+
loader: 'tsx',
|
|
97
|
+
format: 'esm',
|
|
98
|
+
target: 'es2022',
|
|
99
|
+
minify: false,
|
|
100
|
+
treeShaking: false,
|
|
101
|
+
// `keepNames` keeps function/class names for stack traces — the
|
|
102
|
+
// matcher's source-map-less debug surface depends on these.
|
|
103
|
+
keepNames: true,
|
|
104
|
+
// `sourcemap` off — sourcemaps would inject non-deterministic
|
|
105
|
+
// path strings into the output, breaking digest stability.
|
|
106
|
+
sourcemap: false,
|
|
107
|
+
});
|
|
108
|
+
// Note on `BLUEPRINT_EXTERNAL_MODULES`: with `transformSync` (not
|
|
109
|
+
// `buildSync`) imports are preserved verbatim in the output —
|
|
110
|
+
// tree-shaking off + no bundle resolution means the import
|
|
111
|
+
// statements pass through unmodified. The externals list is the
|
|
112
|
+
// CONTRACT consumers (iframe runtime, matcher) honor at module-
|
|
113
|
+
// load time. The conformance gate's import-walk enforces the
|
|
114
|
+
// allow-list on the source side before publish.
|
|
115
|
+
const bytes = Buffer.from(result.code, 'utf-8');
|
|
116
|
+
const compiledBytes = bytes.toString('base64');
|
|
117
|
+
const compiledDigest = createHash('sha256').update(bytes).digest('hex');
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
compiledBytes,
|
|
121
|
+
compiledDigest,
|
|
122
|
+
compiledSize: bytes.byteLength,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
// esbuild's BuildFailure shape exposes `errors: Message[]` with
|
|
127
|
+
// text + location. Other throw shapes (TypeError, OOM) surface
|
|
128
|
+
// as a single generic error.
|
|
129
|
+
const errors = extractEsbuildErrors(err);
|
|
130
|
+
return { ok: false, errors };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function extractEsbuildErrors(err) {
|
|
134
|
+
if (err === null || typeof err !== 'object') {
|
|
135
|
+
return [{ message: err instanceof Error ? err.message : String(err) }];
|
|
136
|
+
}
|
|
137
|
+
const maybeErrors = err.errors;
|
|
138
|
+
if (!Array.isArray(maybeErrors) || maybeErrors.length === 0) {
|
|
139
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
140
|
+
return [{ message }];
|
|
141
|
+
}
|
|
142
|
+
const out = [];
|
|
143
|
+
for (const m of maybeErrors) {
|
|
144
|
+
if (m === null || typeof m !== 'object')
|
|
145
|
+
continue;
|
|
146
|
+
const msg = m;
|
|
147
|
+
const message = typeof msg.text === 'string' && msg.text.length > 0 ? msg.text : 'esbuild error';
|
|
148
|
+
if (msg.location !== null &&
|
|
149
|
+
msg.location !== undefined &&
|
|
150
|
+
(typeof msg.location.line === 'number' || typeof msg.location.column === 'number')) {
|
|
151
|
+
const location = {};
|
|
152
|
+
if (typeof msg.location.line === 'number')
|
|
153
|
+
location.line = msg.location.line;
|
|
154
|
+
if (typeof msg.location.column === 'number')
|
|
155
|
+
location.column = msg.location.column;
|
|
156
|
+
out.push({ message, location });
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
out.push({ message });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|