@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,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conformance check — a pure function shared by the registry server,
|
|
3
|
+
* the hosted registry, and the publish op so they all enforce the
|
|
4
|
+
* same gate.
|
|
5
|
+
*
|
|
6
|
+
* ## Gadget gates
|
|
7
|
+
*
|
|
8
|
+
* 1. **`manifest_invalid`** — manifest does not parse against the
|
|
9
|
+
* strict zod schema in `@ggui-ai/artifact-manifest`.
|
|
10
|
+
*
|
|
11
|
+
* 2. **`bundle_parse_error`** — submitted `bundle` text does not
|
|
12
|
+
* parse as ESM via `oxc-parser`'s `parseSync`.
|
|
13
|
+
*
|
|
14
|
+
* 3. **`disallowed_import`** — AST-walk over the bundle's
|
|
15
|
+
* `staticImports` + `dynamicImports` rejects any source not in the
|
|
16
|
+
* allowlist + verifies a matching named or default export exists.
|
|
17
|
+
* Dynamic imports with a non-literal specifier
|
|
18
|
+
* (`import(getEvilPkg())`) are rejected outright since the static
|
|
19
|
+
* gate cannot resolve the target.
|
|
20
|
+
*
|
|
21
|
+
* 4. **`missing_default_export`** — same walk; for every entry in
|
|
22
|
+
* the manifest's `exports[]` the bundle must carry either a
|
|
23
|
+
* `default` export or a named export matching that entry's
|
|
24
|
+
* `hook` / `component` name.
|
|
25
|
+
*
|
|
26
|
+
* ## Blueprint gates
|
|
27
|
+
*
|
|
28
|
+
* Blueprints carry TSX source instead of a pre-compiled bundle; the
|
|
29
|
+
* gate compiles + AST-walks the source so the registry rejects
|
|
30
|
+
* un-installable blueprints at publish time instead of at iframe load
|
|
31
|
+
* time.
|
|
32
|
+
*
|
|
33
|
+
* 5. **`blueprint_source_too_large`** — `manifest.source` exceeds
|
|
34
|
+
* {@link MAX_BLUEPRINT_SOURCE_BYTES}.
|
|
35
|
+
*
|
|
36
|
+
* 6. **`blueprint_compile_error`** — `oxc-parser` rejects the TSX
|
|
37
|
+
* source (syntax error, JSX-not-resolved, etc.). `oxc-parser`'s
|
|
38
|
+
* `lang: 'tsx'` validates JSX + TS syntax on its own, so this
|
|
39
|
+
* gate is parse-only — no separate compile step is needed here
|
|
40
|
+
* (the import-walk runs directly against `manifest.source`).
|
|
41
|
+
*
|
|
42
|
+
* 7. **`blueprint_disallowed_import`** — source imports a module
|
|
43
|
+
* outside the always-allowlist. Walks both `staticImports` and
|
|
44
|
+
* `dynamicImports`. Dynamic imports with a non-literal specifier
|
|
45
|
+
* (`import(getEvilPkg())`) are rejected outright since the static
|
|
46
|
+
* gate cannot resolve the target. Blueprints have NO `peerDeps`
|
|
47
|
+
* channel — the only legal imports are `react`, `react-dom`,
|
|
48
|
+
* `react/jsx-runtime`, `@ggui-ai/gadgets`.
|
|
49
|
+
*
|
|
50
|
+
* 8. **`blueprint_missing_default_export`** — source has no default
|
|
51
|
+
* export. The iframe runtime mounts the default export as the root
|
|
52
|
+
* component; a blueprint without one cannot render.
|
|
53
|
+
*
|
|
54
|
+
* 9. **`fixture_props_shape_mismatch`** — both `manifest.fixtureProps`
|
|
55
|
+
* and `manifest.contract.propsSpec` are present, and the fixture is
|
|
56
|
+
* missing a key marked `required: true` on the propsSpec (or the
|
|
57
|
+
* fixture is not a JSON object).
|
|
58
|
+
*
|
|
59
|
+
* Each gate short-circuits the next. Within the import walk, every
|
|
60
|
+
* disallowed import + the missing-export check are all collected
|
|
61
|
+
* together (one walk, every violation surfaced).
|
|
62
|
+
*
|
|
63
|
+
* The optional runtime-probe gate (`blueprint_runtime_probe_failed`) is
|
|
64
|
+
* defined in this file's {@link ConformanceErrorCode} union but executed
|
|
65
|
+
* by {@link BlueprintProbeRunner}-implementing packages — kept out of
|
|
66
|
+
* registry-core to keep this layer free of DOM-emulation deps.
|
|
67
|
+
*/
|
|
68
|
+
import { type ArtifactManifest } from '@ggui-ai/artifact-manifest';
|
|
69
|
+
import { type StaticExport, type StaticImport } from 'oxc-parser';
|
|
70
|
+
/**
|
|
71
|
+
* Body shape of the conformance request. `bundle` is the UTF-8 text of
|
|
72
|
+
* the compiled gadget entry; required for `kind: "gadget"` manifests,
|
|
73
|
+
* ignored otherwise.
|
|
74
|
+
*/
|
|
75
|
+
export interface ConformanceRequestPayload {
|
|
76
|
+
readonly manifest: unknown;
|
|
77
|
+
readonly bundle?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Stable error-code enum. Strings are the wire contract — the publish
|
|
81
|
+
* CLI matches on these for human-readable rendering.
|
|
82
|
+
*
|
|
83
|
+
* Also exported as the `ConformanceFailureCode` alias for callers
|
|
84
|
+
* reading the `conformanceFailureCode` sub-discriminator field on the
|
|
85
|
+
* publish error body. Same union, two names — the `*FailureCode` name
|
|
86
|
+
* reads naturally on the publish/error side; the `*ErrorCode` name
|
|
87
|
+
* reads naturally on the standalone-conformance-check side.
|
|
88
|
+
*/
|
|
89
|
+
export type ConformanceErrorCode = 'manifest_invalid' | 'bundle_parse_error' | 'disallowed_import' | 'missing_default_export' | 'blueprint_source_too_large' | 'blueprint_compile_error' | 'blueprint_disallowed_import' | 'blueprint_missing_default_export' | 'fixture_props_shape_mismatch' | 'blueprint_runtime_probe_failed';
|
|
90
|
+
export type ConformanceFailureCode = ConformanceErrorCode;
|
|
91
|
+
export interface ConformanceError {
|
|
92
|
+
readonly code: ConformanceErrorCode;
|
|
93
|
+
readonly message: string;
|
|
94
|
+
/**
|
|
95
|
+
* Zod `path` array for `manifest_invalid`, otherwise omitted. Zod's
|
|
96
|
+
* issue paths are `PropertyKey[]` (string | number | symbol); the
|
|
97
|
+
* symbol case is projected via `String(...)` in {@link zodIssueToError}.
|
|
98
|
+
*/
|
|
99
|
+
readonly path?: readonly (string | number)[];
|
|
100
|
+
/**
|
|
101
|
+
* Per-code structured detail — `{ line, column }` for parse errors,
|
|
102
|
+
* `{ source }` for disallowed imports, etc.
|
|
103
|
+
*/
|
|
104
|
+
readonly detail?: unknown;
|
|
105
|
+
}
|
|
106
|
+
export interface ConformanceResponseBody {
|
|
107
|
+
readonly ok: boolean;
|
|
108
|
+
readonly errors: readonly ConformanceError[];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Hard upper bound on blueprint TSX source size. Symmetric with the
|
|
112
|
+
* gadget bundle limit ({@link MAX_BUNDLE_BYTES} in `publish.ts`) so an
|
|
113
|
+
* operator with no `MAX_BUNDLE_BYTES` mental model still gets the same
|
|
114
|
+
* order of magnitude. Source size is measured in UTF-8 bytes, not
|
|
115
|
+
* JavaScript string length — multi-byte characters count for their
|
|
116
|
+
* encoded width.
|
|
117
|
+
*/
|
|
118
|
+
export declare const MAX_BLUEPRINT_SOURCE_BYTES: number;
|
|
119
|
+
/**
|
|
120
|
+
* Run the conformance check synchronously. Pure function: no AWS, no
|
|
121
|
+
* network, no env lookups. Returns the locked response body shape.
|
|
122
|
+
* The OSS server, the cloud Lambda, and the publish op all call this.
|
|
123
|
+
*
|
|
124
|
+
* Static gates only — the optional runtime probe lives behind a
|
|
125
|
+
* {@link BlueprintProbeRunner} seam so registry-core stays free of
|
|
126
|
+
* DOM-emulation deps (happy-dom + react-dom/server land in the caller
|
|
127
|
+
* that wants the probe).
|
|
128
|
+
*/
|
|
129
|
+
export declare function checkConformance(payload: ConformanceRequestPayload): ConformanceResponseBody;
|
|
130
|
+
/**
|
|
131
|
+
* Runtime-probe seam. Implemented by callers that opt into the
|
|
132
|
+
* sandboxed render check. registry-core does not provide a default
|
|
133
|
+
* impl because the canonical happy-dom-based runner pulls in
|
|
134
|
+
* `happy-dom` + `react-dom/server` — deps the lean conformance HTTP
|
|
135
|
+
* endpoint should not pay for.
|
|
136
|
+
*
|
|
137
|
+
* Publish flow wires this through `PublishArtifactDeps.blueprintProbe`
|
|
138
|
+
* (optional). When absent, the probe is skipped and only the static
|
|
139
|
+
* gates run.
|
|
140
|
+
*/
|
|
141
|
+
export interface BlueprintProbeRunner {
|
|
142
|
+
/**
|
|
143
|
+
* Render the compiled blueprint with the manifest's fixtureProps in
|
|
144
|
+
* a sandboxed DOM. Resolve to `ok: true` on a clean render; resolve
|
|
145
|
+
* to `ok: false` with a single error carrying code
|
|
146
|
+
* `'blueprint_runtime_probe_failed'` on any thrown error during
|
|
147
|
+
* compile / mount / render.
|
|
148
|
+
*/
|
|
149
|
+
probe(manifest: Extract<ArtifactManifest, {
|
|
150
|
+
kind: 'blueprint';
|
|
151
|
+
}>): Promise<ConformanceResponseBody>;
|
|
152
|
+
}
|
|
153
|
+
export type { StaticImport, StaticExport };
|
|
154
|
+
//# sourceMappingURL=conformance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../../src/ops/conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AAGpB;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,oBAAoB,GAC5B,kBAAkB,GAClB,oBAAoB,GACpB,mBAAmB,GACnB,wBAAwB,GACxB,4BAA4B,GAC5B,yBAAyB,GACzB,6BAA6B,GAC7B,kCAAkC,GAClC,8BAA8B,GAC9B,gCAAgC,CAAC;AAErC,MAAM,MAAM,sBAAsB,GAAG,oBAAoB,CAAC;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC7C;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC9C;AAkBD;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,QAAkB,CAAC;AAE1D;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,yBAAyB,GACjC,uBAAuB,CAgBzB;AAmbD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,KAAK,CACH,QAAQ,EAAE,OAAO,CAAC,gBAAgB,EAAE;QAAE,IAAI,EAAE,WAAW,CAAA;KAAE,CAAC,GACzD,OAAO,CAAC,uBAAuB,CAAC,CAAC;CACrC;AAED,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conformance check — a pure function shared by the registry server,
|
|
3
|
+
* the hosted registry, and the publish op so they all enforce the
|
|
4
|
+
* same gate.
|
|
5
|
+
*
|
|
6
|
+
* ## Gadget gates
|
|
7
|
+
*
|
|
8
|
+
* 1. **`manifest_invalid`** — manifest does not parse against the
|
|
9
|
+
* strict zod schema in `@ggui-ai/artifact-manifest`.
|
|
10
|
+
*
|
|
11
|
+
* 2. **`bundle_parse_error`** — submitted `bundle` text does not
|
|
12
|
+
* parse as ESM via `oxc-parser`'s `parseSync`.
|
|
13
|
+
*
|
|
14
|
+
* 3. **`disallowed_import`** — AST-walk over the bundle's
|
|
15
|
+
* `staticImports` + `dynamicImports` rejects any source not in the
|
|
16
|
+
* allowlist + verifies a matching named or default export exists.
|
|
17
|
+
* Dynamic imports with a non-literal specifier
|
|
18
|
+
* (`import(getEvilPkg())`) are rejected outright since the static
|
|
19
|
+
* gate cannot resolve the target.
|
|
20
|
+
*
|
|
21
|
+
* 4. **`missing_default_export`** — same walk; for every entry in
|
|
22
|
+
* the manifest's `exports[]` the bundle must carry either a
|
|
23
|
+
* `default` export or a named export matching that entry's
|
|
24
|
+
* `hook` / `component` name.
|
|
25
|
+
*
|
|
26
|
+
* ## Blueprint gates
|
|
27
|
+
*
|
|
28
|
+
* Blueprints carry TSX source instead of a pre-compiled bundle; the
|
|
29
|
+
* gate compiles + AST-walks the source so the registry rejects
|
|
30
|
+
* un-installable blueprints at publish time instead of at iframe load
|
|
31
|
+
* time.
|
|
32
|
+
*
|
|
33
|
+
* 5. **`blueprint_source_too_large`** — `manifest.source` exceeds
|
|
34
|
+
* {@link MAX_BLUEPRINT_SOURCE_BYTES}.
|
|
35
|
+
*
|
|
36
|
+
* 6. **`blueprint_compile_error`** — `oxc-parser` rejects the TSX
|
|
37
|
+
* source (syntax error, JSX-not-resolved, etc.). `oxc-parser`'s
|
|
38
|
+
* `lang: 'tsx'` validates JSX + TS syntax on its own, so this
|
|
39
|
+
* gate is parse-only — no separate compile step is needed here
|
|
40
|
+
* (the import-walk runs directly against `manifest.source`).
|
|
41
|
+
*
|
|
42
|
+
* 7. **`blueprint_disallowed_import`** — source imports a module
|
|
43
|
+
* outside the always-allowlist. Walks both `staticImports` and
|
|
44
|
+
* `dynamicImports`. Dynamic imports with a non-literal specifier
|
|
45
|
+
* (`import(getEvilPkg())`) are rejected outright since the static
|
|
46
|
+
* gate cannot resolve the target. Blueprints have NO `peerDeps`
|
|
47
|
+
* channel — the only legal imports are `react`, `react-dom`,
|
|
48
|
+
* `react/jsx-runtime`, `@ggui-ai/gadgets`.
|
|
49
|
+
*
|
|
50
|
+
* 8. **`blueprint_missing_default_export`** — source has no default
|
|
51
|
+
* export. The iframe runtime mounts the default export as the root
|
|
52
|
+
* component; a blueprint without one cannot render.
|
|
53
|
+
*
|
|
54
|
+
* 9. **`fixture_props_shape_mismatch`** — both `manifest.fixtureProps`
|
|
55
|
+
* and `manifest.contract.propsSpec` are present, and the fixture is
|
|
56
|
+
* missing a key marked `required: true` on the propsSpec (or the
|
|
57
|
+
* fixture is not a JSON object).
|
|
58
|
+
*
|
|
59
|
+
* Each gate short-circuits the next. Within the import walk, every
|
|
60
|
+
* disallowed import + the missing-export check are all collected
|
|
61
|
+
* together (one walk, every violation surfaced).
|
|
62
|
+
*
|
|
63
|
+
* The optional runtime-probe gate (`blueprint_runtime_probe_failed`) is
|
|
64
|
+
* defined in this file's {@link ConformanceErrorCode} union but executed
|
|
65
|
+
* by {@link BlueprintProbeRunner}-implementing packages — kept out of
|
|
66
|
+
* registry-core to keep this layer free of DOM-emulation deps.
|
|
67
|
+
*/
|
|
68
|
+
import { safeParseArtifactManifest, } from '@ggui-ai/artifact-manifest';
|
|
69
|
+
import { parseSync, } from 'oxc-parser';
|
|
70
|
+
/**
|
|
71
|
+
* Always-permitted import sources for gadget bundles. Mirrors the
|
|
72
|
+
* runtime CSP the iframe enforces — these are the only modules the
|
|
73
|
+
* data-URL shim resolves besides the manifest's `peerDeps`.
|
|
74
|
+
*
|
|
75
|
+
* Kept as a `readonly Set` so the walker check is O(1) and the array
|
|
76
|
+
* cannot be mutated by callers. The exact contents are wire-stable;
|
|
77
|
+
* widening requires a follow-up + migration note.
|
|
78
|
+
*/
|
|
79
|
+
const ALWAYS_ALLOWED_IMPORTS = new Set([
|
|
80
|
+
'react',
|
|
81
|
+
'react-dom',
|
|
82
|
+
'react/jsx-runtime',
|
|
83
|
+
'@ggui-ai/gadgets',
|
|
84
|
+
]);
|
|
85
|
+
/**
|
|
86
|
+
* Hard upper bound on blueprint TSX source size. Symmetric with the
|
|
87
|
+
* gadget bundle limit ({@link MAX_BUNDLE_BYTES} in `publish.ts`) so an
|
|
88
|
+
* operator with no `MAX_BUNDLE_BYTES` mental model still gets the same
|
|
89
|
+
* order of magnitude. Source size is measured in UTF-8 bytes, not
|
|
90
|
+
* JavaScript string length — multi-byte characters count for their
|
|
91
|
+
* encoded width.
|
|
92
|
+
*/
|
|
93
|
+
export const MAX_BLUEPRINT_SOURCE_BYTES = 5 * 1024 * 1024;
|
|
94
|
+
/**
|
|
95
|
+
* Run the conformance check synchronously. Pure function: no AWS, no
|
|
96
|
+
* network, no env lookups. Returns the locked response body shape.
|
|
97
|
+
* The OSS server, the cloud Lambda, and the publish op all call this.
|
|
98
|
+
*
|
|
99
|
+
* Static gates only — the optional runtime probe lives behind a
|
|
100
|
+
* {@link BlueprintProbeRunner} seam so registry-core stays free of
|
|
101
|
+
* DOM-emulation deps (happy-dom + react-dom/server land in the caller
|
|
102
|
+
* that wants the probe).
|
|
103
|
+
*/
|
|
104
|
+
export function checkConformance(payload) {
|
|
105
|
+
// ── Gate 1: manifest schema ────────────────────────────────────────
|
|
106
|
+
const manifestResult = safeParseArtifactManifest(payload.manifest);
|
|
107
|
+
if (!manifestResult.success) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
errors: manifestResult.error.issues.map(zodIssueToError),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const manifest = manifestResult.data;
|
|
114
|
+
if (manifest.kind === 'blueprint') {
|
|
115
|
+
return checkBlueprintConformance(manifest);
|
|
116
|
+
}
|
|
117
|
+
return checkGadgetConformance(manifest, payload.bundle);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Gadget gate sequence — parses + import-walks the submitted bundle.
|
|
121
|
+
* Extracted from {@link checkConformance} so the blueprint branch can
|
|
122
|
+
* stand on its own without nesting.
|
|
123
|
+
*/
|
|
124
|
+
function checkGadgetConformance(manifest, bundle) {
|
|
125
|
+
// ── Gate 2: bundle is parseable ESM ────────────────────────────────
|
|
126
|
+
if (typeof bundle !== 'string' || bundle.length === 0) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
errors: [
|
|
130
|
+
{
|
|
131
|
+
code: 'bundle_parse_error',
|
|
132
|
+
message: 'gadget submission is missing the `bundle` field — bundle text is required for kind=gadget conformance.',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const parsed = parseSync('bundle.js', bundle, {
|
|
138
|
+
lang: 'js',
|
|
139
|
+
sourceType: 'module',
|
|
140
|
+
});
|
|
141
|
+
if (parsed.errors.length > 0) {
|
|
142
|
+
const first = parsed.errors[0];
|
|
143
|
+
if (!first) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
errors: [{ code: 'bundle_parse_error', message: 'unknown parse error' }],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const firstLabel = first.labels[0];
|
|
150
|
+
const offset = firstLabel?.start ?? 0;
|
|
151
|
+
const { line, column } = offsetToLineCol(bundle, offset);
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
errors: [
|
|
155
|
+
{
|
|
156
|
+
code: 'bundle_parse_error',
|
|
157
|
+
message: first.message,
|
|
158
|
+
detail: { line, column, offset },
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// ── Gate 3: import allowlist + export check (combined walk) ────────
|
|
164
|
+
const peerDepKeys = manifest.peerDeps
|
|
165
|
+
? new Set(Object.keys(manifest.peerDeps))
|
|
166
|
+
: new Set();
|
|
167
|
+
const errors = [];
|
|
168
|
+
for (const imp of parsed.module.staticImports) {
|
|
169
|
+
const source = imp.moduleRequest.value;
|
|
170
|
+
if (!isImportAllowed(source, peerDepKeys)) {
|
|
171
|
+
const { line, column } = offsetToLineCol(bundle, imp.start);
|
|
172
|
+
errors.push({
|
|
173
|
+
code: 'disallowed_import',
|
|
174
|
+
message: `import source \`${source}\` is not in the conformance allowlist. Allowed: \`react\`, \`react-dom\`, \`react/jsx-runtime\`, \`@ggui-ai/gadgets\`, plus any package declared in \`peerDeps\`.`,
|
|
175
|
+
detail: { source, line, column },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Dynamic imports — same allow-list, plus a hard reject on non-literal
|
|
180
|
+
// expressions (`import(getEvilPkg())`) since the static gate cannot
|
|
181
|
+
// know the target. Pre-launch posture: a known bypass path that
|
|
182
|
+
// would otherwise sail through; rejected outright.
|
|
183
|
+
for (const dyn of parsed.module.dynamicImports) {
|
|
184
|
+
const dynError = checkDynamicImport(bundle, dyn, 'disallowed_import', (source) => isImportAllowed(source, peerDepKeys), 'Allowed: `react`, `react-dom`, `react/jsx-runtime`, `@ggui-ai/gadgets`, plus any package declared in `peerDeps`.');
|
|
185
|
+
if (dynError)
|
|
186
|
+
errors.push(dynError);
|
|
187
|
+
}
|
|
188
|
+
// A gadget package declares ≥1 export; the bundle MUST carry a
|
|
189
|
+
// matching named export (or `default`) for every one of them.
|
|
190
|
+
for (const exp of manifest.exports) {
|
|
191
|
+
const isHook = 'hook' in exp;
|
|
192
|
+
const exportName = isHook ? exp.hook : exp.component;
|
|
193
|
+
if (!hasMatchingExport(parsed.module.staticExports, exportName)) {
|
|
194
|
+
errors.push({
|
|
195
|
+
code: 'missing_default_export',
|
|
196
|
+
message: `bundle exports neither \`default\` nor a named export matching the manifest's ${isHook ? 'hook' : 'component'} "${exportName}". The publish CLI fails closed when the LLM cannot resolve the export entry.`,
|
|
197
|
+
detail: { expectedHook: exportName },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { ok: errors.length === 0, errors };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Blueprint gate sequence — compile + import-walk + export + fixture
|
|
205
|
+
* shape. Static-only; the runtime probe lives behind the optional
|
|
206
|
+
* {@link BlueprintProbeRunner} seam.
|
|
207
|
+
*/
|
|
208
|
+
function checkBlueprintConformance(manifest) {
|
|
209
|
+
// ── Gate 5: source size ──────────────────────────────────────────
|
|
210
|
+
const sourceBytes = utf8ByteLength(manifest.source);
|
|
211
|
+
if (sourceBytes > MAX_BLUEPRINT_SOURCE_BYTES) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
errors: [
|
|
215
|
+
{
|
|
216
|
+
code: 'blueprint_source_too_large',
|
|
217
|
+
message: `blueprint source is ${sourceBytes} bytes; maximum is ${MAX_BLUEPRINT_SOURCE_BYTES} bytes (${MAX_BLUEPRINT_SOURCE_BYTES / (1024 * 1024)} MiB).`,
|
|
218
|
+
detail: { sourceBytes, maxBytes: MAX_BLUEPRINT_SOURCE_BYTES },
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// ── Gate 6: TSX parse + syntax validation ────────────────────────
|
|
224
|
+
// oxc-parser's `lang: 'tsx'` rejects JSX + TS syntax errors directly,
|
|
225
|
+
// so a separate esbuild compile is redundant work. Parsing the SOURCE
|
|
226
|
+
// (not a compiled output) is deliberate — author intent (including
|
|
227
|
+
// unused imports that signal mis-configured deps) is what the
|
|
228
|
+
// allow-list gates, not whatever a bundler would optimize away.
|
|
229
|
+
const parsed = parseSync('blueprint.tsx', manifest.source, {
|
|
230
|
+
lang: 'tsx',
|
|
231
|
+
sourceType: 'module',
|
|
232
|
+
});
|
|
233
|
+
if (parsed.errors.length > 0) {
|
|
234
|
+
const first = parsed.errors[0];
|
|
235
|
+
const offset = first?.labels[0]?.start ?? 0;
|
|
236
|
+
const { line, column } = offsetToLineCol(manifest.source, offset);
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
errors: [
|
|
240
|
+
{
|
|
241
|
+
code: 'blueprint_compile_error',
|
|
242
|
+
message: first?.message ?? 'syntax error in blueprint source',
|
|
243
|
+
detail: { line, column, offset },
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const errors = [];
|
|
249
|
+
for (const imp of parsed.module.staticImports) {
|
|
250
|
+
const source = imp.moduleRequest.value;
|
|
251
|
+
if (!ALWAYS_ALLOWED_IMPORTS.has(source)) {
|
|
252
|
+
const { line, column } = offsetToLineCol(manifest.source, imp.start);
|
|
253
|
+
errors.push({
|
|
254
|
+
code: 'blueprint_disallowed_import',
|
|
255
|
+
message: `import source \`${source}\` is not allowed in a blueprint. Blueprints have no \`peerDeps\` channel; the only legal imports are \`react\`, \`react-dom\`, \`react/jsx-runtime\`, \`@ggui-ai/gadgets\`.`,
|
|
256
|
+
detail: { source, line, column },
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Dynamic imports — same allow-list, plus a hard reject on non-literal
|
|
261
|
+
// expressions (`import(getEvilPkg())`) since the static gate cannot
|
|
262
|
+
// know the target. Pre-launch posture: a known bypass path; rejected
|
|
263
|
+
// outright. Blueprints have no peerDeps channel, so the literal must
|
|
264
|
+
// resolve into ALWAYS_ALLOWED_IMPORTS.
|
|
265
|
+
for (const dyn of parsed.module.dynamicImports) {
|
|
266
|
+
const dynError = checkDynamicImport(manifest.source, dyn, 'blueprint_disallowed_import', (source) => ALWAYS_ALLOWED_IMPORTS.has(source), 'Blueprints have no `peerDeps` channel; the only legal imports are `react`, `react-dom`, `react/jsx-runtime`, `@ggui-ai/gadgets`.');
|
|
267
|
+
if (dynError)
|
|
268
|
+
errors.push(dynError);
|
|
269
|
+
}
|
|
270
|
+
// ── Gate 8: default export ───────────────────────────────────────
|
|
271
|
+
if (!hasDefaultExport(parsed.module.staticExports)) {
|
|
272
|
+
errors.push({
|
|
273
|
+
code: 'blueprint_missing_default_export',
|
|
274
|
+
message: 'blueprint source has no default export. The iframe runtime mounts the default export as the root component — a blueprint without one cannot render.',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// ── Gate 9: fixtureProps × contract.propsSpec shape ──────────────
|
|
278
|
+
if (manifest.fixtureProps !== undefined &&
|
|
279
|
+
manifest.contract?.propsSpec !== undefined) {
|
|
280
|
+
const shapeError = validateFixturePropsShape(manifest.fixtureProps, manifest.contract.propsSpec);
|
|
281
|
+
if (shapeError !== null) {
|
|
282
|
+
errors.push(shapeError);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { ok: errors.length === 0, errors };
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Allowlist check — short-circuits on the implicit four sources, then
|
|
289
|
+
* falls through to the manifest's `peerDeps` keys. Subpath imports of
|
|
290
|
+
* a `peerDeps` root are also allowed (e.g. `mapbox-gl/dist/mapbox-gl.css`
|
|
291
|
+
* when `mapbox-gl` is a peerDep).
|
|
292
|
+
*/
|
|
293
|
+
function isImportAllowed(source, peerDepKeys) {
|
|
294
|
+
if (ALWAYS_ALLOWED_IMPORTS.has(source))
|
|
295
|
+
return true;
|
|
296
|
+
if (peerDepKeys.has(source))
|
|
297
|
+
return true;
|
|
298
|
+
const slashIdx = source.indexOf('/');
|
|
299
|
+
if (slashIdx > 0 && !source.startsWith('@')) {
|
|
300
|
+
const root = source.slice(0, slashIdx);
|
|
301
|
+
if (peerDepKeys.has(root))
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
if (source.startsWith('@')) {
|
|
305
|
+
const firstSlash = source.indexOf('/');
|
|
306
|
+
if (firstSlash > 0) {
|
|
307
|
+
const secondSlash = source.indexOf('/', firstSlash + 1);
|
|
308
|
+
if (secondSlash > 0) {
|
|
309
|
+
const root = source.slice(0, secondSlash);
|
|
310
|
+
if (peerDepKeys.has(root))
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
function hasMatchingExport(staticExports, expectedHook) {
|
|
318
|
+
for (const exp of staticExports) {
|
|
319
|
+
for (const entry of exp.entries) {
|
|
320
|
+
if (entry.exportName.kind === 'Default')
|
|
321
|
+
return true;
|
|
322
|
+
if (entry.exportName.kind === 'Name' && entry.exportName.name === expectedHook) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
function hasDefaultExport(staticExports) {
|
|
330
|
+
for (const exp of staticExports) {
|
|
331
|
+
for (const entry of exp.entries) {
|
|
332
|
+
if (entry.exportName.kind === 'Default')
|
|
333
|
+
return true;
|
|
334
|
+
// oxc-parser reports the alias form `export { X as default }`
|
|
335
|
+
// as `kind: 'Name'` with `name: 'default'`. Both shapes are
|
|
336
|
+
// semantically default exports — the iframe runtime mounts
|
|
337
|
+
// whichever the bundler emits as `module.exports.default` /
|
|
338
|
+
// `import().default`.
|
|
339
|
+
if (entry.exportName.kind === 'Name' && entry.exportName.name === 'default') {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Fixture shape check — verifies that every `properties[key]` marked
|
|
348
|
+
* `required: true` on the contract's propsSpec is present on the
|
|
349
|
+
* fixture, and that the fixture itself is a JSON object (not array,
|
|
350
|
+
* not null, not primitive).
|
|
351
|
+
*
|
|
352
|
+
* Deliberately narrow: full per-prop JSON-Schema validation would
|
|
353
|
+
* require materializing each `properties[k].schema` into a validator
|
|
354
|
+
* (ajv or zod-via-json-schema). At conformance time the goal is to
|
|
355
|
+
* catch "author shipped fixtureProps with the wrong top-level shape";
|
|
356
|
+
* deep type-mismatch within a fixture key is caught by the runtime
|
|
357
|
+
* probe, which actually renders the blueprint with the fixture.
|
|
358
|
+
*/
|
|
359
|
+
function validateFixturePropsShape(fixtureProps, propsSpec) {
|
|
360
|
+
if (fixtureProps === null ||
|
|
361
|
+
typeof fixtureProps !== 'object' ||
|
|
362
|
+
Array.isArray(fixtureProps)) {
|
|
363
|
+
return {
|
|
364
|
+
code: 'fixture_props_shape_mismatch',
|
|
365
|
+
message: `fixtureProps must be a JSON object (got ${describeJsonType(fixtureProps)}). The runtime probe renders the blueprint with \`<Component {...fixtureProps} />\`; a non-object fixture cannot spread.`,
|
|
366
|
+
detail: { received: describeJsonType(fixtureProps) },
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
// After the `typeof === 'object' && !null && !Array.isArray` narrowing
|
|
370
|
+
// above, `fixtureProps` is `object`. `in` works on `object` — no
|
|
371
|
+
// `Record<string, unknown>` cast needed.
|
|
372
|
+
const missing = [];
|
|
373
|
+
for (const [key, entry] of Object.entries(propsSpec.properties)) {
|
|
374
|
+
if (entry.required === true && !(key in fixtureProps)) {
|
|
375
|
+
missing.push(key);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (missing.length > 0) {
|
|
379
|
+
return {
|
|
380
|
+
code: 'fixture_props_shape_mismatch',
|
|
381
|
+
message: `fixtureProps is missing required ${missing.length === 1 ? 'key' : 'keys'} \`${missing.join('`, `')}\` declared on contract.propsSpec.properties. Either add the ${missing.length === 1 ? 'key' : 'keys'} to fixtureProps or relax \`required\` on the propsSpec.`,
|
|
382
|
+
detail: { missingKeys: missing },
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
function describeJsonType(v) {
|
|
388
|
+
if (v === null)
|
|
389
|
+
return 'null';
|
|
390
|
+
if (Array.isArray(v))
|
|
391
|
+
return 'array';
|
|
392
|
+
return typeof v;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Walk a single dynamic-import expression. The oxc-parser AST gives
|
|
396
|
+
* us only `start`/`end` byte offsets for the `moduleRequest`, so we
|
|
397
|
+
* extract the raw source slice and detect a literal string ourselves.
|
|
398
|
+
*
|
|
399
|
+
* Pre-launch posture: non-literal expressions (`import(getEvilPkg())`)
|
|
400
|
+
* are rejected outright. The static gate cannot know the resolved
|
|
401
|
+
* target and ignoring them would create a silent bypass of the
|
|
402
|
+
* allow-list. Symmetric across gadgets (allow-list = ALWAYS_ALLOWED +
|
|
403
|
+
* peerDeps) and blueprints (allow-list = ALWAYS_ALLOWED only).
|
|
404
|
+
*/
|
|
405
|
+
function checkDynamicImport(source, dyn, code, isAllowed, allowedSuffix) {
|
|
406
|
+
const raw = source.slice(dyn.moduleRequest.start, dyn.moduleRequest.end);
|
|
407
|
+
const literal = parseStringLiteral(raw);
|
|
408
|
+
const { line, column } = offsetToLineCol(source, dyn.start);
|
|
409
|
+
if (literal === null) {
|
|
410
|
+
return {
|
|
411
|
+
code,
|
|
412
|
+
message: `dynamic import with a non-literal expression (\`import(${raw.length > 40 ? raw.slice(0, 37) + '...' : raw})\`) is not allowed — the static gate cannot verify the target against the allow-list. Inline the specifier as a string literal.`,
|
|
413
|
+
detail: { source: '<dynamic-expression>', expression: raw, line, column },
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
if (!isAllowed(literal)) {
|
|
417
|
+
return {
|
|
418
|
+
code,
|
|
419
|
+
message: `dynamic import source \`${literal}\` is not in the conformance allowlist. ${allowedSuffix}`,
|
|
420
|
+
detail: { source: literal, line, column },
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Parse a JS string literal from its raw source slice. Returns the
|
|
427
|
+
* decoded value for single-quoted, double-quoted, or template-literal
|
|
428
|
+
* specifiers with NO interpolation (e.g. `` `react` ``). Returns
|
|
429
|
+
* `null` for any non-literal expression (identifier, call, template
|
|
430
|
+
* with `${...}`, etc.) — callers treat null as "non-literal, reject".
|
|
431
|
+
*
|
|
432
|
+
* Deliberately conservative: we do not decode escape sequences in the
|
|
433
|
+
* specifier because import specifiers in practice are bare package
|
|
434
|
+
* names. A specifier that requires escape decoding (`'react'`)
|
|
435
|
+
* is rare enough that surfacing it as "non-literal, reject" is the
|
|
436
|
+
* safer pre-launch default than risking a half-correct decoder.
|
|
437
|
+
*/
|
|
438
|
+
function parseStringLiteral(raw) {
|
|
439
|
+
if (raw.length < 2)
|
|
440
|
+
return null;
|
|
441
|
+
const first = raw[0];
|
|
442
|
+
const last = raw[raw.length - 1];
|
|
443
|
+
if (first !== last)
|
|
444
|
+
return null;
|
|
445
|
+
if (first !== "'" && first !== '"' && first !== '`')
|
|
446
|
+
return null;
|
|
447
|
+
const inner = raw.slice(1, -1);
|
|
448
|
+
if (first === '`' && inner.includes('${'))
|
|
449
|
+
return null;
|
|
450
|
+
// Reject any escape sequence — bare package names don't need them,
|
|
451
|
+
// and a half-correct unescape would be a contract surface we'd have
|
|
452
|
+
// to maintain forever.
|
|
453
|
+
if (inner.includes('\\'))
|
|
454
|
+
return null;
|
|
455
|
+
// Reject unescaped quote of the matching kind (would mean we
|
|
456
|
+
// mis-identified the boundaries — defensive).
|
|
457
|
+
if (inner.includes(first))
|
|
458
|
+
return null;
|
|
459
|
+
return inner;
|
|
460
|
+
}
|
|
461
|
+
function zodIssueToError(issue) {
|
|
462
|
+
const path = issue.path.map((seg) => typeof seg === 'symbol' ? String(seg) : seg);
|
|
463
|
+
return {
|
|
464
|
+
code: 'manifest_invalid',
|
|
465
|
+
message: issue.message,
|
|
466
|
+
path,
|
|
467
|
+
detail: { zodCode: issue.code },
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function offsetToLineCol(source, offset) {
|
|
471
|
+
const clamped = Math.max(0, Math.min(offset, source.length));
|
|
472
|
+
let line = 1;
|
|
473
|
+
let lineStart = 0;
|
|
474
|
+
for (let i = 0; i < clamped; i++) {
|
|
475
|
+
if (source.charCodeAt(i) === 10) {
|
|
476
|
+
line += 1;
|
|
477
|
+
lineStart = i + 1;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return { line, column: clamped - lineStart + 1 };
|
|
481
|
+
}
|
|
482
|
+
function utf8ByteLength(s) {
|
|
483
|
+
// `TextEncoder` is universally available in Node 18+ and edge runtimes.
|
|
484
|
+
// Avoids the `Buffer.byteLength` Node-only dep that registry-core would
|
|
485
|
+
// otherwise inherit.
|
|
486
|
+
return new TextEncoder().encode(s).length;
|
|
487
|
+
}
|