deepspace 0.3.11 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -396,6 +396,7 @@ declare const MSG: {
396
396
  readonly USER_LIST: "user.list";
397
397
  readonly SET_ROLE: "user.set_role";
398
398
  readonly USER_UPDATE: "user.update";
399
+ readonly AUTH: "auth";
399
400
  readonly YJS_JOIN: "yjs.join";
400
401
  readonly YJS_LEAVE: "yjs.leave";
401
402
  readonly ACK: "records.ack";
@@ -639,6 +640,8 @@ type ServerMessage = BaseMessage<typeof MSG.QUERY_RESULT, {
639
640
  error: string;
640
641
  }> | BaseMessage<typeof MSG.RESUBSCRIBE, EmptyPayload> | BaseMessage<typeof MSG.LIST_SCHEMAS, {
641
642
  schemas: unknown;
643
+ }> | BaseMessage<typeof MSG.AUTH, {
644
+ canWrite: boolean;
642
645
  }> | BaseMessage<typeof MSG.USER_INFO, unknown> | BaseMessage<typeof MSG.USER_LIST, {
643
646
  users: unknown[];
644
647
  }> | BaseMessage<typeof MSG.YJS_JOIN, {
@@ -5972,13 +5975,20 @@ interface UseGameRoomResult {
5972
5975
  running: boolean;
5973
5976
  /** Whether WebSocket is connected */
5974
5977
  connected: boolean;
5975
- /** Send a game input */
5978
+ /**
5979
+ * Whether this connection has write access. False for viewers /
5980
+ * unauthenticated spectators — they receive state broadcasts but the
5981
+ * send callbacks below no-op. UIs should disable input controls when
5982
+ * false so spectators see why their actions don't take effect.
5983
+ */
5984
+ canWrite: boolean;
5985
+ /** Send a game input (no-op for spectators) */
5976
5986
  sendInput: (action: string, data?: Record<string, unknown>) => void;
5977
- /** Mark self as ready */
5987
+ /** Mark self as ready (no-op for spectators) */
5978
5988
  setReady: () => void;
5979
- /** Request game start */
5989
+ /** Request game start (no-op for spectators) */
5980
5990
  startGame: () => void;
5981
- /** Request game end */
5991
+ /** Request game end (no-op for spectators) */
5982
5992
  endGame: () => void;
5983
5993
  }
5984
5994
  declare function useGameRoom(roomId: string): UseGameRoomResult;
@@ -6019,21 +6029,28 @@ interface UseCanvasResult {
6019
6029
  viewports: ViewportClient[];
6020
6030
  /** Whether WebSocket is connected */
6021
6031
  connected: boolean;
6022
- /** Add a shape */
6032
+ /**
6033
+ * Whether this connection has write access (member/admin role). The
6034
+ * write callbacks below no-op when this is false; UIs should also use
6035
+ * it to disable controls so viewers see why their clicks do nothing.
6036
+ * Viewport reporting stays available regardless — it's presence info.
6037
+ */
6038
+ canWrite: boolean;
6039
+ /** Add a shape (no-op when canWrite is false) */
6023
6040
  addShape: (shape: Partial<CanvasShapeClient>) => void;
6024
- /** Move a shape */
6041
+ /** Move a shape (no-op when canWrite is false) */
6025
6042
  moveShape: (shapeId: string, x: number, y: number) => void;
6026
- /** Resize a shape */
6043
+ /** Resize a shape (no-op when canWrite is false) */
6027
6044
  resizeShape: (shapeId: string, width: number, height: number, x?: number, y?: number) => void;
6028
- /** Delete a shape */
6045
+ /** Delete a shape (no-op when canWrite is false) */
6029
6046
  deleteShape: (shapeId: string) => void;
6030
- /** Update shape properties */
6047
+ /** Update shape properties (no-op when canWrite is false) */
6031
6048
  updateShape: (shapeId: string, props: Record<string, unknown>) => void;
6032
- /** Report local viewport */
6049
+ /** Report local viewport (allowed for viewers — presence-like) */
6033
6050
  setViewport: (viewport: Omit<ViewportClient, 'userId'>) => void;
6034
- /** Undo last action */
6051
+ /** Undo last action (no-op when canWrite is false) */
6035
6052
  undo: () => void;
6036
- /** Redo last undone action */
6053
+ /** Redo last undone action (no-op when canWrite is false) */
6037
6054
  redo: () => void;
6038
6055
  }
6039
6056
  declare function useCanvas(roomId: string): UseCanvasResult;
@@ -6070,11 +6087,18 @@ interface UseCronMonitorResult {
6070
6087
  history: CronHistoryEntry[];
6071
6088
  /** Whether WebSocket is connected */
6072
6089
  connected: boolean;
6073
- /** Manually trigger a task */
6090
+ /**
6091
+ * Whether this connection can mutate cron state (trigger / pause /
6092
+ * resume). False for viewers and unauthenticated connections — the
6093
+ * mutation callbacks below no-op and UIs should disable those
6094
+ * controls. Reading tasks/history stays available either way.
6095
+ */
6096
+ canWrite: boolean;
6097
+ /** Manually trigger a task (no-op when canWrite is false) */
6074
6098
  trigger: (taskName: string) => void;
6075
- /** Pause a task */
6099
+ /** Pause a task (no-op when canWrite is false) */
6076
6100
  pause: (taskName: string) => void;
6077
- /** Resume a paused task */
6101
+ /** Resume a paused task (no-op when canWrite is false) */
6078
6102
  resume: (taskName: string) => void;
6079
6103
  }
6080
6104
  declare function useCronMonitor(roomId: string): UseCronMonitorResult;
package/dist/index.js CHANGED
@@ -407,6 +407,13 @@ var MSG = {
407
407
  USER_LIST: "user.list",
408
408
  SET_ROLE: "user.set_role",
409
409
  USER_UPDATE: "user.update",
410
+ // ---- Auth signal -----------------------------------------------------
411
+ // Server → client one-shot on connect from rooms that enforce role-based
412
+ // writes (Canvas, Game, Cron). Tells the client whether this connection
413
+ // can write so its hook can disable local write APIs and the UI can
414
+ // render controls accordingly. Without this, a viewer's click would
415
+ // round-trip to the server and silently come back as ERROR.
416
+ AUTH: "auth",
410
417
  // ---- Yjs JSON control frames (binary sync lives in ./yjs.ts) ---------
411
418
  YJS_JOIN: "yjs.join",
412
419
  YJS_LEAVE: "yjs.leave",
@@ -717,12 +724,17 @@ function DeepSpaceAuthProvider({ children }) {
717
724
  }
718
725
 
719
726
  // src/client/auth/hooks.ts
727
+ var sessionHasResolved = false;
728
+ function stickyIsLoaded(isPending) {
729
+ if (!isPending) sessionHasResolved = true;
730
+ return sessionHasResolved;
731
+ }
720
732
  function useAuth() {
721
733
  const session = useSession();
722
734
  const isSignedIn = !!session.data?.user;
723
735
  const userId = session.data?.user?.id ?? null;
724
736
  return {
725
- isLoaded: !session.isPending,
737
+ isLoaded: stickyIsLoaded(session.isPending),
726
738
  isSignedIn,
727
739
  userId,
728
740
  sessionId: session.data?.session?.id ?? null
@@ -732,7 +744,7 @@ function useAuthUser() {
732
744
  const session = useSession();
733
745
  const user = session.data?.user ?? null;
734
746
  return {
735
- isLoaded: !session.isPending,
747
+ isLoaded: stickyIsLoaded(session.isPending),
736
748
  isSignedIn: !!user,
737
749
  user: user ? {
738
750
  id: user.id,
@@ -2848,6 +2860,7 @@ function useGameRoom(roomId) {
2848
2860
  const [players, setPlayers] = useState8([]);
2849
2861
  const [running, setRunning] = useState8(false);
2850
2862
  const [connected, setConnected] = useState8(false);
2863
+ const [canWrite, setCanWrite] = useState8(false);
2851
2864
  const wsRef = useRef9(null);
2852
2865
  useEffect11(() => {
2853
2866
  let ws = null;
@@ -2869,6 +2882,9 @@ function useGameRoom(roomId) {
2869
2882
  };
2870
2883
  ws.onmessage = (event) => {
2871
2884
  dispatch(event.data, {
2885
+ [MSG.AUTH]: (p) => {
2886
+ setCanWrite(p.canWrite);
2887
+ },
2872
2888
  [MSG.GAME_STATE]: (p) => {
2873
2889
  setGameState(p.state);
2874
2890
  setTick(p.tick);
@@ -2906,6 +2922,7 @@ function useGameRoom(roomId) {
2906
2922
  wsLog("disconnected", `game:${roomId}`);
2907
2923
  wsRef.current = null;
2908
2924
  setConnected(false);
2925
+ setCanWrite(false);
2909
2926
  if (alive) reconnectTimer = setTimeout(connect, 1e3);
2910
2927
  };
2911
2928
  ws.onerror = () => ws?.close();
@@ -2924,27 +2941,23 @@ function useGameRoom(roomId) {
2924
2941
  wsRef.current = null;
2925
2942
  };
2926
2943
  }, [roomId]);
2927
- const sendInput = useCallback10((action, data = {}) => {
2928
- const ws = wsRef.current;
2929
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
2930
- ws.send(encode(clientBuild.gameInput(action, data)));
2931
- }, []);
2932
- const setReady = useCallback10(() => {
2933
- const ws = wsRef.current;
2934
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
2935
- ws.send(encode(clientBuild.gamePlayerReady()));
2936
- }, []);
2937
- const startGame = useCallback10(() => {
2938
- const ws = wsRef.current;
2939
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
2940
- ws.send(encode(clientBuild.gameStart()));
2941
- }, []);
2942
- const endGame = useCallback10(() => {
2943
- const ws = wsRef.current;
2944
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
2945
- ws.send(encode(clientBuild.gameEnd()));
2946
- }, []);
2947
- return { state: gameState, tick, players, running, connected, sendInput, setReady, startGame, endGame };
2944
+ const send = useCallback10(
2945
+ (message) => {
2946
+ if (!canWrite) return;
2947
+ const ws = wsRef.current;
2948
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
2949
+ ws.send(encode(message));
2950
+ },
2951
+ [canWrite]
2952
+ );
2953
+ const sendInput = useCallback10(
2954
+ (action, data = {}) => send(clientBuild.gameInput(action, data)),
2955
+ [send]
2956
+ );
2957
+ const setReady = useCallback10(() => send(clientBuild.gamePlayerReady()), [send]);
2958
+ const startGame = useCallback10(() => send(clientBuild.gameStart()), [send]);
2959
+ const endGame = useCallback10(() => send(clientBuild.gameEnd()), [send]);
2960
+ return { state: gameState, tick, players, running, connected, canWrite, sendInput, setReady, startGame, endGame };
2948
2961
  }
2949
2962
 
2950
2963
  // src/client/storage/hooks/useCanvas.ts
@@ -2953,6 +2966,7 @@ function useCanvas(roomId) {
2953
2966
  const [shapes, setShapes] = useState9([]);
2954
2967
  const [viewports, setViewports] = useState9([]);
2955
2968
  const [connected, setConnected] = useState9(false);
2969
+ const [canWrite, setCanWrite] = useState9(false);
2956
2970
  const wsRef = useRef10(null);
2957
2971
  useEffect12(() => {
2958
2972
  let ws = null;
@@ -2974,6 +2988,9 @@ function useCanvas(roomId) {
2974
2988
  };
2975
2989
  ws.onmessage = (event) => {
2976
2990
  dispatch(event.data, {
2991
+ [MSG.AUTH]: (p) => {
2992
+ setCanWrite(p.canWrite);
2993
+ },
2977
2994
  [MSG.CANVAS_SHAPES]: (p) => {
2978
2995
  setShapes(p.shapes);
2979
2996
  setViewports(p.viewports);
@@ -3026,6 +3043,7 @@ function useCanvas(roomId) {
3026
3043
  wsLog("disconnected", `canvas:${roomId}`);
3027
3044
  wsRef.current = null;
3028
3045
  setConnected(false);
3046
+ setCanWrite(false);
3029
3047
  if (alive) reconnectTimer = setTimeout(connect, 1e3);
3030
3048
  };
3031
3049
  ws.onerror = () => ws?.close();
@@ -3052,33 +3070,40 @@ function useCanvas(roomId) {
3052
3070
  },
3053
3071
  []
3054
3072
  );
3073
+ const sendWrite = useCallback11(
3074
+ (message) => {
3075
+ if (!canWrite) return;
3076
+ sendBuilt(message);
3077
+ },
3078
+ [sendBuilt, canWrite]
3079
+ );
3055
3080
  const addShape = useCallback11(
3056
- (shape) => sendBuilt(clientBuild.canvasAdd(shape)),
3057
- [sendBuilt]
3081
+ (shape) => sendWrite(clientBuild.canvasAdd(shape)),
3082
+ [sendWrite]
3058
3083
  );
3059
3084
  const moveShape = useCallback11(
3060
- (shapeId, x, y) => sendBuilt(clientBuild.canvasMove(shapeId, x, y)),
3061
- [sendBuilt]
3085
+ (shapeId, x, y) => sendWrite(clientBuild.canvasMove(shapeId, x, y)),
3086
+ [sendWrite]
3062
3087
  );
3063
3088
  const resizeShape = useCallback11(
3064
- (shapeId, width, height, x, y) => sendBuilt(clientBuild.canvasResize(shapeId, width, height, x, y)),
3065
- [sendBuilt]
3089
+ (shapeId, width, height, x, y) => sendWrite(clientBuild.canvasResize(shapeId, width, height, x, y)),
3090
+ [sendWrite]
3066
3091
  );
3067
3092
  const deleteShape = useCallback11(
3068
- (shapeId) => sendBuilt(clientBuild.canvasDelete(shapeId)),
3069
- [sendBuilt]
3093
+ (shapeId) => sendWrite(clientBuild.canvasDelete(shapeId)),
3094
+ [sendWrite]
3070
3095
  );
3071
3096
  const updateShape = useCallback11(
3072
- (shapeId, props) => sendBuilt(clientBuild.canvasUpdate(shapeId, props)),
3073
- [sendBuilt]
3097
+ (shapeId, props) => sendWrite(clientBuild.canvasUpdate(shapeId, props)),
3098
+ [sendWrite]
3074
3099
  );
3075
3100
  const setViewport = useCallback11(
3076
3101
  (viewport) => sendBuilt(clientBuild.canvasViewport(viewport)),
3077
3102
  [sendBuilt]
3078
3103
  );
3079
- const undo = useCallback11(() => sendBuilt(clientBuild.canvasUndo()), [sendBuilt]);
3080
- const redo = useCallback11(() => sendBuilt(clientBuild.canvasRedo()), [sendBuilt]);
3081
- return { shapes, viewports, connected, addShape, moveShape, resizeShape, deleteShape, updateShape, setViewport, undo, redo };
3104
+ const undo = useCallback11(() => sendWrite(clientBuild.canvasUndo()), [sendWrite]);
3105
+ const redo = useCallback11(() => sendWrite(clientBuild.canvasRedo()), [sendWrite]);
3106
+ return { shapes, viewports, connected, canWrite, addShape, moveShape, resizeShape, deleteShape, updateShape, setViewport, undo, redo };
3082
3107
  }
3083
3108
 
3084
3109
  // src/client/storage/hooks/useCronMonitor.ts
@@ -3087,6 +3112,7 @@ function useCronMonitor(roomId) {
3087
3112
  const [tasks, setTasks] = useState10([]);
3088
3113
  const [history, setHistory] = useState10([]);
3089
3114
  const [connected, setConnected] = useState10(false);
3115
+ const [canWrite, setCanWrite] = useState10(false);
3090
3116
  const wsRef = useRef11(null);
3091
3117
  useEffect13(() => {
3092
3118
  let ws = null;
@@ -3104,6 +3130,9 @@ function useCronMonitor(roomId) {
3104
3130
  ws.onopen = () => setConnected(true);
3105
3131
  ws.onmessage = (event) => {
3106
3132
  dispatch(event.data, {
3133
+ [MSG.AUTH]: (p) => {
3134
+ setCanWrite(p.canWrite);
3135
+ },
3107
3136
  [MSG.CRON_TASKS]: (p) => {
3108
3137
  setTasks(p.tasks);
3109
3138
  },
@@ -3119,6 +3148,7 @@ function useCronMonitor(roomId) {
3119
3148
  ws.onclose = () => {
3120
3149
  wsRef.current = null;
3121
3150
  setConnected(false);
3151
+ setCanWrite(false);
3122
3152
  if (alive) reconnectTimer = setTimeout(connect, 1e3);
3123
3153
  };
3124
3154
  ws.onerror = () => ws?.close();
@@ -3131,27 +3161,28 @@ function useCronMonitor(roomId) {
3131
3161
  wsRef.current = null;
3132
3162
  };
3133
3163
  }, [roomId]);
3134
- const sendBuilt = useCallback12(
3164
+ const sendWrite = useCallback12(
3135
3165
  (message) => {
3166
+ if (!canWrite) return;
3136
3167
  const ws = wsRef.current;
3137
3168
  if (!ws || ws.readyState !== WebSocket.OPEN) return;
3138
3169
  ws.send(encode(message));
3139
3170
  },
3140
- []
3171
+ [canWrite]
3141
3172
  );
3142
3173
  const trigger = useCallback12(
3143
- (taskName) => sendBuilt(clientBuild.cronTrigger(taskName)),
3144
- [sendBuilt]
3174
+ (taskName) => sendWrite(clientBuild.cronTrigger(taskName)),
3175
+ [sendWrite]
3145
3176
  );
3146
3177
  const pause = useCallback12(
3147
- (taskName) => sendBuilt(clientBuild.cronPause(taskName)),
3148
- [sendBuilt]
3178
+ (taskName) => sendWrite(clientBuild.cronPause(taskName)),
3179
+ [sendWrite]
3149
3180
  );
3150
3181
  const resume = useCallback12(
3151
- (taskName) => sendBuilt(clientBuild.cronResume(taskName)),
3152
- [sendBuilt]
3182
+ (taskName) => sendWrite(clientBuild.cronResume(taskName)),
3183
+ [sendWrite]
3153
3184
  );
3154
- return { tasks, history, connected, trigger, pause, resume };
3185
+ return { tasks, history, connected, canWrite, trigger, pause, resume };
3155
3186
  }
3156
3187
 
3157
3188
  // src/client/storage/hooks/useJobs.ts