@neta-art/cohub 5.9.0 → 6.0.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.
Files changed (55) hide show
  1. package/README.md +42 -46
  2. package/dist/board/animation.d.ts +156 -67
  3. package/dist/board/animation.js +162 -277
  4. package/dist/board/codec.js +1 -1
  5. package/dist/board/export/index.d.ts +6 -0
  6. package/dist/board/export/index.js +6 -3
  7. package/dist/board/export/scene.d.ts +6 -0
  8. package/dist/board/export/scene.js +26 -2
  9. package/dist/board/geometry.d.ts +27 -1
  10. package/dist/board/geometry.js +92 -1
  11. package/dist/board/headless/index.d.ts +7 -1
  12. package/dist/board/headless/index.js +6 -2
  13. package/dist/board/index.d.ts +8 -6
  14. package/dist/board/index.js +7 -6
  15. package/dist/board/mutation.d.ts +15 -0
  16. package/dist/board/mutation.js +67 -0
  17. package/dist/board/render/css-color.d.ts +4 -0
  18. package/dist/board/render/css-color.js +36 -0
  19. package/dist/board/render/index.d.ts +2 -1
  20. package/dist/board/render/index.js +2 -1
  21. package/dist/board/render/renderers/base-card-renderer.js +3 -20
  22. package/dist/board/render/themes/clean-theme.js +6 -3
  23. package/dist/chunks/http.d.ts +30 -7
  24. package/dist/chunks/http.js +785 -289
  25. package/dist/chunks/websocket.d.ts +2675 -187
  26. package/dist/http.d.ts +3 -3
  27. package/dist/index.d.ts +158 -299
  28. package/dist/index.js +164 -279
  29. package/dist/protocol/dist/board-authoring.d.ts +1 -0
  30. package/dist/protocol/dist/board-authoring.js +217 -0
  31. package/dist/protocol/dist/board-capability-registry.d.ts +2 -0
  32. package/dist/protocol/dist/board-capability-registry.js +74 -0
  33. package/dist/protocol/dist/board-codec.d.ts +2 -0
  34. package/dist/protocol/dist/board-codec.js +4 -0
  35. package/dist/protocol/dist/board-composition.d.ts +258 -0
  36. package/dist/protocol/dist/board-composition.js +318 -0
  37. package/dist/protocol/dist/board-constants.d.ts +2 -1
  38. package/dist/protocol/dist/board-constants.js +40 -10
  39. package/dist/protocol/dist/board-document.d.ts +28 -2
  40. package/dist/protocol/dist/board-document.js +16 -3
  41. package/dist/protocol/dist/board-node.d.ts +1 -11
  42. package/dist/protocol/dist/board-node.js +1 -81
  43. package/dist/protocol/dist/board-upgrade.d.ts +1 -0
  44. package/dist/protocol/dist/board-upgrade.js +2 -0
  45. package/dist/protocol/dist/board-url.d.ts +2 -1
  46. package/dist/protocol/dist/board-url.js +8 -3
  47. package/dist/protocol/dist/board.d.ts +135 -76
  48. package/dist/protocol/dist/board.js +50 -69
  49. package/dist/protocol/dist/index.d.ts +9 -4
  50. package/dist/protocol/dist/index.js +9 -4
  51. package/dist/types.d.ts +3 -2
  52. package/docs/work-runtime-guide.md +19 -3
  53. package/package.json +3 -2
  54. package/dist/board/nodes.d.ts +0 -113
  55. package/dist/board/nodes.js +0 -154
@@ -0,0 +1,318 @@
1
+ import { z } from "zod";
2
+ //#region ../protocol/dist/board-composition.js
3
+ const idSchema = z.string().min(1).max(160);
4
+ const finiteSchema = z.number().finite();
5
+ const jsonObjectSchema = z.record(z.string(), z.unknown());
6
+ const BOARD_EASINGS = [
7
+ "linear",
8
+ "ease-in-quad",
9
+ "ease-out-quad",
10
+ "ease-in-out-quad",
11
+ "ease-in-cubic",
12
+ "ease-out-cubic",
13
+ "ease-in-out-cubic",
14
+ "ease-out-quart",
15
+ "ease-out-expo"
16
+ ];
17
+ const BoardEasingSchema = z.enum(BOARD_EASINGS);
18
+ const BoardAnimationTargetSchema = z.discriminatedUnion("type", [
19
+ z.object({
20
+ type: z.literal("item"),
21
+ itemId: idSchema
22
+ }).strict(),
23
+ z.object({
24
+ type: z.literal("effect"),
25
+ effectId: idSchema
26
+ }).strict(),
27
+ z.object({ type: z.literal("camera") }).strict(),
28
+ z.object({ type: z.literal("board") }).strict()
29
+ ]);
30
+ const vector2Schema = z.object({
31
+ x: finiteSchema,
32
+ y: finiteSchema
33
+ }).strict();
34
+ const scaleSchema = z.union([finiteSchema.positive(), z.object({
35
+ x: finiteSchema.positive(),
36
+ y: finiteSchema.positive()
37
+ }).strict()]);
38
+ const BOARD_ANIMATION_CHANNELS = {
39
+ "transform.translation": {
40
+ targets: ["item"],
41
+ value: vector2Schema,
42
+ interpolations: ["linear", "step"],
43
+ coordinateSpace: "world-offset",
44
+ unit: "board"
45
+ },
46
+ "transform.rotation": {
47
+ targets: ["item"],
48
+ value: finiteSchema,
49
+ interpolations: ["linear", "step"],
50
+ unit: "radian"
51
+ },
52
+ "transform.scale": {
53
+ targets: ["item"],
54
+ value: scaleSchema,
55
+ interpolations: ["linear", "step"],
56
+ unit: "ratio"
57
+ },
58
+ "style.opacity": {
59
+ targets: ["item"],
60
+ value: finiteSchema.min(0).max(1),
61
+ interpolations: ["linear", "step"],
62
+ unit: "ratio"
63
+ }
64
+ };
65
+ const BoardTrackKeyframeSchema = z.object({
66
+ time: finiteSchema.nonnegative(),
67
+ value: z.unknown(),
68
+ easing: BoardEasingSchema.optional()
69
+ }).strict();
70
+ const BoardTrackSchema = z.object({
71
+ id: idSchema,
72
+ target: BoardAnimationTargetSchema,
73
+ channel: z.string().min(1).max(160),
74
+ channelVersion: z.number().int().positive().default(1),
75
+ interpolation: z.enum(["linear", "step"]).default("linear"),
76
+ fill: z.enum([
77
+ "none",
78
+ "backwards",
79
+ "forwards",
80
+ "both"
81
+ ]).default("none"),
82
+ keyframes: z.array(BoardTrackKeyframeSchema).min(1).max(1e5),
83
+ metadata: jsonObjectSchema.default({})
84
+ }).strict().superRefine((track, context) => {
85
+ let previous = -1;
86
+ for (const [index, keyframe] of track.keyframes.entries()) {
87
+ if (keyframe.time <= previous) context.addIssue({
88
+ code: "custom",
89
+ message: "keyframe times must be strictly increasing",
90
+ path: [
91
+ "keyframes",
92
+ index,
93
+ "time"
94
+ ]
95
+ });
96
+ previous = keyframe.time;
97
+ }
98
+ const channel = BOARD_ANIMATION_CHANNELS[track.channel];
99
+ if (!channel) {
100
+ if (!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+){2,}$/.test(track.channel)) context.addIssue({
101
+ code: "custom",
102
+ message: `unknown channel: ${track.channel}`,
103
+ path: ["channel"]
104
+ });
105
+ return;
106
+ }
107
+ if (!channel.targets.includes(track.target.type)) context.addIssue({
108
+ code: "custom",
109
+ message: `${track.channel} cannot target ${track.target.type}`,
110
+ path: ["target"]
111
+ });
112
+ if (!channel.interpolations.includes(track.interpolation)) context.addIssue({
113
+ code: "custom",
114
+ message: `${track.channel} does not support ${track.interpolation} interpolation`,
115
+ path: ["interpolation"]
116
+ });
117
+ for (const [index, keyframe] of track.keyframes.entries()) {
118
+ const parsed = channel.value.safeParse(keyframe.value);
119
+ if (!parsed.success) context.addIssue({
120
+ code: "custom",
121
+ message: parsed.error.issues[0]?.message ?? `invalid value for ${track.channel}`,
122
+ path: [
123
+ "keyframes",
124
+ index,
125
+ "value"
126
+ ]
127
+ });
128
+ }
129
+ });
130
+ const extensionKindSchema = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/).max(160);
131
+ const BoardProceduralClipSchema = z.object({
132
+ id: idSchema,
133
+ kind: extensionKindSchema,
134
+ kindVersion: z.number().int().positive(),
135
+ target: BoardAnimationTargetSchema,
136
+ start: finiteSchema.nonnegative(),
137
+ duration: finiteSchema.positive(),
138
+ layer: z.enum([
139
+ "behind",
140
+ "content",
141
+ "front",
142
+ "screen"
143
+ ]).default("content"),
144
+ fill: z.enum([
145
+ "none",
146
+ "backwards",
147
+ "forwards",
148
+ "both"
149
+ ]).default("none"),
150
+ easing: BoardEasingSchema.default("linear"),
151
+ params: jsonObjectSchema.default({}),
152
+ assetRefs: z.array(z.object({
153
+ type: z.enum(["space-file", "extension"]),
154
+ ref: z.string().min(1).max(4096),
155
+ digest: z.string().min(16).max(160).optional()
156
+ }).strict()).default([]),
157
+ seed: idSchema,
158
+ metadata: jsonObjectSchema.default({})
159
+ }).strict();
160
+ const BoardTimelineMarkerSchema = z.object({
161
+ id: idSchema,
162
+ time: finiteSchema.nonnegative(),
163
+ duration: finiteSchema.nonnegative().optional(),
164
+ metadata: jsonObjectSchema.default({})
165
+ }).strict();
166
+ const BoardTimelineSchema = z.object({
167
+ duration: finiteSchema.nonnegative(),
168
+ tracks: z.array(BoardTrackSchema).max(5e4).default([]),
169
+ clips: z.array(BoardProceduralClipSchema).max(5e4).default([]),
170
+ markers: z.array(BoardTimelineMarkerSchema).max(1e4).default([])
171
+ }).strict().superRefine((timeline, context) => {
172
+ const ids = /* @__PURE__ */ new Set();
173
+ const channels = /* @__PURE__ */ new Set();
174
+ for (const [index, track] of timeline.tracks.entries()) {
175
+ if (ids.has(track.id)) context.addIssue({
176
+ code: "custom",
177
+ message: `duplicate timeline id: ${track.id}`,
178
+ path: [
179
+ "tracks",
180
+ index,
181
+ "id"
182
+ ]
183
+ });
184
+ ids.add(track.id);
185
+ const targetId = track.target.type === "item" ? track.target.itemId : track.target.type === "effect" ? track.target.effectId : track.target.type;
186
+ const key = `${track.target.type}:${targetId}:${track.channel}`;
187
+ if (channels.has(key)) context.addIssue({
188
+ code: "custom",
189
+ message: `multiple tracks control ${key}`,
190
+ path: [
191
+ "tracks",
192
+ index,
193
+ "channel"
194
+ ]
195
+ });
196
+ channels.add(key);
197
+ for (const [keyframeIndex, keyframe] of track.keyframes.entries()) if (keyframe.time > timeline.duration) context.addIssue({
198
+ code: "custom",
199
+ message: "keyframe exceeds timeline duration",
200
+ path: [
201
+ "tracks",
202
+ index,
203
+ "keyframes",
204
+ keyframeIndex,
205
+ "time"
206
+ ]
207
+ });
208
+ }
209
+ for (const [index, clip] of timeline.clips.entries()) {
210
+ if (ids.has(clip.id)) context.addIssue({
211
+ code: "custom",
212
+ message: `duplicate timeline id: ${clip.id}`,
213
+ path: [
214
+ "clips",
215
+ index,
216
+ "id"
217
+ ]
218
+ });
219
+ ids.add(clip.id);
220
+ if (clip.start + clip.duration > timeline.duration) context.addIssue({
221
+ code: "custom",
222
+ message: "clip exceeds timeline duration",
223
+ path: [
224
+ "clips",
225
+ index,
226
+ "duration"
227
+ ]
228
+ });
229
+ }
230
+ for (const [index, marker] of timeline.markers.entries()) {
231
+ if (ids.has(marker.id)) context.addIssue({
232
+ code: "custom",
233
+ message: `duplicate timeline id: ${marker.id}`,
234
+ path: [
235
+ "markers",
236
+ index,
237
+ "id"
238
+ ]
239
+ });
240
+ ids.add(marker.id);
241
+ if (marker.time + (marker.duration ?? 0) > timeline.duration) context.addIssue({
242
+ code: "custom",
243
+ message: "marker exceeds timeline duration",
244
+ path: ["markers", index]
245
+ });
246
+ }
247
+ });
248
+ const BoardCompositionPlaybackSchema = z.object({
249
+ loop: z.boolean().default(false),
250
+ endBehavior: z.enum(["hold", "reset"]).default("hold"),
251
+ reducedMotion: z.discriminatedUnion("mode", [
252
+ z.object({ mode: z.literal("base") }).strict(),
253
+ z.object({
254
+ mode: z.literal("time"),
255
+ time: finiteSchema.nonnegative()
256
+ }).strict(),
257
+ z.object({
258
+ mode: z.literal("marker"),
259
+ markerId: idSchema
260
+ }).strict()
261
+ ]).default({ mode: "base" })
262
+ }).strict();
263
+ const compositionFields = {
264
+ id: idSchema,
265
+ name: z.string().min(1).max(255),
266
+ timeline: BoardTimelineSchema,
267
+ playback: BoardCompositionPlaybackSchema.default({
268
+ loop: false,
269
+ endBehavior: "hold",
270
+ reducedMotion: { mode: "base" }
271
+ }),
272
+ metadata: jsonObjectSchema.default({})
273
+ };
274
+ function validateCompositionFallback(composition, context) {
275
+ const fallback = composition.playback.reducedMotion;
276
+ if (fallback.mode === "time" && fallback.time > composition.timeline.duration) context.addIssue({
277
+ code: "custom",
278
+ message: "reduced-motion time exceeds timeline duration",
279
+ path: [
280
+ "playback",
281
+ "reducedMotion",
282
+ "time"
283
+ ]
284
+ });
285
+ if (fallback.mode === "marker" && !composition.timeline.markers.some((marker) => marker.id === fallback.markerId)) context.addIssue({
286
+ code: "custom",
287
+ message: `marker does not exist: ${fallback.markerId}`,
288
+ path: [
289
+ "playback",
290
+ "reducedMotion",
291
+ "markerId"
292
+ ]
293
+ });
294
+ }
295
+ const BoardCompositionInputSchema = z.object(compositionFields).strict().superRefine(validateCompositionFallback);
296
+ const BoardCompositionSchema = z.object({
297
+ ...compositionFields,
298
+ revision: z.number().int().nonnegative().default(0)
299
+ }).strict().superRefine(validateCompositionFallback);
300
+ /** Accept inspect/get output as apply input while stripping server-owned revision. */
301
+ function parseBoardCompositionInput(value) {
302
+ if (value && typeof value === "object" && !Array.isArray(value)) {
303
+ const { revision: _revision, ...input } = value;
304
+ return BoardCompositionInputSchema.parse(input);
305
+ }
306
+ return BoardCompositionInputSchema.parse(value);
307
+ }
308
+ Object.entries(BOARD_ANIMATION_CHANNELS).map(([id, definition]) => ({
309
+ id,
310
+ version: 1,
311
+ targets: definition.targets,
312
+ interpolations: definition.interpolations,
313
+ ..."coordinateSpace" in definition ? { coordinateSpace: definition.coordinateSpace } : {},
314
+ ..."unit" in definition ? { unit: definition.unit } : {},
315
+ valueSchema: z.toJSONSchema(definition.value)
316
+ }));
317
+ //#endregion
318
+ export { BOARD_ANIMATION_CHANNELS, BOARD_EASINGS, BoardAnimationTargetSchema, BoardCompositionInputSchema, BoardCompositionPlaybackSchema, BoardCompositionSchema, BoardEasingSchema, BoardProceduralClipSchema, BoardTimelineMarkerSchema, BoardTimelineSchema, BoardTrackSchema, parseBoardCompositionInput };
@@ -1,4 +1,5 @@
1
1
  //#region ../protocol/dist/board-constants.d.ts
2
+ type BoardCoordinateSpace = "world" | "frame-local" | "world-offset" | "screen" | "screen-offset" | "normalized";
2
3
  type BoardRenderCost = {
3
4
  particles: number;
4
5
  vertices: number;
@@ -30,4 +31,4 @@ declare const BOARD_STROKE_MIN_SIZE = 1;
30
31
  declare const BOARD_STROKE_MAX_SIZE = 64;
31
32
  declare function clampBoardStrokeSize(size: number): number;
32
33
  //#endregion
33
- export { BOARD_STROKE_MAX_SIZE, BOARD_STROKE_MIN_SIZE, BoardCapability, BoardRenderCost, clampBoardStrokeSize };
34
+ export { BOARD_STROKE_MAX_SIZE, BOARD_STROKE_MIN_SIZE, BoardCapability, BoardCoordinateSpace, BoardRenderCost, clampBoardStrokeSize };
@@ -11,7 +11,6 @@ const DEFAULT_BOARD_RENDER_LIMITS = {
11
11
  simulationSteps: 1e5
12
12
  };
13
13
  const BOARD_BUILTIN_CLIP_KINDS = [
14
- "motion.keyframes",
15
14
  "motion.path",
16
15
  "draw.reveal",
17
16
  "draw.handwrite",
@@ -20,9 +19,7 @@ const BOARD_BUILTIN_CLIP_KINDS = [
20
19
  "effects.trail",
21
20
  "effects.impact",
22
21
  "effects.flash",
23
- "effects.color",
24
- "camera.pan",
25
- "camera.zoom",
22
+ "camera.focus",
26
23
  "camera.shake"
27
24
  ];
28
25
  const BOARD_BUILTIN_EFFECT_KINDS = ["effects.pulse", "effects.float"];
@@ -60,12 +57,45 @@ function clampBoardStrokeSize(size) {
60
57
  */
61
58
  const BOARD_FONT_STACK = "\"Geist\", system-ui, -apple-system, \"Noto Sans CJK SC\", \"Noto Sans SC\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif";
62
59
  const BOARD_MONO_FONT_STACK = "\"Geist Mono\", \"Fira Code\", ui-monospace, \"Noto Sans Mono CJK SC\", monospace";
63
- const BOARD_BUILTIN_CAPABILITIES = [...BOARD_BUILTIN_CLIP_KINDS.map((id) => ({
64
- kind: "clip",
65
- id,
66
- version: 1,
67
- renderers: ["webgpu", "webgl"]
68
- })), ...BOARD_BUILTIN_EFFECT_KINDS.map((id) => ({
60
+ function clipSchema(id) {
61
+ switch (id) {
62
+ case "motion.path": return { params: { points: {
63
+ coordinateSpace: "world-offset",
64
+ unit: "board"
65
+ } } };
66
+ case "effects.particles": return { params: { bounds: {
67
+ coordinateSpace: "world",
68
+ unit: "board"
69
+ } } };
70
+ case "camera.focus": return { params: {
71
+ focus: {
72
+ coordinateSpace: "world",
73
+ unit: "board"
74
+ },
75
+ padding: {
76
+ coordinateSpace: "screen",
77
+ unit: "css-px"
78
+ },
79
+ minZoom: { unit: "ratio" },
80
+ maxZoom: { unit: "ratio" }
81
+ } };
82
+ case "camera.shake": return { params: { amount: {
83
+ coordinateSpace: "screen-offset",
84
+ unit: "css-px"
85
+ } } };
86
+ default: return;
87
+ }
88
+ }
89
+ const BOARD_BUILTIN_CAPABILITIES = [...BOARD_BUILTIN_CLIP_KINDS.map((id) => {
90
+ const schema = clipSchema(id);
91
+ return {
92
+ kind: "clip",
93
+ id,
94
+ version: 1,
95
+ renderers: ["webgpu", "webgl"],
96
+ ...schema ? { schema } : {}
97
+ };
98
+ }), ...BOARD_BUILTIN_EFFECT_KINDS.map((id) => ({
69
99
  kind: "effect",
70
100
  id,
71
101
  version: 1,
@@ -1,5 +1,5 @@
1
1
  import { BOARD_DOCUMENT_KIND, BOARD_EXTENSION } from "./board.js";
2
- import { BOARD_REMOTE_URL_MAX_LENGTH, BoardRemoteUrlSchema, normalizeBoardRemoteUrl } from "./board-url.js";
2
+ import { BOARD_REMOTE_URL_MAX_LENGTH, BoardRemoteUrlSchema, isPublicBoardRemoteAddress, normalizeBoardRemoteUrl } from "./board-url.js";
3
3
  import { z } from "zod";
4
4
  //#region ../protocol/dist/board-document.d.ts
5
5
  declare const BoardFrameSchema: z.ZodObject<{
@@ -33,6 +33,19 @@ declare const BoardAppearanceSchema: z.ZodObject<{
33
33
  }>>;
34
34
  color: z.ZodOptional<z.ZodString>;
35
35
  imageUrl: z.ZodOptional<z.ZodString>;
36
+ fit: z.ZodOptional<z.ZodEnum<{
37
+ contain: "contain";
38
+ cover: "cover";
39
+ repeat: "repeat";
40
+ }>>;
41
+ position: z.ZodOptional<z.ZodEnum<{
42
+ bottom: "bottom";
43
+ center: "center";
44
+ left: "left";
45
+ right: "right";
46
+ top: "top";
47
+ }>>;
48
+ opacity: z.ZodOptional<z.ZodNumber>;
36
49
  }, z.core.$strip>>;
37
50
  grid: z.ZodDefault<z.ZodObject<{
38
51
  visible: z.ZodDefault<z.ZodBoolean>;
@@ -1024,6 +1037,19 @@ declare const BoardDocumentSchema: z.ZodObject<{
1024
1037
  }>>;
1025
1038
  color: z.ZodOptional<z.ZodString>;
1026
1039
  imageUrl: z.ZodOptional<z.ZodString>;
1040
+ fit: z.ZodOptional<z.ZodEnum<{
1041
+ contain: "contain";
1042
+ cover: "cover";
1043
+ repeat: "repeat";
1044
+ }>>;
1045
+ position: z.ZodOptional<z.ZodEnum<{
1046
+ bottom: "bottom";
1047
+ center: "center";
1048
+ left: "left";
1049
+ right: "right";
1050
+ top: "top";
1051
+ }>>;
1052
+ opacity: z.ZodOptional<z.ZodNumber>;
1027
1053
  }, z.core.$strip>>;
1028
1054
  grid: z.ZodDefault<z.ZodObject<{
1029
1055
  visible: z.ZodDefault<z.ZodBoolean>;
@@ -1492,4 +1518,4 @@ declare function isMediaItem(item: BoardItem): item is BoardImageItem | BoardVid
1492
1518
  /** Whether an item references a workspace file (media or file card). */
1493
1519
  declare function isFileBackedItem(item: BoardItem): item is BoardImageItem | BoardVideoItem | BoardAudioItem | BoardFileItem;
1494
1520
  //#endregion
1495
- export { BOARD_DOCUMENT_KIND, BOARD_EXTENSION, BOARD_REMOTE_URL_MAX_LENGTH, BOARD_TASK_ARTIFACT_LIMIT, BoardAppearance, BoardAppearanceSchema, BoardArrowItem, BoardArrowItemSchema, BoardAudioItem, BoardAudioItemSchema, BoardDocument, BoardDocumentSchema, BoardDrawItem, BoardDrawItemSchema, BoardFileItem, BoardFileItemSchema, BoardFileSnapshot, BoardFileSnapshotSchema, BoardFrame, BoardFrameItem, BoardFrameItemSchema, BoardFrameSchema, BoardGeoItem, BoardGeoItemSchema, BoardImageItem, BoardImageItemSchema, BoardItem, BoardItemSchema, BoardItemStyle, BoardItemStyleSchema, BoardKnownItem, BoardMediaSnapshot, BoardMediaSnapshotSchema, BoardPoint, BoardPointSchema, BoardRemoteUrlSchema, BoardTaskArtifact, BoardTaskArtifactSchema, BoardTaskItem, BoardTaskItemSchema, BoardTaskSnapshot, BoardTaskSnapshotSchema, BoardTextItem, BoardTextItemSchema, BoardUnknownItem, BoardVideoItem, BoardVideoItemSchema, BoardViewport, BoardViewportSchema, DrawPoint, DrawPointSchema, KNOWN_BOARD_ITEM_TYPES, KnownBoardItemType, SpaceFileRef, SpaceFileRefSchema, UNKNOWN_BOARD_ITEM_TYPE, isFileBackedItem, isMediaItem, isUnknownItem, normalizeBoardRemoteUrl, parseBoardDocument, parseBoardItemLoose, unknownRealType, withResolvedConnections };
1521
+ export { BOARD_DOCUMENT_KIND, BOARD_EXTENSION, BOARD_REMOTE_URL_MAX_LENGTH, BOARD_TASK_ARTIFACT_LIMIT, BoardAppearance, BoardAppearanceSchema, BoardArrowItem, BoardArrowItemSchema, BoardAudioItem, BoardAudioItemSchema, BoardDocument, BoardDocumentSchema, BoardDrawItem, BoardDrawItemSchema, BoardFileItem, BoardFileItemSchema, BoardFileSnapshot, BoardFileSnapshotSchema, BoardFrame, BoardFrameItem, BoardFrameItemSchema, BoardFrameSchema, BoardGeoItem, BoardGeoItemSchema, BoardImageItem, BoardImageItemSchema, BoardItem, BoardItemSchema, BoardItemStyle, BoardItemStyleSchema, BoardKnownItem, BoardMediaSnapshot, BoardMediaSnapshotSchema, BoardPoint, BoardPointSchema, BoardRemoteUrlSchema, BoardTaskArtifact, BoardTaskArtifactSchema, BoardTaskItem, BoardTaskItemSchema, BoardTaskSnapshot, BoardTaskSnapshotSchema, BoardTextItem, BoardTextItemSchema, BoardUnknownItem, BoardVideoItem, BoardVideoItemSchema, BoardViewport, BoardViewportSchema, DrawPoint, DrawPointSchema, KNOWN_BOARD_ITEM_TYPES, KnownBoardItemType, SpaceFileRef, SpaceFileRefSchema, UNKNOWN_BOARD_ITEM_TYPE, isFileBackedItem, isMediaItem, isPublicBoardRemoteAddress, isUnknownItem, normalizeBoardRemoteUrl, parseBoardDocument, parseBoardItemLoose, unknownRealType, withResolvedConnections };
@@ -1,7 +1,7 @@
1
1
  import { BOARD_ARROW_STROKE_SIZE } from "./board-constants.js";
2
2
  import { BoardConnectionSchema } from "./board-connection.js";
3
3
  import { BOARD_DOCUMENT_KIND, BOARD_EXTENSION } from "./board.js";
4
- import { BOARD_REMOTE_URL_MAX_LENGTH, BoardRemoteUrlSchema, normalizeBoardRemoteUrl } from "./board-url.js";
4
+ import { BOARD_REMOTE_URL_MAX_LENGTH, BoardRemoteUrlSchema, isPublicBoardRemoteAddress, normalizeBoardRemoteUrl } from "./board-url.js";
5
5
  import { z } from "zod";
6
6
  //#region ../protocol/dist/board-document.js
7
7
  const BoardFrameSchema = z.object({
@@ -34,7 +34,20 @@ const BoardAppearanceSchema = z.object({
34
34
  "custom"
35
35
  ]).default("dots"),
36
36
  color: z.string().optional(),
37
- imageUrl: z.string().url().optional()
37
+ imageUrl: BoardRemoteUrlSchema.optional(),
38
+ fit: z.enum([
39
+ "cover",
40
+ "contain",
41
+ "repeat"
42
+ ]).optional(),
43
+ position: z.enum([
44
+ "center",
45
+ "top",
46
+ "bottom",
47
+ "left",
48
+ "right"
49
+ ]).optional(),
50
+ opacity: z.number().finite().min(0).max(1).optional()
38
51
  }).default({ kind: "solid" }),
39
52
  grid: z.object({
40
53
  visible: z.boolean().default(false),
@@ -435,4 +448,4 @@ function isFileBackedItem(item) {
435
448
  return item.type === "image" || item.type === "video" || item.type === "audio" || item.type === "file";
436
449
  }
437
450
  //#endregion
438
- export { BOARD_DOCUMENT_KIND, BOARD_EXTENSION, BOARD_REMOTE_URL_MAX_LENGTH, BOARD_TASK_ARTIFACT_LIMIT, BoardAppearanceSchema, BoardArrowItemSchema, BoardAudioItemSchema, BoardDocumentSchema, BoardDrawItemSchema, BoardFileItemSchema, BoardFileSnapshotSchema, BoardFrameItemSchema, BoardFrameSchema, BoardGeoItemSchema, BoardImageItemSchema, BoardItemSchema, BoardItemStyleSchema, BoardMediaSnapshotSchema, BoardPointSchema, BoardRemoteUrlSchema, BoardTaskArtifactSchema, BoardTaskItemSchema, BoardTaskSnapshotSchema, BoardTextItemSchema, BoardVideoItemSchema, BoardViewportSchema, DrawPointSchema, KNOWN_BOARD_ITEM_TYPES, SpaceFileRefSchema, UNKNOWN_BOARD_ITEM_TYPE, isFileBackedItem, isMediaItem, isUnknownItem, normalizeBoardRemoteUrl, parseBoardDocument, parseBoardItemLoose, unknownRealType, withResolvedConnections };
451
+ export { BOARD_DOCUMENT_KIND, BOARD_EXTENSION, BOARD_REMOTE_URL_MAX_LENGTH, BOARD_TASK_ARTIFACT_LIMIT, BoardAppearanceSchema, BoardArrowItemSchema, BoardAudioItemSchema, BoardDocumentSchema, BoardDrawItemSchema, BoardFileItemSchema, BoardFileSnapshotSchema, BoardFrameItemSchema, BoardFrameSchema, BoardGeoItemSchema, BoardImageItemSchema, BoardItemSchema, BoardItemStyleSchema, BoardMediaSnapshotSchema, BoardPointSchema, BoardRemoteUrlSchema, BoardTaskArtifactSchema, BoardTaskItemSchema, BoardTaskSnapshotSchema, BoardTextItemSchema, BoardVideoItemSchema, BoardViewportSchema, DrawPointSchema, KNOWN_BOARD_ITEM_TYPES, SpaceFileRefSchema, UNKNOWN_BOARD_ITEM_TYPE, isFileBackedItem, isMediaItem, isPublicBoardRemoteAddress, isUnknownItem, normalizeBoardRemoteUrl, parseBoardDocument, parseBoardItemLoose, unknownRealType, withResolvedConnections };
@@ -4,15 +4,5 @@ declare const BOARD_COLOR_IDS: readonly ["brand", "neutral", "black", "white", "
4
4
  type BoardColorId = (typeof BOARD_COLOR_IDS)[number];
5
5
  declare const BOARD_GEO_KINDS: readonly ["rectangle", "rounded", "ellipse", "diamond", "triangle"];
6
6
  type BoardGeoKind = (typeof BOARD_GEO_KINDS)[number];
7
- type BoardNodeValidationDiagnostic = {
8
- severity: "error";
9
- code: "INVALID_BOARD_NODE" | "INVALID_BOARD_GEOMETRY";
10
- message: string;
11
- path: string;
12
- expected?: string;
13
- received?: unknown;
14
- allowedValues?: readonly string[];
15
- coordinateSpace?: "frame-local" | "world";
16
- };
17
7
  //#endregion
18
- export { BOARD_COLOR_IDS, BOARD_GEO_KINDS, BoardColorId, BoardGeoKind, BoardNodeValidationDiagnostic };
8
+ export { BOARD_COLOR_IDS, BOARD_GEO_KINDS, BoardColorId, BoardGeoKind };
@@ -155,85 +155,5 @@ function jsonSchema(schema) {
155
155
  return z.toJSONSchema(schema);
156
156
  }
157
157
  jsonSchema(nodeEnvelopeSchema), Object.fromEntries(BOARD_NATIVE_NODE_TYPES.map((type) => [type, jsonSchema(dataSchemas[type])])), Object.fromEntries(BOARD_NATIVE_NODE_TYPES.map((type) => [type, jsonSchema(viewSchemas[type])]));
158
- function issueDiagnostic(issue, path) {
159
- const fullPath = [path, ...issue.path].join(".");
160
- const values = "values" in issue && Array.isArray(issue.values) ? issue.values.filter((value) => typeof value === "string") : void 0;
161
- return {
162
- severity: "error",
163
- code: "INVALID_BOARD_NODE",
164
- message: `${fullPath}: ${issue.message}`,
165
- path: fullPath,
166
- ...values?.length ? { allowedValues: values } : {}
167
- };
168
- }
169
- function drawGeometryDiagnostic(node, data, path) {
170
- let minX = Number.POSITIVE_INFINITY;
171
- let minY = Number.POSITIVE_INFINITY;
172
- let maxX = Number.NEGATIVE_INFINITY;
173
- let maxY = Number.NEGATIVE_INFINITY;
174
- for (const point of data.points) {
175
- const radius = Math.max(.5, data.size / 2 * (.5 + point.p));
176
- minX = Math.min(minX, point.x - radius);
177
- minY = Math.min(minY, point.y - radius);
178
- maxX = Math.max(maxX, point.x + radius);
179
- maxY = Math.max(maxY, point.y + radius);
180
- }
181
- const width = Math.max(1, maxX - minX);
182
- const height = Math.max(1, maxY - minY);
183
- const tolerance = Math.max(.01, node.width * 1e-6, node.height * 1e-6);
184
- if (Math.abs(minX) <= tolerance && Math.abs(minY) <= tolerance && Math.abs(width - node.width) <= tolerance && Math.abs(height - node.height) <= tolerance) return null;
185
- return {
186
- severity: "error",
187
- code: "INVALID_BOARD_GEOMETRY",
188
- message: `${path}.data.points must use frame-local coordinates and match the node frame`,
189
- path: `${path}.data.points`,
190
- expected: "frame-local points with bounds matching width and height",
191
- coordinateSpace: "frame-local"
192
- };
193
- }
194
- function validateBoardNodeInput(node, path = "node") {
195
- const envelopeResult = nodeEnvelopeSchema.safeParse(node);
196
- if (!envelopeResult.success) return envelopeResult.error.issues.map((issue) => issueDiagnostic(issue, path));
197
- if (typeof node.type !== "string" || !BOARD_NATIVE_NODE_TYPES.includes(node.type)) return [{
198
- severity: "error",
199
- code: "INVALID_BOARD_NODE",
200
- message: `${path}.type is not supported`,
201
- path: `${path}.type`,
202
- expected: "BoardNativeNodeType",
203
- received: node.type,
204
- allowedValues: BOARD_NATIVE_NODE_TYPES
205
- }];
206
- const type = node.type;
207
- const dataResult = dataSchemas[type].safeParse(node.data ?? {});
208
- if (!dataResult.success) return dataResult.error.issues.map((issue) => issueDiagnostic(issue, `${path}.data`));
209
- const viewResult = viewSchemas[type].safeParse(node.view ?? {});
210
- if (!viewResult.success) return viewResult.error.issues.map((issue) => issueDiagnostic(issue, `${path}.view`));
211
- if (type === "image" || type === "video" || type === "audio" || type === "file") {
212
- if (node.refKind !== "space_file" || typeof node.refPath !== "string" || !node.refPath) return [{
213
- severity: "error",
214
- code: "INVALID_BOARD_NODE",
215
- message: `${path} requires a space file reference`,
216
- path: `${path}.refPath`,
217
- expected: "non-empty refPath with refKind space_file"
218
- }];
219
- }
220
- if (type === "draw") {
221
- const diagnostic = drawGeometryDiagnostic(node, dataResult.data, path);
222
- return diagnostic ? [diagnostic] : [];
223
- }
224
- if (type === "arrow") {
225
- const data = dataResult.data;
226
- const inside = (point) => point.x >= node.x && point.x <= node.x + node.width && point.y >= node.y && point.y <= node.y + node.height;
227
- if (!inside(data.start) || !inside(data.end)) return [{
228
- severity: "error",
229
- code: "INVALID_BOARD_GEOMETRY",
230
- message: `${path}.data endpoints must be covered by the node frame`,
231
- path: `${path}.data`,
232
- expected: "world-space endpoints inside the node frame",
233
- coordinateSpace: "world"
234
- }];
235
- }
236
- return [];
237
- }
238
158
  //#endregion
239
- export { BOARD_COLOR_IDS, BOARD_GEO_KINDS, BOARD_NATIVE_NODE_TYPES, BoardColorIdSchema, BoardGeoKindSchema, validateBoardNodeInput };
159
+ export { BOARD_COLOR_IDS, BOARD_GEO_KINDS, BOARD_NATIVE_NODE_TYPES, BoardColorIdSchema, BoardGeoKindSchema };
@@ -0,0 +1 @@
1
+ import "./board.js";
@@ -0,0 +1,2 @@
1
+ import "./board.js";
2
+ export {};
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  //#region ../protocol/dist/board-url.d.ts
3
3
  declare const BOARD_REMOTE_URL_MAX_LENGTH = 4096;
4
+ declare function isPublicBoardRemoteAddress(value: string): boolean;
4
5
  /**
5
6
  * Normalize a browser-loadable public HTTP(S) URL. This blocks explicit local
6
7
  * addresses; any future server-side fetcher must additionally validate DNS
@@ -9,4 +10,4 @@ declare const BOARD_REMOTE_URL_MAX_LENGTH = 4096;
9
10
  declare function normalizeBoardRemoteUrl(value: unknown): string | undefined;
10
11
  declare const BoardRemoteUrlSchema: z.ZodString;
11
12
  //#endregion
12
- export { BOARD_REMOTE_URL_MAX_LENGTH, BoardRemoteUrlSchema, normalizeBoardRemoteUrl };
13
+ export { BOARD_REMOTE_URL_MAX_LENGTH, BoardRemoteUrlSchema, isPublicBoardRemoteAddress, normalizeBoardRemoteUrl };
@@ -53,11 +53,16 @@ function isBlockedIpv6(host) {
53
53
  if ((first & 65280) === 65280) return true;
54
54
  return parts[0] === "2001" && parts[1] === "0db8";
55
55
  }
56
+ function isPublicBoardRemoteAddress(value) {
57
+ const address = value.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
58
+ if (parseIpv4(address)) return !isBlockedIpv4(address);
59
+ return address.includes(":") && !isBlockedIpv6(address);
60
+ }
56
61
  function isBlockedHost(hostname) {
57
62
  const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
58
63
  if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
59
- if (parseIpv4(host)) return isBlockedIpv4(host);
60
- return host.includes(":") && isBlockedIpv6(host);
64
+ if (parseIpv4(host) || host.includes(":")) return !isPublicBoardRemoteAddress(host);
65
+ return false;
61
66
  }
62
67
  /**
63
68
  * Normalize a browser-loadable public HTTP(S) URL. This blocks explicit local
@@ -80,4 +85,4 @@ function normalizeBoardRemoteUrl(value) {
80
85
  }
81
86
  const BoardRemoteUrlSchema = z.string().max(BOARD_REMOTE_URL_MAX_LENGTH).refine((value) => normalizeBoardRemoteUrl(value) !== void 0, { message: "URL must be a public HTTP(S) URL without credentials" });
82
87
  //#endregion
83
- export { BOARD_REMOTE_URL_MAX_LENGTH, BoardRemoteUrlSchema, normalizeBoardRemoteUrl };
88
+ export { BOARD_REMOTE_URL_MAX_LENGTH, BoardRemoteUrlSchema, isPublicBoardRemoteAddress, normalizeBoardRemoteUrl };