@neta-art/cohub 5.7.0 → 5.8.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 (54) hide show
  1. package/README.md +18 -5
  2. package/dist/board/core/palette.d.ts +3 -2
  3. package/dist/board/core/shape-types.d.ts +3 -1
  4. package/dist/board/core/shape-types.js +3 -7
  5. package/dist/board/core/tool-styles.d.ts +2 -1
  6. package/dist/board/image-key.js +3 -5
  7. package/dist/board/index.d.ts +9 -4
  8. package/dist/board/index.js +7 -3
  9. package/dist/board/media-playback.d.ts +22 -0
  10. package/dist/board/media-playback.js +67 -0
  11. package/dist/board/media.d.ts +7 -0
  12. package/dist/board/media.js +70 -0
  13. package/dist/board/nodes.d.ts +113 -0
  14. package/dist/board/nodes.js +154 -0
  15. package/dist/board/render/index.d.ts +3 -1
  16. package/dist/board/render/index.js +4 -2
  17. package/dist/board/render/media-interaction.d.ts +19 -0
  18. package/dist/board/render/media-interaction.js +26 -0
  19. package/dist/board/render/renderers/audio-card-renderer.js +1 -1
  20. package/dist/board/render/renderers/board-renderer-registry.js +2 -2
  21. package/dist/board/render/renderers/draw-card-renderer.js +1 -1
  22. package/dist/board/render/renderers/file-card-renderer.js +1 -1
  23. package/dist/board/render/renderers/frame-card-renderer.js +1 -1
  24. package/dist/board/render/renderers/geo-card-renderer.js +1 -1
  25. package/dist/board/render/renderers/image-card-renderer.js +1 -1
  26. package/dist/board/render/renderers/task-card-renderer.js +16 -13
  27. package/dist/board/render/renderers/text-card-renderer.js +1 -1
  28. package/dist/board/render/renderers/unknown-card-renderer.js +1 -1
  29. package/dist/board/render/renderers/video-card-renderer.js +1 -1
  30. package/dist/board/render/video-thumbnail.d.ts +16 -0
  31. package/dist/board/render/video-thumbnail.js +86 -0
  32. package/dist/board/task.d.ts +10 -5
  33. package/dist/board/task.js +159 -103
  34. package/dist/chunks/environment.d.ts +6 -6
  35. package/dist/chunks/environment.js +6 -6
  36. package/dist/chunks/http.d.ts +71 -5
  37. package/dist/chunks/http.js +1535 -167
  38. package/dist/chunks/transport.js +8 -1
  39. package/dist/chunks/websocket.d.ts +138 -2
  40. package/dist/http.d.ts +3 -3
  41. package/dist/index.d.ts +245 -4
  42. package/dist/index.js +438 -788
  43. package/dist/protocol/dist/board-document.d.ts +155 -48
  44. package/dist/protocol/dist/board-document.js +40 -19
  45. package/dist/protocol/dist/board-node.d.ts +18 -0
  46. package/dist/protocol/dist/board-node.js +239 -0
  47. package/dist/protocol/dist/board-url.d.ts +12 -0
  48. package/dist/protocol/dist/board-url.js +83 -0
  49. package/dist/protocol/dist/board.d.ts +5 -0
  50. package/dist/protocol/dist/index.d.ts +2 -1
  51. package/dist/protocol/dist/index.js +2 -1
  52. package/dist/protocol/dist/provenance.js +1 -0
  53. package/docs/work-runtime-guide.md +7 -7
  54. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { D as getRealtimeSpaceRoom, E as getRealtimeBoardRoom, n as HttpTransport, t as HttpError } from "./transport.js";
2
2
  import { a as resolveApiBaseUrl } from "./environment.js";
3
3
  import { z } from "zod";
4
+ import "perfect-freehand";
4
5
  //#region src/apis/channels.ts
5
6
  var ChannelsApi = class {
6
7
  transport;
@@ -641,6 +642,1412 @@ function ensureRealtimeConnected(websocketClient) {
641
642
  });
642
643
  }
643
644
  //#endregion
645
+ //#region ../protocol/dist/board.js
646
+ const BOARD_DOCUMENT_KIND = "cohub.board";
647
+ const BOARD_MANIFEST_KIND = "cohub.board.manifest";
648
+ const idSchema$1 = z.string().min(1).max(160);
649
+ const extensionIdSchema = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/).max(160);
650
+ const jsonObjectSchema = z.record(z.string(), z.unknown());
651
+ const finiteSchema$1 = z.number().finite();
652
+ z.object({
653
+ kind: z.literal(BOARD_MANIFEST_KIND),
654
+ version: z.literal(1),
655
+ boardId: z.string().uuid(),
656
+ title: z.string().min(1).max(255)
657
+ });
658
+ const BoardTargetSchema = z.discriminatedUnion("type", [
659
+ z.object({
660
+ type: z.literal("node"),
661
+ nodeId: idSchema$1
662
+ }),
663
+ z.object({
664
+ type: z.literal("effect"),
665
+ effectId: idSchema$1
666
+ }),
667
+ z.object({ type: z.literal("board") }),
668
+ z.object({ type: z.literal("camera") })
669
+ ]);
670
+ const BoardAssetRefSchema = z.object({
671
+ type: z.enum(["space-file", "extension"]),
672
+ ref: z.string().min(1).max(4096),
673
+ digest: z.string().min(16).max(160).optional()
674
+ });
675
+ const BoardKeyframeSchema = z.object({
676
+ at: finiteSchema$1.nonnegative(),
677
+ value: z.unknown(),
678
+ easing: z.string().min(1).max(80).optional()
679
+ });
680
+ const BoardClipSchema = z.object({
681
+ id: idSchema$1,
682
+ sequenceId: idSchema$1,
683
+ kind: extensionIdSchema,
684
+ kindVersion: z.number().int().positive(),
685
+ target: BoardTargetSchema,
686
+ start: finiteSchema$1.nonnegative(),
687
+ duration: finiteSchema$1.positive(),
688
+ layer: z.enum([
689
+ "behind",
690
+ "content",
691
+ "front",
692
+ "screen"
693
+ ]).default("content"),
694
+ fill: z.enum([
695
+ "none",
696
+ "backwards",
697
+ "forwards",
698
+ "both"
699
+ ]).default("none"),
700
+ easing: z.string().min(1).max(80).default("linear"),
701
+ params: jsonObjectSchema.default({}),
702
+ keyframes: z.array(BoardKeyframeSchema).default([]),
703
+ assetRefs: z.array(BoardAssetRefSchema).default([]),
704
+ seed: z.string().min(1).max(160),
705
+ metadata: jsonObjectSchema.default({})
706
+ });
707
+ const BoardEffectSchema = z.object({
708
+ id: idSchema$1,
709
+ boardId: z.string().uuid(),
710
+ target: z.discriminatedUnion("type", [z.object({
711
+ type: z.literal("node"),
712
+ nodeId: idSchema$1
713
+ }), z.object({ type: z.literal("board") })]),
714
+ kind: extensionIdSchema,
715
+ kindVersion: z.number().int().positive(),
716
+ enabled: z.boolean().default(true),
717
+ lifecycle: z.enum([
718
+ "persistent",
719
+ "when-visible",
720
+ "manual"
721
+ ]),
722
+ timeOrigin: z.enum([
723
+ "board",
724
+ "visible",
725
+ "activation"
726
+ ]),
727
+ layer: z.enum([
728
+ "behind",
729
+ "front",
730
+ "screen"
731
+ ]).default("front"),
732
+ seed: z.string().min(1).max(160),
733
+ params: jsonObjectSchema.default({}),
734
+ assetRefs: z.array(BoardAssetRefSchema).default([]),
735
+ metadata: jsonObjectSchema.default({}),
736
+ revision: z.number().int().nonnegative()
737
+ });
738
+ const BoardSequenceSchema = z.object({
739
+ id: idSchema$1,
740
+ boardId: z.string().uuid(),
741
+ name: z.string().min(1).max(255),
742
+ duration: finiteSchema$1.nonnegative(),
743
+ seed: z.string().min(1).max(160),
744
+ restPose: jsonObjectSchema.default({}),
745
+ metadata: jsonObjectSchema.default({}),
746
+ revision: z.number().int().nonnegative()
747
+ });
748
+ const BoardNodeInputSchema = z.object({
749
+ nodeId: idSchema$1,
750
+ type: z.string().min(1).max(40),
751
+ parentId: idSchema$1.nullable(),
752
+ orderKey: z.string().max(4096).nullable(),
753
+ x: finiteSchema$1,
754
+ y: finiteSchema$1,
755
+ width: finiteSchema$1.positive(),
756
+ height: finiteSchema$1.positive(),
757
+ rotation: finiteSchema$1,
758
+ refKind: z.string().max(40).nullable(),
759
+ refPath: z.string().max(4096).nullable(),
760
+ refUrl: z.string().max(4096).nullable(),
761
+ view: jsonObjectSchema,
762
+ style: jsonObjectSchema,
763
+ data: jsonObjectSchema
764
+ });
765
+ z.object({
766
+ path: z.string().min(1),
767
+ /** Reused by clients when board creation is interrupted and retried. */
768
+ mutationId: z.string().max(128).optional(),
769
+ title: z.string().min(1).max(255).optional(),
770
+ metadata: jsonObjectSchema.optional(),
771
+ nodes: z.array(BoardNodeInputSchema).max(5e4).optional(),
772
+ connections: z.array(BoardConnectionSchema).max(5e4).optional(),
773
+ effects: z.array(BoardEffectSchema.omit({
774
+ boardId: true,
775
+ revision: true
776
+ })).optional(),
777
+ sequences: z.array(z.object({
778
+ sequence: BoardSequenceSchema.omit({
779
+ boardId: true,
780
+ revision: true
781
+ }),
782
+ clips: z.array(BoardClipSchema.omit({ sequenceId: true }))
783
+ })).optional()
784
+ });
785
+ z.object({
786
+ include: z.array(z.enum([
787
+ "nodes",
788
+ "connections",
789
+ "effects",
790
+ "sequences",
791
+ "clips",
792
+ "playback"
793
+ ])).optional(),
794
+ viewport: z.object({
795
+ x: finiteSchema$1,
796
+ y: finiteSchema$1,
797
+ width: finiteSchema$1.positive(),
798
+ height: finiteSchema$1.positive()
799
+ }).optional()
800
+ });
801
+ /** Persisted on `boards.metadata.playback`: how a Board plays when opened. */
802
+ const BoardPlaybackPolicySchema = z.object({
803
+ sequenceId: idSchema$1,
804
+ /** Delay before the first local playback after opening the Board, in milliseconds. */
805
+ delayMs: finiteSchema$1.nonnegative().default(0),
806
+ loop: z.boolean().default(false)
807
+ });
808
+ function parseBoardPlaybackPolicy(metadata) {
809
+ const parsed = BoardPlaybackPolicySchema.safeParse(metadata.playback);
810
+ return parsed.success ? parsed.data : null;
811
+ }
812
+ z.discriminatedUnion("type", [
813
+ z.object({
814
+ commandId: idSchema$1,
815
+ type: z.literal("play"),
816
+ sequenceId: idSchema$1,
817
+ position: finiteSchema$1.nonnegative().optional(),
818
+ timeScale: finiteSchema$1.positive().max(4).optional(),
819
+ shared: z.boolean().optional(),
820
+ seed: idSchema$1.optional()
821
+ }),
822
+ z.object({
823
+ commandId: idSchema$1,
824
+ type: z.literal("pause"),
825
+ playbackId: z.string().uuid()
826
+ }),
827
+ z.object({
828
+ commandId: idSchema$1,
829
+ type: z.literal("seek"),
830
+ playbackId: z.string().uuid(),
831
+ position: finiteSchema$1.nonnegative()
832
+ }),
833
+ z.object({
834
+ commandId: idSchema$1,
835
+ type: z.literal("stop"),
836
+ playbackId: z.string().uuid()
837
+ })
838
+ ]);
839
+ //#endregion
840
+ //#region ../protocol/dist/board-url.js
841
+ const BOARD_REMOTE_URL_MAX_LENGTH = 4096;
842
+ function parseIpv4(host) {
843
+ const parts = host.split(".").map(Number);
844
+ return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) ? parts : null;
845
+ }
846
+ function isBlockedIpv4(host) {
847
+ const parts = parseIpv4(host);
848
+ if (!parts) return false;
849
+ const [first, second, third] = parts;
850
+ if (first === 0 || first === 10 || first === 127) return true;
851
+ if (first === 100 && second >= 64 && second <= 127) return true;
852
+ if (first === 169 && second === 254) return true;
853
+ if (first === 172 && second >= 16 && second <= 31) return true;
854
+ if (first === 192 && second === 168) return true;
855
+ if (first === 192 && second === 0 && (third === 0 || third === 2)) return true;
856
+ if (first === 192 && second === 88 && third === 99) return true;
857
+ if (first === 198 && (second === 18 || second === 19)) return true;
858
+ if (first === 198 && second === 51 && third === 100) return true;
859
+ if (first === 203 && second === 0 && third === 113) return true;
860
+ return first >= 224;
861
+ }
862
+ function expandIpv6(host) {
863
+ const [head, tail, extra] = host.toLowerCase().split("::");
864
+ if (extra !== void 0) return null;
865
+ const headParts = head ? head.split(":").filter(Boolean) : [];
866
+ const tailParts = tail ? tail.split(":").filter(Boolean) : [];
867
+ const missing = 8 - headParts.length - tailParts.length;
868
+ if (missing < 0 || tail === void 0 && missing !== 0) return null;
869
+ const parts = [
870
+ ...headParts,
871
+ ...Array.from({ length: missing }, () => "0"),
872
+ ...tailParts
873
+ ];
874
+ if (parts.length !== 8 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) return null;
875
+ return parts.map((part) => part.padStart(4, "0"));
876
+ }
877
+ function isBlockedIpv6(host) {
878
+ const parts = expandIpv6(host);
879
+ if (!parts) return true;
880
+ if (parts.every((part) => part === "0000")) return true;
881
+ if (parts.slice(0, 7).every((part) => part === "0000") && parts[7] === "0001") return true;
882
+ if (parts.slice(0, 5).every((part) => part === "0000") && parts[5] === "ffff") {
883
+ const high = Number.parseInt(parts[6] ?? "0", 16);
884
+ const low = Number.parseInt(parts[7] ?? "0", 16);
885
+ return isBlockedIpv4(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
886
+ }
887
+ if (parts.slice(0, 6).every((part) => part === "0000")) return true;
888
+ const first = Number.parseInt(parts[0] ?? "0", 16);
889
+ if ((first & 65024) === 64512) return true;
890
+ if ((first & 65472) === 65152 || (first & 65472) === 65216) return true;
891
+ if ((first & 65280) === 65280) return true;
892
+ return parts[0] === "2001" && parts[1] === "0db8";
893
+ }
894
+ function isBlockedHost(hostname) {
895
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
896
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
897
+ if (parseIpv4(host)) return isBlockedIpv4(host);
898
+ return host.includes(":") && isBlockedIpv6(host);
899
+ }
900
+ /**
901
+ * Normalize a browser-loadable public HTTP(S) URL. This blocks explicit local
902
+ * addresses; any future server-side fetcher must additionally validate DNS
903
+ * resolution to defend against rebinding.
904
+ */
905
+ function normalizeBoardRemoteUrl(value) {
906
+ if (typeof value !== "string") return void 0;
907
+ const input = value.trim();
908
+ if (!input || input.length > 4096) return void 0;
909
+ try {
910
+ const url = new URL(input);
911
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
912
+ if (url.username || url.password || isBlockedHost(url.hostname)) return;
913
+ const normalized = url.toString();
914
+ return normalized.length <= 4096 ? normalized : void 0;
915
+ } catch {
916
+ return;
917
+ }
918
+ }
919
+ 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" });
920
+ //#endregion
921
+ //#region ../protocol/dist/board-document.js
922
+ const BoardFrameSchema = z.object({
923
+ x: z.number().finite(),
924
+ y: z.number().finite(),
925
+ width: z.number().finite().positive(),
926
+ height: z.number().finite().positive(),
927
+ rotation: z.number().finite().default(0)
928
+ });
929
+ /**
930
+ * The board camera. This is local UI state, not synced content: semantic ops
931
+ * (see diffBoardDocuments) never describe the viewport, and the editor holds
932
+ * the live camera separately from the persisted document. Here it only serves
933
+ * as an initial camera hint when a document is first loaded.
934
+ */
935
+ const BoardViewportSchema = z.object({
936
+ x: z.number().finite(),
937
+ y: z.number().finite(),
938
+ zoom: z.number().finite().min(.05).max(8)
939
+ });
940
+ const BoardAppearanceSchema = z.object({
941
+ theme: z.string().min(1).default("clean"),
942
+ background: z.object({
943
+ kind: z.enum([
944
+ "solid",
945
+ "dots",
946
+ "grid",
947
+ "image",
948
+ "shader",
949
+ "custom"
950
+ ]).default("dots"),
951
+ color: z.string().optional(),
952
+ imageUrl: z.string().url().optional()
953
+ }).default({ kind: "solid" }),
954
+ grid: z.object({
955
+ visible: z.boolean().default(false),
956
+ size: z.number().finite().min(4).default(24),
957
+ opacity: z.number().finite().min(0).max(1).default(.12)
958
+ }).default({
959
+ visible: false,
960
+ size: 24,
961
+ opacity: .12
962
+ }),
963
+ mood: z.enum([
964
+ "clean",
965
+ "playful",
966
+ "arcane",
967
+ "cyber",
968
+ "natural"
969
+ ]).default("clean")
970
+ });
971
+ /** Optional visual chrome still carried by a few older shapes. */
972
+ const BoardItemStyleSchema = z.object({
973
+ variant: z.string().min(1).default("default"),
974
+ theme: z.string().min(1).optional(),
975
+ accentColor: z.string().optional(),
976
+ size: z.enum([
977
+ "sm",
978
+ "md",
979
+ "lg"
980
+ ]).default("md"),
981
+ emphasis: z.enum([
982
+ "normal",
983
+ "rare",
984
+ "epic",
985
+ "legendary"
986
+ ]).default("normal"),
987
+ effects: z.array(z.string().min(1)).default([])
988
+ });
989
+ const SpaceFileRefSchema = z.object({
990
+ kind: z.literal("space-file"),
991
+ path: z.string().min(1)
992
+ });
993
+ const BoardMediaSnapshotSchema = z.object({
994
+ title: z.string().optional(),
995
+ mimeType: z.string().optional(),
996
+ size: z.number().finite().nonnegative().optional(),
997
+ mtimeMs: z.number().finite().nonnegative().optional(),
998
+ /** Intrinsic pixel size once known. */
999
+ naturalWidth: z.number().finite().positive().optional(),
1000
+ naturalHeight: z.number().finite().positive().optional()
1001
+ });
1002
+ const BoardItemBaseSchema = z.object({
1003
+ id: z.string().min(1),
1004
+ frame: BoardFrameSchema,
1005
+ /** When true the shape cannot be moved, resized, or deleted. */
1006
+ locked: z.boolean().optional(),
1007
+ style: BoardItemStyleSchema.optional(),
1008
+ metadata: z.record(z.string(), z.unknown()).optional()
1009
+ });
1010
+ /** Freestanding text — no card chrome; its frame always follows the glyphs. */
1011
+ const BoardTextItemSchema = BoardItemBaseSchema.extend({
1012
+ type: z.literal("text"),
1013
+ text: z.string().default(""),
1014
+ color: z.string().min(1).default("neutral"),
1015
+ fontSize: z.number().finite().min(2).max(512).default(24)
1016
+ });
1017
+ const BoardGeoItemSchema = BoardItemBaseSchema.extend({
1018
+ type: z.literal("geo"),
1019
+ geo: z.string().min(1).default("rectangle"),
1020
+ text: z.string().default(""),
1021
+ color: z.string().min(1).default("brand"),
1022
+ fillOpacity: z.number().finite().min(0).max(1).default(0)
1023
+ });
1024
+ /** A raw freehand sample. Pressure defaults to 0.5 (mouse) when absent. */
1025
+ const DrawPointSchema = z.object({
1026
+ x: z.number().finite(),
1027
+ y: z.number().finite(),
1028
+ p: z.number().finite().min(0).max(1).default(.5)
1029
+ });
1030
+ /** A world-space point. */
1031
+ const BoardPointSchema = z.object({
1032
+ x: z.number().finite(),
1033
+ y: z.number().finite()
1034
+ });
1035
+ const BoardDrawItemSchema = BoardItemBaseSchema.extend({
1036
+ type: z.literal("draw"),
1037
+ points: z.array(DrawPointSchema).default([]),
1038
+ color: z.string().min(1).default("brand"),
1039
+ size: z.number().finite().positive().default(4)
1040
+ });
1041
+ /**
1042
+ * A free arrow — a standalone annotation stroke between two world points.
1043
+ *
1044
+ * Arrows do not relate nodes. A relation between two nodes is a
1045
+ * `BoardConnection`, which is stored separately and resolves its geometry from
1046
+ * the live node frames. Keeping the two apart is what lets an arrow be a plain
1047
+ * shape (its own frame, freely movable) while a connection stays purely
1048
+ * semantic — neither has to pretend to be the other.
1049
+ */
1050
+ const BoardArrowItemSchema = BoardItemBaseSchema.extend({
1051
+ type: z.literal("arrow"),
1052
+ start: BoardPointSchema,
1053
+ end: BoardPointSchema,
1054
+ bend: z.number().finite().min(-.85).max(.85).default(0),
1055
+ color: z.string().min(1).default("brand"),
1056
+ size: z.number().finite().positive().default(BOARD_ARROW_STROKE_SIZE),
1057
+ arrowStart: z.boolean().default(false),
1058
+ arrowEnd: z.boolean().default(true),
1059
+ label: z.string().default("")
1060
+ });
1061
+ /** A frame container for organising shapes. */
1062
+ const BoardFrameItemSchema = BoardItemBaseSchema.extend({
1063
+ type: z.literal("frame"),
1064
+ label: z.string().default("Frame"),
1065
+ color: z.string().min(1).default("neutral")
1066
+ });
1067
+ /** Image node — space file only, natural aspect, no chrome. */
1068
+ const BoardImageItemSchema = BoardItemBaseSchema.extend({
1069
+ type: z.literal("image"),
1070
+ ref: SpaceFileRefSchema,
1071
+ snapshot: BoardMediaSnapshotSchema.optional(),
1072
+ /** Optional normalized crop in source image space (0..1). */
1073
+ crop: z.object({
1074
+ x: z.number().finite().min(0).max(1).default(0),
1075
+ y: z.number().finite().min(0).max(1).default(0),
1076
+ w: z.number().finite().min(0).max(1).default(1),
1077
+ h: z.number().finite().min(0).max(1).default(1)
1078
+ }).optional()
1079
+ });
1080
+ /** Video node — space file only. Playback state is local UI, never synced. */
1081
+ const BoardVideoItemSchema = BoardItemBaseSchema.extend({
1082
+ type: z.literal("video"),
1083
+ ref: SpaceFileRefSchema,
1084
+ snapshot: BoardMediaSnapshotSchema.optional()
1085
+ });
1086
+ /** Audio node — space file only. Playback state is local UI, never synced. */
1087
+ const BoardAudioItemSchema = BoardItemBaseSchema.extend({
1088
+ type: z.literal("audio"),
1089
+ ref: SpaceFileRefSchema,
1090
+ snapshot: BoardMediaSnapshotSchema.extend({ durationMs: z.number().finite().nonnegative().optional() }).optional()
1091
+ });
1092
+ /**
1093
+ * Cached display facts for a file card.
1094
+ *
1095
+ * Purely derived from the referenced file and versioned by `mtimeMs`, so it is a
1096
+ * cache and never a second source of truth: the workspace file remains
1097
+ * authoritative, and a stale snapshot is detectable rather than silently wrong.
1098
+ * It is stored so that opening a board renders complete cards immediately, with
1099
+ * no per-node file read on the first paint.
1100
+ */
1101
+ const BoardFileSnapshotSchema = z.object({
1102
+ title: z.string().optional(),
1103
+ mimeType: z.string().optional(),
1104
+ size: z.number().finite().nonnegative().optional(),
1105
+ mtimeMs: z.number().finite().nonnegative().optional(),
1106
+ /** Cleaned leading prose. Capped when written (see FILE_EXCERPT_MAX_CHARS). */
1107
+ excerpt: z.string().optional(),
1108
+ /** Cover image inside the space, already resolved to a workspace path. */
1109
+ coverPath: z.string().optional(),
1110
+ /** Cover image at an absolute https URL, as declared by the file itself. */
1111
+ coverUrl: z.string().optional()
1112
+ });
1113
+ /**
1114
+ * File node — a thumbnail entry point to any workspace file.
1115
+ *
1116
+ * This is the fallback for every dropped file that is not natively an image or
1117
+ * video, including binaries and unknown extensions: a board should never refuse
1118
+ * a file, only present it with less detail. Presentation tiers are derived from
1119
+ * the snapshot (see filePreviewKind), not stored, so there is no display state
1120
+ * to drift from the facts.
1121
+ */
1122
+ const BoardFileItemSchema = BoardItemBaseSchema.extend({
1123
+ type: z.literal("file"),
1124
+ ref: SpaceFileRefSchema,
1125
+ snapshot: BoardFileSnapshotSchema.optional()
1126
+ });
1127
+ const BoardTaskMediaArtifactFields = {
1128
+ id: z.string().min(1).max(240),
1129
+ title: z.string().max(240).optional(),
1130
+ url: BoardRemoteUrlSchema,
1131
+ mimeType: z.string().max(160).optional()
1132
+ };
1133
+ const BoardTaskArtifactSchema = z.discriminatedUnion("type", [
1134
+ z.object({
1135
+ ...BoardTaskMediaArtifactFields,
1136
+ type: z.literal("image"),
1137
+ naturalWidth: z.number().positive().optional(),
1138
+ naturalHeight: z.number().positive().optional()
1139
+ }).strict(),
1140
+ z.object({
1141
+ ...BoardTaskMediaArtifactFields,
1142
+ type: z.literal("video"),
1143
+ previewUrl: BoardRemoteUrlSchema.optional(),
1144
+ durationMs: z.number().int().positive().optional(),
1145
+ naturalWidth: z.number().positive().optional(),
1146
+ naturalHeight: z.number().positive().optional()
1147
+ }).strict(),
1148
+ z.object({
1149
+ ...BoardTaskMediaArtifactFields,
1150
+ type: z.literal("audio"),
1151
+ previewUrl: BoardRemoteUrlSchema.optional(),
1152
+ durationMs: z.number().int().positive().optional()
1153
+ }).strict(),
1154
+ z.object({
1155
+ id: z.string().min(1).max(240),
1156
+ type: z.literal("text"),
1157
+ title: z.string().max(240).optional(),
1158
+ textExcerpt: z.string().min(1).max(480)
1159
+ }).strict()
1160
+ ]);
1161
+ /**
1162
+ * Cached task facts used for an immediate first paint. The task run remains the
1163
+ * source of truth and live clients refresh this projection by `taskRunId`.
1164
+ */
1165
+ const BoardTaskSnapshotSchema = z.object({
1166
+ taskType: z.string().min(1).max(120),
1167
+ status: z.enum([
1168
+ "pending",
1169
+ "running",
1170
+ "completed",
1171
+ "failed"
1172
+ ]),
1173
+ title: z.string().min(1).max(240),
1174
+ model: z.string().max(160).optional(),
1175
+ promptExcerpt: z.string().max(480).optional(),
1176
+ artifactCount: z.number().int().nonnegative(),
1177
+ artifacts: z.array(BoardTaskArtifactSchema).max(6).default([]),
1178
+ updatedAt: z.string().optional()
1179
+ }).strict();
1180
+ /** A stable reference to a task run with a small, replaceable display cache. */
1181
+ const BoardTaskItemSchema = BoardItemBaseSchema.extend({
1182
+ type: z.literal("task"),
1183
+ taskRunId: z.string().min(1),
1184
+ snapshot: BoardTaskSnapshotSchema
1185
+ });
1186
+ /**
1187
+ * A forward-compatible carrier for shape types this client does not recognise.
1188
+ * Its discriminant is the literal `"unknown"` so the item union still narrows
1189
+ * cleanly on `type`. The real type string and every original field are preserved
1190
+ * verbatim in `raw`.
1191
+ */
1192
+ const UNKNOWN_BOARD_ITEM_TYPE = "unknown";
1193
+ const BoardItemSchema = z.any().transform((raw) => parseBoardItemLoose(raw));
1194
+ /**
1195
+ * Parse a single item leniently: known types are validated and normalised;
1196
+ * anything else becomes a lossless unknown item. Never throws on shape data —
1197
+ * a malformed known item degrades to unknown rather than failing the document.
1198
+ */
1199
+ function parseBoardItemLoose(raw) {
1200
+ if (!raw || typeof raw !== "object") return makeUnknownItem(raw);
1201
+ switch (raw.type) {
1202
+ case "image": {
1203
+ const parsed = BoardImageItemSchema.safeParse(raw);
1204
+ return parsed.success ? parsed.data : makeUnknownItem(raw);
1205
+ }
1206
+ case "video": {
1207
+ const parsed = BoardVideoItemSchema.safeParse(raw);
1208
+ return parsed.success ? parsed.data : makeUnknownItem(raw);
1209
+ }
1210
+ case "audio": {
1211
+ const parsed = BoardAudioItemSchema.safeParse(raw);
1212
+ return parsed.success ? parsed.data : makeUnknownItem(raw);
1213
+ }
1214
+ case "file": {
1215
+ const parsed = BoardFileItemSchema.safeParse(raw);
1216
+ return parsed.success ? parsed.data : makeUnknownItem(raw);
1217
+ }
1218
+ case "task": {
1219
+ const parsed = BoardTaskItemSchema.safeParse(raw);
1220
+ return parsed.success ? parsed.data : makeUnknownItem(raw);
1221
+ }
1222
+ case "text": {
1223
+ const parsed = BoardTextItemSchema.safeParse(raw);
1224
+ return parsed.success ? parsed.data : makeUnknownItem(raw);
1225
+ }
1226
+ case "geo": {
1227
+ const parsed = BoardGeoItemSchema.safeParse(raw);
1228
+ return parsed.success ? parsed.data : makeUnknownItem(raw);
1229
+ }
1230
+ case "draw": {
1231
+ const parsed = BoardDrawItemSchema.safeParse(raw);
1232
+ return parsed.success ? parsed.data : makeUnknownItem(raw);
1233
+ }
1234
+ case "arrow": {
1235
+ const parsed = BoardArrowItemSchema.safeParse(raw);
1236
+ return parsed.success ? parsed.data : makeUnknownItem(raw);
1237
+ }
1238
+ case "frame": {
1239
+ const parsed = BoardFrameItemSchema.safeParse(raw);
1240
+ return parsed.success ? parsed.data : makeUnknownItem(raw);
1241
+ }
1242
+ default: return makeUnknownItem(raw);
1243
+ }
1244
+ }
1245
+ function makeUnknownItem(raw) {
1246
+ const record = raw && typeof raw === "object" ? raw : {};
1247
+ const frameParsed = BoardFrameSchema.safeParse(record.frame);
1248
+ const styleParsed = BoardItemStyleSchema.safeParse(record.style);
1249
+ return {
1250
+ id: typeof record.id === "string" && record.id ? record.id : "unknown",
1251
+ type: UNKNOWN_BOARD_ITEM_TYPE,
1252
+ frame: frameParsed.success ? frameParsed.data : {
1253
+ x: 0,
1254
+ y: 0,
1255
+ width: 120,
1256
+ height: 80,
1257
+ rotation: 0
1258
+ },
1259
+ ...record.locked === true ? { locked: true } : {},
1260
+ ...styleParsed.success && record.style ? { style: styleParsed.data } : {},
1261
+ ...record.metadata && typeof record.metadata === "object" ? { metadata: record.metadata } : {},
1262
+ raw: record
1263
+ };
1264
+ }
1265
+ z.object({
1266
+ kind: z.literal(BOARD_DOCUMENT_KIND),
1267
+ version: z.literal(1),
1268
+ appearance: BoardAppearanceSchema.default({
1269
+ theme: "clean",
1270
+ background: { kind: "solid" },
1271
+ grid: {
1272
+ visible: false,
1273
+ size: 24,
1274
+ opacity: .12
1275
+ },
1276
+ mood: "clean"
1277
+ }),
1278
+ viewport: BoardViewportSchema,
1279
+ items: z.array(BoardItemSchema),
1280
+ /**
1281
+ * Node relations. Separate from `items` because a connection has no frame of
1282
+ * its own — its geometry is derived from the nodes it joins, so it is a
1283
+ * relation over the item set rather than a member of it.
1284
+ *
1285
+ * Connections referencing a missing node are dropped on parse: a relation to
1286
+ * nothing is not a relation, and keeping one would let an invisible dangling
1287
+ * edge accumulate silently. Callers that need to know write through the
1288
+ * transaction API, which reports the reference error instead.
1289
+ */
1290
+ connections: z.array(BoardConnectionSchema).default([])
1291
+ });
1292
+ //#endregion
1293
+ //#region ../protocol/dist/board-node.js
1294
+ const BOARD_COLOR_IDS = [
1295
+ "brand",
1296
+ "neutral",
1297
+ "black",
1298
+ "white",
1299
+ "blue",
1300
+ "green",
1301
+ "amber",
1302
+ "violet",
1303
+ "rose"
1304
+ ];
1305
+ const BoardColorIdSchema = z.enum(BOARD_COLOR_IDS);
1306
+ const BOARD_GEO_KINDS = [
1307
+ "rectangle",
1308
+ "rounded",
1309
+ "ellipse",
1310
+ "diamond",
1311
+ "triangle"
1312
+ ];
1313
+ const BoardGeoKindSchema = z.enum(BOARD_GEO_KINDS);
1314
+ const BOARD_NATIVE_NODE_TYPES = [
1315
+ "image",
1316
+ "video",
1317
+ "audio",
1318
+ "file",
1319
+ "task",
1320
+ "text",
1321
+ "geo",
1322
+ "draw",
1323
+ "arrow",
1324
+ "frame"
1325
+ ];
1326
+ const metadataSchema = z.record(z.string(), z.unknown());
1327
+ const commonData = {
1328
+ locked: z.boolean().optional(),
1329
+ metadata: metadataSchema.optional()
1330
+ };
1331
+ const pointSchema = z.object({
1332
+ x: z.number().finite(),
1333
+ y: z.number().finite(),
1334
+ p: z.number().finite().min(0).max(1).default(.5)
1335
+ }).strict();
1336
+ const worldPointSchema = z.object({
1337
+ x: z.number().finite(),
1338
+ y: z.number().finite()
1339
+ }).strict();
1340
+ const strokeSizeSchema = z.number().finite().min(1).max(64);
1341
+ const mediaViewSchema = z.object({
1342
+ title: z.string().optional(),
1343
+ mimeType: z.string().optional(),
1344
+ size: z.number().finite().nonnegative().optional(),
1345
+ mtimeMs: z.number().finite().nonnegative().optional(),
1346
+ naturalWidth: z.number().finite().positive().optional(),
1347
+ naturalHeight: z.number().finite().positive().optional()
1348
+ }).strict();
1349
+ const audioViewSchema = mediaViewSchema.extend({ durationMs: z.number().finite().nonnegative().optional() });
1350
+ const dataSchemas = {
1351
+ text: z.object({
1352
+ ...commonData,
1353
+ text: z.string().default(""),
1354
+ color: BoardColorIdSchema.default("neutral"),
1355
+ fontSize: z.number().finite().min(2).max(512).default(24)
1356
+ }).strict(),
1357
+ geo: z.object({
1358
+ ...commonData,
1359
+ geo: BoardGeoKindSchema.default("rectangle"),
1360
+ text: z.string().default(""),
1361
+ color: BoardColorIdSchema.default("brand"),
1362
+ fillOpacity: z.number().finite().min(0).max(1).default(0)
1363
+ }).strict(),
1364
+ draw: z.object({
1365
+ ...commonData,
1366
+ points: z.array(pointSchema).min(1),
1367
+ color: BoardColorIdSchema.default("brand"),
1368
+ size: strokeSizeSchema.default(4)
1369
+ }).strict(),
1370
+ arrow: z.object({
1371
+ ...commonData,
1372
+ start: worldPointSchema,
1373
+ end: worldPointSchema,
1374
+ bend: z.number().finite().min(-.85).max(.85).default(0),
1375
+ color: BoardColorIdSchema.default("brand"),
1376
+ size: strokeSizeSchema.default(BOARD_ARROW_STROKE_SIZE),
1377
+ arrowStart: z.boolean().default(false),
1378
+ arrowEnd: z.boolean().default(true),
1379
+ label: z.string().default("")
1380
+ }).strict(),
1381
+ frame: z.object({
1382
+ ...commonData,
1383
+ label: z.string().default("Frame"),
1384
+ color: BoardColorIdSchema.default("neutral")
1385
+ }).strict(),
1386
+ image: z.object({
1387
+ ...commonData,
1388
+ crop: z.object({
1389
+ x: z.number().finite().min(0).max(1),
1390
+ y: z.number().finite().min(0).max(1),
1391
+ w: z.number().finite().min(0).max(1),
1392
+ h: z.number().finite().min(0).max(1)
1393
+ }).strict().optional()
1394
+ }).strict(),
1395
+ video: z.object(commonData).strict(),
1396
+ audio: z.object(commonData).strict(),
1397
+ file: z.object(commonData).strict(),
1398
+ task: z.object({
1399
+ ...commonData,
1400
+ taskRunId: z.string().min(1)
1401
+ }).strict()
1402
+ };
1403
+ const fileViewSchema = z.object({
1404
+ title: z.string().optional(),
1405
+ mimeType: z.string().optional(),
1406
+ size: z.number().finite().nonnegative().optional(),
1407
+ mtimeMs: z.number().finite().nonnegative().optional(),
1408
+ excerpt: z.string().optional(),
1409
+ coverPath: z.string().optional(),
1410
+ coverUrl: z.string().url().optional()
1411
+ }).strict();
1412
+ const taskViewSchema = BoardTaskSnapshotSchema;
1413
+ const emptyViewSchema = z.object({}).strict();
1414
+ const viewSchemas = {
1415
+ image: mediaViewSchema,
1416
+ video: mediaViewSchema,
1417
+ audio: audioViewSchema,
1418
+ file: fileViewSchema,
1419
+ task: taskViewSchema,
1420
+ text: emptyViewSchema,
1421
+ geo: emptyViewSchema,
1422
+ draw: emptyViewSchema,
1423
+ arrow: emptyViewSchema,
1424
+ frame: emptyViewSchema
1425
+ };
1426
+ const nodeEnvelopeSchema = z.object({
1427
+ nodeId: z.string().min(1).max(160),
1428
+ type: z.string().min(1).max(40),
1429
+ parentId: z.string().min(1).max(160).nullable(),
1430
+ orderKey: z.string().max(4096).nullable(),
1431
+ x: z.number().finite(),
1432
+ y: z.number().finite(),
1433
+ width: z.number().finite().positive(),
1434
+ height: z.number().finite().positive(),
1435
+ rotation: z.number().finite(),
1436
+ refKind: z.string().max(40).nullable(),
1437
+ refPath: z.string().max(4096).nullable(),
1438
+ refUrl: z.string().max(4096).nullable(),
1439
+ view: z.record(z.string(), z.unknown()),
1440
+ style: z.record(z.string(), z.unknown()),
1441
+ data: z.record(z.string(), z.unknown())
1442
+ }).strict();
1443
+ function jsonSchema(schema) {
1444
+ return z.toJSONSchema(schema);
1445
+ }
1446
+ const BOARD_NODE_CONTRACT = {
1447
+ types: BOARD_NATIVE_NODE_TYPES,
1448
+ colors: BOARD_COLOR_IDS,
1449
+ geos: BOARD_GEO_KINDS,
1450
+ coordinates: {
1451
+ frame: "world",
1452
+ drawPoints: "frame-local",
1453
+ arrowEndpoints: "world"
1454
+ },
1455
+ references: {
1456
+ nodeTypes: [
1457
+ "image",
1458
+ "video",
1459
+ "audio",
1460
+ "file"
1461
+ ],
1462
+ kind: "space_file",
1463
+ pathField: "refPath"
1464
+ },
1465
+ schemas: {
1466
+ envelope: jsonSchema(nodeEnvelopeSchema),
1467
+ data: Object.fromEntries(BOARD_NATIVE_NODE_TYPES.map((type) => [type, jsonSchema(dataSchemas[type])])),
1468
+ view: Object.fromEntries(BOARD_NATIVE_NODE_TYPES.map((type) => [type, jsonSchema(viewSchemas[type])]))
1469
+ }
1470
+ };
1471
+ function issueDiagnostic(issue, path) {
1472
+ const fullPath = [path, ...issue.path].join(".");
1473
+ const values = "values" in issue && Array.isArray(issue.values) ? issue.values.filter((value) => typeof value === "string") : void 0;
1474
+ return {
1475
+ severity: "error",
1476
+ code: "INVALID_BOARD_NODE",
1477
+ message: `${fullPath}: ${issue.message}`,
1478
+ path: fullPath,
1479
+ ...values?.length ? { allowedValues: values } : {}
1480
+ };
1481
+ }
1482
+ function drawGeometryDiagnostic(node, data, path) {
1483
+ let minX = Number.POSITIVE_INFINITY;
1484
+ let minY = Number.POSITIVE_INFINITY;
1485
+ let maxX = Number.NEGATIVE_INFINITY;
1486
+ let maxY = Number.NEGATIVE_INFINITY;
1487
+ for (const point of data.points) {
1488
+ const radius = Math.max(.5, data.size / 2 * (.5 + point.p));
1489
+ minX = Math.min(minX, point.x - radius);
1490
+ minY = Math.min(minY, point.y - radius);
1491
+ maxX = Math.max(maxX, point.x + radius);
1492
+ maxY = Math.max(maxY, point.y + radius);
1493
+ }
1494
+ const width = Math.max(1, maxX - minX);
1495
+ const height = Math.max(1, maxY - minY);
1496
+ const tolerance = Math.max(.01, node.width * 1e-6, node.height * 1e-6);
1497
+ if (Math.abs(minX) <= tolerance && Math.abs(minY) <= tolerance && Math.abs(width - node.width) <= tolerance && Math.abs(height - node.height) <= tolerance) return null;
1498
+ return {
1499
+ severity: "error",
1500
+ code: "INVALID_BOARD_GEOMETRY",
1501
+ message: `${path}.data.points must use frame-local coordinates and match the node frame`,
1502
+ path: `${path}.data.points`,
1503
+ expected: "frame-local points with bounds matching width and height",
1504
+ coordinateSpace: "frame-local"
1505
+ };
1506
+ }
1507
+ function validateBoardNodeInput(node, path = "node") {
1508
+ const envelopeResult = nodeEnvelopeSchema.safeParse(node);
1509
+ if (!envelopeResult.success) return envelopeResult.error.issues.map((issue) => issueDiagnostic(issue, path));
1510
+ if (typeof node.type !== "string" || !BOARD_NATIVE_NODE_TYPES.includes(node.type)) return [{
1511
+ severity: "error",
1512
+ code: "INVALID_BOARD_NODE",
1513
+ message: `${path}.type is not supported`,
1514
+ path: `${path}.type`,
1515
+ expected: "BoardNativeNodeType",
1516
+ received: node.type,
1517
+ allowedValues: BOARD_NATIVE_NODE_TYPES
1518
+ }];
1519
+ const type = node.type;
1520
+ const dataResult = dataSchemas[type].safeParse(node.data ?? {});
1521
+ if (!dataResult.success) return dataResult.error.issues.map((issue) => issueDiagnostic(issue, `${path}.data`));
1522
+ const viewResult = viewSchemas[type].safeParse(node.view ?? {});
1523
+ if (!viewResult.success) return viewResult.error.issues.map((issue) => issueDiagnostic(issue, `${path}.view`));
1524
+ if (type === "image" || type === "video" || type === "audio" || type === "file") {
1525
+ if (node.refKind !== "space_file" || typeof node.refPath !== "string" || !node.refPath) return [{
1526
+ severity: "error",
1527
+ code: "INVALID_BOARD_NODE",
1528
+ message: `${path} requires a space file reference`,
1529
+ path: `${path}.refPath`,
1530
+ expected: "non-empty refPath with refKind space_file"
1531
+ }];
1532
+ }
1533
+ if (type === "draw") {
1534
+ const diagnostic = drawGeometryDiagnostic(node, dataResult.data, path);
1535
+ return diagnostic ? [diagnostic] : [];
1536
+ }
1537
+ if (type === "arrow") {
1538
+ const data = dataResult.data;
1539
+ const inside = (point) => point.x >= node.x && point.x <= node.x + node.width && point.y >= node.y && point.y <= node.y + node.height;
1540
+ if (!inside(data.start) || !inside(data.end)) return [{
1541
+ severity: "error",
1542
+ code: "INVALID_BOARD_GEOMETRY",
1543
+ message: `${path}.data endpoints must be covered by the node frame`,
1544
+ path: `${path}.data`,
1545
+ expected: "world-space endpoints inside the node frame",
1546
+ coordinateSpace: "world"
1547
+ }];
1548
+ }
1549
+ return [];
1550
+ }
1551
+ //#endregion
1552
+ //#region ../protocol/dist/realtime/board-awareness.js
1553
+ const idSchema = z.string().min(1).max(160);
1554
+ const finiteSchema = z.number().finite();
1555
+ const BoardAwarenessPointSchema = z.object({
1556
+ x: finiteSchema,
1557
+ y: finiteSchema
1558
+ });
1559
+ const BoardAwarenessDrawPointSchema = BoardAwarenessPointSchema.extend({ p: finiteSchema.min(0).max(1) });
1560
+ const BoardAwarenessFrameSchema = z.object({
1561
+ x: finiteSchema,
1562
+ y: finiteSchema,
1563
+ width: finiteSchema.positive(),
1564
+ height: finiteSchema.positive(),
1565
+ rotation: finiteSchema
1566
+ });
1567
+ const BoardAwarenessNodePreviewSchema = z.object({
1568
+ nodeId: idSchema,
1569
+ frame: BoardAwarenessFrameSchema,
1570
+ /** Live endpoints of a free arrow being dragged. */
1571
+ arrow: z.object({
1572
+ start: BoardAwarenessPointSchema,
1573
+ end: BoardAwarenessPointSchema,
1574
+ bend: finiteSchema
1575
+ }).optional()
1576
+ });
1577
+ const BoardAwarenessStateUpdateSchema = z.object({
1578
+ type: z.literal("state"),
1579
+ client: z.object({ formFactor: z.enum(["desktop", "mobile"]) }).optional(),
1580
+ cursor: BoardAwarenessPointSchema.extend({ pointerType: z.enum([
1581
+ "mouse",
1582
+ "pen",
1583
+ "touch"
1584
+ ]) }).nullable(),
1585
+ tool: z.string().min(1).max(40),
1586
+ selection: z.object({
1587
+ ids: z.array(idSchema).max(64),
1588
+ count: z.number().int().nonnegative().max(5e4),
1589
+ bounds: BoardAwarenessFrameSchema.nullable()
1590
+ }),
1591
+ editingId: idSchema.nullable()
1592
+ });
1593
+ const BoardAwarenessGestureSchema = z.discriminatedUnion("kind", [
1594
+ z.object({
1595
+ kind: z.literal("draw"),
1596
+ id: idSchema,
1597
+ nodeId: idSchema,
1598
+ color: z.string().min(1).max(64),
1599
+ size: finiteSchema.positive().max(256),
1600
+ from: z.number().int().nonnegative().max(1e5),
1601
+ points: z.array(BoardAwarenessDrawPointSchema).min(1).max(64)
1602
+ }),
1603
+ z.object({
1604
+ kind: z.literal("arrow"),
1605
+ id: idSchema,
1606
+ nodeId: idSchema,
1607
+ start: BoardAwarenessPointSchema,
1608
+ current: BoardAwarenessPointSchema,
1609
+ color: z.string().min(1).max(64),
1610
+ size: finiteSchema.positive().max(256).default(BOARD_ARROW_STROKE_SIZE)
1611
+ }),
1612
+ z.object({
1613
+ kind: z.literal("box"),
1614
+ id: idSchema,
1615
+ nodeId: idSchema,
1616
+ shape: z.enum(["geo", "frame"]),
1617
+ start: BoardAwarenessPointSchema,
1618
+ current: BoardAwarenessPointSchema,
1619
+ color: z.string().min(1).max(64),
1620
+ geo: z.string().min(1).max(40)
1621
+ }),
1622
+ z.object({
1623
+ kind: z.literal("connection"),
1624
+ id: idSchema,
1625
+ sourceNodeId: idSchema,
1626
+ targetNodeId: idSchema.nullable(),
1627
+ current: BoardAwarenessPointSchema,
1628
+ color: z.string().min(1).max(64),
1629
+ size: finiteSchema.positive().max(256).default(BOARD_CONNECTION_STROKE_SIZE)
1630
+ }),
1631
+ z.object({
1632
+ kind: z.literal("transform"),
1633
+ id: idSchema,
1634
+ mode: z.enum([
1635
+ "translate",
1636
+ "resize",
1637
+ "rotate",
1638
+ "arrow",
1639
+ "connection"
1640
+ ]),
1641
+ nodes: z.array(BoardAwarenessNodePreviewSchema).max(64),
1642
+ bounds: BoardAwarenessFrameSchema.nullable()
1643
+ })
1644
+ ]);
1645
+ const BoardAwarenessUpdateSchema = z.discriminatedUnion("type", [
1646
+ BoardAwarenessStateUpdateSchema,
1647
+ z.object({
1648
+ type: z.literal("gesture"),
1649
+ gesture: BoardAwarenessGestureSchema
1650
+ }),
1651
+ z.object({
1652
+ type: z.literal("gesture.end"),
1653
+ gestureId: idSchema,
1654
+ resultingNodeIds: z.array(idSchema).max(64)
1655
+ }),
1656
+ z.object({
1657
+ type: z.literal("gesture.cancel"),
1658
+ gestureId: idSchema
1659
+ })
1660
+ ]);
1661
+ const BoardAwarenessClientPayloadSchema = z.object({
1662
+ spaceId: z.string().uuid(),
1663
+ boardId: z.string().uuid(),
1664
+ seq: z.number().int().nonnegative(),
1665
+ update: BoardAwarenessUpdateSchema
1666
+ });
1667
+ //#endregion
1668
+ //#region ../protocol/dist/public-identifiers.js
1669
+ const USERNAME_PATTERN = /^(?!-)(?!.*--)[a-z0-9-]{1,39}(?<!-)$/;
1670
+ const SPACE_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9_-]{0,78}[a-z0-9])?$/;
1671
+ /**
1672
+ * Platform-owned path segments that must not be newly assigned to public
1673
+ * identities. Existing stored values remain readable through parse helpers.
1674
+ */
1675
+ const RESERVED_PLATFORM_PATH_SEGMENTS = Object.freeze([
1676
+ "admin",
1677
+ "api",
1678
+ "assets",
1679
+ "auth",
1680
+ "callback",
1681
+ "changelog",
1682
+ "docs",
1683
+ "explore",
1684
+ "invite",
1685
+ "landing",
1686
+ "login",
1687
+ "logout",
1688
+ "new",
1689
+ "org",
1690
+ "pricing",
1691
+ "pwa",
1692
+ "referrals",
1693
+ "sessions",
1694
+ "settings",
1695
+ "spaces",
1696
+ "static",
1697
+ "teams",
1698
+ "trending",
1699
+ "u",
1700
+ "user",
1701
+ "users",
1702
+ "work-auth"
1703
+ ]);
1704
+ new Set(RESERVED_PLATFORM_PATH_SEGMENTS);
1705
+ [...RESERVED_PLATFORM_PATH_SEGMENTS];
1706
+ function parseUsername(value) {
1707
+ if (value === null || value === void 0) return null;
1708
+ const normalized = value.trim().toLowerCase();
1709
+ return USERNAME_PATTERN.test(normalized) ? normalized : null;
1710
+ }
1711
+ function parseSpaceSlug(value) {
1712
+ if (value === null || value === void 0) return null;
1713
+ const normalized = value.trim();
1714
+ return SPACE_SLUG_PATTERN.test(normalized) ? normalized : null;
1715
+ }
1716
+ //#endregion
1717
+ //#region ../protocol/dist/ui-command.js
1718
+ /**
1719
+ * Lets an agent drive the Cohub frontend that originated the work. Routing comes
1720
+ * from request provenance, never a caller-supplied target, so a command only
1721
+ * reaches the actor's own instances.
1722
+ */
1723
+ const UI_COMMAND_VERSION = 1;
1724
+ /** Persisted and broadcast, so every field is capped; MAX_BYTES bounds the whole. */
1725
+ const UI_COMMAND_PAYLOAD_MAX_BYTES = 32 * 1024;
1726
+ const UI_COMMAND_MAX_BYTES = 40 * 1024;
1727
+ const UI_COMMAND_LAUNCH_MAX_LENGTH = 2048;
1728
+ const UI_COMMAND_DEFAULT_TIMEOUT_MS = 600 * 1e3;
1729
+ const UI_COMMAND_MAX_TIMEOUT_MS = 720 * 60 * 1e3;
1730
+ const UI_COMMAND_SETTLEMENT_GRACE_SECONDS = 600;
1731
+ /** Keeps pending commands reportable for the full wait window plus settlement grace. */
1732
+ const UI_COMMAND_PENDING_TTL_SECONDS = 43800;
1733
+ const UI_COMMAND_TERMINAL_TTL_SECONDS = 1800;
1734
+ const UI_COMMAND_TERMINAL_STATUSES = [
1735
+ "applied",
1736
+ "no_active_client",
1737
+ "ui_host_unavailable",
1738
+ "rejected",
1739
+ "unsupported",
1740
+ "timeout"
1741
+ ];
1742
+ const isTerminalUiCommandStatus = (status) => UI_COMMAND_TERMINAL_STATUSES.includes(status);
1743
+ const METHOD_RE = /^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/;
1744
+ const isUiSurfaceMethod = (value) => typeof value === "string" && METHOD_RE.test(value);
1745
+ const WORK_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1746
+ const UI_COMMAND_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
1747
+ const parseUiCommandId = (value) => {
1748
+ if (typeof value !== "string") return null;
1749
+ const trimmed = value.trim();
1750
+ return UI_COMMAND_ID_RE.test(trimmed) ? trimmed : null;
1751
+ };
1752
+ const isRecord$1 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
1753
+ const asTrimmed = (value) => {
1754
+ if (typeof value !== "string") return null;
1755
+ const trimmed = value.trim();
1756
+ return trimmed ? trimmed : null;
1757
+ };
1758
+ const parseLaunch = (value) => {
1759
+ if (!isRecord$1(value)) return void 0;
1760
+ const search = asTrimmed(value.search);
1761
+ const hash = asTrimmed(value.hash);
1762
+ if (!search && !hash) return void 0;
1763
+ return {
1764
+ ...search ? { search: search.startsWith("?") ? search : `?${search}` } : {},
1765
+ ...hash ? { hash: hash.startsWith("#") ? hash : `#${hash}` } : {}
1766
+ };
1767
+ };
1768
+ const measureUiCommandPayload = (value) => {
1769
+ if (value === void 0) return 0;
1770
+ try {
1771
+ return new TextEncoder().encode(JSON.stringify(value) ?? "").length;
1772
+ } catch {
1773
+ return null;
1774
+ }
1775
+ };
1776
+ const parseUiCommand = (input) => {
1777
+ if (!isRecord$1(input)) return {
1778
+ command: null,
1779
+ error: "command must be an object"
1780
+ };
1781
+ if (input.type !== "preview.show") return {
1782
+ command: null,
1783
+ error: "command.type must be one of: preview.show"
1784
+ };
1785
+ const preview = input.preview;
1786
+ if (!isRecord$1(preview)) return {
1787
+ command: null,
1788
+ error: "command.preview is required"
1789
+ };
1790
+ if (preview.kind !== "work") return {
1791
+ command: null,
1792
+ error: "command.preview.kind must be one of: work"
1793
+ };
1794
+ const workId = asTrimmed(preview.workId);
1795
+ if (!workId) return {
1796
+ command: null,
1797
+ error: "command.preview.workId is required"
1798
+ };
1799
+ if (!WORK_ID_RE.test(workId)) return {
1800
+ command: null,
1801
+ error: "command.preview.workId must be a Work id"
1802
+ };
1803
+ const label = asTrimmed(preview.label);
1804
+ if (label && label.length > 200) return {
1805
+ command: null,
1806
+ error: `command.preview.label exceeds 200 characters`
1807
+ };
1808
+ const launch = parseLaunch(preview.launch);
1809
+ if (launch) {
1810
+ for (const [field, value] of [["search", launch.search], ["hash", launch.hash]]) if (value && value.length > 2048) return {
1811
+ command: null,
1812
+ error: `command.preview.launch.${field} exceeds ${UI_COMMAND_LAUNCH_MAX_LENGTH} characters`
1813
+ };
1814
+ }
1815
+ let request;
1816
+ if (input.request !== void 0 && input.request !== null) {
1817
+ if (!isRecord$1(input.request)) return {
1818
+ command: null,
1819
+ error: "command.request must be an object"
1820
+ };
1821
+ const method = asTrimmed(input.request.method);
1822
+ if (!method) return {
1823
+ command: null,
1824
+ error: "command.request.method is required"
1825
+ };
1826
+ if (!isUiSurfaceMethod(method)) return {
1827
+ command: null,
1828
+ error: "command.request.method has an unsupported format"
1829
+ };
1830
+ const size = measureUiCommandPayload(input.request.input);
1831
+ if (size === null) return {
1832
+ command: null,
1833
+ error: "command.request.input must be JSON-serializable"
1834
+ };
1835
+ if (size > 32768) return {
1836
+ command: null,
1837
+ error: `command.request.input exceeds ${UI_COMMAND_PAYLOAD_MAX_BYTES} bytes`
1838
+ };
1839
+ request = {
1840
+ method,
1841
+ ...input.request.input === void 0 ? {} : { input: input.request.input }
1842
+ };
1843
+ }
1844
+ const command = {
1845
+ type: "preview.show",
1846
+ preview: {
1847
+ kind: "work",
1848
+ workId,
1849
+ ...label ? { label } : {},
1850
+ ...launch ? { launch } : {}
1851
+ },
1852
+ ...request ? { request } : {}
1853
+ };
1854
+ const totalSize = measureUiCommandPayload(command);
1855
+ if (totalSize === null) return {
1856
+ command: null,
1857
+ error: "command must be JSON-serializable"
1858
+ };
1859
+ if (totalSize > 40960) return {
1860
+ command: null,
1861
+ error: `command exceeds ${UI_COMMAND_MAX_BYTES} bytes`
1862
+ };
1863
+ return {
1864
+ command,
1865
+ error: null
1866
+ };
1867
+ };
1868
+ //#endregion
1869
+ //#region src/board/core/draw-geometry.ts
1870
+ /** Radius of a sample in world units given the stroke size and pressure. */
1871
+ function sampleRadius(size, pressure) {
1872
+ const clamped = Math.min(1, Math.max(0, pressure));
1873
+ return Math.max(.5, size / 2 * (.5 + clamped));
1874
+ }
1875
+ /** Axis-aligned bounds of a stroke in its local space, padded by stroke width. */
1876
+ function computeDrawBounds(points, size) {
1877
+ if (points.length === 0) return {
1878
+ x: 0,
1879
+ y: 0,
1880
+ width: 1,
1881
+ height: 1
1882
+ };
1883
+ let minX = Number.POSITIVE_INFINITY;
1884
+ let minY = Number.POSITIVE_INFINITY;
1885
+ let maxX = Number.NEGATIVE_INFINITY;
1886
+ let maxY = Number.NEGATIVE_INFINITY;
1887
+ for (const point of points) {
1888
+ const r = sampleRadius(size, point.p);
1889
+ minX = Math.min(minX, point.x - r);
1890
+ minY = Math.min(minY, point.y - r);
1891
+ maxX = Math.max(maxX, point.x + r);
1892
+ maxY = Math.max(maxY, point.y + r);
1893
+ }
1894
+ return {
1895
+ x: minX,
1896
+ y: minY,
1897
+ width: Math.max(1, maxX - minX),
1898
+ height: Math.max(1, maxY - minY)
1899
+ };
1900
+ }
1901
+ //#endregion
1902
+ //#region src/board/nodes.ts
1903
+ var BoardInputError = class extends Error {
1904
+ code = "INVALID_BOARD_NODE";
1905
+ diagnostics;
1906
+ body;
1907
+ constructor(diagnostics) {
1908
+ const message = diagnostics[0]?.message ?? "Invalid Board node";
1909
+ super(message);
1910
+ this.name = "BoardInputError";
1911
+ this.diagnostics = diagnostics;
1912
+ this.body = {
1913
+ code: this.code,
1914
+ message,
1915
+ diagnostics
1916
+ };
1917
+ }
1918
+ };
1919
+ function baseNode(spec, frame) {
1920
+ return {
1921
+ nodeId: spec.id,
1922
+ type: spec.type,
1923
+ parentId: spec.parentId ?? null,
1924
+ orderKey: spec.orderKey ?? null,
1925
+ x: frame.x,
1926
+ y: frame.y,
1927
+ width: frame.width,
1928
+ height: frame.height,
1929
+ rotation: frame.rotation ?? 0,
1930
+ refKind: null,
1931
+ refPath: null,
1932
+ refUrl: null,
1933
+ view: {},
1934
+ style: spec.style ?? {},
1935
+ data: {}
1936
+ };
1937
+ }
1938
+ function arrowFrame(start, end) {
1939
+ const padding = 16;
1940
+ return {
1941
+ x: Math.min(start.x, end.x) - padding,
1942
+ y: Math.min(start.y, end.y) - padding,
1943
+ width: Math.max(1, Math.abs(end.x - start.x) + padding * 2),
1944
+ height: Math.max(1, Math.abs(end.y - start.y) + padding * 2)
1945
+ };
1946
+ }
1947
+ /**
1948
+ * Create one validated wire node from semantic Board input.
1949
+ *
1950
+ * Box nodes take an explicit world-space frame. Draw samples and arrow endpoints
1951
+ * take world coordinates; the builder derives their frame and local storage form.
1952
+ */
1953
+ function createBoardNode(spec) {
1954
+ let node;
1955
+ if (spec.type === "draw") {
1956
+ const size = spec.size ?? 4;
1957
+ const worldPoints = spec.points.map((point) => ({
1958
+ x: point.x,
1959
+ y: point.y,
1960
+ p: point.p ?? .5
1961
+ }));
1962
+ const bounds = computeDrawBounds(worldPoints, size);
1963
+ node = baseNode(spec, bounds);
1964
+ node.data = {
1965
+ points: worldPoints.map((point) => ({
1966
+ x: point.x - bounds.x,
1967
+ y: point.y - bounds.y,
1968
+ p: point.p
1969
+ })),
1970
+ color: spec.color ?? "brand",
1971
+ size
1972
+ };
1973
+ } else if (spec.type === "arrow") {
1974
+ node = baseNode(spec, arrowFrame(spec.start, spec.end));
1975
+ node.data = {
1976
+ start: spec.start,
1977
+ end: spec.end,
1978
+ bend: spec.bend ?? 0,
1979
+ color: spec.color ?? "brand",
1980
+ size: spec.size ?? 2.5,
1981
+ arrowStart: spec.arrowStart ?? false,
1982
+ arrowEnd: spec.arrowEnd ?? true,
1983
+ label: spec.label ?? ""
1984
+ };
1985
+ } else {
1986
+ node = baseNode(spec, spec.frame);
1987
+ switch (spec.type) {
1988
+ case "text":
1989
+ node.data = {
1990
+ text: spec.text ?? "",
1991
+ color: spec.color ?? "neutral",
1992
+ fontSize: spec.fontSize ?? 24
1993
+ };
1994
+ break;
1995
+ case "geo":
1996
+ node.data = {
1997
+ geo: spec.geo ?? "rectangle",
1998
+ text: spec.text ?? "",
1999
+ color: spec.color ?? "brand",
2000
+ fillOpacity: spec.fillOpacity ?? 0
2001
+ };
2002
+ break;
2003
+ case "frame":
2004
+ node.data = {
2005
+ label: spec.label ?? "Frame",
2006
+ color: spec.color ?? "neutral"
2007
+ };
2008
+ break;
2009
+ case "image":
2010
+ node.refKind = "space_file";
2011
+ node.refPath = spec.path;
2012
+ node.view = spec.snapshot ?? {};
2013
+ node.data = spec.crop ? { crop: spec.crop } : {};
2014
+ break;
2015
+ case "video":
2016
+ case "audio":
2017
+ node.refKind = "space_file";
2018
+ node.refPath = spec.path;
2019
+ node.view = spec.snapshot ?? {};
2020
+ break;
2021
+ case "file":
2022
+ node.refKind = "space_file";
2023
+ node.refPath = spec.path;
2024
+ node.view = spec.snapshot ?? {};
2025
+ break;
2026
+ case "task":
2027
+ node.view = spec.snapshot;
2028
+ node.data = { taskRunId: spec.taskRunId };
2029
+ break;
2030
+ }
2031
+ }
2032
+ assertBoardNodes([node]);
2033
+ return node;
2034
+ }
2035
+ function validateBoardNodes(nodes, path = "nodes") {
2036
+ return nodes.flatMap((node, index) => validateBoardNodeInput(node, `${path}.${index}`));
2037
+ }
2038
+ function assertBoardNodes(nodes, path = "nodes") {
2039
+ const diagnostics = validateBoardNodes(nodes, path);
2040
+ if (diagnostics.length > 0) throw new BoardInputError(diagnostics);
2041
+ }
2042
+ function assertBoardTransactionNodeCreates(operations) {
2043
+ const diagnostics = operations.flatMap((operation, index) => {
2044
+ if (operation.type !== "node.create") return [];
2045
+ const payload = operation.payload;
2046
+ return payload.node ? validateBoardNodeInput(payload.node, `operations.${index}.payload.node`) : [];
2047
+ });
2048
+ if (diagnostics.length > 0) throw new BoardInputError(diagnostics);
2049
+ }
2050
+ //#endregion
644
2051
  //#region src/session-patch-reducer.ts
645
2052
  const blockSubPathPattern = /^\/message\/content\/blocks\/(\d+)\/(.+)$/;
646
2053
  const blockPathPattern = /^\/message\/content\/blocks\/(\d+)$/;
@@ -1041,8 +2448,8 @@ const createSessionPatchReducer = () => new SessionPatchReducer();
1041
2448
  //#region src/session-generation-stream.ts
1042
2449
  const SNAPSHOT_RECOVERY_TIMEOUT_MS = 2500;
1043
2450
  const SNAPSHOT_RECOVERY_MAX_BUFFERED_EVENTS = 256;
1044
- const isRecord$1 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
1045
- const isContentBlockArray = (value) => Array.isArray(value) && value.every((item) => isRecord$1(item) && typeof item.type === "string");
2451
+ const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
2452
+ const isContentBlockArray = (value) => Array.isArray(value) && value.every((item) => isRecord(item) && typeof item.type === "string");
1046
2453
  const getMessageKind = (message) => {
1047
2454
  const kind = message.meta?.messageKind;
1048
2455
  return typeof kind === "string" ? kind : null;
@@ -1082,7 +2489,7 @@ function parseAssistantMessageCommit(message) {
1082
2489
  }
1083
2490
  function messageRecordToIntermediate(message) {
1084
2491
  if (!isContentBlockArray(message.content) || message.content.length === 0) return null;
1085
- const meta = isRecord$1(message.meta) ? message.meta : {};
2492
+ const meta = isRecord(message.meta) ? message.meta : {};
1086
2493
  return {
1087
2494
  id: message.id,
1088
2495
  sessionId: message.sessionId,
@@ -1200,9 +2607,9 @@ function mergeMessagesSharingToolUseId(messages) {
1200
2607
  return merged;
1201
2608
  }
1202
2609
  function getCompactionPlacement(message) {
1203
- const meta = isRecord$1(message.meta) ? message.meta : null;
1204
- const compaction = isRecord$1(meta?.compaction) ? meta.compaction : null;
1205
- const placement = isRecord$1(compaction?.placement) ? compaction.placement : null;
2610
+ const meta = isRecord(message.meta) ? message.meta : null;
2611
+ const compaction = isRecord(meta?.compaction) ? meta.compaction : null;
2612
+ const placement = isRecord(compaction?.placement) ? compaction.placement : null;
1206
2613
  return meta?.messageKind === "compacted" ? {
1207
2614
  beforeMessageId: typeof placement?.beforeMessageId === "string" ? placement.beforeMessageId : null,
1208
2615
  sequence: typeof message.sequence === "number" ? message.sequence : null
@@ -1476,7 +2883,7 @@ var SessionGenerationStreamClient = class SessionGenerationStreamClient {
1476
2883
  }
1477
2884
  handlePersisted(event, handlers) {
1478
2885
  const message = event.payload.message;
1479
- if (!isRecord$1(message)) return;
2886
+ if (!isRecord(message)) return;
1480
2887
  const commit = parseAssistantMessageCommit(message);
1481
2888
  if (commit.kind === "intermediate") {
1482
2889
  const intermediate = messageRecordToIntermediate(commit.message);
@@ -1511,7 +2918,7 @@ var SessionGenerationStreamClient = class SessionGenerationStreamClient {
1511
2918
  }
1512
2919
  handleFinalized(event, handlers) {
1513
2920
  const turn = event.payload.turn;
1514
- if (!isRecord$1(turn)) return;
2921
+ if (!isRecord(turn)) return;
1515
2922
  const typedTurn = turn;
1516
2923
  if (typedTurn.status === "interrupted") this.reducer.interrupt({
1517
2924
  spaceId: this.spaceId,
@@ -1543,7 +2950,7 @@ var SessionGenerationStreamClient = class SessionGenerationStreamClient {
1543
2950
  return;
1544
2951
  case "session.turn.updated": {
1545
2952
  const turn = event.payload.turn;
1546
- if (!isRecord$1(turn)) return;
2953
+ if (!isRecord(turn)) return;
1547
2954
  this.emit(handlers, {
1548
2955
  type: "turn_updated",
1549
2956
  turn,
@@ -1756,6 +3163,80 @@ var SpacesApi = class {
1756
3163
  });
1757
3164
  }
1758
3165
  };
3166
+ function inlineFileDataUrl(file) {
3167
+ const mimeType = file.mimeType ?? "application/octet-stream";
3168
+ return file.encoding === "base64" ? `data:${mimeType};base64,${file.content}` : `data:${mimeType};charset=utf-8,${encodeURIComponent(file.content)}`;
3169
+ }
3170
+ function spaceFileAbortReason(signal) {
3171
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
3172
+ }
3173
+ function waitForSpaceFileRetry(ms, signal) {
3174
+ if (signal?.aborted) return Promise.reject(spaceFileAbortReason(signal));
3175
+ return new Promise((resolve, reject) => {
3176
+ const timeout = setTimeout(finish, ms);
3177
+ const onAbort = () => {
3178
+ if (signal) finish(spaceFileAbortReason(signal));
3179
+ };
3180
+ function finish(error) {
3181
+ clearTimeout(timeout);
3182
+ signal?.removeEventListener("abort", onAbort);
3183
+ if (error !== void 0) reject(error);
3184
+ else resolve();
3185
+ }
3186
+ signal?.addEventListener("abort", onAbort, { once: true });
3187
+ });
3188
+ }
3189
+ function createSpaceFileDeadlineSignal(signal, timeoutMs) {
3190
+ const controller = new AbortController();
3191
+ let timedOut = false;
3192
+ const onAbort = () => {
3193
+ if (signal) controller.abort(spaceFileAbortReason(signal));
3194
+ };
3195
+ if (signal?.aborted) onAbort();
3196
+ else signal?.addEventListener("abort", onAbort, { once: true });
3197
+ const timeout = setTimeout(() => {
3198
+ if (controller.signal.aborted) return;
3199
+ timedOut = true;
3200
+ controller.abort(new DOMException("Space file URL resolution timed out", "TimeoutError"));
3201
+ }, timeoutMs);
3202
+ return {
3203
+ signal: controller.signal,
3204
+ timedOut: () => timedOut,
3205
+ dispose: () => {
3206
+ clearTimeout(timeout);
3207
+ signal?.removeEventListener("abort", onAbort);
3208
+ }
3209
+ };
3210
+ }
3211
+ var SpacePublicFilesApi = class {
3212
+ transport;
3213
+ spaceId;
3214
+ constructor(transport, spaceId) {
3215
+ this.transport = transport;
3216
+ this.spaceId = spaceId;
3217
+ }
3218
+ createUpload(input, options = {}) {
3219
+ return this.transport.request(`/api/spaces/${this.spaceId}/public/uploads`, {
3220
+ method: "POST",
3221
+ headers: { "Content-Type": "application/json" },
3222
+ body: JSON.stringify(input),
3223
+ signal: options.signal
3224
+ });
3225
+ }
3226
+ list(path = "", options = {}) {
3227
+ const params = new URLSearchParams();
3228
+ if (path) params.set("path", path);
3229
+ if (options.recursive) params.set("recursive", "true");
3230
+ if (options.limit != null) params.set("limit", String(options.limit));
3231
+ if (options.cursor) params.set("cursor", options.cursor);
3232
+ const query = params.toString();
3233
+ return this.transport.request(`/api/spaces/${this.spaceId}/public${query ? `?${query}` : ""}`, { fetch: options.fetch });
3234
+ }
3235
+ url(path, customFetch) {
3236
+ const params = new URLSearchParams({ path });
3237
+ return this.transport.request(`/api/spaces/${this.spaceId}/public/url?${params.toString()}`, { fetch: customFetch });
3238
+ }
3239
+ };
1759
3240
  var SpaceFilesApi = class {
1760
3241
  transport;
1761
3242
  spaceId;
@@ -1769,9 +3250,39 @@ var SpaceFilesApi = class {
1769
3250
  const query = params.toString();
1770
3251
  return this.transport.request(`/api/spaces/${this.spaceId}/fs/tree${query ? `?${query}` : ""}`, { fetch: customFetch });
1771
3252
  }
1772
- read(path, customFetch) {
3253
+ read(path, customFetch, signal) {
1773
3254
  const params = new URLSearchParams({ path });
1774
- return this.transport.request(`/api/spaces/${this.spaceId}/fs/file?${params.toString()}`, { fetch: customFetch });
3255
+ return this.transport.request(`/api/spaces/${this.spaceId}/fs/file?${params.toString()}`, {
3256
+ fetch: customFetch,
3257
+ signal
3258
+ });
3259
+ }
3260
+ /** Resolve a browser-ready file URL, waiting for CDN delivery when necessary. */
3261
+ async resolveUrl(path, options = {}) {
3262
+ const purpose = options.purpose ?? "preview";
3263
+ const requestedTimeoutMs = options.timeoutMs ?? 15e3;
3264
+ const timeoutMs = Number.isFinite(requestedTimeoutMs) ? Math.max(0, requestedTimeoutMs) : 15e3;
3265
+ const deadlineAt = Date.now() + timeoutMs;
3266
+ const deadline = createSpaceFileDeadlineSignal(options.signal, timeoutMs);
3267
+ try {
3268
+ while (true) {
3269
+ const file = await this.read(path, options.fetch, deadline.signal);
3270
+ if (deadline.timedOut()) return null;
3271
+ if ("content" in file) {
3272
+ if (file.delivery === "url" && file.url) return file.url;
3273
+ return purpose === "preview" ? inlineFileDataUrl(file) : null;
3274
+ }
3275
+ const remainingMs = deadlineAt - Date.now();
3276
+ if (remainingMs <= 0) return null;
3277
+ const retryAfterMs = Math.max(250, Math.min(file.retryAfterMs, 2e3));
3278
+ await waitForSpaceFileRetry(Math.min(retryAfterMs, remainingMs), deadline.signal);
3279
+ }
3280
+ } catch (error) {
3281
+ if (deadline.timedOut()) return null;
3282
+ throw error;
3283
+ } finally {
3284
+ deadline.dispose();
3285
+ }
1775
3286
  }
1776
3287
  /** Pending workspace changes vs the space head checkpoint. */
1777
3288
  diff(customFetch) {
@@ -2725,6 +4236,7 @@ var SpaceBoardsApi = class {
2725
4236
  return new BoardClient(this.spaceId, boardId, this.transport, this.websocketClient);
2726
4237
  }
2727
4238
  create(input) {
4239
+ assertBoardNodes(input.nodes ?? []);
2728
4240
  return this.transport.request(`/api/spaces/${this.spaceId}/boards`, {
2729
4241
  method: "POST",
2730
4242
  headers: { "Content-Type": "application/json" },
@@ -2749,6 +4261,7 @@ var SpaceBoardsApi = class {
2749
4261
  });
2750
4262
  }
2751
4263
  async apply(transaction) {
4264
+ assertBoardTransactionNodeCreates(transaction.operations);
2752
4265
  try {
2753
4266
  return await this.transport.request(`/api/spaces/${this.spaceId}/boards/${transaction.boardId}/transactions`, {
2754
4267
  method: "POST",
@@ -2904,6 +4417,7 @@ var SpaceClient = class {
2904
4417
  transport;
2905
4418
  websocketClient;
2906
4419
  files;
4420
+ publicFiles;
2907
4421
  sessions;
2908
4422
  turns;
2909
4423
  members;
@@ -2924,6 +4438,7 @@ var SpaceClient = class {
2924
4438
  this.transport = transport;
2925
4439
  this.websocketClient = websocketClient;
2926
4440
  this.files = new SpaceFilesApi(transport, id);
4441
+ this.publicFiles = new SpacePublicFilesApi(transport, id);
2927
4442
  this.sessions = new SpaceSessionsApi(transport, id, websocketClient);
2928
4443
  this.turns = new SpaceTurnsApi(transport, id);
2929
4444
  this.members = new SpaceMembersApi(transport, id);
@@ -3160,158 +4675,6 @@ var TasksApi = class {
3160
4675
  }
3161
4676
  };
3162
4677
  //#endregion
3163
- //#region ../protocol/dist/ui-command.js
3164
- /**
3165
- * Lets an agent drive the Cohub frontend that originated the work. Routing comes
3166
- * from request provenance, never a caller-supplied target, so a command only
3167
- * reaches the actor's own instances.
3168
- */
3169
- const UI_COMMAND_VERSION = 1;
3170
- /** Persisted and broadcast, so every field is capped; MAX_BYTES bounds the whole. */
3171
- const UI_COMMAND_PAYLOAD_MAX_BYTES = 32 * 1024;
3172
- const UI_COMMAND_MAX_BYTES = 40 * 1024;
3173
- const UI_COMMAND_LAUNCH_MAX_LENGTH = 2048;
3174
- const UI_COMMAND_DEFAULT_TIMEOUT_MS = 600 * 1e3;
3175
- const UI_COMMAND_MAX_TIMEOUT_MS = 720 * 60 * 1e3;
3176
- const UI_COMMAND_SETTLEMENT_GRACE_SECONDS = 600;
3177
- /** Keeps pending commands reportable for the full wait window plus settlement grace. */
3178
- const UI_COMMAND_PENDING_TTL_SECONDS = 43800;
3179
- const UI_COMMAND_TERMINAL_TTL_SECONDS = 1800;
3180
- const UI_COMMAND_TERMINAL_STATUSES = [
3181
- "applied",
3182
- "no_active_client",
3183
- "ui_host_unavailable",
3184
- "rejected",
3185
- "unsupported",
3186
- "timeout"
3187
- ];
3188
- const isTerminalUiCommandStatus = (status) => UI_COMMAND_TERMINAL_STATUSES.includes(status);
3189
- const METHOD_RE = /^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/;
3190
- const isUiSurfaceMethod = (value) => typeof value === "string" && METHOD_RE.test(value);
3191
- const WORK_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
3192
- const UI_COMMAND_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
3193
- const parseUiCommandId = (value) => {
3194
- if (typeof value !== "string") return null;
3195
- const trimmed = value.trim();
3196
- return UI_COMMAND_ID_RE.test(trimmed) ? trimmed : null;
3197
- };
3198
- const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
3199
- const asTrimmed = (value) => {
3200
- if (typeof value !== "string") return null;
3201
- const trimmed = value.trim();
3202
- return trimmed ? trimmed : null;
3203
- };
3204
- const parseLaunch = (value) => {
3205
- if (!isRecord(value)) return void 0;
3206
- const search = asTrimmed(value.search);
3207
- const hash = asTrimmed(value.hash);
3208
- if (!search && !hash) return void 0;
3209
- return {
3210
- ...search ? { search: search.startsWith("?") ? search : `?${search}` } : {},
3211
- ...hash ? { hash: hash.startsWith("#") ? hash : `#${hash}` } : {}
3212
- };
3213
- };
3214
- const measureUiCommandPayload = (value) => {
3215
- if (value === void 0) return 0;
3216
- try {
3217
- return new TextEncoder().encode(JSON.stringify(value) ?? "").length;
3218
- } catch {
3219
- return null;
3220
- }
3221
- };
3222
- const parseUiCommand = (input) => {
3223
- if (!isRecord(input)) return {
3224
- command: null,
3225
- error: "command must be an object"
3226
- };
3227
- if (input.type !== "preview.show") return {
3228
- command: null,
3229
- error: "command.type must be one of: preview.show"
3230
- };
3231
- const preview = input.preview;
3232
- if (!isRecord(preview)) return {
3233
- command: null,
3234
- error: "command.preview is required"
3235
- };
3236
- if (preview.kind !== "work") return {
3237
- command: null,
3238
- error: "command.preview.kind must be one of: work"
3239
- };
3240
- const workId = asTrimmed(preview.workId);
3241
- if (!workId) return {
3242
- command: null,
3243
- error: "command.preview.workId is required"
3244
- };
3245
- if (!WORK_ID_RE.test(workId)) return {
3246
- command: null,
3247
- error: "command.preview.workId must be a Work id"
3248
- };
3249
- const label = asTrimmed(preview.label);
3250
- if (label && label.length > 200) return {
3251
- command: null,
3252
- error: `command.preview.label exceeds 200 characters`
3253
- };
3254
- const launch = parseLaunch(preview.launch);
3255
- if (launch) {
3256
- for (const [field, value] of [["search", launch.search], ["hash", launch.hash]]) if (value && value.length > 2048) return {
3257
- command: null,
3258
- error: `command.preview.launch.${field} exceeds ${UI_COMMAND_LAUNCH_MAX_LENGTH} characters`
3259
- };
3260
- }
3261
- let request;
3262
- if (input.request !== void 0 && input.request !== null) {
3263
- if (!isRecord(input.request)) return {
3264
- command: null,
3265
- error: "command.request must be an object"
3266
- };
3267
- const method = asTrimmed(input.request.method);
3268
- if (!method) return {
3269
- command: null,
3270
- error: "command.request.method is required"
3271
- };
3272
- if (!isUiSurfaceMethod(method)) return {
3273
- command: null,
3274
- error: "command.request.method has an unsupported format"
3275
- };
3276
- const size = measureUiCommandPayload(input.request.input);
3277
- if (size === null) return {
3278
- command: null,
3279
- error: "command.request.input must be JSON-serializable"
3280
- };
3281
- if (size > 32768) return {
3282
- command: null,
3283
- error: `command.request.input exceeds ${UI_COMMAND_PAYLOAD_MAX_BYTES} bytes`
3284
- };
3285
- request = {
3286
- method,
3287
- ...input.request.input === void 0 ? {} : { input: input.request.input }
3288
- };
3289
- }
3290
- const command = {
3291
- type: "preview.show",
3292
- preview: {
3293
- kind: "work",
3294
- workId,
3295
- ...label ? { label } : {},
3296
- ...launch ? { launch } : {}
3297
- },
3298
- ...request ? { request } : {}
3299
- };
3300
- const totalSize = measureUiCommandPayload(command);
3301
- if (totalSize === null) return {
3302
- command: null,
3303
- error: "command must be JSON-serializable"
3304
- };
3305
- if (totalSize > 40960) return {
3306
- command: null,
3307
- error: `command exceeds ${UI_COMMAND_MAX_BYTES} bytes`
3308
- };
3309
- return {
3310
- command,
3311
- error: null
3312
- };
3313
- };
3314
- //#endregion
3315
4678
  //#region src/apis/ui-commands.ts
3316
4679
  const DEFAULT_POLL_INTERVAL_MS = 300;
3317
4680
  const resolveTimeoutMs = (timeoutMs) => {
@@ -3386,6 +4749,7 @@ var UiCommandsApi = class {
3386
4749
  };
3387
4750
  //#endregion
3388
4751
  //#region src/apis/user.ts
4752
+ const usageDate = (value) => value instanceof Date ? value.toISOString() : value;
3389
4753
  var UserApi = class {
3390
4754
  transport;
3391
4755
  transportBaseUrl;
@@ -3428,9 +4792,13 @@ var UserApi = class {
3428
4792
  getSession(sessionId, customFetch) {
3429
4793
  return this.transport.request(`/api/sessions/${sessionId}`, { fetch: customFetch });
3430
4794
  }
3431
- getUsage(days = 30, customFetch) {
3432
- const params = new URLSearchParams({ days: String(days) });
3433
- return this.transport.request(`/api/me/usage?${params.toString()}`, { fetch: customFetch });
4795
+ getActivity(options = {}, customFetch) {
4796
+ const params = new URLSearchParams();
4797
+ if (options.days !== void 0) params.set("days", String(options.days));
4798
+ if (options.from !== void 0) params.set("from", usageDate(options.from));
4799
+ if (options.to !== void 0) params.set("to", usageDate(options.to));
4800
+ const query = params.toString();
4801
+ return this.transport.request(`/api/me/activity${query ? `?${query}` : ""}`, { fetch: customFetch });
3434
4802
  }
3435
4803
  async setAuthToken(token) {
3436
4804
  const trimmedToken = token.trim();
@@ -3657,4 +5025,4 @@ var CohubHttpClient = class {
3657
5025
  };
3658
5026
  const createHttpClient = (options) => new CohubHttpClient(options);
3659
5027
  //#endregion
3660
- export { parseAssistantMessageCommit as A, ReferencesApi as B, SpaceClient as C, buildSpacePath as D, buildSpaceInvitePath as E, BOARD_ARROW_STROKE_SIZE as F, ModelsApi as G, PublicAssetsApi as H, BOARD_BUILTIN_CAPABILITIES as I, ChannelsApi as J, GenerationsApi as K, BOARD_CONNECTION_STROKE_SIZE as L, createSessionPatchReducer as M, ensureRealtimeConnected as N, SessionGenerationStreamClient as O, BoardConnectionSchema as P, DEFAULT_BOARD_RENDER_LIMITS as R, BoardTransactionError as S, PublicInviteApi as T, SkillsApi as U, SearchApi as V, PromptsApi as W, isUiSurfaceMethod as _, ReferralsApi as a, TasksApi as b, UiCommandsApi as c, UI_COMMAND_PAYLOAD_MAX_BYTES as d, UI_COMMAND_PENDING_TTL_SECONDS as f, isTerminalUiCommandStatus as g, UI_COMMAND_VERSION as h, WorksApi as i, SessionPatchReducer as j, createSessionGenerationStreamClient as k, UI_COMMAND_DEFAULT_TIMEOUT_MS as l, UI_COMMAND_TERMINAL_TTL_SECONDS as m, createHttpClient as n, UsersApi as o, UI_COMMAND_SETTLEMENT_GRACE_SECONDS as p, CronJobsApi as q, WorkCommerceApi as r, UserApi as s, CohubHttpClient as t, UI_COMMAND_MAX_TIMEOUT_MS as u, parseUiCommand as v, SpacesApi as w, BoardClient as x, parseUiCommandId as y, SessionAccessApi as z };
5028
+ export { SearchApi as $, UI_COMMAND_PENDING_TTL_SECONDS as A, BoardAwarenessClientPayloadSchema as B, BoardInputError as C, UI_COMMAND_DEFAULT_TIMEOUT_MS as D, validateBoardNodes as E, isUiSurfaceMethod as F, validateBoardNodeInput as G, BOARD_GEO_KINDS as H, parseUiCommand as I, ensureRealtimeConnected as J, BoardPlaybackPolicySchema as K, parseUiCommandId as L, UI_COMMAND_TERMINAL_TTL_SECONDS as M, UI_COMMAND_VERSION as N, UI_COMMAND_MAX_TIMEOUT_MS as O, isTerminalUiCommandStatus as P, ReferencesApi as Q, parseSpaceSlug as R, createSessionPatchReducer as S, createBoardNode as T, BOARD_NATIVE_NODE_TYPES as U, BOARD_COLOR_IDS as V, BOARD_NODE_CONTRACT as W, DEFAULT_BOARD_RENDER_LIMITS as X, BOARD_BUILTIN_CAPABILITIES as Y, SessionAccessApi as Z, buildSpacePath as _, ReferralsApi as a, CronJobsApi as at, parseAssistantMessageCommit as b, UiCommandsApi as c, BoardTransactionError as d, PublicAssetsApi as et, SpaceClient as f, buildSpaceInvitePath as g, PublicInviteApi as h, WorksApi as i, GenerationsApi as it, UI_COMMAND_SETTLEMENT_GRACE_SECONDS as j, UI_COMMAND_PAYLOAD_MAX_BYTES as k, TasksApi as l, SpacesApi as m, createHttpClient as n, PromptsApi as nt, UsersApi as o, ChannelsApi as ot, SpacePublicFilesApi as p, parseBoardPlaybackPolicy as q, WorkCommerceApi as r, ModelsApi as rt, UserApi as s, CohubHttpClient as t, SkillsApi as tt, BoardClient as u, SessionGenerationStreamClient as v, assertBoardNodes as w, SessionPatchReducer as x, createSessionGenerationStreamClient as y, parseUsername as z };