@ggui-ai/protocol 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 +46 -0
- package/dist/bridge/invoke-agent.d.ts +65 -0
- package/dist/bridge/invoke-agent.d.ts.map +1 -0
- package/dist/bridge/invoke-agent.js +113 -0
- package/dist/envelope-adapters.d.ts +24 -0
- package/dist/envelope-adapters.d.ts.map +1 -0
- package/dist/envelope-adapters.js +14 -0
- package/dist/envelopes/builders.d.ts +145 -0
- package/dist/envelopes/builders.d.ts.map +1 -0
- package/dist/envelopes/builders.js +113 -0
- package/dist/errors/unknown-permission-name.d.ts +12 -0
- package/dist/errors/unknown-permission-name.d.ts.map +1 -0
- package/dist/errors/unknown-permission-name.js +29 -0
- package/dist/errors/version-mismatch.d.ts +55 -0
- package/dist/errors/version-mismatch.d.ts.map +1 -0
- package/dist/errors/version-mismatch.js +52 -0
- package/dist/gadgets/resolve-contract-gadgets.d.ts +93 -0
- package/dist/gadgets/resolve-contract-gadgets.d.ts.map +1 -0
- package/dist/gadgets/resolve-contract-gadgets.js +119 -0
- package/dist/gadgets/stdlib-gadgets.d.ts +43 -0
- package/dist/gadgets/stdlib-gadgets.d.ts.map +1 -0
- package/dist/gadgets/stdlib-gadgets.js +161 -0
- package/dist/iframe-bridge.d.ts +63 -0
- package/dist/iframe-bridge.d.ts.map +1 -0
- package/dist/iframe-bridge.js +166 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/integrations/mcp-apps.d.ts +1218 -0
- package/dist/integrations/mcp-apps.d.ts.map +1 -0
- package/dist/integrations/mcp-apps.js +427 -0
- package/dist/navigation/index.d.ts +3 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +1 -0
- package/dist/navigation/stack-navigation.d.ts +55 -0
- package/dist/navigation/stack-navigation.d.ts.map +1 -0
- package/dist/navigation/stack-navigation.js +80 -0
- package/dist/recommended-prompts.d.ts +56 -0
- package/dist/recommended-prompts.d.ts.map +1 -0
- package/dist/recommended-prompts.js +55 -0
- package/dist/registry/blueprint-key.d.ts +9 -0
- package/dist/registry/blueprint-key.d.ts.map +1 -0
- package/dist/registry/blueprint-key.js +28 -0
- package/dist/registry/canonicalize-contract.d.ts +35 -0
- package/dist/registry/canonicalize-contract.d.ts.map +1 -0
- package/dist/registry/canonicalize-contract.js +166 -0
- package/dist/registry/summarize-contract.d.ts +46 -0
- package/dist/registry/summarize-contract.d.ts.map +1 -0
- package/dist/registry/summarize-contract.js +63 -0
- package/dist/schema-learning/derive-contract.d.ts +67 -0
- package/dist/schema-learning/derive-contract.d.ts.map +1 -0
- package/dist/schema-learning/derive-contract.js +117 -0
- package/dist/schema-learning/merge.d.ts +32 -0
- package/dist/schema-learning/merge.d.ts.map +1 -0
- package/dist/schema-learning/merge.js +146 -0
- package/dist/schemas/blueprint.d.ts +32 -0
- package/dist/schemas/blueprint.d.ts.map +1 -0
- package/dist/schemas/blueprint.js +92 -0
- package/dist/schemas/data-contract.d.ts +750 -0
- package/dist/schemas/data-contract.d.ts.map +1 -0
- package/dist/schemas/data-contract.js +663 -0
- package/dist/schemas/gadget-name-grammar.d.ts +29 -0
- package/dist/schemas/gadget-name-grammar.d.ts.map +1 -0
- package/dist/schemas/gadget-name-grammar.js +28 -0
- package/dist/schemas/handshake-suggestion.d.ts +46 -0
- package/dist/schemas/handshake-suggestion.d.ts.map +1 -0
- package/dist/schemas/handshake-suggestion.js +107 -0
- package/dist/schemas/invoke.d.ts +337 -0
- package/dist/schemas/invoke.d.ts.map +1 -0
- package/dist/schemas/invoke.js +169 -0
- package/dist/schemas/mcp.d.ts +301 -0
- package/dist/schemas/mcp.d.ts.map +1 -0
- package/dist/schemas/mcp.js +373 -0
- package/dist/schemas/ops-blueprint.d.ts +176 -0
- package/dist/schemas/ops-blueprint.d.ts.map +1 -0
- package/dist/schemas/ops-blueprint.js +259 -0
- package/dist/schemas/sync-check.d.ts +11 -0
- package/dist/schemas/sync-check.d.ts.map +1 -0
- package/dist/schemas/sync-check.js +60 -0
- package/dist/screen-blueprints/define.d.ts +22 -0
- package/dist/screen-blueprints/define.d.ts.map +1 -0
- package/dist/screen-blueprints/define.js +3 -0
- package/dist/screen-blueprints/index.d.ts +4 -0
- package/dist/screen-blueprints/index.d.ts.map +1 -0
- package/dist/screen-blueprints/index.js +3 -0
- package/dist/screen-blueprints/match.d.ts +35 -0
- package/dist/screen-blueprints/match.d.ts.map +1 -0
- package/dist/screen-blueprints/match.js +51 -0
- package/dist/screen-blueprints/types.d.ts +164 -0
- package/dist/screen-blueprints/types.d.ts.map +1 -0
- package/dist/screen-blueprints/types.js +1 -0
- package/dist/stream/stream-parser.d.ts +62 -0
- package/dist/stream/stream-parser.d.ts.map +1 -0
- package/dist/stream/stream-parser.js +199 -0
- package/dist/transport/websocket.d.ts +178 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +1 -0
- package/dist/types/app-config.d.ts +61 -0
- package/dist/types/app-config.d.ts.map +1 -0
- package/dist/types/app-config.js +1 -0
- package/dist/types/auth.d.ts +61 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +1 -0
- package/dist/types/blueprint.d.ts +206 -0
- package/dist/types/blueprint.d.ts.map +1 -0
- package/dist/types/blueprint.js +1 -0
- package/dist/types/canvas-lifecycle.d.ts +105 -0
- package/dist/types/canvas-lifecycle.d.ts.map +1 -0
- package/dist/types/canvas-lifecycle.js +38 -0
- package/dist/types/capabilities.d.ts +40 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +19 -0
- package/dist/types/contract-inference.d.ts +401 -0
- package/dist/types/contract-inference.d.ts.map +1 -0
- package/dist/types/contract-inference.js +44 -0
- package/dist/types/credential.d.ts +41 -0
- package/dist/types/credential.d.ts.map +1 -0
- package/dist/types/credential.js +32 -0
- package/dist/types/data-bindings.d.ts +322 -0
- package/dist/types/data-bindings.d.ts.map +1 -0
- package/dist/types/data-bindings.js +29 -0
- package/dist/types/data-contract.d.ts +1296 -0
- package/dist/types/data-contract.d.ts.map +1 -0
- package/dist/types/data-contract.js +111 -0
- package/dist/types/events.d.ts +182 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +8 -0
- package/dist/types/feedback.d.ts +24 -0
- package/dist/types/feedback.d.ts.map +1 -0
- package/dist/types/feedback.js +7 -0
- package/dist/types/gadget.d.ts +121 -0
- package/dist/types/gadget.d.ts.map +1 -0
- package/dist/types/gadget.js +24 -0
- package/dist/types/handshake-suggestion.d.ts +264 -0
- package/dist/types/handshake-suggestion.d.ts.map +1 -0
- package/dist/types/handshake-suggestion.js +70 -0
- package/dist/types/host-context.d.ts +163 -0
- package/dist/types/host-context.d.ts.map +1 -0
- package/dist/types/host-context.js +142 -0
- package/dist/types/interface-context.d.ts +105 -0
- package/dist/types/interface-context.d.ts.map +1 -0
- package/dist/types/interface-context.js +115 -0
- package/dist/types/invoke.d.ts +28 -0
- package/dist/types/invoke.d.ts.map +1 -0
- package/dist/types/invoke.js +7 -0
- package/dist/types/live-channel.d.ts +613 -0
- package/dist/types/live-channel.d.ts.map +1 -0
- package/dist/types/live-channel.js +1 -0
- package/dist/types/llm.d.ts +61 -0
- package/dist/types/llm.d.ts.map +1 -0
- package/dist/types/llm.js +186 -0
- package/dist/types/mcp-proxy.d.ts +67 -0
- package/dist/types/mcp-proxy.d.ts.map +1 -0
- package/dist/types/mcp-proxy.js +46 -0
- package/dist/types/mcp.d.ts +637 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +30 -0
- package/dist/types/openrouter-models.d.ts +22 -0
- package/dist/types/openrouter-models.d.ts.map +1 -0
- package/dist/types/openrouter-models.js +4843 -0
- package/dist/types/region.d.ts +26 -0
- package/dist/types/region.d.ts.map +1 -0
- package/dist/types/region.js +36 -0
- package/dist/types/session.d.ts +419 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +1 -0
- package/dist/types/thread.d.ts +207 -0
- package/dist/types/thread.d.ts.map +1 -0
- package/dist/types/thread.js +57 -0
- package/dist/types/ui-generator.d.ts +100 -0
- package/dist/types/ui-generator.d.ts.map +1 -0
- package/dist/types/ui-generator.js +53 -0
- package/dist/validation/ajv-runtime.d.ts +140 -0
- package/dist/validation/ajv-runtime.d.ts.map +1 -0
- package/dist/validation/ajv-runtime.js +452 -0
- package/dist/validation/content-hash.d.ts +3 -0
- package/dist/validation/content-hash.d.ts.map +1 -0
- package/dist/validation/content-hash.js +21 -0
- package/dist/validation/contract-validator.d.ts +244 -0
- package/dist/validation/contract-validator.d.ts.map +1 -0
- package/dist/validation/contract-validator.js +711 -0
- package/dist/validation/cross-references.d.ts +105 -0
- package/dist/validation/cross-references.d.ts.map +1 -0
- package/dist/validation/cross-references.js +164 -0
- package/dist/validation/hygiene-rules.d.ts +250 -0
- package/dist/validation/hygiene-rules.d.ts.map +1 -0
- package/dist/validation/hygiene-rules.js +564 -0
- package/dist/validation/lint-contract.d.ts +130 -0
- package/dist/validation/lint-contract.d.ts.map +1 -0
- package/dist/validation/lint-contract.js +225 -0
- package/dist/validation/name-invariants.d.ts +117 -0
- package/dist/validation/name-invariants.d.ts.map +1 -0
- package/dist/validation/name-invariants.js +172 -0
- package/dist/validation/reserved-channels.d.ts +156 -0
- package/dist/validation/reserved-channels.d.ts.map +1 -0
- package/dist/validation/reserved-channels.js +356 -0
- package/dist/validation/resolve-stream-channel.d.ts +78 -0
- package/dist/validation/resolve-stream-channel.d.ts.map +1 -0
- package/dist/validation/resolve-stream-channel.js +64 -0
- package/dist/validation/sanitize-error.d.ts +46 -0
- package/dist/validation/sanitize-error.d.ts.map +1 -0
- package/dist/validation/sanitize-error.js +88 -0
- package/dist/validation/schema-compat-invariants.d.ts +140 -0
- package/dist/validation/schema-compat-invariants.d.ts.map +1 -0
- package/dist/validation/schema-compat-invariants.js +220 -0
- package/dist/validation/schema-meta-validation.d.ts +60 -0
- package/dist/validation/schema-meta-validation.d.ts.map +1 -0
- package/dist/validation/schema-meta-validation.js +131 -0
- package/dist/validation/schema-subset.d.ts +165 -0
- package/dist/validation/schema-subset.d.ts.map +1 -0
- package/dist/validation/schema-subset.js +295 -0
- package/dist/validation/ui-security.d.ts +54 -0
- package/dist/validation/ui-security.d.ts.map +1 -0
- package/dist/validation/ui-security.js +138 -0
- package/dist/validation/zod-to-json-schema.d.ts +63 -0
- package/dist/validation/zod-to-json-schema.d.ts.map +1 -0
- package/dist/validation/zod-to-json-schema.js +126 -0
- package/dist/version.d.ts +1458 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +1459 -0
- package/package.json +113 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer B (inner JSON Schema meta-validation) for a `DataContract`.
|
|
3
|
+
*
|
|
4
|
+
* Walks the six inner JSON Schema fields the agent authors:
|
|
5
|
+
* 1. `propsSpec.properties[*].schema`
|
|
6
|
+
* 2. `actionSpec[*].schema` (optional per entry)
|
|
7
|
+
* 3. `streamSpec[*].schema`
|
|
8
|
+
* 4. `contextSpec[*].schema`
|
|
9
|
+
* 5. `agentCapabilities.tools[*].inputSchema` (optional per entry)
|
|
10
|
+
* 6. `agentCapabilities.tools[*].outputSchema` (optional per entry)
|
|
11
|
+
*
|
|
12
|
+
* For each present schema, runs `compileForValidation()` from
|
|
13
|
+
* `ajv-runtime`. Ajv's `strict: true` mode throws on malformed JSON
|
|
14
|
+
* Schemas at compile-time — unknown keywords, missing `items` on
|
|
15
|
+
* array nodes, properties values that are not schemas, etc. This is
|
|
16
|
+
* the meta-validation pass: it asserts the contract's own type
|
|
17
|
+
* descriptions are well-formed BEFORE any runtime data flows.
|
|
18
|
+
*
|
|
19
|
+
* Failures collect into a single throw with every malformed field
|
|
20
|
+
* named — agents fix all of them in one round rather than retry-
|
|
21
|
+
* per-field.
|
|
22
|
+
*
|
|
23
|
+
* Designed to slot at:
|
|
24
|
+
* - `ggui_handshake` entry — validates `blueprintDraft.contract`
|
|
25
|
+
* before the negotiator runs.
|
|
26
|
+
* - `ggui_push` entry — validates the `effectiveContract` (either
|
|
27
|
+
* synth-amended or override) before any state mutation.
|
|
28
|
+
*
|
|
29
|
+
* Both call sites already run cross-reference and name-invariant
|
|
30
|
+
* assertions for the same author-recoverable failure class; this
|
|
31
|
+
* sits alongside them.
|
|
32
|
+
*/
|
|
33
|
+
import { compileForValidation } from './ajv-runtime.js';
|
|
34
|
+
/**
|
|
35
|
+
* Typed error for contract schema meta-validation failures. Mirrors
|
|
36
|
+
* the shape of `CrossReferenceError` / `ContractViolationError` for
|
|
37
|
+
* symmetry — push/handshake catch and surface a structured
|
|
38
|
+
* `contract_schema_invalid` error to the agent.
|
|
39
|
+
*/
|
|
40
|
+
export class ContractSchemaMetaError extends Error {
|
|
41
|
+
code = 'contract_schema_invalid';
|
|
42
|
+
violations;
|
|
43
|
+
hint;
|
|
44
|
+
constructor(violations) {
|
|
45
|
+
const summary = violations.map(v => ` - ${v.field}: ${v.message}`).join('\n');
|
|
46
|
+
super(`Contract has malformed JSON Schemas:\n${summary}`);
|
|
47
|
+
this.name = 'ContractSchemaMetaError';
|
|
48
|
+
this.violations = violations;
|
|
49
|
+
this.hint =
|
|
50
|
+
'Fix the JSON Schema at the named field. Common causes: ' +
|
|
51
|
+
'missing `items` on an array schema, a properties entry that ' +
|
|
52
|
+
'is not itself a JSON Schema object, or an unknown keyword. ' +
|
|
53
|
+
'Re-call ggui_push (or ggui_handshake) once corrected.';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function tryCompile(field, schema, violations) {
|
|
57
|
+
// Undefined schema is a structural error, not an Ajv-compile error.
|
|
58
|
+
// Author confusion shape: agent puts a JSON Schema flat at the
|
|
59
|
+
// PropEntry / ActionEntry / StreamChannelEntry / ContextEntry level
|
|
60
|
+
// instead of wrapping it in `schema:`. Surface the missing field
|
|
61
|
+
// explicitly so the agent's recovery loop names the correction
|
|
62
|
+
// (`add a "schema" field`) instead of the opaque
|
|
63
|
+
// "Cannot read properties of undefined (reading 'type')" crash that
|
|
64
|
+
// Ajv would otherwise throw from `injectClosedShape`.
|
|
65
|
+
if (schema === undefined || schema === null) {
|
|
66
|
+
violations.push({
|
|
67
|
+
field,
|
|
68
|
+
message: `Missing 'schema' field. Each entry in propsSpec.properties / actionSpec / streamSpec / contextSpec is a WRAPPER that contains a JSON Schema in its 'schema:' field — the JSON Schema does NOT sit flat at the entry level. Example: propsSpec.properties.todos = { schema: { type: 'array', items: { ... } }, required: true }.`,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
compileForValidation(schema);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
77
|
+
violations.push({ field, message: truncate(raw, 240) });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function truncate(s, max) {
|
|
81
|
+
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Walks the contract's six inner JSON Schema fields and asserts
|
|
85
|
+
* each compiles cleanly under Ajv's strict mode. Throws
|
|
86
|
+
* {@link ContractSchemaMetaError} on the first push attempt that
|
|
87
|
+
* smuggles a malformed schema; collects every offender in one pass
|
|
88
|
+
* before throwing so the agent sees the full list.
|
|
89
|
+
*/
|
|
90
|
+
export function assertContractSchemasValid(contract) {
|
|
91
|
+
const violations = [];
|
|
92
|
+
if (contract.propsSpec?.properties) {
|
|
93
|
+
for (const [name, entry] of Object.entries(contract.propsSpec.properties)) {
|
|
94
|
+
tryCompile(`propsSpec.properties.${name}.schema`, entry.schema, violations);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (contract.actionSpec) {
|
|
98
|
+
for (const [name, entry] of Object.entries(contract.actionSpec)) {
|
|
99
|
+
if (entry.schema) {
|
|
100
|
+
tryCompile(`actionSpec.${name}.schema`, entry.schema, violations);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (contract.streamSpec) {
|
|
105
|
+
for (const [name, entry] of Object.entries(contract.streamSpec)) {
|
|
106
|
+
if (entry.schema) {
|
|
107
|
+
tryCompile(`streamSpec.${name}.schema`, entry.schema, violations);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (contract.contextSpec) {
|
|
112
|
+
for (const [name, entry] of Object.entries(contract.contextSpec)) {
|
|
113
|
+
if (entry.schema) {
|
|
114
|
+
tryCompile(`contextSpec.${name}.schema`, entry.schema, violations);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (contract.agentCapabilities?.tools) {
|
|
119
|
+
for (const [name, entry] of Object.entries(contract.agentCapabilities.tools)) {
|
|
120
|
+
if (entry.inputSchema) {
|
|
121
|
+
tryCompile(`agentCapabilities.tools.${name}.inputSchema`, entry.inputSchema, violations);
|
|
122
|
+
}
|
|
123
|
+
if (entry.outputSchema) {
|
|
124
|
+
tryCompile(`agentCapabilities.tools.${name}.outputSchema`, entry.outputSchema, violations);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (violations.length > 0) {
|
|
129
|
+
throw new ContractSchemaMetaError(violations);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-subset algorithm — answers "can every value the `subset`
|
|
3
|
+
* schema accepts also pass the `superset` schema?".
|
|
4
|
+
*
|
|
5
|
+
* **Why this lives in the protocol package.** The schema-alignment
|
|
6
|
+
* contract is enforced at push-time + blueprint-registration; the
|
|
7
|
+
* canonical failure is {@link ContractErrorCode}
|
|
8
|
+
* `'SCHEMA_MISMATCH_ERROR'` — same envelope shape + channel as every
|
|
9
|
+
* other named contract violation. See the schema-compat docstrings
|
|
10
|
+
* on {@link ActionEntry.schema} and {@link StreamChannelEntry.schema}
|
|
11
|
+
* for the author-invariant the check enforces.
|
|
12
|
+
*
|
|
13
|
+
* **Check points.**
|
|
14
|
+
*
|
|
15
|
+
* - Pre-commit of a `StackItem` with `actionSpec` / `streamSpec`
|
|
16
|
+
* entries that reference tools: each action's declared schema
|
|
17
|
+
* MUST be a subset of the tool's inputSchema (what the action
|
|
18
|
+
* payload is allowed to send ⊆ what the tool accepts). Each
|
|
19
|
+
* stream channel's declared schema MUST be a subset of the
|
|
20
|
+
* tool's return schema (what the channel emits ⊆ what the tool
|
|
21
|
+
* returns — inverted because the DIRECTION reverses).
|
|
22
|
+
* - Policy via {@link CreateGguiServerOptions.schemaCompatCheck}:
|
|
23
|
+
* `'reject'` (default) / `'warn'` / `'off'`.
|
|
24
|
+
*
|
|
25
|
+
* **Algorithm scope (P0).**
|
|
26
|
+
*
|
|
27
|
+
* - `type` match — primitive types must agree; only `undefined`
|
|
28
|
+
* on the superset side is a wildcard.
|
|
29
|
+
* - `required` — subset's required set MUST be a subset of
|
|
30
|
+
* superset's required set (tighter required on the subset side
|
|
31
|
+
* = strictly fewer values accepted ⇒ OK; tighter on the
|
|
32
|
+
* superset side would accept FEWER values than the subset ⇒
|
|
33
|
+
* violation).
|
|
34
|
+
* - `properties` — recursion: every subset property MUST be a
|
|
35
|
+
* subset of the matching superset property.
|
|
36
|
+
* - `additionalProperties` — semantics:
|
|
37
|
+
*
|
|
38
|
+
* - superset `true` (default when omitted) → subset is
|
|
39
|
+
* unconstrained on extra keys — OK.
|
|
40
|
+
* - superset `false` → subset MUST also be `false` (anything
|
|
41
|
+
* else widens).
|
|
42
|
+
* - superset JsonSchema → subset's additionalProperties MUST
|
|
43
|
+
* be a subset of the superset's (recurse), OR `false`
|
|
44
|
+
* (never emits extras, always fits).
|
|
45
|
+
* - `items` — arrays: subset's `items` MUST be a subset of
|
|
46
|
+
* superset's `items`. When either side omits `items`, the check
|
|
47
|
+
* is permissive in that direction.
|
|
48
|
+
*
|
|
49
|
+
* **P1 scope (deferred).**
|
|
50
|
+
*
|
|
51
|
+
* - `oneOf` / `anyOf` covering — subset union members must each
|
|
52
|
+
* be covered by at least one superset member.
|
|
53
|
+
* - `enum` — subset's enum values must all be in the superset's
|
|
54
|
+
* enum (or superset has no enum constraint).
|
|
55
|
+
* - `const` — subset's const must equal superset's const (or
|
|
56
|
+
* superset has no const constraint).
|
|
57
|
+
*
|
|
58
|
+
* **P2 scope (deferred — known limitations documented for
|
|
59
|
+
* third-party authors).**
|
|
60
|
+
*
|
|
61
|
+
* - `$ref` — no local or remote resolution; schemas with `$ref`
|
|
62
|
+
* are flagged as {@link SubsetViolationReason.unsupported}.
|
|
63
|
+
* - `allOf` — not merged before comparison.
|
|
64
|
+
* - String / number constraints — `minimum` / `maximum` /
|
|
65
|
+
* `minLength` / `maxLength` / `pattern` / `format` are NOT
|
|
66
|
+
* compared. A superset's narrower bound is not detected as a
|
|
67
|
+
* violation.
|
|
68
|
+
* - Tuple items (`items: JsonSchema[]`) — not in the current
|
|
69
|
+
* {@link JsonSchema} type, so not supported here.
|
|
70
|
+
*
|
|
71
|
+
* **Determinism contract.** No randomness, no IO, no thrown
|
|
72
|
+
* exceptions for normal violations. Every incompatibility is
|
|
73
|
+
* reported as a {@link SubsetViolation} with enough field-path
|
|
74
|
+
* context for the emitted `SCHEMA_MISMATCH_ERROR` to name the
|
|
75
|
+
* mismatch cleanly. Thrown errors are reserved for programmer-
|
|
76
|
+
* bug conditions (a caller passes `null` where a JsonSchema is
|
|
77
|
+
* expected).
|
|
78
|
+
*
|
|
79
|
+
* @see ./schema-compat-invariants.ts — the protocol-level invariants
|
|
80
|
+
* that call this algorithm at push-time.
|
|
81
|
+
*/
|
|
82
|
+
import type { JsonSchema, JsonValue } from '../types/data-contract.js';
|
|
83
|
+
/**
|
|
84
|
+
* Category of subset violation. Narrow enough that a downstream
|
|
85
|
+
* consumer can pattern-match on it if it wants to render a
|
|
86
|
+
* specialized message; wide enough to admit future P1/P2 reasons
|
|
87
|
+
* without a protocol-level bump.
|
|
88
|
+
*/
|
|
89
|
+
export type SubsetViolationReason = 'type-mismatch'
|
|
90
|
+
/** Subset declares a property the superset does not allow (via
|
|
91
|
+
* `properties` or `additionalProperties: false`). */
|
|
92
|
+
| 'extra-property'
|
|
93
|
+
/** Subset marks a property required that is not required on the
|
|
94
|
+
* superset — accepted, but only when the superset also allows the
|
|
95
|
+
* property at all. The combined check produces this reason only
|
|
96
|
+
* when the superset REJECTS the property entirely (missing from
|
|
97
|
+
* properties AND additionalProperties: false). */
|
|
98
|
+
| 'required-widens'
|
|
99
|
+
/** Superset marks a property required that the subset does not
|
|
100
|
+
* require. The subset may omit a value the superset would reject. */
|
|
101
|
+
| 'missing-required'
|
|
102
|
+
/** Array items schema mismatch. */
|
|
103
|
+
| 'items-mismatch'
|
|
104
|
+
/** `additionalProperties: false` on superset, non-false on subset. */
|
|
105
|
+
| 'additional-properties-widens'
|
|
106
|
+
/** Schema uses a construct this P0 implementation does not support
|
|
107
|
+
* (e.g. `$ref`, `allOf`, `oneOf`/`anyOf`, `enum`, `const`). The
|
|
108
|
+
* pair is flagged instead of silently passing. */
|
|
109
|
+
| 'unsupported';
|
|
110
|
+
/**
|
|
111
|
+
* A single point of incompatibility between `superset` and `subset`.
|
|
112
|
+
* Carries enough context for the caller to render a message that
|
|
113
|
+
* names the field path + both sides.
|
|
114
|
+
*/
|
|
115
|
+
export interface SubsetViolation {
|
|
116
|
+
/**
|
|
117
|
+
* Dotted field path from the root of the compared schemas.
|
|
118
|
+
* `''` (empty) means the root schemas themselves mismatched.
|
|
119
|
+
* `'properties.foo.items'` means the `items` of the `foo` property
|
|
120
|
+
* mismatched. Uses `.items` for array element descent and `.<key>`
|
|
121
|
+
* for object property descent. No escaping — property names
|
|
122
|
+
* containing `.` will produce ambiguous paths but are valid JSON.
|
|
123
|
+
*/
|
|
124
|
+
readonly path: string;
|
|
125
|
+
/** Category of violation. */
|
|
126
|
+
readonly reason: SubsetViolationReason;
|
|
127
|
+
/** The superset side's value at `path`, as a short JSON string
|
|
128
|
+
* (stringified, truncated at 120 chars). `undefined` when the
|
|
129
|
+
* superset has no explicit value at the path. */
|
|
130
|
+
readonly superset?: string;
|
|
131
|
+
/** The subset side's value at `path`, same formatting rules as
|
|
132
|
+
* {@link SubsetViolation.superset}. */
|
|
133
|
+
readonly subset?: string;
|
|
134
|
+
/** Human-readable summary suitable for inclusion in a
|
|
135
|
+
* `SCHEMA_MISMATCH_ERROR` envelope. Producers MAY ignore this
|
|
136
|
+
* and render their own message from `path` + `reason` if they
|
|
137
|
+
* prefer a consistent localized format. */
|
|
138
|
+
readonly message: string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Result of {@link isSchemaSubset}. Wraps `compatible` with the
|
|
142
|
+
* violation list so callers that only need the boolean can check
|
|
143
|
+
* `result.compatible`, and callers that emit envelopes can project
|
|
144
|
+
* the violations into the error details.
|
|
145
|
+
*/
|
|
146
|
+
export interface SchemaSubsetResult {
|
|
147
|
+
readonly compatible: boolean;
|
|
148
|
+
readonly violations: readonly SubsetViolation[];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Compare two JSON Schemas under the "subset acceptance" relation:
|
|
152
|
+
* returns `compatible: true` iff every JSON value that `subset`
|
|
153
|
+
* accepts would also be accepted by `superset` (under the P0 scope
|
|
154
|
+
* documented at the top of this file).
|
|
155
|
+
*
|
|
156
|
+
* Neither argument is mutated. Order matters: `isSchemaSubset(a, b)`
|
|
157
|
+
* checks "is b a subset of a", NOT "is a a subset of b".
|
|
158
|
+
*
|
|
159
|
+
* `null` / non-object inputs throw — they are programmer errors,
|
|
160
|
+
* not schema violations. Every legitimate incompatibility is
|
|
161
|
+
* reported via the returned {@link SubsetViolation} list.
|
|
162
|
+
*/
|
|
163
|
+
export declare function isSchemaSubset(superset: JsonSchema, subset: JsonSchema): SchemaSubsetResult;
|
|
164
|
+
export type { JsonValue };
|
|
165
|
+
//# sourceMappingURL=schema-subset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-subset.d.ts","sourceRoot":"","sources":["../../src/validation/schema-subset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAEvE;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAC7B,eAAe;AACjB;sDACsD;GACpD,gBAAgB;AAClB;;;;mDAImD;GACjD,iBAAiB;AACnB;sEACsE;GACpE,kBAAkB;AACpB,mCAAmC;GACjC,gBAAgB;AAClB,sEAAsE;GACpE,8BAA8B;AAChC;;mDAEmD;GACjD,aAAa,CAAC;AAElB;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6BAA6B;IAC7B,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC;IACvC;;sDAEkD;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;4CACwC;IACxC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;gDAG4C;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,SAAS,eAAe,EAAE,CAAC;CACjD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,UAAU,EACpB,MAAM,EAAE,UAAU,GACjB,kBAAkB,CAiBpB;AAoTD,YAAY,EAAE,SAAS,EAAE,CAAC"}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare two JSON Schemas under the "subset acceptance" relation:
|
|
3
|
+
* returns `compatible: true` iff every JSON value that `subset`
|
|
4
|
+
* accepts would also be accepted by `superset` (under the P0 scope
|
|
5
|
+
* documented at the top of this file).
|
|
6
|
+
*
|
|
7
|
+
* Neither argument is mutated. Order matters: `isSchemaSubset(a, b)`
|
|
8
|
+
* checks "is b a subset of a", NOT "is a a subset of b".
|
|
9
|
+
*
|
|
10
|
+
* `null` / non-object inputs throw — they are programmer errors,
|
|
11
|
+
* not schema violations. Every legitimate incompatibility is
|
|
12
|
+
* reported via the returned {@link SubsetViolation} list.
|
|
13
|
+
*/
|
|
14
|
+
export function isSchemaSubset(superset, subset) {
|
|
15
|
+
if (superset === null || typeof superset !== 'object') {
|
|
16
|
+
throw new TypeError(`isSchemaSubset: superset must be a JsonSchema object (received ${typeof superset})`);
|
|
17
|
+
}
|
|
18
|
+
if (subset === null || typeof subset !== 'object') {
|
|
19
|
+
throw new TypeError(`isSchemaSubset: subset must be a JsonSchema object (received ${typeof subset})`);
|
|
20
|
+
}
|
|
21
|
+
const violations = [];
|
|
22
|
+
compare(superset, subset, '', violations);
|
|
23
|
+
return {
|
|
24
|
+
compatible: violations.length === 0,
|
|
25
|
+
violations,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// ── Internal ──────────────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Structural deep-equality for two JSON Schemas. A schema is always a
|
|
31
|
+
* subset of itself, so an equal pair short-circuits {@link compare} —
|
|
32
|
+
* this is what lets a pair that uses an otherwise-unsupported
|
|
33
|
+
* construct (`enum`, `oneOf`, `const`, …) pass when the two sides are
|
|
34
|
+
* identical (e.g. a source-fed `streamSpec` channel whose schema is
|
|
35
|
+
* the backing tool's `outputSchema` verbatim).
|
|
36
|
+
*/
|
|
37
|
+
function schemasDeepEqual(a, b) {
|
|
38
|
+
if (a === b)
|
|
39
|
+
return true;
|
|
40
|
+
if (a === null || b === null)
|
|
41
|
+
return false;
|
|
42
|
+
if (typeof a !== 'object' || typeof b !== 'object')
|
|
43
|
+
return false;
|
|
44
|
+
const aArr = Array.isArray(a);
|
|
45
|
+
if (aArr !== Array.isArray(b))
|
|
46
|
+
return false;
|
|
47
|
+
if (aArr) {
|
|
48
|
+
const ab = a;
|
|
49
|
+
const bb = b;
|
|
50
|
+
if (ab.length !== bb.length)
|
|
51
|
+
return false;
|
|
52
|
+
return ab.every((v, i) => schemasDeepEqual(v, bb[i]));
|
|
53
|
+
}
|
|
54
|
+
const ao = a;
|
|
55
|
+
const bo = b;
|
|
56
|
+
const aKeys = Object.keys(ao);
|
|
57
|
+
if (aKeys.length !== Object.keys(bo).length)
|
|
58
|
+
return false;
|
|
59
|
+
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) &&
|
|
60
|
+
schemasDeepEqual(ao[k], bo[k]));
|
|
61
|
+
}
|
|
62
|
+
function compare(superset, subset, path, out) {
|
|
63
|
+
// Identical schemas: a schema is trivially a subset of itself, so
|
|
64
|
+
// skip the structural walk. This is the ONLY path by which a pair
|
|
65
|
+
// using a P1/P2-unsupported construct (`enum`, `oneOf`, `const`, …)
|
|
66
|
+
// can be proved compatible — and it is sound, because equal schemas
|
|
67
|
+
// accept exactly the same value set.
|
|
68
|
+
if (schemasDeepEqual(superset, subset))
|
|
69
|
+
return;
|
|
70
|
+
// P2 unsupported constructs — flag instead of silently passing.
|
|
71
|
+
// Presence on EITHER side is flagged because a recursive check on
|
|
72
|
+
// an unresolved `$ref` / un-merged `allOf` would produce false
|
|
73
|
+
// negatives. Subset-only presence is also flagged so a caller that
|
|
74
|
+
// authors a narrower-by-`$ref` schema knows the subset check can't
|
|
75
|
+
// prove it.
|
|
76
|
+
if (hasUnsupported(superset) || hasUnsupported(subset)) {
|
|
77
|
+
out.push({
|
|
78
|
+
path,
|
|
79
|
+
reason: 'unsupported',
|
|
80
|
+
superset: safeStringify(supersetUnsupportedField(superset)),
|
|
81
|
+
subset: safeStringify(supersetUnsupportedField(subset)),
|
|
82
|
+
message: `${pathLabel(path)}: schema uses an unsupported construct ` +
|
|
83
|
+
`($ref / allOf / oneOf / anyOf / enum / const) — the subset ` +
|
|
84
|
+
`algorithm cannot prove compatibility for these constructs.`,
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Type match. Superset `undefined` is a wildcard (accepts any
|
|
89
|
+
// type). Subset `undefined` against a specific superset type is a
|
|
90
|
+
// violation — "no declared type" is wider than any specific type.
|
|
91
|
+
const sup = normalizeType(superset);
|
|
92
|
+
const sub = normalizeType(subset);
|
|
93
|
+
if (sup !== undefined) {
|
|
94
|
+
if (sub === undefined || sub !== sup) {
|
|
95
|
+
out.push({
|
|
96
|
+
path,
|
|
97
|
+
reason: 'type-mismatch',
|
|
98
|
+
superset: sup,
|
|
99
|
+
subset: sub ?? '(unspecified)',
|
|
100
|
+
message: `${pathLabel(path)}: type mismatch — superset accepts ` +
|
|
101
|
+
`'${sup}' but subset ${sub === undefined ? 'does not declare a type' : `declares '${sub}'`}.`,
|
|
102
|
+
});
|
|
103
|
+
// Type mismatch invalidates downstream object/array structural
|
|
104
|
+
// checks — if the types don't match, deeper comparison is noise.
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Object structure.
|
|
109
|
+
if (sup === 'object' || sub === 'object') {
|
|
110
|
+
compareObject(superset, subset, path, out);
|
|
111
|
+
}
|
|
112
|
+
// Array items. We descend through items only when both sides have
|
|
113
|
+
// `type: 'array'` (or superset omitted type and subset declares
|
|
114
|
+
// array — but that case is caught by the type block above as a
|
|
115
|
+
// mismatch). Omission on either side is permissive.
|
|
116
|
+
if (sup === 'array' || sub === 'array') {
|
|
117
|
+
compareArray(superset, subset, path, out);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function compareObject(superset, subset, path, out) {
|
|
121
|
+
const supProps = superset.properties ?? {};
|
|
122
|
+
const subProps = subset.properties ?? {};
|
|
123
|
+
const supRequired = new Set(superset.required ?? []);
|
|
124
|
+
const subRequired = new Set(subset.required ?? []);
|
|
125
|
+
const supAdditional = resolveAdditional(superset.additionalProperties);
|
|
126
|
+
const subAdditional = resolveAdditional(subset.additionalProperties);
|
|
127
|
+
// Every subset property must either be in supProps (recurse) OR
|
|
128
|
+
// be allowed by supAdditional.
|
|
129
|
+
for (const key of Object.keys(subProps)) {
|
|
130
|
+
const subChild = subProps[key];
|
|
131
|
+
if (!subChild)
|
|
132
|
+
continue; // paranoia — Object.keys guarantees it exists
|
|
133
|
+
const supChild = supProps[key];
|
|
134
|
+
const childPath = path === '' ? `properties.${key}` : `${path}.properties.${key}`;
|
|
135
|
+
if (supChild) {
|
|
136
|
+
compare(supChild, subChild, childPath, out);
|
|
137
|
+
}
|
|
138
|
+
else if (supAdditional.kind === 'allow') {
|
|
139
|
+
// Allowed by `additionalProperties: true` on superset —
|
|
140
|
+
// structurally unconstrained, so subset's shape is fine.
|
|
141
|
+
}
|
|
142
|
+
else if (supAdditional.kind === 'schema') {
|
|
143
|
+
compare(supAdditional.schema, subChild, childPath, out);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// superset: additionalProperties false AND key not in
|
|
147
|
+
// properties. Subset would allow a value the superset rejects.
|
|
148
|
+
out.push({
|
|
149
|
+
path: childPath,
|
|
150
|
+
reason: 'extra-property',
|
|
151
|
+
superset: '(not allowed)',
|
|
152
|
+
subset: safeStringify(subChild),
|
|
153
|
+
message: `${pathLabel(childPath)}: subset allows property '${key}' ` +
|
|
154
|
+
`that superset rejects (not in superset.properties and ` +
|
|
155
|
+
`superset.additionalProperties is false).`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Additional-properties compatibility. A subset that allows
|
|
160
|
+
// additional properties when the superset rejects them is wider
|
|
161
|
+
// in the "extra keys" dimension.
|
|
162
|
+
if (supAdditional.kind === 'reject') {
|
|
163
|
+
if (subAdditional.kind === 'allow') {
|
|
164
|
+
out.push({
|
|
165
|
+
path,
|
|
166
|
+
reason: 'additional-properties-widens',
|
|
167
|
+
superset: 'false',
|
|
168
|
+
subset: 'true',
|
|
169
|
+
message: `${pathLabel(path)}: subset.additionalProperties is true but ` +
|
|
170
|
+
`superset.additionalProperties is false — subset accepts ` +
|
|
171
|
+
`extra keys the superset rejects.`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else if (subAdditional.kind === 'schema') {
|
|
175
|
+
out.push({
|
|
176
|
+
path,
|
|
177
|
+
reason: 'additional-properties-widens',
|
|
178
|
+
superset: 'false',
|
|
179
|
+
subset: safeStringify(subset.additionalProperties),
|
|
180
|
+
message: `${pathLabel(path)}: subset declares an additionalProperties ` +
|
|
181
|
+
`schema, but superset.additionalProperties is false.`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// subAdditional === 'reject' → equal; no violation.
|
|
185
|
+
}
|
|
186
|
+
else if (supAdditional.kind === 'schema') {
|
|
187
|
+
if (subAdditional.kind === 'allow') {
|
|
188
|
+
out.push({
|
|
189
|
+
path,
|
|
190
|
+
reason: 'additional-properties-widens',
|
|
191
|
+
superset: safeStringify(superset.additionalProperties),
|
|
192
|
+
subset: 'true',
|
|
193
|
+
message: `${pathLabel(path)}: subset.additionalProperties is true ` +
|
|
194
|
+
`(unconstrained) but superset constrains additional ` +
|
|
195
|
+
`properties to a schema.`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
else if (subAdditional.kind === 'schema') {
|
|
199
|
+
const childPath = path === '' ? 'additionalProperties' : `${path}.additionalProperties`;
|
|
200
|
+
compare(supAdditional.schema, subAdditional.schema, childPath, out);
|
|
201
|
+
}
|
|
202
|
+
// subAdditional === 'reject' → subset never emits extras, fits inside.
|
|
203
|
+
}
|
|
204
|
+
// supAdditional.kind === 'allow' → subset's extras are all legal.
|
|
205
|
+
// Required-set checks. A required-on-subset-only property is fine
|
|
206
|
+
// (subset is stricter). A required-on-superset-only property
|
|
207
|
+
// means the subset can produce values missing that key, which the
|
|
208
|
+
// superset would reject.
|
|
209
|
+
for (const key of supRequired) {
|
|
210
|
+
if (!subRequired.has(key)) {
|
|
211
|
+
// Only a violation if the property is actually reachable on
|
|
212
|
+
// the subset — if the subset simply doesn't mention the key
|
|
213
|
+
// at all (and its additionalProperties rejects), the subset
|
|
214
|
+
// can't even emit a value with that key, so a missing-required
|
|
215
|
+
// on the superset is something the subset-produced value will
|
|
216
|
+
// flunk.
|
|
217
|
+
out.push({
|
|
218
|
+
path: path === '' ? `required.${key}` : `${path}.required.${key}`,
|
|
219
|
+
reason: 'missing-required',
|
|
220
|
+
superset: '(required)',
|
|
221
|
+
subset: subRequired.has(key) ? '(required)' : '(optional or absent)',
|
|
222
|
+
message: `${pathLabel(path)}: superset requires '${key}' but subset ` +
|
|
223
|
+
`does not — subset can emit values missing '${key}' that ` +
|
|
224
|
+
`the superset rejects.`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function compareArray(superset, subset, path, out) {
|
|
230
|
+
const supItems = superset.items;
|
|
231
|
+
const subItems = subset.items;
|
|
232
|
+
// Permissive when either side omits items.
|
|
233
|
+
if (!supItems || !subItems)
|
|
234
|
+
return;
|
|
235
|
+
const childPath = path === '' ? 'items' : `${path}.items`;
|
|
236
|
+
compare(supItems, subItems, childPath, out);
|
|
237
|
+
}
|
|
238
|
+
function normalizeType(schema) {
|
|
239
|
+
return schema.type;
|
|
240
|
+
}
|
|
241
|
+
function resolveAdditional(value) {
|
|
242
|
+
// JSON Schema draft-07: omitted ⇒ additionalProperties `true`.
|
|
243
|
+
if (value === undefined || value === true)
|
|
244
|
+
return { kind: 'allow' };
|
|
245
|
+
if (value === false)
|
|
246
|
+
return { kind: 'reject' };
|
|
247
|
+
return { kind: 'schema', schema: value };
|
|
248
|
+
}
|
|
249
|
+
function hasUnsupported(schema) {
|
|
250
|
+
return (supersetUnsupportedField(schema) !== undefined);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Returns the first unsupported P2-scope field present on the
|
|
254
|
+
* schema, or `undefined` when none. Order is stable so violation
|
|
255
|
+
* messages are deterministic.
|
|
256
|
+
*/
|
|
257
|
+
function supersetUnsupportedField(schema) {
|
|
258
|
+
// JsonSchema type does not declare `$ref` / `allOf`, but raw JSON
|
|
259
|
+
// can carry them — a zod-converted schema or a hand-authored
|
|
260
|
+
// fixture will. We check via property lookup without widening the
|
|
261
|
+
// type.
|
|
262
|
+
const bag = schema;
|
|
263
|
+
if (typeof bag['$ref'] === 'string')
|
|
264
|
+
return '$ref';
|
|
265
|
+
if (Array.isArray(bag['allOf']))
|
|
266
|
+
return 'allOf';
|
|
267
|
+
if (Array.isArray(schema.oneOf))
|
|
268
|
+
return 'oneOf';
|
|
269
|
+
if (Array.isArray(schema.anyOf))
|
|
270
|
+
return 'anyOf';
|
|
271
|
+
if (Array.isArray(schema.enum))
|
|
272
|
+
return 'enum';
|
|
273
|
+
if (schema.const !== undefined)
|
|
274
|
+
return 'const';
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
function safeStringify(value) {
|
|
278
|
+
if (value === undefined)
|
|
279
|
+
return '(undefined)';
|
|
280
|
+
let json;
|
|
281
|
+
try {
|
|
282
|
+
json = JSON.stringify(value);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
json = String(value);
|
|
286
|
+
}
|
|
287
|
+
if (json === undefined)
|
|
288
|
+
return '(undefined)';
|
|
289
|
+
if (json.length > 120)
|
|
290
|
+
return json.slice(0, 117) + '...';
|
|
291
|
+
return json;
|
|
292
|
+
}
|
|
293
|
+
function pathLabel(path) {
|
|
294
|
+
return path === '' ? '(root)' : path;
|
|
295
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classification of a UI component by its portability.
|
|
3
|
+
*
|
|
4
|
+
* - `sandboxed` — pure React + `@ggui-ai/design` primitives. Portable,
|
|
5
|
+
* publishable, runs in any ggui rendering context.
|
|
6
|
+
* - `fullstack` — uses adapters (`@ggui-ai/react` hooks, server
|
|
7
|
+
* connectors). Requires a client bundle, app-scoped.
|
|
8
|
+
*
|
|
9
|
+
* Colocated with the classifier (`classifyUi`) because that's the
|
|
10
|
+
* only runtime producer of this value — the `UiManifest` schema in
|
|
11
|
+
* `@ggui-ai/project-config` imports this vocabulary and validates
|
|
12
|
+
* against it.
|
|
13
|
+
*/
|
|
14
|
+
export type UiClass = 'sandboxed' | 'fullstack';
|
|
15
|
+
export interface DangerousPattern {
|
|
16
|
+
/** Regex to match against source/compiled code. */
|
|
17
|
+
pattern: RegExp;
|
|
18
|
+
/** Human-readable name of the pattern. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Why this is blocked and what to do instead. */
|
|
21
|
+
suggestion: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Patterns that are NEVER allowed in sandboxed UI components.
|
|
25
|
+
* These are security-critical — changes here affect every validation consumer.
|
|
26
|
+
*/
|
|
27
|
+
export declare const DANGEROUS_PATTERNS: DangerousPattern[];
|
|
28
|
+
/** Import prefixes that indicate a fullstack UI (requires client bundle). */
|
|
29
|
+
export declare const FULLSTACK_IMPORT_PREFIXES: readonly ["@ggui-ai/wire", "@ggui-ai/react", "@app/components"];
|
|
30
|
+
/**
|
|
31
|
+
* Classify a component as sandboxed or fullstack based on its imports.
|
|
32
|
+
*
|
|
33
|
+
* - **sandboxed**: Pure React + @ggui-ai/design primitives. Portable, publishable.
|
|
34
|
+
* - **fullstack**: Uses @ggui-ai/wire, @ggui-ai/react, or @app/components. Private.
|
|
35
|
+
*
|
|
36
|
+
* Works on both source (.tsx) and compiled (.js) code.
|
|
37
|
+
*/
|
|
38
|
+
export declare function classifyUi(code: string): UiClass;
|
|
39
|
+
export interface SecurityCheckResult {
|
|
40
|
+
/** True if no dangerous patterns found. */
|
|
41
|
+
safe: boolean;
|
|
42
|
+
/** List of violations found. */
|
|
43
|
+
violations: Array<{
|
|
44
|
+
name: string;
|
|
45
|
+
suggestion: string;
|
|
46
|
+
line?: number;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check code for dangerous patterns.
|
|
51
|
+
* Works on both source and compiled code.
|
|
52
|
+
*/
|
|
53
|
+
export declare function checkSecurity(code: string): SecurityCheckResult;
|
|
54
|
+
//# sourceMappingURL=ui-security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui-security.d.ts","sourceRoot":"","sources":["../../src/validation/ui-security.ts"],"names":[],"mappings":"AAcA;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,WAAW,CAAC;AAMhD,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,gBAAgB,EAiFhD,CAAC;AAIF,6EAA6E;AAC7E,eAAO,MAAM,yBAAyB,iEAI5B,CAAC;AAEX;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUhD;AAUD,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,IAAI,EAAE,OAAO,CAAC;IACd,gCAAgC;IAChC,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;CACJ;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAc/D"}
|