@neta-art/cohub 8.11.0 → 8.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/board/animation.js +14 -2
  2. package/dist/board/core/file-preview.d.ts +2 -2
  3. package/dist/board/core/file-preview.js +2 -2
  4. package/dist/board/index.d.ts +2 -1
  5. package/dist/board/index.js +2 -1
  6. package/dist/board/mutation.js +2 -1
  7. package/dist/board/render/board-background.d.ts +16 -0
  8. package/dist/board/render/{themes/clean-theme.js → board-background.js} +28 -35
  9. package/dist/board/render/index.d.ts +3 -3
  10. package/dist/board/render/index.js +2 -2
  11. package/dist/board/render/renderers/file-card-renderer.js +3 -32
  12. package/dist/board/replay.d.ts +52 -0
  13. package/dist/board/replay.js +261 -0
  14. package/dist/board/semantic-document.d.ts +9 -2
  15. package/dist/board/semantic-document.js +1 -3
  16. package/dist/chunks/environment.d.ts +296 -9
  17. package/dist/chunks/environment.js +1 -0
  18. package/dist/chunks/http.d.ts +54 -4
  19. package/dist/chunks/http.js +67 -12
  20. package/dist/chunks/websocket.d.ts +1 -1
  21. package/dist/http.d.ts +3 -3
  22. package/dist/index.d.ts +11 -4
  23. package/dist/index.js +201 -51
  24. package/dist/protocol/dist/board-animation.d.ts +1 -0
  25. package/dist/protocol/dist/board-animation.js +34 -0
  26. package/dist/protocol/dist/board-authoring.d.ts +2 -1
  27. package/dist/protocol/dist/board-capability-registry.js +53 -3
  28. package/dist/protocol/dist/board-codec.js +149 -2
  29. package/dist/protocol/dist/board-constants.js +29 -7
  30. package/dist/protocol/dist/board-document.d.ts +30 -16
  31. package/dist/protocol/dist/board-document.js +15 -12
  32. package/dist/protocol/dist/board-effect.d.ts +4 -2
  33. package/dist/protocol/dist/board-effect.js +3 -2
  34. package/dist/protocol/dist/board.d.ts +148 -3
  35. package/dist/protocol/dist/board.js +7 -0
  36. package/dist/protocol/dist/index.d.ts +4 -2
  37. package/dist/protocol/dist/provenance.d.ts +21 -0
  38. package/dist/types.d.ts +1 -1
  39. package/docs/app-runtime-guide.md +7 -4
  40. package/package.json +1 -1
  41. package/dist/board/render/themes/board-theme-registry.d.ts +0 -22
  42. package/dist/board/render/themes/board-theme-registry.js +0 -13
  43. package/dist/board/render/themes/clean-theme.d.ts +0 -5
package/dist/index.js CHANGED
@@ -8,8 +8,8 @@ import { z } from "zod";
8
8
  const BOARD_DOCUMENT_KIND = "cohub.board";
9
9
  const BOARD_MANIFEST_KIND = "cohub.board.manifest";
10
10
  const idSchema = z.string().min(1).max(160);
11
- const jsonObjectSchema = z.record(z.string(), z.unknown());
12
- const finiteSchema = z.number().finite();
11
+ const jsonObjectSchema$1 = z.record(z.string(), z.unknown());
12
+ const finiteSchema$1 = z.number().finite();
13
13
  z.object({
14
14
  kind: z.literal(BOARD_MANIFEST_KIND),
15
15
  version: z.literal(1),
@@ -17,18 +17,18 @@ z.object({
17
17
  title: z.string().min(1).max(255)
18
18
  });
19
19
  z.object({
20
- centerX: finiteSchema,
21
- centerY: finiteSchema,
22
- zoom: finiteSchema.positive()
20
+ centerX: finiteSchema$1,
21
+ centerY: finiteSchema$1,
22
+ zoom: finiteSchema$1.positive()
23
23
  });
24
24
  const BoardCameraFocusSchema = z.discriminatedUnion("type", [
25
25
  z.object({
26
26
  type: z.literal("rect"),
27
27
  rect: z.object({
28
- x: finiteSchema,
29
- y: finiteSchema,
30
- width: finiteSchema.positive(),
31
- height: finiteSchema.positive()
28
+ x: finiteSchema$1,
29
+ y: finiteSchema$1,
30
+ width: finiteSchema$1.positive(),
31
+ height: finiteSchema$1.positive()
32
32
  })
33
33
  }),
34
34
  z.object({
@@ -47,9 +47,9 @@ const BoardCameraFocusSchema = z.discriminatedUnion("type", [
47
47
  const BoardCameraFocusParamsSchema = z.object({
48
48
  focus: BoardCameraFocusSchema,
49
49
  fit: z.enum(["contain", "cover"]).default("contain"),
50
- padding: finiteSchema.nonnegative().default(32),
51
- minZoom: finiteSchema.positive().optional(),
52
- maxZoom: finiteSchema.positive().optional()
50
+ padding: finiteSchema$1.nonnegative().default(32),
51
+ minZoom: finiteSchema$1.positive().optional(),
52
+ maxZoom: finiteSchema$1.positive().optional()
53
53
  }).superRefine((value, context) => {
54
54
  if (value.minZoom !== void 0 && value.maxZoom !== void 0 && value.minZoom > value.maxZoom) context.addIssue({
55
55
  code: "custom",
@@ -62,17 +62,17 @@ z.object({
62
62
  type: z.string().min(1).max(40),
63
63
  parentId: idSchema.nullable(),
64
64
  orderKey: z.string().max(4096).nullable(),
65
- x: finiteSchema,
66
- y: finiteSchema,
67
- width: finiteSchema.positive(),
68
- height: finiteSchema.positive(),
69
- rotation: finiteSchema,
65
+ x: finiteSchema$1,
66
+ y: finiteSchema$1,
67
+ width: finiteSchema$1.positive(),
68
+ height: finiteSchema$1.positive(),
69
+ rotation: finiteSchema$1,
70
70
  refKind: z.string().max(40).nullable(),
71
71
  refPath: z.string().max(4096).nullable(),
72
72
  refUrl: z.string().max(4096).nullable(),
73
- view: jsonObjectSchema,
74
- style: jsonObjectSchema,
75
- data: jsonObjectSchema
73
+ view: jsonObjectSchema$1,
74
+ style: jsonObjectSchema$1,
75
+ data: jsonObjectSchema$1
76
76
  });
77
77
  const stripBoardServerFields = (value) => {
78
78
  if (!value || typeof value !== "object" || Array.isArray(value)) return value;
@@ -86,7 +86,7 @@ const BoardCreateInputSchema = z.object({
86
86
  /** Reused by clients when board creation is interrupted and retried. */
87
87
  mutationId: z.string().max(128).optional(),
88
88
  title: z.string().min(1).max(255).optional(),
89
- metadata: jsonObjectSchema.optional(),
89
+ metadata: jsonObjectSchema$1.optional(),
90
90
  items: z.array(BoardAuthoringItemSchema).max(5e4).optional(),
91
91
  connections: z.array(BoardConnectionSchema).max(5e4).optional(),
92
92
  effects: z.array(BoardCreateEffectInputSchema).max(5e4).optional(),
@@ -132,17 +132,24 @@ z.object({
132
132
  effectIds: z.array(idSchema).max(5e4).optional(),
133
133
  compositionIds: z.array(idSchema).max(5e4).optional(),
134
134
  viewport: z.object({
135
- x: finiteSchema,
136
- y: finiteSchema,
137
- width: finiteSchema.positive(),
138
- height: finiteSchema.positive()
135
+ x: finiteSchema$1,
136
+ y: finiteSchema$1,
137
+ width: finiteSchema$1.positive(),
138
+ height: finiteSchema$1.positive()
139
139
  }).optional()
140
140
  });
141
+ z.object({
142
+ /** Return transactions with `version < before`. Omit for the newest page. */
143
+ before: z.number().int().positive().optional(),
144
+ limit: z.number().int().min(1).max(500).default(200),
145
+ /** Include the current rows on the newest page (default true). Tail refreshes turn this off. */
146
+ snapshot: z.boolean().default(true)
147
+ });
141
148
  /** Persisted on `boards.metadata.playback`: how a Board plays when opened. */
142
149
  const BoardPlaybackPolicySchema = z.object({
143
150
  compositionId: idSchema,
144
151
  /** Delay before the first local playback after opening the Board, in milliseconds. */
145
- delayMs: finiteSchema.nonnegative().default(0)
152
+ delayMs: finiteSchema$1.nonnegative().default(0)
146
153
  });
147
154
  function parseBoardPlaybackPolicy(metadata) {
148
155
  const parsed = BoardPlaybackPolicySchema.safeParse(metadata.playback);
@@ -153,8 +160,8 @@ z.discriminatedUnion("type", [
153
160
  commandId: idSchema,
154
161
  type: z.literal("play"),
155
162
  compositionId: idSchema,
156
- position: finiteSchema.nonnegative().optional(),
157
- timeScale: finiteSchema.positive().max(4).optional(),
163
+ position: finiteSchema$1.nonnegative().optional(),
164
+ timeScale: finiteSchema$1.positive().max(4).optional(),
158
165
  shared: z.boolean().optional(),
159
166
  seed: idSchema.optional()
160
167
  }),
@@ -167,7 +174,7 @@ z.discriminatedUnion("type", [
167
174
  commandId: idSchema,
168
175
  type: z.literal("seek"),
169
176
  playbackId: z.string().uuid(),
170
- position: finiteSchema.nonnegative()
177
+ position: finiteSchema$1.nonnegative()
171
178
  }),
172
179
  z.object({
173
180
  commandId: idSchema,
@@ -176,9 +183,49 @@ z.discriminatedUnion("type", [
176
183
  })
177
184
  ]);
178
185
  //#endregion
186
+ //#region ../protocol/dist/board-animation.js
187
+ const extensionKindSchema = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/).max(160);
188
+ const jsonObjectSchema = z.record(z.string(), z.unknown());
189
+ const finiteSchema = z.number().finite();
190
+ /** Parameters shared by the built-in deal entrance preset. */
191
+ const BoardDealParamsSchema = z.object({
192
+ lift: finiteSchema.nonnegative().max(2e3).optional(),
193
+ swing: finiteSchema.nonnegative().max(2e3).optional(),
194
+ curve: finiteSchema.nonnegative().max(2e3).optional(),
195
+ tilt: finiteSchema.nonnegative().max(180).optional(),
196
+ scale: finiteSchema.positive().max(2).optional(),
197
+ duration: finiteSchema.positive().max(1e4).optional(),
198
+ landing: finiteSchema.nonnegative().max(1e4).optional()
199
+ }).strict();
200
+ /** A versioned, reusable animation preset reference. */
201
+ const BoardAnimationSpecSchema = z.object({
202
+ kind: extensionKindSchema,
203
+ kindVersion: z.number().int().positive().default(1),
204
+ params: jsonObjectSchema.default({})
205
+ }).strict().superRefine((spec, context) => {
206
+ if (spec.kind !== "effects.deal" || spec.kindVersion !== 1) return;
207
+ const parsed = BoardDealParamsSchema.safeParse(spec.params);
208
+ if (parsed.success) return;
209
+ context.addIssue({
210
+ code: "custom",
211
+ message: parsed.error.issues[0]?.message ?? "invalid effects.deal parameters",
212
+ path: ["params", ...parsed.error.issues[0]?.path ?? []]
213
+ });
214
+ });
215
+ /** Board-wide default motion policies. Omitted policies mean no motion. */
216
+ const BoardMotionSchema = z.object({ enter: BoardAnimationSpecSchema.optional() }).strict();
217
+ //#endregion
179
218
  //#region ../protocol/dist/board-capability-registry.js
180
219
  const CLIPS = new Set(BOARD_BUILTIN_CLIP_KINDS);
181
- new Set(BOARD_BUILTIN_EFFECT_KINDS);
220
+ const EFFECTS = new Set(BOARD_BUILTIN_EFFECT_KINDS);
221
+ const BUILTIN_VERSIONS = new Set(BOARD_BUILTIN_CAPABILITIES.map((c) => `${c.kind}:${c.id}@${c.version}`));
222
+ /**
223
+ * True when a built-in renderer exists for exactly this kind@version. A known
224
+ * kind at an unknown version is not built-in: it must not pass as one.
225
+ */
226
+ function isBuiltinBoardCapability(kind, id, version) {
227
+ return BUILTIN_VERSIONS.has(`${kind}:${id}@${version}`);
228
+ }
182
229
  const record = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
183
230
  const finite = (value) => typeof value === "number" && Number.isFinite(value);
184
231
  function validateBuiltinBoardClip(clip, path = "clip") {
@@ -212,6 +259,47 @@ function validateBuiltinBoardClip(clip, path = "clip") {
212
259
  }
213
260
  return errors;
214
261
  }
262
+ function validateBuiltinBoardEffect(effect, path = "effect") {
263
+ const diagnostics = [];
264
+ if (effect.lifecycle === "on-enter") {
265
+ if (effect.target.type !== "item") diagnostics.push({
266
+ severity: "error",
267
+ code: "INVALID_BOARD_EFFECT",
268
+ message: "on-enter effects must target an item",
269
+ path: `${path}.target`
270
+ });
271
+ if (effect.timeOrigin !== "activation") diagnostics.push({
272
+ severity: "error",
273
+ code: "INVALID_BOARD_EFFECT",
274
+ message: "on-enter effects must use activation time",
275
+ path: `${path}.timeOrigin`
276
+ });
277
+ } else if (EFFECTS.has(effect.kind) && effect.target.type !== "item") diagnostics.push({
278
+ severity: "error",
279
+ code: "INVALID_BOARD_EFFECT",
280
+ message: `${effect.kind} must target an item`,
281
+ path: `${path}.target`
282
+ });
283
+ if (effect.kind === "effects.deal" && effect.kindVersion === 1) {
284
+ if (effect.lifecycle !== "on-enter") diagnostics.push({
285
+ severity: "error",
286
+ code: "INVALID_BOARD_EFFECT",
287
+ message: "effects.deal must use the on-enter lifecycle",
288
+ path: `${path}.lifecycle`
289
+ });
290
+ const parsed = BoardDealParamsSchema.safeParse(effect.params);
291
+ if (!parsed.success) diagnostics.push({
292
+ severity: "error",
293
+ code: "INVALID_BOARD_EFFECT",
294
+ message: parsed.error.issues[0]?.message ?? "invalid effects.deal parameters",
295
+ path: `${path}.params`
296
+ });
297
+ }
298
+ return diagnostics;
299
+ }
300
+ function estimateBuiltinBoardEffectCost(effect) {
301
+ return isBuiltinBoardCapability("effect", effect.kind, effect.kindVersion) ? { drawCalls: 1 } : {};
302
+ }
215
303
  function estimateBuiltinBoardClipCost(clip) {
216
304
  switch (clip.kind) {
217
305
  case "effects.particles": {
@@ -352,7 +440,18 @@ const BoardViewportSchema = z.object({
352
440
  zoom: z.number().finite().min(.05).max(8)
353
441
  });
354
442
  const BoardAppearanceSchema = z.object({
355
- theme: z.string().min(1).default("clean"),
443
+ /** Legacy field retained when reading older Board documents; not used for rendering. */
444
+ theme: z.string().min(1).optional(),
445
+ /** Board-wide motion policies. Omitted policies mean no motion. */
446
+ motion: BoardMotionSchema.optional(),
447
+ /** Legacy field retained when reading older Board documents; not used for rendering. */
448
+ mood: z.enum([
449
+ "clean",
450
+ "playful",
451
+ "arcane",
452
+ "cyber",
453
+ "natural"
454
+ ]).optional(),
356
455
  background: z.object({
357
456
  kind: z.enum([
358
457
  "solid",
@@ -386,14 +485,7 @@ const BoardAppearanceSchema = z.object({
386
485
  visible: false,
387
486
  size: 24,
388
487
  opacity: .12
389
- }),
390
- mood: z.enum([
391
- "clean",
392
- "playful",
393
- "arcane",
394
- "cyber",
395
- "natural"
396
- ]).default("clean")
488
+ })
397
489
  });
398
490
  /** Optional visual chrome still carried by a few older shapes. */
399
491
  const BoardItemStyleSchema = z.object({
@@ -695,14 +787,12 @@ z.object({
695
787
  kind: z.literal(BOARD_DOCUMENT_KIND),
696
788
  version: z.literal(1),
697
789
  appearance: BoardAppearanceSchema.default({
698
- theme: "clean",
699
790
  background: { kind: "solid" },
700
791
  grid: {
701
792
  visible: false,
702
793
  size: 24,
703
794
  opacity: .12
704
- },
705
- mood: "clean"
795
+ }
706
796
  }),
707
797
  viewport: BoardViewportSchema,
708
798
  items: z.array(BoardItemSchema),
@@ -1294,6 +1384,12 @@ const buildAppRuntimeCloseRequest = () => ({
1294
1384
  version: 1,
1295
1385
  type: "close.request"
1296
1386
  });
1387
+ const buildAppRuntimeConfigureRequest = (input) => ({
1388
+ protocol: APP_RUNTIME_PROTOCOL,
1389
+ version: 1,
1390
+ type: "configure.request",
1391
+ ...input
1392
+ });
1297
1393
  //#endregion
1298
1394
  //#region ../protocol/dist/app-navigation.js
1299
1395
  const APP_NAVIGATION_PROTOCOL = "cohub.app.navigation";
@@ -2452,6 +2548,14 @@ var AppRuntimeApi = class {
2452
2548
  requestClose() {
2453
2549
  this.transport.notify?.(buildAppRuntimeCloseRequest());
2454
2550
  }
2551
+ /**
2552
+ * Requests the host to update the overlay's geometry or pointer hit regions.
2553
+ * Only meaningful when the App was opened as an `overlay` surface; the host
2554
+ * is free to clamp or ignore values that violate its layout policy.
2555
+ */
2556
+ requestConfigure(input) {
2557
+ this.transport.notify?.(buildAppRuntimeConfigureRequest(input));
2558
+ }
2455
2559
  async getAccessToken(options) {
2456
2560
  await this.ensureStorageKeys();
2457
2561
  if (this.token && !options?.forceRefresh) return this.token;
@@ -2553,7 +2657,7 @@ var AppRuntimeApi = class {
2553
2657
  * One consent: create a viewer-owned Space (full `CreateSpaceInput`, same
2554
2658
  * as `spaces.create`) and grant the scopes on it. Never silent — each
2555
2659
  * confirm mints a new Space. The host creates with the viewer's account
2556
- * token; the app never calls `POST /api/spaces` itself.
2660
+ * token.
2557
2661
  * `{ granted: false, space }` means the Space was created but not provisioned;
2558
2662
  * no grant was issued. A viewer deny is `{ granted: false, space: null }`.
2559
2663
  */
@@ -2766,6 +2870,11 @@ var CohubClient = class {
2766
2870
  onContextChanged: (listener) => this.appRuntime.onContextChanged(listener),
2767
2871
  /** Ask the host to close this App's surface. */
2768
2872
  requestClose: () => this.appRuntime.requestClose(),
2873
+ /**
2874
+ * Request that the host update this overlay's geometry or pointer hit
2875
+ * regions. Only meaningful for `overlay` surfaces; ignored otherwise.
2876
+ */
2877
+ requestConfigure: (input) => this.appRuntime.requestConfigure(input),
2769
2878
  embed: {
2770
2879
  /** Host another App's public page in an iframe and forward the shell location to it. */
2771
2880
  attach: attachAppEmbed },
@@ -3130,6 +3239,20 @@ const isDefinitiveAuthorizationFailure = (error) => error instanceof AppAuthoriz
3130
3239
  403,
3131
3240
  404
3132
3241
  ].includes(error.status);
3242
+ /**
3243
+ * Whether a new authorize request asks for exactly the consent a pending
3244
+ * dialog already shows: same scopes, same target and same mode. Such a request
3245
+ * joins the dialog instead of replacing it, so an App that asks twice (for
3246
+ * example on every context update) gets one dialog and one shared answer.
3247
+ */
3248
+ function isSameConsent(pending, next) {
3249
+ if (next.createSpace || pending.createSpace) return false;
3250
+ if (Boolean(pending.selectSpace) !== next.selectSpace) return false;
3251
+ if (pending.spaceId !== next.spaceId) return false;
3252
+ const a = normalizePermissionScopes(pending.scopes);
3253
+ const b = normalizePermissionScopes(next.scopes);
3254
+ return a.length === b.length && a.every((scope) => b.includes(scope));
3255
+ }
3133
3256
  function sanitizeReason(value) {
3134
3257
  if (typeof value !== "string") return void 0;
3135
3258
  const trimmed = value.trim();
@@ -3352,7 +3475,7 @@ function createAppBridgeCore(config) {
3352
3475
  return Array.from(bySpace.values());
3353
3476
  }
3354
3477
  function allowsOwnerAutoAuthorization() {
3355
- return authorizationContext.surface === "background" || authorizationContext.surface === "app";
3478
+ return authorizationContext.surface === "background" || authorizationContext.surface === "app" || authorizationContext.surface === "overlay";
3356
3479
  }
3357
3480
  async function ensureBaseToken(forceRefresh = false) {
3358
3481
  if (appToken && !forceRefresh) return appToken;
@@ -3683,7 +3806,18 @@ function createAppBridgeCore(config) {
3683
3806
  }, true);
3684
3807
  return;
3685
3808
  }
3686
- if (state.pendingAuth) dismissPendingAuth();
3809
+ if (state.pendingAuth) {
3810
+ if (isSameConsent(state.pendingAuth, {
3811
+ scopes,
3812
+ spaceId,
3813
+ selectSpace,
3814
+ createSpace
3815
+ })) {
3816
+ state.pendingAuth.joinedRequestIds = [...state.pendingAuth.joinedRequestIds ?? [], data.requestId];
3817
+ return;
3818
+ }
3819
+ dismissPendingAuth();
3820
+ }
3687
3821
  if (createSpace) {
3688
3822
  mintedSpace = null;
3689
3823
  state.pendingAuth = {
@@ -3753,12 +3887,16 @@ function createAppBridgeCore(config) {
3753
3887
  }, true);
3754
3888
  }
3755
3889
  }
3890
+ /** Answers the dialog's request and every request that joined it. */
3891
+ function replyPendingAuth(pending, payload) {
3892
+ for (const requestId of [pending.requestId, ...pending.joinedRequestIds ?? []]) replyForRequest(requestId, payload, true);
3893
+ }
3756
3894
  function dismissPendingAuth() {
3757
3895
  if (!state.pendingAuth) return;
3758
- replyForRequest(state.pendingAuth.requestId, {
3896
+ replyPendingAuth(state.pendingAuth, {
3759
3897
  type: "cohub.app.authorize.result",
3760
3898
  token: null
3761
- }, true);
3899
+ });
3762
3900
  state.pendingAuth = null;
3763
3901
  state.authOpen = false;
3764
3902
  state.authError = null;
@@ -3822,7 +3960,7 @@ function createAppBridgeCore(config) {
3822
3960
  if (pending.createSpace) {
3823
3961
  mintedSpace ??= await createViewerSpace(pending.createSpace);
3824
3962
  if (!mintedSpace.provisioned) {
3825
- replyForRequest(pending.requestId, authorizeResult(null, mintedSpace.id, mintedSpace.name ?? pending.createSpace.name), true);
3963
+ replyPendingAuth(pending, authorizeResult(null, mintedSpace.id, mintedSpace.name ?? pending.createSpace.name));
3826
3964
  state.authOpen = false;
3827
3965
  state.pendingAuth = null;
3828
3966
  mintedSpace = null;
@@ -3834,7 +3972,7 @@ function createAppBridgeCore(config) {
3834
3972
  const result = await authorize(pending.scopes, requestedSpaceId);
3835
3973
  setGrantedAppScopes(await getViewerUuid(), app.id, result.scopes, result.spaceId);
3836
3974
  if (pending.selectSpace || pending.createSpace) writeLastPickedSpace(result.spaceId);
3837
- replyForRequest(pending.requestId, authorizeResult(result.token, result.spaceId, spaceName), true);
3975
+ replyPendingAuth(pending, authorizeResult(result.token, result.spaceId, spaceName));
3838
3976
  state.authOpen = false;
3839
3977
  state.pendingAuth = null;
3840
3978
  mintedSpace = null;
@@ -4033,6 +4171,15 @@ var BoardExtensionRegistry = class {
4033
4171
  const diagnostics = [];
4034
4172
  const composition = parsed.data;
4035
4173
  const cost = { ...ZERO_COST };
4174
+ for (const effect of input.effects ?? []) {
4175
+ diagnostics.push(...validateBuiltinBoardEffect(effect, `effects.${effect.id}`));
4176
+ if (!this.#extensions.has(`effect:${effect.kind}@${effect.kindVersion}`)) diagnostics.push({
4177
+ severity: "warning",
4178
+ code: "UNKNOWN_EFFECT",
4179
+ message: `No renderer is registered for ${effect.kind}@${effect.kindVersion}`,
4180
+ path: `effects.${effect.id}`
4181
+ });
4182
+ }
4036
4183
  const events = [];
4037
4184
  for (const clip of composition.timeline.clips) {
4038
4185
  const definition = this.#extensions.get(`clip:${clip.kind}@${clip.kindVersion}`);
@@ -4066,7 +4213,10 @@ var BoardExtensionRegistry = class {
4066
4213
  active[key] += event.cost[key] * event.direction;
4067
4214
  cost[key] = Math.max(cost[key], active[key]);
4068
4215
  }
4069
- for (const effect of input.effects ?? []) if (effect.kind === "effects.pulse" || effect.kind === "effects.float") cost.drawCalls += 1;
4216
+ for (const effect of input.effects ?? []) {
4217
+ const estimate = estimateBuiltinBoardEffectCost(effect);
4218
+ for (const key of Object.keys(cost)) cost[key] += estimate[key] ?? 0;
4219
+ }
4070
4220
  const limits = input.limits ?? DEFAULT_BOARD_LIMITS;
4071
4221
  for (const key of Object.keys(cost)) if (cost[key] > limits[key]) diagnostics.push({
4072
4222
  severity: "warning",
@@ -4087,4 +4237,4 @@ function createBoardExtensionRegistry() {
4087
4237
  }
4088
4238
  const BOARD_CHANNELS = BOARD_ANIMATION_CHANNELS;
4089
4239
  //#endregion
4090
- export { APP_COMPOSER_CHIP_CONTENT_MAX_BYTES, APP_COMPOSER_CHIP_KEY_MAX_LENGTH, APP_COMPOSER_CHIP_LABEL_MAX_LENGTH, APP_SURFACE_READY_TIMEOUT_MS, APP_SURFACE_REQUEST_TIMEOUT_MS, AppCommerceApi, AppRealtimeApi, AppRefParseError, AppRoom, AppRuntimeApi, AppRuntimeApi as WorkRuntimeApi, AppSurfaceApi, AppsApi, BILLING_ACCESS_BLOCKED_ERROR_CODE, BOARD_ANIMATION_CHANNEL_CAPABILITIES, BOARD_CHANNELS, BOARD_COLOR_IDS, BOARD_GEO_KINDS, BillingApi, BoardAuthoringItemSchema, BoardClient, BoardCompositionInputSchema, BoardCompositionSchema, BoardEffectInputSchema, BoardEffectSchema, BoardExtensionRegistry, BoardItemPatchSchema, BoardPlaybackPolicySchema, BoardSemanticCommandSchema, COHUB_ENVIRONMENTS, COHUB_SOURCE_HEADER, COHUB_SOURCE_HEADER_NAMES, CohubClient, CohubHttpClient, DEFAULT_BOARD_LIMITS, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, DESKTOP_COMMAND_PENDING_TTL_SECONDS, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, DESKTOP_COMMAND_VERSION, DesktopCommandsApi, FEATURE_NOT_ENTITLED_ERROR_CODE, GenerationPolicyError, HttpError, PERMISSIONS, ParentBridgeTransport, PopupBrokerTransport, REQUEST_SOURCE_VIA_MAX_LENGTH, ReferencesApi, ReferralsApi, SessionGenerationStreamClient, SessionPatchReducer, SpacePublicFilesApi, UiCommandsApi, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRealtimeApi, WorkRefParseError, WorkRoom, WorkSurfaceApi, assertGenerationRequestAllowedByPolicy, attachAppEmbed, buildAppSurfaceRequest, buildSpaceInvitePath, buildSpacePath, clearGrantedAppScopes, compileComposition, composition, createAppBridgeCore, createAppBridgeCore as createWorkBridgeCore, createAppRuntime, createAppRuntime as createWorkRuntime, createBoardExtensionRegistry, createCohubClient, createHttpClient, createSessionGenerationStreamClient, createSessionPatchReducer, createSlugAppIdResolver, createSlugAppIdResolver as createSlugWorkIdResolver, createVoiceInputClient, createWebsocketClient, decodeGenerationPolicy, encodeGenerationPolicy, extractBillingPayload, filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, findGenerationModelPolicy, formatAppRef, formatWorkRef, getAllowedGenerationModelIds, getCohubContext, hasGrantedAppScopes, hasRequestSourceIdentity, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, normalizeBaseUrl, normalizeGenerationPolicy, normalizeRequestSource, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardCompositionInput, parseBoardEffectInput, parseBoardPlaybackPolicy, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseWorkRef, proceduralClip, readRequestSourceFromEnv, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebsocketUrl, sampleCompositionTracks, sampleEasing, sampleTrack, sanitizeAccessToken, scopeListHasPermission, setGrantedAppScopes, track };
4240
+ export { APP_COMPOSER_CHIP_CONTENT_MAX_BYTES, APP_COMPOSER_CHIP_KEY_MAX_LENGTH, APP_COMPOSER_CHIP_LABEL_MAX_LENGTH, APP_SURFACE_READY_TIMEOUT_MS, APP_SURFACE_REQUEST_TIMEOUT_MS, AppCommerceApi, AppRealtimeApi, AppRefParseError, AppRoom, AppRuntimeApi, AppRuntimeApi as WorkRuntimeApi, AppSurfaceApi, AppsApi, BILLING_ACCESS_BLOCKED_ERROR_CODE, BOARD_ANIMATION_CHANNEL_CAPABILITIES, BOARD_CHANNELS, BOARD_COLOR_IDS, BOARD_GEO_KINDS, BillingApi, BoardAnimationSpecSchema, BoardAuthoringItemSchema, BoardClient, BoardCompositionInputSchema, BoardCompositionSchema, BoardDealParamsSchema, BoardEffectInputSchema, BoardEffectSchema, BoardExtensionRegistry, BoardItemPatchSchema, BoardPlaybackPolicySchema, BoardSemanticCommandSchema, COHUB_ENVIRONMENTS, COHUB_SOURCE_HEADER, COHUB_SOURCE_HEADER_NAMES, CohubClient, CohubHttpClient, DEFAULT_BOARD_LIMITS, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, DESKTOP_COMMAND_PENDING_TTL_SECONDS, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, DESKTOP_COMMAND_VERSION, DesktopCommandsApi, FEATURE_NOT_ENTITLED_ERROR_CODE, GenerationPolicyError, HttpError, PERMISSIONS, ParentBridgeTransport, PopupBrokerTransport, REQUEST_SOURCE_VIA_MAX_LENGTH, ReferencesApi, ReferralsApi, SessionGenerationStreamClient, SessionPatchReducer, SpacePublicFilesApi, UiCommandsApi, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRealtimeApi, WorkRefParseError, WorkRoom, WorkSurfaceApi, assertGenerationRequestAllowedByPolicy, attachAppEmbed, buildAppSurfaceRequest, buildSpaceInvitePath, buildSpacePath, clearGrantedAppScopes, compileComposition, composition, createAppBridgeCore, createAppBridgeCore as createWorkBridgeCore, createAppRuntime, createAppRuntime as createWorkRuntime, createBoardExtensionRegistry, createCohubClient, createHttpClient, createSessionGenerationStreamClient, createSessionPatchReducer, createSlugAppIdResolver, createSlugAppIdResolver as createSlugWorkIdResolver, createVoiceInputClient, createWebsocketClient, decodeGenerationPolicy, encodeGenerationPolicy, extractBillingPayload, filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, findGenerationModelPolicy, formatAppRef, formatWorkRef, getAllowedGenerationModelIds, getCohubContext, hasGrantedAppScopes, hasRequestSourceIdentity, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, normalizeBaseUrl, normalizeGenerationPolicy, normalizeRequestSource, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardCompositionInput, parseBoardEffectInput, parseBoardPlaybackPolicy, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseWorkRef, proceduralClip, readRequestSourceFromEnv, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebsocketUrl, sampleCompositionTracks, sampleEasing, sampleTrack, sanitizeAccessToken, scopeListHasPermission, setGrantedAppScopes, track };
@@ -0,0 +1 @@
1
+ import { z } from "zod";
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ //#region ../protocol/dist/board-animation.js
3
+ const extensionKindSchema = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/).max(160);
4
+ const jsonObjectSchema = z.record(z.string(), z.unknown());
5
+ const finiteSchema = z.number().finite();
6
+ /** Parameters shared by the built-in deal entrance preset. */
7
+ const BoardDealParamsSchema = z.object({
8
+ lift: finiteSchema.nonnegative().max(2e3).optional(),
9
+ swing: finiteSchema.nonnegative().max(2e3).optional(),
10
+ curve: finiteSchema.nonnegative().max(2e3).optional(),
11
+ tilt: finiteSchema.nonnegative().max(180).optional(),
12
+ scale: finiteSchema.positive().max(2).optional(),
13
+ duration: finiteSchema.positive().max(1e4).optional(),
14
+ landing: finiteSchema.nonnegative().max(1e4).optional()
15
+ }).strict();
16
+ /** A versioned, reusable animation preset reference. */
17
+ const BoardAnimationSpecSchema = z.object({
18
+ kind: extensionKindSchema,
19
+ kindVersion: z.number().int().positive().default(1),
20
+ params: jsonObjectSchema.default({})
21
+ }).strict().superRefine((spec, context) => {
22
+ if (spec.kind !== "effects.deal" || spec.kindVersion !== 1) return;
23
+ const parsed = BoardDealParamsSchema.safeParse(spec.params);
24
+ if (parsed.success) return;
25
+ context.addIssue({
26
+ code: "custom",
27
+ message: parsed.error.issues[0]?.message ?? "invalid effects.deal parameters",
28
+ path: ["params", ...parsed.error.issues[0]?.path ?? []]
29
+ });
30
+ });
31
+ /** Board-wide default motion policies. Omitted policies mean no motion. */
32
+ const BoardMotionSchema = z.object({ enter: BoardAnimationSpecSchema.optional() }).strict();
33
+ //#endregion
34
+ export { BoardAnimationSpecSchema, BoardDealParamsSchema, BoardMotionSchema };
@@ -1140,10 +1140,11 @@ declare const BoardSemanticCommandSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
1140
1140
  type: z.ZodLiteral<"board">;
1141
1141
  }, z.core.$strict>], "type">;
1142
1142
  kind: z.ZodString;
1143
- kindVersion: z.ZodNumber;
1143
+ kindVersion: z.ZodDefault<z.ZodNumber>;
1144
1144
  enabled: z.ZodDefault<z.ZodBoolean>;
1145
1145
  lifecycle: z.ZodEnum<{
1146
1146
  manual: "manual";
1147
+ "on-enter": "on-enter";
1147
1148
  persistent: "persistent";
1148
1149
  "when-visible": "when-visible";
1149
1150
  }>;
@@ -1,8 +1,17 @@
1
- import { BOARD_BUILTIN_CLIP_KINDS, BOARD_BUILTIN_EFFECT_KINDS, DEFAULT_BOARD_RENDER_LIMITS } from "./board-constants.js";
1
+ import { BOARD_BUILTIN_CAPABILITIES, BOARD_BUILTIN_CLIP_KINDS, BOARD_BUILTIN_EFFECT_KINDS, DEFAULT_BOARD_RENDER_LIMITS } from "./board-constants.js";
2
2
  import { BoardCameraFocusParamsSchema } from "./board.js";
3
+ import { BoardDealParamsSchema } from "./board-animation.js";
3
4
  //#region ../protocol/dist/board-capability-registry.js
4
5
  const CLIPS = new Set(BOARD_BUILTIN_CLIP_KINDS);
5
- new Set(BOARD_BUILTIN_EFFECT_KINDS);
6
+ const EFFECTS = new Set(BOARD_BUILTIN_EFFECT_KINDS);
7
+ const BUILTIN_VERSIONS = new Set(BOARD_BUILTIN_CAPABILITIES.map((c) => `${c.kind}:${c.id}@${c.version}`));
8
+ /**
9
+ * True when a built-in renderer exists for exactly this kind@version. A known
10
+ * kind at an unknown version is not built-in: it must not pass as one.
11
+ */
12
+ function isBuiltinBoardCapability(kind, id, version) {
13
+ return BUILTIN_VERSIONS.has(`${kind}:${id}@${version}`);
14
+ }
6
15
  const record = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
7
16
  const finite = (value) => typeof value === "number" && Number.isFinite(value);
8
17
  function validateBuiltinBoardClip(clip, path = "clip") {
@@ -36,6 +45,47 @@ function validateBuiltinBoardClip(clip, path = "clip") {
36
45
  }
37
46
  return errors;
38
47
  }
48
+ function validateBuiltinBoardEffect(effect, path = "effect") {
49
+ const diagnostics = [];
50
+ if (effect.lifecycle === "on-enter") {
51
+ if (effect.target.type !== "item") diagnostics.push({
52
+ severity: "error",
53
+ code: "INVALID_BOARD_EFFECT",
54
+ message: "on-enter effects must target an item",
55
+ path: `${path}.target`
56
+ });
57
+ if (effect.timeOrigin !== "activation") diagnostics.push({
58
+ severity: "error",
59
+ code: "INVALID_BOARD_EFFECT",
60
+ message: "on-enter effects must use activation time",
61
+ path: `${path}.timeOrigin`
62
+ });
63
+ } else if (EFFECTS.has(effect.kind) && effect.target.type !== "item") diagnostics.push({
64
+ severity: "error",
65
+ code: "INVALID_BOARD_EFFECT",
66
+ message: `${effect.kind} must target an item`,
67
+ path: `${path}.target`
68
+ });
69
+ if (effect.kind === "effects.deal" && effect.kindVersion === 1) {
70
+ if (effect.lifecycle !== "on-enter") diagnostics.push({
71
+ severity: "error",
72
+ code: "INVALID_BOARD_EFFECT",
73
+ message: "effects.deal must use the on-enter lifecycle",
74
+ path: `${path}.lifecycle`
75
+ });
76
+ const parsed = BoardDealParamsSchema.safeParse(effect.params);
77
+ if (!parsed.success) diagnostics.push({
78
+ severity: "error",
79
+ code: "INVALID_BOARD_EFFECT",
80
+ message: parsed.error.issues[0]?.message ?? "invalid effects.deal parameters",
81
+ path: `${path}.params`
82
+ });
83
+ }
84
+ return diagnostics;
85
+ }
86
+ function estimateBuiltinBoardEffectCost(effect) {
87
+ return isBuiltinBoardCapability("effect", effect.kind, effect.kindVersion) ? { drawCalls: 1 } : {};
88
+ }
39
89
  function estimateBuiltinBoardClipCost(clip) {
40
90
  switch (clip.kind) {
41
91
  case "effects.particles": {
@@ -70,4 +120,4 @@ function estimateBuiltinBoardClipCost(clip) {
70
120
  }
71
121
  }
72
122
  //#endregion
73
- export { estimateBuiltinBoardClipCost, validateBuiltinBoardClip };
123
+ export { estimateBuiltinBoardClipCost, estimateBuiltinBoardEffectCost, isBuiltinBoardCapability, validateBuiltinBoardClip, validateBuiltinBoardEffect };