@jigs.design/protocol 0.0.0-stage → 0.2.0
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 +21 -0
- package/README.md +28 -2
- package/dist/diagnostics.d.ts +61 -0
- package/dist/diagnostics.js +29 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/parse.d.ts +9 -0
- package/dist/parse.js +151 -0
- package/dist/protocol.d.ts +132 -0
- package/dist/protocol.js +4 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.js +22 -0
- package/dist/urls.d.ts +14 -0
- package/dist/urls.js +59 -0
- package/package.json +24 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tom Bates
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @jigs.design/protocol
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
What the server, the browser and the sandbox agree on: the program format, the session and chat types, the diagnostics, and the messages between the page, the frame and the worker. It has no dependencies and runs nothing, so every other package can import it: the compiler for its diagnostic types, the host for the prop schemas, the browser packages for the wire.
|
|
4
|
+
|
|
5
|
+
`@jigs.design/server`, `@jigs.design/client` and `@jigs.design/react` re-export the shared types, so most code imports them from one of those.
|
|
6
|
+
|
|
7
|
+
## Exports
|
|
8
|
+
|
|
9
|
+
| Export | What it is |
|
|
10
|
+
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
11
|
+
| `Program`, `PROGRAM_FORMAT`, `ComponentSchema`, `PropSchema`, `ProcedureEntry` | A compiled program and what it carries |
|
|
12
|
+
| `SessionEvent`, `SessionBody`, `Message`, `ChatBlock`, `JsonValue` | The session stream, the request and chat blocks |
|
|
13
|
+
| `Diagnostic`, `Severity`, `severities`, `Span`, `DiagnosticFix` | The one diagnostic shape the compiler reports, the server sends and the eval results store |
|
|
14
|
+
| `JigsErrorCode`, `jigsErrorCodes`, `JigsErrorData`, `Invalidation` | Failures and the queries a mutation refreshed |
|
|
15
|
+
| `HostMessage`, `FrameMessage`, `WorkerMessage`, `Op`, `ROOT_ID` and the rest | The frame protocol |
|
|
16
|
+
| `parseProgram`, `parseSessionEvent`, `parseErrorData`, `parseInvalidations` | The checks the browser runs on JSON from the server, since it imports no schema library |
|
|
17
|
+
| `isAllowedImage`, `isAllowedLink`, `isImageHostEntry`, `normalizeImageHost` | The image and link allowlists |
|
|
18
|
+
|
|
19
|
+
## Source
|
|
20
|
+
|
|
21
|
+
- `src/types.ts`: the shared types and `PROGRAM_FORMAT`.
|
|
22
|
+
- `src/diagnostics.ts`: `Diagnostic` and the severities, with what each means.
|
|
23
|
+
- `src/protocol.ts`: messages between the page, the frame and the worker.
|
|
24
|
+
- `src/parse.ts`: checks for JSON arriving in the browser.
|
|
25
|
+
- `src/urls.ts`: allowed images and links.
|
|
26
|
+
|
|
27
|
+
## Docs
|
|
28
|
+
|
|
29
|
+
[Diagnostics](../../apps/docs/docs/spec/diagnostics.mdx), [Frame protocol](../../apps/docs/docs/renderers/protocol.mdx) and [Packages](../../apps/docs/docs/packages/index.mdx).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/diagnostics.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The one diagnostic shape: the compiler reports it, the server adds its own
|
|
4
|
+
* checks in it and sends it, the browser parses it, and the eval results
|
|
5
|
+
* store it.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* How much a diagnostic matters, from most to least.
|
|
9
|
+
*
|
|
10
|
+
* - `error`: what the model wrote is wrong there. The program still runs,
|
|
11
|
+
* with that part dropped or replaced, but the turn is not valid: the server
|
|
12
|
+
* asks the model for a repair, and a strict compile (a stored spec, a
|
|
13
|
+
* situation's example, `jigs.compile`) fails.
|
|
14
|
+
* - `warning`: the source itself may be fine, but it was written for a
|
|
15
|
+
* language or catalog newer than this server's, so it may not mean what it
|
|
16
|
+
* says. No repair, since the model cannot fix it; a strict compile fails.
|
|
17
|
+
* - `info`: nothing to do. The compiler made a change itself (a fix it
|
|
18
|
+
* applied, a tag it closed), or a stored spec names an older catalog.
|
|
19
|
+
*/
|
|
20
|
+
export declare const severities: readonly ["error", "warning", "info"];
|
|
21
|
+
export type Severity = (typeof severities)[number];
|
|
22
|
+
/**
|
|
23
|
+
* Where a diagnostic points. Every span has a line; one about something the
|
|
24
|
+
* compiler can place in the source also has its range, as offsets into the
|
|
25
|
+
* source the line counts (JavaScript string indices), and the column of its
|
|
26
|
+
* start.
|
|
27
|
+
*/
|
|
28
|
+
export interface Span {
|
|
29
|
+
/** 1-based, on `line`. */
|
|
30
|
+
column?: number;
|
|
31
|
+
/** Just past the range. */
|
|
32
|
+
end?: number;
|
|
33
|
+
line: number;
|
|
34
|
+
/** The range's first character. */
|
|
35
|
+
start?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A rewrite that makes a diagnostic's problem go away: replace each edit's
|
|
39
|
+
* span with its text. The compiler attaches one only when there is a single
|
|
40
|
+
* way to write it, and applies it itself, so its diagnostic is `info`.
|
|
41
|
+
*/
|
|
42
|
+
export interface DiagnosticFix {
|
|
43
|
+
edits: Array<{
|
|
44
|
+
span: Span;
|
|
45
|
+
text: string;
|
|
46
|
+
}>;
|
|
47
|
+
/** What the fix does, such as "Replace with `q.status === "pending"`". */
|
|
48
|
+
title: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* One problem or note about a source. `Code` narrows `code` where one
|
|
52
|
+
* producer's codes are known, as the compiler's are.
|
|
53
|
+
*/
|
|
54
|
+
export interface Diagnostic<Code extends string = string> {
|
|
55
|
+
code: Code;
|
|
56
|
+
fix?: DiagnosticFix;
|
|
57
|
+
message: string;
|
|
58
|
+
severity: Severity;
|
|
59
|
+
span: Span;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/diagnostics.ts
|
|
2
|
+
/**
|
|
3
|
+
* The one diagnostic shape: the compiler reports it, the server adds its own
|
|
4
|
+
* checks in it and sends it, the browser parses it, and the eval results
|
|
5
|
+
* store it.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* How much a diagnostic matters, from most to least.
|
|
9
|
+
*
|
|
10
|
+
* - `error`: what the model wrote is wrong there. The program still runs,
|
|
11
|
+
* with that part dropped or replaced, but the turn is not valid: the server
|
|
12
|
+
* asks the model for a repair, and a strict compile (a stored spec, a
|
|
13
|
+
* situation's example, `jigs.compile`) fails.
|
|
14
|
+
* - `warning`: the source itself may be fine, but it was written for a
|
|
15
|
+
* language or catalog newer than this server's, so it may not mean what it
|
|
16
|
+
* says. No repair, since the model cannot fix it; a strict compile fails.
|
|
17
|
+
* - `info`: nothing to do. The compiler made a change itself (a fix it
|
|
18
|
+
* applied, a tag it closed), or a stored spec names an older catalog.
|
|
19
|
+
*/
|
|
20
|
+
const severities = [
|
|
21
|
+
"error",
|
|
22
|
+
"warning",
|
|
23
|
+
"info"
|
|
24
|
+
];
|
|
25
|
+
function isSeverity(value) {
|
|
26
|
+
return severities.some((severity) => severity === value);
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { isSeverity, severities };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Diagnostic, DiagnosticFix, Severity, Span, severities } from "./diagnostics.js";
|
|
2
|
+
import { ChatBlock, ChatBlockStatus, ChildrenKind, ComponentSchema, Invalidation, JigsErrorCode, JigsErrorData, JsonValue, Message, NumberPropSchema, PROGRAM_FORMAT, ProcedureEntry, ProcedureKind, Program, PropSchema, SessionBody, SessionEvent, SessionOptions, jigsErrorCodes } from "./types.js";
|
|
3
|
+
import { FrameMessage, HostMessage, MutationCall, MutationResult, Op, QueryRead, QueryResult, ROOT_ID, Row, RuntimeError, WireArg, WireProps, WireValue, WorkerMessage } from "./protocol.js";
|
|
4
|
+
import { parseErrorData, parseInvalidations, parseProgram, parseSessionEvent } from "./parse.js";
|
|
5
|
+
import { isAllowedImage, isAllowedLink, isImageHostEntry, normalizeImageHost } from "./urls.js";
|
|
6
|
+
export { type ChatBlock, type ChatBlockStatus, type ChildrenKind, type ComponentSchema, type Diagnostic, type DiagnosticFix, type FrameMessage, type HostMessage, type Invalidation, type JigsErrorCode, type JigsErrorData, type JsonValue, type Message, type MutationCall, type MutationResult, type NumberPropSchema, type Op, PROGRAM_FORMAT, type ProcedureEntry, type ProcedureKind, type Program, type PropSchema, type QueryRead, type QueryResult, ROOT_ID, type Row, type RuntimeError, type SessionBody, type SessionEvent, type SessionOptions, type Severity, type Span, type WireArg, type WireProps, type WireValue, type WorkerMessage, isAllowedImage, isAllowedLink, isImageHostEntry, jigsErrorCodes, normalizeImageHost, parseErrorData, parseInvalidations, parseProgram, parseSessionEvent, severities };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PROGRAM_FORMAT, jigsErrorCodes } from "./types.js";
|
|
2
|
+
import { severities } from "./diagnostics.js";
|
|
3
|
+
import { ROOT_ID } from "./protocol.js";
|
|
4
|
+
import { parseErrorData, parseInvalidations, parseProgram, parseSessionEvent } from "./parse.js";
|
|
5
|
+
import { isAllowedImage, isAllowedLink, isImageHostEntry, normalizeImageHost } from "./urls.js";
|
|
6
|
+
export { PROGRAM_FORMAT, ROOT_ID, isAllowedImage, isAllowedLink, isImageHostEntry, jigsErrorCodes, normalizeImageHost, parseErrorData, parseInvalidations, parseProgram, parseSessionEvent, severities };
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Invalidation, JigsErrorData, Program, SessionEvent } from "./types.js";
|
|
2
|
+
//#region src/parse.d.ts
|
|
3
|
+
export declare function parseErrorData(value: unknown): JigsErrorData | null;
|
|
4
|
+
/** The `invalidate` list of a mutation's reply. */
|
|
5
|
+
export declare function parseInvalidations(value: unknown): ReadonlyArray<Invalidation> | null;
|
|
6
|
+
export declare function parseProgram(value: unknown): Program | null;
|
|
7
|
+
/** One session event from its JSON text, or null when it is not one. */
|
|
8
|
+
export declare function parseSessionEvent(text: string): SessionEvent | null;
|
|
9
|
+
//#endregion
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { jigsErrorCodes } from "./types.js";
|
|
2
|
+
import { isSeverity } from "./diagnostics.js";
|
|
3
|
+
//#region src/parse.ts
|
|
4
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
const isString = (value) => typeof value === "string";
|
|
6
|
+
const isStringList = (value) => Array.isArray(value) && value.every(isString);
|
|
7
|
+
function recordOf(value, item) {
|
|
8
|
+
if (!isRecord(value)) return null;
|
|
9
|
+
const out = {};
|
|
10
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
11
|
+
const parsed = item(entry);
|
|
12
|
+
if (parsed === null) return null;
|
|
13
|
+
out[key] = parsed;
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
const isErrorCode = (value) => jigsErrorCodes.some((code) => code === value);
|
|
18
|
+
function parseErrorData(value) {
|
|
19
|
+
if (!isRecord(value) || !isErrorCode(value.code) || !isString(value.message)) return null;
|
|
20
|
+
return {
|
|
21
|
+
code: value.code,
|
|
22
|
+
message: value.message
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function parseInvalidation(value) {
|
|
26
|
+
if (!isRecord(value) || !isString(value.name)) return null;
|
|
27
|
+
if (!("input" in value)) return { name: value.name };
|
|
28
|
+
return {
|
|
29
|
+
input: value.input,
|
|
30
|
+
name: value.name
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** The `invalidate` list of a mutation's reply. */
|
|
34
|
+
function parseInvalidations(value) {
|
|
35
|
+
if (!Array.isArray(value)) return null;
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const entry of value) {
|
|
38
|
+
const parsed = parseInvalidation(entry);
|
|
39
|
+
if (parsed === null) return null;
|
|
40
|
+
out.push(parsed);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function parsePropSchema(value) {
|
|
45
|
+
if (!isRecord(value)) return null;
|
|
46
|
+
switch (value.kind) {
|
|
47
|
+
case "boolean":
|
|
48
|
+
case "handler":
|
|
49
|
+
case "image":
|
|
50
|
+
case "link":
|
|
51
|
+
case "list":
|
|
52
|
+
case "string": return { kind: value.kind };
|
|
53
|
+
case "enum": return isStringList(value.values) ? {
|
|
54
|
+
kind: "enum",
|
|
55
|
+
values: value.values
|
|
56
|
+
} : null;
|
|
57
|
+
case "number": {
|
|
58
|
+
const schema = { kind: "number" };
|
|
59
|
+
if (typeof value.min === "number") schema.min = value.min;
|
|
60
|
+
if (typeof value.max === "number") schema.max = value.max;
|
|
61
|
+
return schema;
|
|
62
|
+
}
|
|
63
|
+
default: return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const isChildrenKind = (value) => value === "block" || value === "inline" || value === "none";
|
|
67
|
+
function parseComponentSchema(value) {
|
|
68
|
+
if (!isRecord(value) || !isChildrenKind(value.children)) return null;
|
|
69
|
+
const props = recordOf(value.props, parsePropSchema);
|
|
70
|
+
return props === null ? null : {
|
|
71
|
+
children: value.children,
|
|
72
|
+
props
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function parseProcedure(value) {
|
|
76
|
+
if (!isRecord(value) || value.kind !== "query" && value.kind !== "mutation" && value.kind !== "action" || !isStringList(value.invalidates)) return null;
|
|
77
|
+
return {
|
|
78
|
+
invalidates: value.invalidates,
|
|
79
|
+
kind: value.kind
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function parseProgram(value) {
|
|
83
|
+
if (!isRecord(value) || value.format !== 1 || !isString(value.code) || !isStringList(value.imageHosts)) return null;
|
|
84
|
+
const components = recordOf(value.components, parseComponentSchema);
|
|
85
|
+
const procedures = recordOf(value.procedures, parseProcedure);
|
|
86
|
+
const prose = recordOf(value.prose, (entry) => isString(entry) ? entry : null);
|
|
87
|
+
if (components === null || procedures === null || prose === null) return null;
|
|
88
|
+
return {
|
|
89
|
+
code: value.code,
|
|
90
|
+
components,
|
|
91
|
+
format: 1,
|
|
92
|
+
imageHosts: value.imageHosts,
|
|
93
|
+
procedures,
|
|
94
|
+
prose
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function parseDiagnostic(value) {
|
|
98
|
+
if (!isRecord(value) || !isString(value.code) || !isString(value.message) || !isSeverity(value.severity) || !isRecord(value.span) || typeof value.span.line !== "number") return null;
|
|
99
|
+
return {
|
|
100
|
+
code: value.code,
|
|
101
|
+
message: value.message,
|
|
102
|
+
severity: value.severity,
|
|
103
|
+
span: { line: value.span.line }
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** The diagnostics that parse; one the browser cannot read is skipped, not the event. */
|
|
107
|
+
function parseDiagnostics(value) {
|
|
108
|
+
if (!Array.isArray(value)) return null;
|
|
109
|
+
return value.flatMap((entry) => {
|
|
110
|
+
const parsed = parseDiagnostic(entry);
|
|
111
|
+
return parsed === null ? [] : [parsed];
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** One session event from its JSON text, or null when it is not one. */
|
|
115
|
+
function parseSessionEvent(text) {
|
|
116
|
+
let value;
|
|
117
|
+
try {
|
|
118
|
+
value = JSON.parse(text);
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (!isRecord(value)) return null;
|
|
123
|
+
if (value.type === "error") {
|
|
124
|
+
const error = parseErrorData(value.error);
|
|
125
|
+
return error === null ? null : {
|
|
126
|
+
error,
|
|
127
|
+
type: "error"
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const diagnostics = parseDiagnostics(value.diagnostics);
|
|
131
|
+
if (diagnostics === null || typeof value.revision !== "number") return null;
|
|
132
|
+
if (value.type === "program") {
|
|
133
|
+
const program = parseProgram(value.program);
|
|
134
|
+
return program === null ? null : {
|
|
135
|
+
diagnostics,
|
|
136
|
+
fresh: value.fresh === true,
|
|
137
|
+
program,
|
|
138
|
+
revision: value.revision,
|
|
139
|
+
type: "program"
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (value.type === "done" && isString(value.source)) return {
|
|
143
|
+
diagnostics,
|
|
144
|
+
revision: value.revision,
|
|
145
|
+
source: value.source,
|
|
146
|
+
type: "done"
|
|
147
|
+
};
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
export { parseErrorData, parseInvalidations, parseProgram, parseSessionEvent };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { JigsErrorData, ProcedureEntry } from "./types.js";
|
|
2
|
+
//#region src/protocol.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Prop value on the wire. Functions never leave the worker; they travel as
|
|
5
|
+
* `{ fn }` ids. Lists of plain objects travel as `{ rows }`.
|
|
6
|
+
*/
|
|
7
|
+
export type WireValue = boolean | number | string | {
|
|
8
|
+
fn: string;
|
|
9
|
+
} | {
|
|
10
|
+
rows: ReadonlyArray<Row>;
|
|
11
|
+
};
|
|
12
|
+
/** One row of a list prop: flat fields only. */
|
|
13
|
+
export type Row = Readonly<Record<string, boolean | number | string | null>>;
|
|
14
|
+
export type WireArg = boolean | number | string;
|
|
15
|
+
/** The props of one node on the wire. */
|
|
16
|
+
export type WireProps = Record<string, WireValue>;
|
|
17
|
+
/** UI operations produced by the worker's reconciler. The root container has id 0. */
|
|
18
|
+
export type Op = {
|
|
19
|
+
id: number;
|
|
20
|
+
op: "create";
|
|
21
|
+
props: WireProps;
|
|
22
|
+
type: string;
|
|
23
|
+
} | {
|
|
24
|
+
id: number;
|
|
25
|
+
op: "createText";
|
|
26
|
+
text: string;
|
|
27
|
+
} | {
|
|
28
|
+
id: number;
|
|
29
|
+
index: number;
|
|
30
|
+
op: "place";
|
|
31
|
+
parent: number;
|
|
32
|
+
} | {
|
|
33
|
+
id: number;
|
|
34
|
+
op: "remove";
|
|
35
|
+
} | {
|
|
36
|
+
id: number;
|
|
37
|
+
op: "set";
|
|
38
|
+
props: Record<string, WireValue | null>;
|
|
39
|
+
} | {
|
|
40
|
+
id: number;
|
|
41
|
+
op: "text";
|
|
42
|
+
text: string;
|
|
43
|
+
};
|
|
44
|
+
export interface RuntimeError {
|
|
45
|
+
line: number | null;
|
|
46
|
+
message: string;
|
|
47
|
+
}
|
|
48
|
+
/** A query as the program sees it from `useQuery`. */
|
|
49
|
+
export type QueryResult = {
|
|
50
|
+
data: null;
|
|
51
|
+
error: null;
|
|
52
|
+
status: "pending";
|
|
53
|
+
} | {
|
|
54
|
+
data: unknown;
|
|
55
|
+
error: null;
|
|
56
|
+
status: "success";
|
|
57
|
+
} | {
|
|
58
|
+
data: null;
|
|
59
|
+
error: JigsErrorData;
|
|
60
|
+
status: "error";
|
|
61
|
+
};
|
|
62
|
+
/** How a mutation settled. */
|
|
63
|
+
export type MutationResult = {
|
|
64
|
+
data: unknown;
|
|
65
|
+
status: "success";
|
|
66
|
+
} | {
|
|
67
|
+
error: JigsErrorData;
|
|
68
|
+
status: "error";
|
|
69
|
+
};
|
|
70
|
+
/** A query the program read during a render. `key` is stable for a name and input. */
|
|
71
|
+
export interface QueryRead {
|
|
72
|
+
input: unknown;
|
|
73
|
+
key: string;
|
|
74
|
+
name: string;
|
|
75
|
+
}
|
|
76
|
+
/** A `mutate()` call made inside a handler. */
|
|
77
|
+
export interface MutationCall {
|
|
78
|
+
id: number;
|
|
79
|
+
input: unknown;
|
|
80
|
+
name: string;
|
|
81
|
+
}
|
|
82
|
+
/** Page to worker. */
|
|
83
|
+
export type HostMessage = {
|
|
84
|
+
args: ReadonlyArray<WireArg>;
|
|
85
|
+
fn: string;
|
|
86
|
+
seq: number;
|
|
87
|
+
type: "call";
|
|
88
|
+
} | {
|
|
89
|
+
id: number;
|
|
90
|
+
result: MutationResult;
|
|
91
|
+
seq: number;
|
|
92
|
+
type: "mutation";
|
|
93
|
+
} | {
|
|
94
|
+
key: string;
|
|
95
|
+
result: QueryResult;
|
|
96
|
+
seq: number;
|
|
97
|
+
type: "query";
|
|
98
|
+
} | {
|
|
99
|
+
code: string;
|
|
100
|
+
/** Drop the state and mutation statuses of the program before. */
|
|
101
|
+
fresh: boolean;
|
|
102
|
+
procedures: Readonly<Record<string, ProcedureEntry>>;
|
|
103
|
+
prose: Readonly<Record<string, string>>;
|
|
104
|
+
seq: number;
|
|
105
|
+
type: "update";
|
|
106
|
+
};
|
|
107
|
+
/** Worker to page. */
|
|
108
|
+
export type WorkerMessage = {
|
|
109
|
+
errors: Array<RuntimeError>;
|
|
110
|
+
mutations: Array<MutationCall>;
|
|
111
|
+
ops: Array<Op>;
|
|
112
|
+
queries: Array<QueryRead>;
|
|
113
|
+
seq: number;
|
|
114
|
+
type: "result";
|
|
115
|
+
} | {
|
|
116
|
+
message: string;
|
|
117
|
+
seq: number;
|
|
118
|
+
type: "failed";
|
|
119
|
+
};
|
|
120
|
+
/** What the frame sends to the page. */
|
|
121
|
+
export type FrameMessage = WorkerMessage | {
|
|
122
|
+
seq: number;
|
|
123
|
+
type: "quarantined";
|
|
124
|
+
} | {
|
|
125
|
+
type: "ready";
|
|
126
|
+
} |
|
|
127
|
+
/** The frame's own security policy is not in force, so it will not run. */
|
|
128
|
+
{
|
|
129
|
+
type: "unprotected";
|
|
130
|
+
};
|
|
131
|
+
export declare const ROOT_ID = 0;
|
|
132
|
+
//#endregion
|
package/dist/protocol.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Diagnostic } from "./diagnostics.js";
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Types the server and the browser share. The server builds these values;
|
|
5
|
+
* the browser receives them as JSON and checks them with `parse.ts`.
|
|
6
|
+
*/
|
|
7
|
+
/** The version of the program format. The runtime refuses any other. */
|
|
8
|
+
export declare const PROGRAM_FORMAT = 1;
|
|
9
|
+
export declare const jigsErrorCodes: readonly ["BAD_INPUT", "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "CONFLICT", "RATE_LIMITED", "INTERNAL", "UNKNOWN_PROCEDURE", "UNAVAILABLE", "NETWORK", "DECLINED"];
|
|
10
|
+
export type JigsErrorCode = (typeof jigsErrorCodes)[number];
|
|
11
|
+
/** A failure as the spec and the browser see it. */
|
|
12
|
+
export interface JigsErrorData {
|
|
13
|
+
code: JigsErrorCode;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
export interface NumberPropSchema {
|
|
17
|
+
kind: "number";
|
|
18
|
+
max?: number;
|
|
19
|
+
min?: number;
|
|
20
|
+
}
|
|
21
|
+
/** How the view checks one prop before a registry component sees it. */
|
|
22
|
+
export type PropSchema = {
|
|
23
|
+
kind: "boolean";
|
|
24
|
+
} | {
|
|
25
|
+
kind: "enum";
|
|
26
|
+
values: ReadonlyArray<string>;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "handler";
|
|
29
|
+
} | {
|
|
30
|
+
kind: "image";
|
|
31
|
+
} | {
|
|
32
|
+
kind: "link";
|
|
33
|
+
} | {
|
|
34
|
+
kind: "list";
|
|
35
|
+
} | NumberPropSchema | {
|
|
36
|
+
kind: "string";
|
|
37
|
+
};
|
|
38
|
+
export type ChildrenKind = "block" | "inline" | "none";
|
|
39
|
+
export interface ComponentSchema {
|
|
40
|
+
children: ChildrenKind;
|
|
41
|
+
props: Readonly<Record<string, PropSchema>>;
|
|
42
|
+
}
|
|
43
|
+
/** `action` is a mutation the embedding page handles instead of the server. */
|
|
44
|
+
export type ProcedureKind = "action" | "mutation" | "query";
|
|
45
|
+
export interface ProcedureEntry {
|
|
46
|
+
invalidates: ReadonlyArray<string>;
|
|
47
|
+
kind: ProcedureKind;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* One query a mutation refreshed, as the server sends it with the mutation's
|
|
51
|
+
* data. Without `input`, every input of the query refetches. With it, only
|
|
52
|
+
* the queries whose input contains those values.
|
|
53
|
+
*/
|
|
54
|
+
export interface Invalidation {
|
|
55
|
+
input?: JsonValue;
|
|
56
|
+
name: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A compiled spec. Opaque to apps: pass it from the server to the view and
|
|
60
|
+
* nowhere else. The view trusts its schemas and image hosts.
|
|
61
|
+
*/
|
|
62
|
+
export interface Program {
|
|
63
|
+
code: string;
|
|
64
|
+
/** Prop schemas of the components the program draws. */
|
|
65
|
+
components: Readonly<Record<string, ComponentSchema>>;
|
|
66
|
+
format: typeof PROGRAM_FORMAT;
|
|
67
|
+
imageHosts: ReadonlyArray<string>;
|
|
68
|
+
/** Procedures the program calls, with their kind and `invalidates` list. */
|
|
69
|
+
procedures: Readonly<Record<string, ProcedureEntry>>;
|
|
70
|
+
/** Text nodes pulled out of the code, so a prose edit swaps a table. */
|
|
71
|
+
prose: Readonly<Record<string, string>>;
|
|
72
|
+
}
|
|
73
|
+
export interface Message {
|
|
74
|
+
content: string;
|
|
75
|
+
role: "assistant" | "user";
|
|
76
|
+
}
|
|
77
|
+
export type SessionEvent = {
|
|
78
|
+
diagnostics: ReadonlyArray<Diagnostic>;
|
|
79
|
+
/**
|
|
80
|
+
* True on the first program of a new UI: the model wrote a whole
|
|
81
|
+
* document rather than editing the last one, or a stored one was
|
|
82
|
+
* restored. The view starts it from fresh state.
|
|
83
|
+
*/
|
|
84
|
+
fresh: boolean;
|
|
85
|
+
program: Program;
|
|
86
|
+
revision: number;
|
|
87
|
+
type: "program";
|
|
88
|
+
} | {
|
|
89
|
+
diagnostics: ReadonlyArray<Diagnostic>;
|
|
90
|
+
revision: number;
|
|
91
|
+
source: string;
|
|
92
|
+
type: "done";
|
|
93
|
+
} | {
|
|
94
|
+
error: JigsErrorData;
|
|
95
|
+
type: "error";
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Where a chat block is: still arriving, being repaired after it closed with
|
|
99
|
+
* errors, or final.
|
|
100
|
+
*/
|
|
101
|
+
export type ChatBlockStatus = "done" | "repairing" | "streaming";
|
|
102
|
+
/**
|
|
103
|
+
* A jig in a chat reply: one fenced `jigs` block, compiled on its own.
|
|
104
|
+
* The server sends it again whenever its program changes, under the same
|
|
105
|
+
* `key`.
|
|
106
|
+
*/
|
|
107
|
+
export interface ChatBlock {
|
|
108
|
+
/** Lines refer to `source`, which starts with its pragma line. */
|
|
109
|
+
diagnostics: ReadonlyArray<Diagnostic>;
|
|
110
|
+
/** Set when the block could not be built, e.g. an edit to a missing UI. */
|
|
111
|
+
error?: JigsErrorData;
|
|
112
|
+
/** The id the model gave the block, e.g. "monthly-spending". */
|
|
113
|
+
id: string;
|
|
114
|
+
/** `${id}@${version}`: unique in a conversation. */
|
|
115
|
+
key: string;
|
|
116
|
+
program: Program;
|
|
117
|
+
/** The checked source, with its pragma. Store this, not the program. */
|
|
118
|
+
source: string;
|
|
119
|
+
status: ChatBlockStatus;
|
|
120
|
+
/** 1 for the first block with this id in the conversation, then 2, 3… */
|
|
121
|
+
version: number;
|
|
122
|
+
}
|
|
123
|
+
/** A value that travels as JSON. */
|
|
124
|
+
export type JsonValue = boolean | null | number | string | ReadonlyArray<JsonValue> | {
|
|
125
|
+
readonly [key: string]: JsonValue;
|
|
126
|
+
};
|
|
127
|
+
/** App options sent with a turn; the server parses them with its `options` schema. */
|
|
128
|
+
export type SessionOptions = Readonly<Record<string, JsonValue>>;
|
|
129
|
+
/** What a session request carries. */
|
|
130
|
+
export interface SessionBody {
|
|
131
|
+
messages: ReadonlyArray<Message>;
|
|
132
|
+
options?: SessionOptions;
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/types.ts
|
|
2
|
+
/**
|
|
3
|
+
* Types the server and the browser share. The server builds these values;
|
|
4
|
+
* the browser receives them as JSON and checks them with `parse.ts`.
|
|
5
|
+
*/
|
|
6
|
+
/** The version of the program format. The runtime refuses any other. */
|
|
7
|
+
const PROGRAM_FORMAT = 1;
|
|
8
|
+
const jigsErrorCodes = [
|
|
9
|
+
"BAD_INPUT",
|
|
10
|
+
"UNAUTHORIZED",
|
|
11
|
+
"FORBIDDEN",
|
|
12
|
+
"NOT_FOUND",
|
|
13
|
+
"CONFLICT",
|
|
14
|
+
"RATE_LIMITED",
|
|
15
|
+
"INTERNAL",
|
|
16
|
+
"UNKNOWN_PROCEDURE",
|
|
17
|
+
"UNAVAILABLE",
|
|
18
|
+
"NETWORK",
|
|
19
|
+
"DECLINED"
|
|
20
|
+
];
|
|
21
|
+
//#endregion
|
|
22
|
+
export { PROGRAM_FORMAT, jigsErrorCodes };
|
package/dist/urls.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/urls.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A hostname, or `*.example.com` for its subdomains (not the apex),
|
|
4
|
+
* optionally followed by a path prefix that ends in `/`, such as
|
|
5
|
+
* `images.example.com/products/`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isImageHostEntry(entry: string): boolean;
|
|
8
|
+
/** The entry with its host lowercased; the path keeps its case. */
|
|
9
|
+
export declare function normalizeImageHost(entry: string): string;
|
|
10
|
+
/** An `https` URL with no credentials or port, on one of `hosts`. */
|
|
11
|
+
export declare function isAllowedImage(src: string, hosts: ReadonlyArray<string>): boolean;
|
|
12
|
+
/** An absolute `http` or `https` URL. Anything else, such as `javascript:`, is refused. */
|
|
13
|
+
export declare function isAllowedLink(href: string): boolean;
|
|
14
|
+
//#endregion
|
package/dist/urls.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/urls.ts
|
|
2
|
+
const HOST = /^(\*\.)?[a-z0-9-]+(\.[a-z0-9-]+)*$/;
|
|
3
|
+
const PATH_PREFIX = /^\/(?:[A-Za-z0-9._~!$&'()*+,;=:@%/-]*\/)?$/;
|
|
4
|
+
/** An image host entry split into its host and optional path prefix. */
|
|
5
|
+
function splitEntry(entry) {
|
|
6
|
+
const slash = entry.indexOf("/");
|
|
7
|
+
return slash === -1 ? {
|
|
8
|
+
host: entry.toLowerCase(),
|
|
9
|
+
path: "/"
|
|
10
|
+
} : {
|
|
11
|
+
host: entry.slice(0, slash).toLowerCase(),
|
|
12
|
+
path: entry.slice(slash)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A hostname, or `*.example.com` for its subdomains (not the apex),
|
|
17
|
+
* optionally followed by a path prefix that ends in `/`, such as
|
|
18
|
+
* `images.example.com/products/`.
|
|
19
|
+
*/
|
|
20
|
+
function isImageHostEntry(entry) {
|
|
21
|
+
const parts = splitEntry(entry);
|
|
22
|
+
return HOST.test(parts.host) && PATH_PREFIX.test(parts.path);
|
|
23
|
+
}
|
|
24
|
+
/** The entry with its host lowercased; the path keeps its case. */
|
|
25
|
+
function normalizeImageHost(entry) {
|
|
26
|
+
const parts = splitEntry(entry);
|
|
27
|
+
return entry.includes("/") ? `${parts.host}${parts.path}` : parts.host;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* An encoded `/`, `\`, `.` or `%`: a server that decodes it after matching can
|
|
31
|
+
* be walked out of the prefix with `..%2F`, or with `..%252F` if it decodes
|
|
32
|
+
* twice, and the URL parser leaves it alone. So a prefix can't serve object
|
|
33
|
+
* keys that hold an encoded `/`, such as Firebase Storage's `o/a%2Fb.jpg`.
|
|
34
|
+
*/
|
|
35
|
+
const ENCODED_SEPARATOR = /%(?:2f|5c|2e|25)/i;
|
|
36
|
+
function matchesHost(url, entry) {
|
|
37
|
+
if (!isImageHostEntry(entry)) return false;
|
|
38
|
+
const parts = splitEntry(entry);
|
|
39
|
+
return (parts.host.startsWith("*.") ? url.hostname.endsWith(parts.host.slice(1)) : url.hostname === parts.host) && url.pathname.startsWith(parts.path) && (parts.path === "/" || !ENCODED_SEPARATOR.test(url.pathname));
|
|
40
|
+
}
|
|
41
|
+
function parseUrl(text) {
|
|
42
|
+
try {
|
|
43
|
+
return new URL(text);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** An `https` URL with no credentials or port, on one of `hosts`. */
|
|
49
|
+
function isAllowedImage(src, hosts) {
|
|
50
|
+
const url = parseUrl(src);
|
|
51
|
+
return url !== null && url.protocol === "https:" && url.username === "" && url.password === "" && url.port === "" && hosts.some((entry) => matchesHost(url, entry));
|
|
52
|
+
}
|
|
53
|
+
/** An absolute `http` or `https` URL. Anything else, such as `javascript:`, is refused. */
|
|
54
|
+
function isAllowedLink(href) {
|
|
55
|
+
const url = parseUrl(href);
|
|
56
|
+
return url !== null && (url.protocol === "https:" || url.protocol === "http:") && url.username === "" && url.password === "";
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { isAllowedImage, isAllowedLink, isImageHostEntry, normalizeImageHost };
|
package/package.json
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jigs.design/protocol",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "The program format, the diagnostics, and the types and messages the server, the browser and the sandbox share. No runtime and no dependencies.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/yoamomonstruos/jigs.git",
|
|
9
|
+
"directory": "packages/protocol"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
}
|
|
6
27
|
}
|