@sigrun/sigrun 0.1.3 → 0.1.5
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/CHANGELOG.md +46 -0
- package/README.md +24 -2
- package/dist/blocks/builders.js +125 -0
- package/dist/blocks/images.js +57 -0
- package/dist/blocks/index.js +20 -0
- package/dist/blocks/limits.js +26 -0
- package/dist/blocks/placements.js +99 -0
- package/dist/blocks/schema.js +559 -0
- package/dist/blocks/validate.js +602 -0
- package/dist/config.js +31 -0
- package/dist/extension.d.ts +476 -9
- package/dist/harness/boot.js +8 -1
- package/dist/harness/event-bus.js +33 -0
- package/dist/harness/identity/users.js +3 -2
- package/dist/harness/loader.js +369 -13
- package/dist/harness/packages.js +71 -2
- package/dist/harness/seam-host.js +69 -2
- package/dist/harness/settings.js +5 -0
- package/dist/jobs/fire.js +11 -0
- package/dist/jobs/spec.js +19 -0
- package/dist/serve.js +33 -41
- package/dist/server/auth/pipeline.js +16 -1
- package/dist/server/conversation-ops.js +482 -10
- package/dist/server/gateway.js +122 -0
- package/dist/server/helpers.js +26 -0
- package/dist/server/host-bridge.js +141 -4
- package/dist/server/index.js +27 -0
- package/dist/server/notifications.js +6 -2
- package/dist/server/routes/conversations.js +37 -93
- package/dist/server/routes/events.js +39 -0
- package/dist/server/routes/ext-assets.js +84 -0
- package/dist/server/routes/extensions.js +31 -1
- package/dist/server/routes/files.js +38 -83
- package/dist/server/routes/meta.js +37 -2
- package/dist/server/routes/providers.js +5 -21
- package/dist/server/routes/submissions.js +36 -111
- package/dist/server/routes/surfaces.js +387 -0
- package/dist/server/routes/users.js +6 -1
- package/dist/server/schema-fixture.js +7 -0
- package/dist/server/session-meta.js +12 -6
- package/dist/server/shutdown.js +85 -0
- package/dist/server/surfaces/access-cache.js +43 -0
- package/dist/server/surfaces/dedup.js +67 -0
- package/dist/server/surfaces/instance.js +42 -0
- package/dist/server/surfaces/mime.js +73 -0
- package/dist/server/surfaces/rate-limit.js +63 -0
- package/dist/server/surfaces/service.js +493 -0
- package/dist/server/surfaces/watchers.js +94 -0
- package/node_modules/@sigrun/shared/dist/blocks/format.d.ts +28 -0
- package/node_modules/@sigrun/shared/dist/blocks/format.js +199 -0
- package/node_modules/@sigrun/shared/dist/blocks/icons.d.ts +12 -0
- package/node_modules/@sigrun/shared/dist/blocks/icons.js +64 -0
- package/node_modules/@sigrun/shared/dist/blocks/index.d.ts +25 -0
- package/node_modules/@sigrun/shared/dist/blocks/index.js +21 -0
- package/node_modules/@sigrun/shared/dist/blocks/instance.d.ts +29 -0
- package/node_modules/@sigrun/shared/dist/blocks/instance.js +51 -0
- package/node_modules/@sigrun/shared/dist/blocks/lttb.d.ts +16 -0
- package/node_modules/@sigrun/shared/dist/blocks/lttb.js +67 -0
- package/node_modules/@sigrun/shared/dist/blocks/ops.d.ts +44 -0
- package/node_modules/@sigrun/shared/dist/blocks/ops.js +294 -0
- package/node_modules/@sigrun/shared/dist/blocks/text.d.ts +43 -0
- package/node_modules/@sigrun/shared/dist/blocks/text.js +790 -0
- package/node_modules/@sigrun/shared/dist/blocks/transport.d.ts +77 -0
- package/node_modules/@sigrun/shared/dist/blocks/transport.js +11 -0
- package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +541 -0
- package/node_modules/@sigrun/shared/dist/blocks/types.js +17 -0
- package/node_modules/@sigrun/shared/dist/jobs.d.ts +9 -0
- package/node_modules/@sigrun/shared/dist/live.d.ts +71 -0
- package/node_modules/@sigrun/shared/dist/live.js +82 -0
- package/node_modules/@sigrun/shared/dist/session-meta.d.ts +19 -3
- package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +83 -3
- package/node_modules/@sigrun/shared/package.json +10 -2
- package/package.json +9 -3
- package/webui/dist/assets/index-B4-GYqEb.js +50 -0
- package/webui/dist/assets/index-BiSMljer.css +1 -0
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-2RfqJO57.css +0 -1
- package/webui/dist/assets/index-DkZgTngD.js +0 -50
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `validateTree` (spec §14.2): every error with its JSON path and a
|
|
3
|
+
* plain message.
|
|
4
|
+
*
|
|
5
|
+
* Two passes:
|
|
6
|
+
* 1. The compiled typebox validators (`schema.ts`) check each node's
|
|
7
|
+
* shape: the walk descends the tree, finds every block, field,
|
|
8
|
+
* action, button and card template, and runs the matching compiled
|
|
9
|
+
* validator against it. Instance paths (relative to the node) join
|
|
10
|
+
* the walk's path, so one broken block is one precise error
|
|
11
|
+
* (`root.main[2].text`), never a union mismatch per sibling.
|
|
12
|
+
* 2. The same walk checks what schemas cannot express: placements
|
|
13
|
+
* (§5.7), the literal-ness of structural props (§4.2), handler names
|
|
14
|
+
* (§6.1), relative bindings outside item templates (§4.2), ids
|
|
15
|
+
* (§4.1), `turn` outside chat (§6.2), `open` schemes (§6.1), image
|
|
16
|
+
* sources (§11.1), limits (§4.5), and the ambient one-line rule
|
|
17
|
+
* (§9.2).
|
|
18
|
+
*/
|
|
19
|
+
import { BLOCK_SET_VERSION } from "@sigrun/shared/blocks";
|
|
20
|
+
import { ACTION_BUTTON_VALIDATOR, ACTION_VALIDATOR, BLOCK_VALIDATORS, CARD_VALIDATOR, FIELD_VALIDATORS, } from "./schema.js";
|
|
21
|
+
import { PLACEMENT_RULES } from "./placements.js";
|
|
22
|
+
import { LIMITS, jsonBytes } from "./limits.js";
|
|
23
|
+
import { checkImageSource } from "./images.js";
|
|
24
|
+
export { PLACEMENT_RULES } from "./placements.js";
|
|
25
|
+
export { LIMITS } from "./limits.js";
|
|
26
|
+
/** The placements the block set defines (spec §3). */
|
|
27
|
+
export const VALID_PLACEMENTS = [
|
|
28
|
+
"view",
|
|
29
|
+
"panel",
|
|
30
|
+
"settings",
|
|
31
|
+
"chat",
|
|
32
|
+
"interaction",
|
|
33
|
+
"ambient",
|
|
34
|
+
];
|
|
35
|
+
function isRec(value) {
|
|
36
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
function isBinding(value) {
|
|
39
|
+
return isRec(value) && typeof value.$ === "string";
|
|
40
|
+
}
|
|
41
|
+
/** Path segment join: `root` + `main` → `root.main`, + `2` → `root.main[2]`. */
|
|
42
|
+
function joinPath(base, key) {
|
|
43
|
+
return /^\d+$/.test(key) ? `${base}[${key}]` : `${base}.${key}`;
|
|
44
|
+
}
|
|
45
|
+
/** Handler-name props across the block set (spec §6.1). */
|
|
46
|
+
const HANDLER_PROPS = ["onMove", "onCheck", "onExpand", "onResize", "onCreate", "onSubmit", "onUpload"];
|
|
47
|
+
/** Block types the ambient one-line rule treats as line-level (§9.2). */
|
|
48
|
+
const AMBIENT_LINE_BLOCKS = new Set(["markdown", "stat", "meter", "log"]);
|
|
49
|
+
/** Structural props that must be literals, per block type (spec §4.2). */
|
|
50
|
+
/** Structural props that must be literals, per block type (spec §4.2). */
|
|
51
|
+
const STRUCTURAL_LITERALS = {
|
|
52
|
+
page: [],
|
|
53
|
+
stack: [],
|
|
54
|
+
columns: [],
|
|
55
|
+
grid: [],
|
|
56
|
+
section: [],
|
|
57
|
+
markdown: [],
|
|
58
|
+
fields: [],
|
|
59
|
+
stat: [],
|
|
60
|
+
callout: [],
|
|
61
|
+
image: [],
|
|
62
|
+
chart: [],
|
|
63
|
+
meter: [],
|
|
64
|
+
log: [],
|
|
65
|
+
tree: [],
|
|
66
|
+
diff: [],
|
|
67
|
+
details: [],
|
|
68
|
+
collection: ["display", "groupBy", "groups", "reorder", "select", "columns", "onMove", "onCheck"],
|
|
69
|
+
form: ["mode", "onSubmit"],
|
|
70
|
+
};
|
|
71
|
+
/** A field's `name` is always structural (§4.2). */
|
|
72
|
+
const FIELD_STRUCTURAL_LITERALS = ["name"];
|
|
73
|
+
function push(ctx, path, message) {
|
|
74
|
+
ctx.errors.push({ path, message });
|
|
75
|
+
}
|
|
76
|
+
/** Convert a JSON-pointer instance path to the spec's dot shape ("/text" → ".text"). */
|
|
77
|
+
function relPath(instancePath) {
|
|
78
|
+
return instancePath
|
|
79
|
+
.split("/")
|
|
80
|
+
.filter((s) => s.length > 0)
|
|
81
|
+
.map((s) => (/^\d+$/.test(s) ? `[${s}]` : `.${s}`))
|
|
82
|
+
.join("");
|
|
83
|
+
}
|
|
84
|
+
/** Append a node's compiled-validator errors, prefixed with its walk path. */
|
|
85
|
+
function checkSchema(ctx, validator, node, path) {
|
|
86
|
+
for (const raw of validator.Errors(node)) {
|
|
87
|
+
const rel = relPath(raw.instancePath);
|
|
88
|
+
const message = raw.message ?? "invalid value";
|
|
89
|
+
// Name the prop the error sits on ("icon: must be equal to one of
|
|
90
|
+
// the allowed values") so the message reads at the path it lands at.
|
|
91
|
+
const segments = raw.instancePath.split("/").filter((s) => s.length > 0);
|
|
92
|
+
const lastProp = [...segments].reverse().find((s) => !/^\d+$/.test(s));
|
|
93
|
+
push(ctx, `${path}${rel}`, lastProp ? `${lastProp}: ${message}` : message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Check a handler-name prop: literal, and in the handlers set when given. */
|
|
97
|
+
function checkHandler(ctx, path, name, prop) {
|
|
98
|
+
if (name === undefined || name === null)
|
|
99
|
+
return;
|
|
100
|
+
if (isRec(name)) {
|
|
101
|
+
push(ctx, path, `${prop} must be a literal (a handler name), not a binding`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (typeof name !== "string")
|
|
105
|
+
return; // the schema already flagged it
|
|
106
|
+
if (ctx.handlers && !ctx.handlers.has(name)) {
|
|
107
|
+
push(ctx, path, `no handler named ${name}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** An `Action` object: shape via its validator, then name/turn/url rules. */
|
|
111
|
+
function checkAction(ctx, action, path) {
|
|
112
|
+
if (!isRec(action))
|
|
113
|
+
return;
|
|
114
|
+
checkSchema(ctx, ACTION_VALIDATOR, action, path);
|
|
115
|
+
const type = action.type;
|
|
116
|
+
if (type === "submit" || type === "move" || type === "edit") {
|
|
117
|
+
if (isRec(action.name)) {
|
|
118
|
+
push(ctx, joinPath(path, "name"), "action name must be a literal");
|
|
119
|
+
}
|
|
120
|
+
else if (typeof action.name === "string" && ctx.handlers && !ctx.handlers.has(action.name)) {
|
|
121
|
+
push(ctx, joinPath(path, "name"), `no handler named ${action.name}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (ctx.placement !== "chat" && action.turn === true) {
|
|
125
|
+
push(ctx, joinPath(path, "turn"), "`turn` is valid only in the chat placement");
|
|
126
|
+
}
|
|
127
|
+
if (type === "open" && typeof action.url === "string" && !/^(https?:|mailto:)/.test(action.url)) {
|
|
128
|
+
push(ctx, joinPath(path, "url"), `open url must be https:, http: or mailto: (got "${action.url.slice(0, 40)}")`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** An action-button list (`page.actions`, `callout.actions`, card `actions`). */
|
|
132
|
+
function checkActionButtons(ctx, buttons, path) {
|
|
133
|
+
if (!Array.isArray(buttons))
|
|
134
|
+
return;
|
|
135
|
+
buttons.forEach((button, i) => {
|
|
136
|
+
if (!isRec(button))
|
|
137
|
+
return;
|
|
138
|
+
const bPath = `${path}[${i}]`;
|
|
139
|
+
checkSchema(ctx, ACTION_BUTTON_VALIDATOR, button, bPath);
|
|
140
|
+
if (isBinding(button.label))
|
|
141
|
+
checkBinding(ctx, button.label, joinPath(bPath, "label"));
|
|
142
|
+
checkAction(ctx, button.action, joinPath(bPath, "action"));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** Relative `./` bindings are legal only inside a card template (§4.2). */
|
|
146
|
+
function checkBinding(ctx, binding, path) {
|
|
147
|
+
if (!ctx.itemTemplate && binding.$.startsWith("./")) {
|
|
148
|
+
push(ctx, joinPath(path, "$"), `relative binding "${binding.$}" is only valid inside a collection item template`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Any relative `./` binding on this node (§4.2): every prop value is
|
|
153
|
+
* scanned — the binding-only props (`items`, `lines`, `nodes`, `rows`,
|
|
154
|
+
* `series[].data`, `submitLabel`, `copy.text`) bind just like the
|
|
155
|
+
* bindable literals, so a hardcoded prop list would miss them. Absolute
|
|
156
|
+
* `/` bindings stay legal everywhere; relative ones only inside item
|
|
157
|
+
* templates. `skip` names props whose own walk already reported (the
|
|
158
|
+
* structural literals above carry their more precise message).
|
|
159
|
+
*/
|
|
160
|
+
function checkNodeRelativeBindings(ctx, node, path, skip) {
|
|
161
|
+
if (ctx.itemTemplate)
|
|
162
|
+
return;
|
|
163
|
+
for (const [prop, value] of Object.entries(node)) {
|
|
164
|
+
if (skip.has(prop))
|
|
165
|
+
continue;
|
|
166
|
+
if (isBinding(value)) {
|
|
167
|
+
checkBinding(ctx, value, joinPath(path, prop));
|
|
168
|
+
}
|
|
169
|
+
else if (Array.isArray(value)) {
|
|
170
|
+
// rows, series, meta, badges: bindings nested one level in
|
|
171
|
+
// literal arrays (e.g. `series[].data`)
|
|
172
|
+
value.forEach((element, i) => {
|
|
173
|
+
if (isBinding(element)) {
|
|
174
|
+
checkBinding(ctx, element, joinPath(joinPath(path, prop), String(i)));
|
|
175
|
+
}
|
|
176
|
+
else if (isRec(element)) {
|
|
177
|
+
for (const [k, v] of Object.entries(element)) {
|
|
178
|
+
if (isBinding(v)) {
|
|
179
|
+
checkBinding(ctx, v, joinPath(joinPath(joinPath(path, prop), String(i)), k));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** One field node (spec §5.6). `path` is the field's own path. */
|
|
188
|
+
function checkField(ctx, node, path) {
|
|
189
|
+
const type = node.type;
|
|
190
|
+
if (typeof type !== "string") {
|
|
191
|
+
// `type` is a structural literal (§4.2); a missing or bound type is
|
|
192
|
+
// a soundness hole, never a silent pass
|
|
193
|
+
push(ctx, joinPath(path, "type"), `field type must be a literal string`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const rules = PLACEMENT_RULES[ctx.placement];
|
|
197
|
+
// placement: allowed field types (forms only exist where forms exist)
|
|
198
|
+
if (!rules.fields.has(type)) {
|
|
199
|
+
push(ctx, path, `${type} field is not allowed in ${ctx.placement}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// schema
|
|
203
|
+
const validator = FIELD_VALIDATORS.get(type);
|
|
204
|
+
if (validator)
|
|
205
|
+
checkSchema(ctx, validator, node, path);
|
|
206
|
+
// structural literals
|
|
207
|
+
for (const prop of FIELD_STRUCTURAL_LITERALS) {
|
|
208
|
+
if (isBinding(node[prop])) {
|
|
209
|
+
push(ctx, joinPath(path, prop), `field name must be a literal, not a binding`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// bindable props: every relative `./` binding on the field (§4.2);
|
|
213
|
+
// the structural-literal props above already carry their message
|
|
214
|
+
checkNodeRelativeBindings(ctx, node, path, new Set([...FIELD_STRUCTURAL_LITERALS, "type"]));
|
|
215
|
+
// handler names
|
|
216
|
+
if (node.onUpload !== undefined) {
|
|
217
|
+
checkHandler(ctx, joinPath(path, "onUpload"), node.onUpload, "onUpload");
|
|
218
|
+
}
|
|
219
|
+
// list fields recurse into one entry's field
|
|
220
|
+
if (type === "list" && isRec(node.item)) {
|
|
221
|
+
checkField(ctx, node.item, joinPath(path, "item"));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** A card template (spec §5.5.1): relative bindings are legal throughout. */
|
|
225
|
+
function checkCard(ctx, card, path) {
|
|
226
|
+
if (!isRec(card))
|
|
227
|
+
return;
|
|
228
|
+
checkSchema(ctx, CARD_VALIDATOR, card, path);
|
|
229
|
+
if (Array.isArray(card.actions)) {
|
|
230
|
+
checkActionButtons(ctx, card.actions, joinPath(path, "actions"));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* One block node. `path` is the node's own path; `rootDepth` marks the
|
|
235
|
+
* tree's root block (root constraints apply there only).
|
|
236
|
+
*/
|
|
237
|
+
function checkBlock(ctx, node, path, rootDepth) {
|
|
238
|
+
const type = node.type;
|
|
239
|
+
if (typeof type !== "string") {
|
|
240
|
+
// `type` is a structural literal (§4.2); a missing or bound type is
|
|
241
|
+
// a soundness hole, never a silent pass
|
|
242
|
+
push(ctx, joinPath(path, "type"), `block type must be a literal string`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const rules = PLACEMENT_RULES[ctx.placement];
|
|
246
|
+
// unknown block types (§4.4) first: `carousel` is not a block of this
|
|
247
|
+
// set at all, so its error names the type, at the type prop's path
|
|
248
|
+
if (!BLOCK_VALIDATORS.has(type)) {
|
|
249
|
+
if (FIELD_VALIDATORS.has(type)) {
|
|
250
|
+
push(ctx, joinPath(path, "type"), `a ${type} field is not a block; fields belong inside a form`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
push(ctx, joinPath(path, "type"), `unknown block type "${type}"`);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// placement: allowed blocks
|
|
258
|
+
if (!rules.blocks.has(type)) {
|
|
259
|
+
push(ctx, path, `${type} block is not allowed in ${ctx.placement}`);
|
|
260
|
+
return; // an illegal block's children are noise
|
|
261
|
+
}
|
|
262
|
+
// root constraints
|
|
263
|
+
if (rootDepth && rules.rootBlock && type !== rules.rootBlock) {
|
|
264
|
+
push(ctx, path, `the root block of a ${ctx.placement} surface must be ${rules.rootBlock}`);
|
|
265
|
+
}
|
|
266
|
+
if (type === "page" && !rules.pageSidebar && node.sidebar !== undefined) {
|
|
267
|
+
push(ctx, joinPath(path, "sidebar"), `page.sidebar is not allowed in ${ctx.placement}`);
|
|
268
|
+
}
|
|
269
|
+
// structural literals (§4.2): checked before the schema so the
|
|
270
|
+
// precise "must be a literal" message is the first error at its path
|
|
271
|
+
for (const prop of STRUCTURAL_LITERALS[type] ?? []) {
|
|
272
|
+
if (isBinding(node[prop])) {
|
|
273
|
+
const isHandler = HANDLER_PROPS.includes(prop);
|
|
274
|
+
push(ctx, joinPath(path, prop), isHandler
|
|
275
|
+
? `${prop} must be a literal (a handler name), not a binding`
|
|
276
|
+
: `${prop} must be a literal, not a binding`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// schema
|
|
280
|
+
const validator = BLOCK_VALIDATORS.get(type);
|
|
281
|
+
if (validator)
|
|
282
|
+
checkSchema(ctx, validator, node, path);
|
|
283
|
+
// collection-specific rules
|
|
284
|
+
if (type === "collection") {
|
|
285
|
+
const display = node.display;
|
|
286
|
+
if (typeof display === "string" &&
|
|
287
|
+
rules.collectionDisplays &&
|
|
288
|
+
!rules.collectionDisplays.includes(display)) {
|
|
289
|
+
push(ctx, joinPath(path, "display"), `collection display ${display} is not allowed in ${ctx.placement} (allowed: ${rules.collectionDisplays.join(", ")})`);
|
|
290
|
+
}
|
|
291
|
+
// reorder requires onMove (spec §5.5)
|
|
292
|
+
if (node.reorder !== undefined && node.onMove === undefined) {
|
|
293
|
+
push(ctx, joinPath(path, "reorder"), "`reorder` requires `onMove`");
|
|
294
|
+
}
|
|
295
|
+
// the card template: relative bindings legal inside it
|
|
296
|
+
if (isRec(node.item)) {
|
|
297
|
+
checkCard({ ...ctx, itemTemplate: true }, node.item, joinPath(path, "item"));
|
|
298
|
+
}
|
|
299
|
+
// bulk actions carry small forms (§5.5); their subtrees are NOT item
|
|
300
|
+
// templates, so relative bindings inside them fail — correct.
|
|
301
|
+
if (Array.isArray(node.bulkActions)) {
|
|
302
|
+
node.bulkActions.forEach((bulk, i) => {
|
|
303
|
+
if (!isRec(bulk))
|
|
304
|
+
return;
|
|
305
|
+
const bPath = joinPath(joinPath(path, "bulkActions"), String(i));
|
|
306
|
+
if (bulk.onSubmit !== undefined) {
|
|
307
|
+
checkHandler(ctx, joinPath(bPath, "onSubmit"), bulk.onSubmit, "onSubmit");
|
|
308
|
+
}
|
|
309
|
+
if (isRec(bulk.form)) {
|
|
310
|
+
const formPath = joinPath(joinPath(bPath, "form"), "root");
|
|
311
|
+
if (isRec(bulk.form.root)) {
|
|
312
|
+
// a nested form is never the surface's root: the placement's
|
|
313
|
+
// root-block constraint must not re-apply to it (§5.7)
|
|
314
|
+
checkBlock(ctx, bulk.form.root, formPath, false);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// handler names
|
|
321
|
+
for (const prop of HANDLER_PROPS) {
|
|
322
|
+
if (node[prop] !== undefined) {
|
|
323
|
+
checkHandler(ctx, joinPath(path, prop), node[prop], prop);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (type === "collection" && isRec(node.time)) {
|
|
327
|
+
const timePath = joinPath(path, "time");
|
|
328
|
+
for (const prop of ["onResize", "onCreate"]) {
|
|
329
|
+
if (node.time[prop] !== undefined) {
|
|
330
|
+
checkHandler(ctx, joinPath(timePath, prop), node.time[prop], prop);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// ids (§4.1): unique, and required on blocks carrying actions/inputs
|
|
335
|
+
const id = node.id;
|
|
336
|
+
if (typeof id === "string") {
|
|
337
|
+
if (ctx.ids.has(id)) {
|
|
338
|
+
push(ctx, path, `duplicate id "${id}" (first seen at ${ctx.ids.get(id) ?? "root"})`);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
ctx.ids.set(id, path);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (id === undefined && carriesActionsOrInputs(type, node)) {
|
|
345
|
+
push(ctx, path, `an id is required on ${type} blocks that carry actions or inputs`);
|
|
346
|
+
}
|
|
347
|
+
// image sources (§11.1)
|
|
348
|
+
if (type === "image" && typeof node.src === "string") {
|
|
349
|
+
const check = checkImageSource(node.src);
|
|
350
|
+
if (!check.ok)
|
|
351
|
+
push(ctx, joinPath(path, "src"), check.message);
|
|
352
|
+
}
|
|
353
|
+
// bindable props: every relative `./` binding on the node (§4.2)
|
|
354
|
+
const structural = STRUCTURAL_LITERALS[type] ?? [];
|
|
355
|
+
checkNodeRelativeBindings(ctx, node, path, new Set(structural));
|
|
356
|
+
// onActivate carries an Action (§6.1): same rules as button actions
|
|
357
|
+
if (node.onActivate !== undefined) {
|
|
358
|
+
checkAction(ctx, node.onActivate, joinPath(path, "onActivate"));
|
|
359
|
+
}
|
|
360
|
+
// actions lists
|
|
361
|
+
if (Array.isArray(node.actions)) {
|
|
362
|
+
checkActionButtons(ctx, node.actions, joinPath(path, "actions"));
|
|
363
|
+
}
|
|
364
|
+
// form: turn is chat-only (§6.2)
|
|
365
|
+
if (type === "form" && ctx.placement !== "chat" && node.turn === true) {
|
|
366
|
+
push(ctx, joinPath(path, "turn"), "`turn` is valid only in the chat placement");
|
|
367
|
+
}
|
|
368
|
+
// children: blocks and (inside forms/sections) fields
|
|
369
|
+
if (Array.isArray(node.children)) {
|
|
370
|
+
const wrapperParent = type === "columns" || type === "grid";
|
|
371
|
+
node.children.forEach((child, i) => {
|
|
372
|
+
if (!isRec(child))
|
|
373
|
+
return;
|
|
374
|
+
// columns/grid children are `{weight?, block}` wrappers with no
|
|
375
|
+
// `type` of their own; the wrapper walk below validates `block`.
|
|
376
|
+
// Other parents (stack, section, form…) type their children as
|
|
377
|
+
// blocks/fields directly, so a stray record-valued `block` prop
|
|
378
|
+
// must NOT skip them.
|
|
379
|
+
if (wrapperParent && isRec(child.block))
|
|
380
|
+
return;
|
|
381
|
+
if (wrapperParent && !isRec(child.block)) {
|
|
382
|
+
// a wrapper child without `block` (e.g. {weight: 1}): the
|
|
383
|
+
// parent schema's required-property error is the precise one
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const cPath = joinPath(joinPath(path, "children"), String(i));
|
|
387
|
+
const childType = child.type;
|
|
388
|
+
if (typeof childType === "string" && FIELD_VALIDATORS.has(childType)) {
|
|
389
|
+
checkField(ctx, child, cPath);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
checkBlock(ctx, child, cPath, false);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
// page columns
|
|
397
|
+
for (const prop of ["sidebar", "main", "detail"]) {
|
|
398
|
+
const blocks = node[prop];
|
|
399
|
+
if (Array.isArray(blocks)) {
|
|
400
|
+
blocks.forEach((child, i) => {
|
|
401
|
+
if (isRec(child)) {
|
|
402
|
+
checkBlock(ctx, child, joinPath(joinPath(path, prop), String(i)), false);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// columns/grid wrappers
|
|
408
|
+
if (type === "columns" || type === "grid") {
|
|
409
|
+
if (Array.isArray(node.children)) {
|
|
410
|
+
node.children.forEach((child, i) => {
|
|
411
|
+
if (!isRec(child) || !isRec(child.block))
|
|
412
|
+
return;
|
|
413
|
+
checkBlock(ctx, child.block, joinPath(joinPath(joinPath(path, "children"), String(i)), "block"), false);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// page.switcher is a choice field
|
|
418
|
+
if (type === "page" && isRec(node.switcher) && node.switcher.type === "choice") {
|
|
419
|
+
checkField(ctx, node.switcher, joinPath(path, "switcher"));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/** Whether a block of this type carries actions or inputs (§4.1 id rule). */
|
|
423
|
+
function carriesActionsOrInputs(type, node) {
|
|
424
|
+
switch (type) {
|
|
425
|
+
case "page":
|
|
426
|
+
return node.actions !== undefined;
|
|
427
|
+
case "callout":
|
|
428
|
+
return node.actions !== undefined;
|
|
429
|
+
case "tree":
|
|
430
|
+
return node.onExpand !== undefined || node.onActivate !== undefined;
|
|
431
|
+
case "form":
|
|
432
|
+
return true; // fields are inputs
|
|
433
|
+
case "collection":
|
|
434
|
+
return (node.onMove !== undefined ||
|
|
435
|
+
node.onCheck !== undefined ||
|
|
436
|
+
node.onActivate !== undefined ||
|
|
437
|
+
node.bulkActions !== undefined ||
|
|
438
|
+
node.reorder !== undefined ||
|
|
439
|
+
node.checkable !== undefined ||
|
|
440
|
+
(node.time !== undefined &&
|
|
441
|
+
isRec(node.time) &&
|
|
442
|
+
(node.time.onResize !== undefined || node.time.onCreate !== undefined)));
|
|
443
|
+
default:
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** Count blocks (§4.5: a collection counts as one). */
|
|
448
|
+
function countBlocks(node) {
|
|
449
|
+
if (!isRec(node))
|
|
450
|
+
return 0;
|
|
451
|
+
if (node.type === "collection")
|
|
452
|
+
return 1;
|
|
453
|
+
let count = 1;
|
|
454
|
+
if (isRec(node.form))
|
|
455
|
+
count += countBlocks(node.form.root); // bulk-action form
|
|
456
|
+
const arrayProps = [
|
|
457
|
+
["children", true],
|
|
458
|
+
["sidebar", true],
|
|
459
|
+
["main", true],
|
|
460
|
+
["detail", true],
|
|
461
|
+
];
|
|
462
|
+
for (const [prop, recurse] of arrayProps) {
|
|
463
|
+
if (!recurse)
|
|
464
|
+
continue;
|
|
465
|
+
const value = node[prop];
|
|
466
|
+
if (Array.isArray(value)) {
|
|
467
|
+
for (const element of value) {
|
|
468
|
+
if (!isRec(element))
|
|
469
|
+
continue;
|
|
470
|
+
if (isRec(element.block)) {
|
|
471
|
+
count += countBlocks(element.block); // columns/grid wrapper
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
count += countBlocks(element);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return count;
|
|
480
|
+
}
|
|
481
|
+
/** The ambient one-line rule for `status`/`footer` (spec §9.2). */
|
|
482
|
+
function checkAmbientLine(ctx, root) {
|
|
483
|
+
if (ctx.placement !== "ambient" || !ctx.ambientPlace || ctx.ambientPlace === "panel")
|
|
484
|
+
return;
|
|
485
|
+
const place = ctx.ambientPlace;
|
|
486
|
+
const rootType = root.type;
|
|
487
|
+
if (rootType === "markdown" || rootType === "stat" || rootType === "meter" || rootType === "log") {
|
|
488
|
+
return; // one line-level block
|
|
489
|
+
}
|
|
490
|
+
if (rootType === "stack") {
|
|
491
|
+
const children = root.children;
|
|
492
|
+
if (Array.isArray(children) && children.length > 1) {
|
|
493
|
+
push(ctx, "root.children", `a ${place} ambient surface allows one line-level block`);
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (rootType === "columns") {
|
|
498
|
+
const children = root.children;
|
|
499
|
+
if (Array.isArray(children)) {
|
|
500
|
+
children.forEach((child, i) => {
|
|
501
|
+
if (!isRec(child) || !isRec(child.block))
|
|
502
|
+
return;
|
|
503
|
+
const innerType = child.block.type;
|
|
504
|
+
if (typeof innerType !== "string")
|
|
505
|
+
return;
|
|
506
|
+
if (!AMBIENT_LINE_BLOCKS.has(innerType)) {
|
|
507
|
+
push(ctx, `root.children[${i}].block`, `${innerType} is not a line-level block for a ${place} ambient surface`);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// fields/callout/diff/details/details bindables are single-line roots
|
|
514
|
+
}
|
|
515
|
+
// ─── the public entry points ──────────────────────────────────────────────
|
|
516
|
+
/**
|
|
517
|
+
* Validate a block tree against a placement (spec §14.2). Returns every
|
|
518
|
+
* error with its JSON path; on success, the narrowed tree.
|
|
519
|
+
*/
|
|
520
|
+
export function validateTree(tree, opts) {
|
|
521
|
+
// version first (§4.4): a tree from another block-set version is
|
|
522
|
+
// rejected before anything else is checked.
|
|
523
|
+
const serverV = opts.serverV ?? BLOCK_SET_VERSION;
|
|
524
|
+
if (isRec(tree) && typeof tree.v === "number" && tree.v !== serverV) {
|
|
525
|
+
return {
|
|
526
|
+
ok: false,
|
|
527
|
+
errors: [
|
|
528
|
+
{
|
|
529
|
+
path: "v",
|
|
530
|
+
message: `unsupported block-set version ${tree.v} (server supports ${serverV})`,
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (!VALID_PLACEMENTS.includes(opts.placement)) {
|
|
536
|
+
return {
|
|
537
|
+
ok: false,
|
|
538
|
+
errors: [{ path: "root", message: `unknown placement "${String(opts.placement)}"` }],
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const errors = [];
|
|
542
|
+
if (!isRec(tree)) {
|
|
543
|
+
return {
|
|
544
|
+
ok: false,
|
|
545
|
+
errors: [{ path: "", message: "a tree must be {v, root}" }],
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (!isRec(tree.root)) {
|
|
549
|
+
return {
|
|
550
|
+
ok: false,
|
|
551
|
+
errors: [{ path: "root", message: "a tree must be {v, root}" }],
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
const root = tree.root;
|
|
555
|
+
const ctx = {
|
|
556
|
+
placement: opts.placement,
|
|
557
|
+
handlers: opts.handlers,
|
|
558
|
+
itemTemplate: false,
|
|
559
|
+
ambientPlace: opts.ambientPlace,
|
|
560
|
+
errors,
|
|
561
|
+
ids: new Map(),
|
|
562
|
+
};
|
|
563
|
+
// limits (§4.5) — sizes are the UTF-8 length of the serialized JSON
|
|
564
|
+
const limit = LIMITS[opts.placement];
|
|
565
|
+
const blocks = countBlocks(root);
|
|
566
|
+
if (blocks > limit.blocks) {
|
|
567
|
+
push(ctx, "root", `surface_too_large: ${blocks} blocks exceeds the ${opts.placement} limit of ${limit.blocks} blocks`);
|
|
568
|
+
}
|
|
569
|
+
const bytes = jsonBytes(tree);
|
|
570
|
+
if (bytes > limit.treeBytes) {
|
|
571
|
+
push(ctx, "root", `surface_too_large: tree is ${bytes} bytes, over the ${opts.placement} limit of ${limit.treeBytes} treeBytes`);
|
|
572
|
+
}
|
|
573
|
+
checkBlock(ctx, root, "root", true);
|
|
574
|
+
checkAmbientLine(ctx, root);
|
|
575
|
+
return errors.length === 0 ? { ok: true, tree: tree } : { ok: false, errors };
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Validate a surface's data object: a JSON object within the placement's
|
|
579
|
+
* data-size limit (spec §4.5).
|
|
580
|
+
*/
|
|
581
|
+
export function validateData(data, placement) {
|
|
582
|
+
if (!isRec(data)) {
|
|
583
|
+
return {
|
|
584
|
+
ok: false,
|
|
585
|
+
errors: [{ path: "", message: "surface data must be a JSON object" }],
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
const bytes = jsonBytes(data);
|
|
589
|
+
const limit = LIMITS[placement];
|
|
590
|
+
if (bytes > limit.dataBytes) {
|
|
591
|
+
return {
|
|
592
|
+
ok: false,
|
|
593
|
+
errors: [
|
|
594
|
+
{
|
|
595
|
+
path: "",
|
|
596
|
+
message: `surface_too_large: data is ${bytes} bytes, over the ${placement} limit of ${limit.dataBytes} dataBytes`,
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
return { ok: true };
|
|
602
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -307,6 +307,22 @@ function parseMetricsPublic(env) {
|
|
|
307
307
|
return false;
|
|
308
308
|
throw new Error(`SIGRUN_METRICS_PUBLIC must be "true" or "false", got ${JSON.stringify(raw)}`);
|
|
309
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Parse `SIGRUN_DEV` (§14.2/§14.4): "true"/"false" case-insensitive, plus
|
|
312
|
+
* the spec's own `SIGRUN_DEV=1`/`=0` shell form. Strict otherwise — `yes`
|
|
313
|
+
* or any other value fails loud naming the variable (the pattern above).
|
|
314
|
+
*/
|
|
315
|
+
function parseDev(env) {
|
|
316
|
+
const raw = env.SIGRUN_DEV;
|
|
317
|
+
if (raw === undefined || raw === "")
|
|
318
|
+
return undefined;
|
|
319
|
+
const lowered = raw.trim().toLowerCase();
|
|
320
|
+
if (lowered === "true" || lowered === "1")
|
|
321
|
+
return true;
|
|
322
|
+
if (lowered === "false" || lowered === "0")
|
|
323
|
+
return false;
|
|
324
|
+
throw new Error(`SIGRUN_DEV must be "true" or "false" (or 1/0), got ${JSON.stringify(raw)}`);
|
|
325
|
+
}
|
|
310
326
|
/**
|
|
311
327
|
* Parse `SIGRUN_AUTH`: a csv of `token`/`proxy` (order preserved,
|
|
312
328
|
* duplicates dropped), `none` ALONE as the empty list (authentication
|
|
@@ -734,6 +750,10 @@ export function resolvedServerSection(config) {
|
|
|
734
750
|
const metrics = config.metricsPublic
|
|
735
751
|
? { public: true }
|
|
736
752
|
: undefined;
|
|
753
|
+
// The development flag (§14.2/§14.4) follows the metrics pattern: a
|
|
754
|
+
// DEFAULTED dev is omitted (persisting `dev: false` would be noise),
|
|
755
|
+
// a dev deployment records it (an operator-visible intent marker).
|
|
756
|
+
const dev = config.dev === true ? true : undefined;
|
|
737
757
|
return {
|
|
738
758
|
port: config.port,
|
|
739
759
|
workspacePath: config.workspace.path,
|
|
@@ -748,6 +768,7 @@ export function resolvedServerSection(config) {
|
|
|
748
768
|
providers: { ...config.providers },
|
|
749
769
|
...(auth === undefined ? {} : { auth }),
|
|
750
770
|
...(metrics === undefined ? {} : { metrics }),
|
|
771
|
+
...(dev === undefined ? {} : { dev }),
|
|
751
772
|
...(backup === undefined ? {} : { backup }),
|
|
752
773
|
};
|
|
753
774
|
}
|
|
@@ -850,6 +871,12 @@ export function envServerSection(env) {
|
|
|
850
871
|
const timezone = env.SIGRUN_TIMEZONE?.trim() || undefined;
|
|
851
872
|
if (timezone !== undefined)
|
|
852
873
|
section.timezone = timezone;
|
|
874
|
+
// The development flag (§14.2/§14.4) — a flat boolean, projected only
|
|
875
|
+
// when the env var is set (a defaulted dev is never persisted: it
|
|
876
|
+
// would read as intent on the next boot).
|
|
877
|
+
const dev = parseDev(env);
|
|
878
|
+
if (dev !== undefined)
|
|
879
|
+
section.dev = dev;
|
|
853
880
|
return section;
|
|
854
881
|
}
|
|
855
882
|
/**
|
|
@@ -924,6 +951,7 @@ export function effectiveServerSection(env, fileServer, config) {
|
|
|
924
951
|
"queueMax",
|
|
925
952
|
]);
|
|
926
953
|
emit("providers.refreshIntervalMinutes", config.providers.refreshIntervalMinutes, ["providers", "refreshIntervalMinutes"]);
|
|
954
|
+
emit("dev", config.dev, ["dev"]);
|
|
927
955
|
if (config.backup?.target === "path") {
|
|
928
956
|
emit("backup.path", config.backup.path, ["backup", "path"]);
|
|
929
957
|
emit("backup.staleAfterHours", config.backupStaleAfterHours, ["backup", "staleAfterHours"]);
|
|
@@ -1070,6 +1098,9 @@ export function loadConfig(env, settingsFile) {
|
|
|
1070
1098
|
},
|
|
1071
1099
|
auth: parseAuth(env, server?.auth),
|
|
1072
1100
|
metricsPublic: parseMetricsPublic(env) ?? server?.metrics?.public ?? false,
|
|
1101
|
+
// The development flag (§14.2/§14.4): strict env parse over the
|
|
1102
|
+
// file's `server.dev`, default false.
|
|
1103
|
+
dev: parseDev(env) ?? server?.dev ?? false,
|
|
1073
1104
|
// The jobs default timezone (§8.2): env over file over the process
|
|
1074
1105
|
// TZ, validated loud (a bad name would otherwise crash the scheduler
|
|
1075
1106
|
// at evaluation time instead of failing boot).
|