@aperture-browser/session-react 0.0.0-nightly.86.594137d99de2 → 0.0.0-nightly.88.6472b75ebdd9

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/README.md CHANGED
@@ -7,13 +7,15 @@ npm install @aperture-browser/session-react effect react react-dom
7
7
  ```
8
8
 
9
9
  ```tsx
10
- import { ApertureSession } from "@aperture-browser/session-react";
10
+ import { useMemo } from "react";
11
+ import { ApertureSession, shareSessionAccess } from "@aperture-browser/session-react";
11
12
  import "@aperture-browser/session-react/styles.css";
12
13
 
13
14
  export function SharedBrowser({ token }: { token: string }) {
15
+ const access = useMemo(() => shareSessionAccess(token, "https://aperture.example"), [token]);
14
16
  return (
15
17
  <div style={{ height: 600 }}>
16
- <ApertureSession baseUrl="https://aperture.example" token={token} />
18
+ <ApertureSession access={access} />
17
19
  </div>
18
20
  );
19
21
  }
@@ -22,3 +24,5 @@ export function SharedBrowser({ token }: { token: string }) {
22
24
  `@aperture-browser/session-react/headless` has the same session without any UI, for apps that build their own controls.
23
25
 
24
26
  Documentation: https://aperture-browser-docs.pages.dev/docs/packages/session-react
27
+
28
+ For consumer-authenticated access, pass a stable `access` object with `{ kind: "relay", baseUrl: "/aperture", sessionId }`. Your backend owns authentication through `@aperture-browser/session-relay`; the browser sends no Aperture credentials. `useSession({ access })` exposes the same connection headlessly.
@@ -1,19 +1,19 @@
1
1
  import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import * as Effect from "effect/Effect";
3
3
  import * as Stream from "effect/Stream";
4
- import { DEFAULT_VIEWPORT, LiveSessionConnection, LiveSessionError, LiveSessionError as LiveSessionError$1, computeRenderMetrics, createViewportPreset, evdevKeycodeByCode, keyboardInputMessage, keyboardModifiers, shouldForwardBrowserShortcut, strictParseOptions, windowsVirtualKeyCodeForCodeOrKey } from "@aperture-browser/live-session";
4
+ import { DEFAULT_VIEWPORT, LiveSessionConnection, LiveSessionError, LiveSessionError as LiveSessionError$1, computeRenderMetrics, createViewportPreset, downloadSessionRecording, evdevKeycodeByCode, getSessionStatus, keyboardInputMessage, keyboardModifiers, shouldForwardBrowserShortcut, strictParseOptions, windowsVirtualKeyCodeForCodeOrKey } from "@aperture-browser/live-session";
5
5
  import * as Option from "effect/Option";
6
6
  import * as PubSub from "effect/PubSub";
7
7
  import * as Schema from "effect/Schema";
8
8
  import * as SubscriptionRef from "effect/SubscriptionRef";
9
9
  import * as Data from "effect/Data";
10
10
  import { jsx, jsxs } from "react/jsx-runtime";
11
- import * as Redacted from "effect/Redacted";
12
- import { SessionsApi, apiClientLayer, baseUrlLayer } from "@aperture-browser/api-client";
13
11
  import * as Layer from "effect/Layer";
14
12
  import * as ManagedRuntime from "effect/ManagedRuntime";
15
13
  import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
16
14
  import * as HttpClient from "effect/unstable/http/HttpClient";
15
+ import { apiClientLayer, baseUrlLayer } from "@aperture-browser/api-client";
16
+ import * as Redacted from "effect/Redacted";
17
17
  //#region src/effect.tsx
18
18
  const RuntimeContext = createContext(null);
19
19
  /** A hook below needed the app runtime, but no RuntimeProvider is above the component. */
@@ -95,10 +95,10 @@ const pointerButtonCode = {
95
95
  right: 273,
96
96
  middle: 274
97
97
  };
98
- function useLiveSession({ sessionId, displayName = null, credentials, sessionToken, role, enabled, webrtcSupported, iceServers, autoSize }) {
99
- const identity = useMemo(() => collaborationIdentity(role, displayName), [displayName, role]);
98
+ function useLiveSession({ access, displayName = null, enabled, webrtcSupported, iceServers, autoSize }) {
99
+ const identity = useMemo(() => collaborationIdentity(displayName), [displayName]);
100
+ const [role, setRole] = useState("viewer");
100
101
  const runtime = useRuntime();
101
- const baseUrl = useBaseUrl();
102
102
  const frameRef = useMemo(() => runtime.runSync(SubscriptionRef.make(null)), [runtime]);
103
103
  const paintPubSub = useMemo(() => runtime.runSync(PubSub.unbounded()), [runtime]);
104
104
  const frames = useMemo(() => SubscriptionRef.changes(frameRef), [frameRef]);
@@ -141,6 +141,7 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
141
141
  const handleMessage = useCallback((message) => {
142
142
  switch (message.type) {
143
143
  case "session.snapshot":
144
+ setRole(message.role);
144
145
  clientIdRef.current = message.clientId;
145
146
  setClientId(message.clientId);
146
147
  holderClientIdRef.current = message.holderClientId ?? null;
@@ -232,9 +233,10 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
232
233
  }
233
234
  }, [publishPaint]);
234
235
  useFork(() => {
235
- if (!enabled || !sessionId || !credentials) {
236
+ if (!enabled || access === null) {
236
237
  connectionRef.current = null;
237
238
  setPhase("idle");
239
+ setRole("viewer");
238
240
  setTargets([]);
239
241
  setActiveTargetId(null);
240
242
  setMediaStream(null);
@@ -251,12 +253,10 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
251
253
  publishPaint({ type: "clear" });
252
254
  return;
253
255
  }
256
+ setRole("viewer");
254
257
  return Effect.gen(function* () {
255
258
  const connection = yield* LiveSessionConnection.make({
256
- baseUrl,
257
- sessionId,
258
- credentials,
259
- sessionToken,
259
+ access,
260
260
  identity,
261
261
  autoSize,
262
262
  iceServers,
@@ -284,16 +284,13 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
284
284
  })));
285
285
  }, [
286
286
  autoSize,
287
- baseUrl,
288
- credentials,
287
+ access,
289
288
  enabled,
290
289
  handleMessage,
291
290
  iceServers,
292
291
  identity,
293
292
  publishFrame,
294
293
  publishPaint,
295
- sessionId,
296
- sessionToken,
297
294
  webrtcSupported
298
295
  ]);
299
296
  const sendReliable = useCallback((message) => connectionRef.current?.sendReliable(message) ?? false, []);
@@ -580,9 +577,9 @@ const StoredIdentity = Schema.Struct({
580
577
  avatarHash: Schema.String.check(Schema.isPattern(/^[0-9a-f]{32}$/))
581
578
  });
582
579
  const decodeStoredIdentity = Schema.decodeUnknownOption(Schema.fromJsonString(StoredIdentity), strictParseOptions);
583
- function collaborationIdentity(role, accountName) {
580
+ function collaborationIdentity(accountName) {
584
581
  const anonymous = loadAnonymousIdentity();
585
- if (role !== "owner" || !accountName?.trim()) return anonymous;
582
+ if (!accountName?.trim()) return anonymous;
586
583
  return {
587
584
  ...anonymous,
588
585
  name: Array.from(accountName.trim()).slice(0, 48).join("")
@@ -1766,7 +1763,7 @@ function focusedElement(node) {
1766
1763
  //#region src/hooks/use-browser-control.ts
1767
1764
  const emptyIceServers$1 = [];
1768
1765
  const AUTO_SIZE_DEFAULT_STORAGE_KEY = "aperture.viewport.auto-size-default";
1769
- function useBrowserControl({ sessionId, credentials, displayName, sessionToken, collaborationRole = "owner", enabled = true, webrtcProducerSupported = false, webrtcIceServers = emptyIceServers$1, onNotice }) {
1766
+ function useBrowserControl({ access, displayName, enabled = true, webrtcProducerSupported = false, webrtcIceServers = emptyIceServers$1, onNotice }) {
1770
1767
  const runtime = useRuntime();
1771
1768
  const onNoticeRef = useRef(onNotice);
1772
1769
  onNoticeRef.current = onNotice;
@@ -1775,21 +1772,19 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
1775
1772
  message
1776
1773
  }));
1777
1774
  const [viewportAutoSizeDefault, setViewportAutoSizeDefaultState] = useState(loadAutoSizeDefault);
1778
- const [viewportAutoSync, setViewportAutoSyncState] = useState(() => collaborationRole !== "viewer" && viewportAutoSizeDefault);
1775
+ const [viewportAutoSync, setViewportAutoSyncState] = useState(() => viewportAutoSizeDefault);
1779
1776
  const viewportAutoSyncRef = useRef(viewportAutoSync);
1780
1777
  viewportAutoSyncRef.current = viewportAutoSync;
1781
1778
  const autoSizePreference = useCallback(() => viewportAutoSyncRef.current, []);
1782
1779
  const live = useLiveSession({
1783
- sessionId,
1780
+ access,
1784
1781
  displayName,
1785
- credentials,
1786
- sessionToken,
1787
- role: collaborationRole,
1788
- enabled: Boolean(enabled && sessionId && credentials),
1782
+ enabled: enabled && access !== null,
1789
1783
  webrtcSupported: webrtcProducerSupported,
1790
1784
  iceServers: webrtcIceServers,
1791
1785
  autoSize: autoSizePreference
1792
1786
  });
1787
+ const collaborationRole = live.collaboration.role;
1793
1788
  const [targetOrder, setTargetOrder] = useState([]);
1794
1789
  const targets = useMemo(() => mergeTargetsInCurrentOrder(targetOrder, live.targets), [live.targets, targetOrder]);
1795
1790
  const liveTargetsRef = useRef(live.targets);
@@ -1800,6 +1795,7 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
1800
1795
  const [browserViewportSize, setBrowserViewportSizeState] = useState(null);
1801
1796
  const [captured, setCaptured] = useState(false);
1802
1797
  const [recordingBusy, setRecordingBusy] = useState(false);
1798
+ const canRecord = collaborationRole === "owner" || collaborationRole === "editor";
1803
1799
  const activeTargetIdRef = useRef(null);
1804
1800
  const viewportRef = useRef(viewport);
1805
1801
  const inputDimensionsRef = useRef(DEFAULT_VIEWPORT);
@@ -1817,13 +1813,12 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
1817
1813
  setViewportState(createBrowserViewport(live.mediaSize, live.mediaSize.deviceScaleFactor));
1818
1814
  }, [live.mediaSize]);
1819
1815
  const sendInput = useCallback((message) => {
1820
- if (!enabled || !sessionId || !credentials) return false;
1816
+ if (!enabled || access === null) return false;
1821
1817
  return live.sendBrowserInput(message, inputDimensionsRef.current);
1822
1818
  }, [
1823
- credentials,
1819
+ access,
1824
1820
  enabled,
1825
- live,
1826
- sessionId
1821
+ live
1827
1822
  ]);
1828
1823
  const activateTarget = useCallback((targetId) => {
1829
1824
  live.selectTarget(targetId);
@@ -1963,27 +1958,26 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
1963
1958
  }), "Recording failed to start"), [live]);
1964
1959
  const startRecording = useCallback((mode) => {
1965
1960
  const targetId = activeTargetIdRef.current;
1966
- if (!targetId || collaborationRole !== "owner" || recordingBusy) return;
1961
+ if (!targetId || !canRecord || recordingBusy) return;
1967
1962
  setRecordingBusy(true);
1968
1963
  runStartRecording(mode, targetId);
1969
1964
  }, [
1970
- collaborationRole,
1965
+ canRecord,
1971
1966
  recordingBusy,
1972
1967
  runStartRecording
1973
1968
  ]);
1974
- const runStopRecording = useEffectCallback((credentials, sessionId, recordingId) => settleRecording(live.request("recording.stop", { recordingId }).pipe(Effect.andThen(SessionsApi.use((sessions) => sessions.downloadSessionRecording(credentials, sessionId, recordingId, sessionToken === void 0 ? void 0 : Redacted.make(sessionToken)))), Effect.flatMap(({ blob, filename }) => {
1969
+ const runStopRecording = useEffectCallback((access, recordingId) => settleRecording(live.request("recording.stop", { recordingId }).pipe(Effect.andThen(downloadSessionRecording(access, recordingId)), Effect.flatMap(({ blob, filename }) => {
1975
1970
  const recording = live.recordings.find((candidate) => candidate.recordingId === recordingId);
1976
- return downloadBlob(blob, filename ?? `${sessionId}-${recording?.targetId ?? "target"}-${recordingId}.webm`);
1977
- }), Effect.andThen(notify("success", "Recording saved"))), "Recording failed to stop"), [live, sessionToken]);
1971
+ return downloadBlob(blob, filename ?? `${access.sessionId}-${recording?.targetId ?? "target"}-${recordingId}.webm`);
1972
+ }), Effect.andThen(notify("success", "Recording saved"))), "Recording failed to stop"), [live]);
1978
1973
  const stopRecording = useCallback((recordingId) => {
1979
- if (!sessionId || !credentials || recordingBusy) return;
1974
+ if (access === null || recordingBusy) return;
1980
1975
  setRecordingBusy(true);
1981
- runStopRecording(credentials, sessionId, recordingId);
1976
+ runStopRecording(access, recordingId);
1982
1977
  }, [
1983
- credentials,
1978
+ access,
1984
1979
  recordingBusy,
1985
- runStopRecording,
1986
- sessionId
1980
+ runStopRecording
1987
1981
  ]);
1988
1982
  const runCancelRecording = useEffectCallback((recordingId) => settleRecording(live.request("recording.cancel", { recordingId }).pipe(Effect.andThen(notify("success", "Recording stopped"))), "Recording failed to stop"), [live]);
1989
1983
  const cancelRecording = useCallback((recordingId) => {
@@ -1993,23 +1987,18 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
1993
1987
  }, [recordingBusy, runCancelRecording]);
1994
1988
  const runSetRemoteCursor = useEffectCallback((visible) => live.request("presentation.cursor.set", { visible }).pipe(Effect.catch((error) => notify("error", errorMessage(error, "Remote cursor could not be updated")))), [live]);
1995
1989
  const setRemoteCursorEnabled = useCallback((visible) => {
1996
- if (sessionId && credentials) runSetRemoteCursor(visible);
1997
- }, [
1998
- credentials,
1999
- runSetRemoteCursor,
2000
- sessionId
2001
- ]);
1990
+ if (access !== null) runSetRemoteCursor(visible);
1991
+ }, [access, runSetRemoteCursor]);
2002
1992
  const runSelectPresentation = useEffectCallback((selection) => live.selectPresentation(selection).pipe(Effect.catch((error) => notify("error", errorMessage(error, "Presentation could not be updated")))), [live]);
2003
1993
  const selectMediaStream = useCallback((selection) => {
2004
- if (!enabled || !sessionId || !credentials || live.mediaSwitching) return false;
1994
+ if (!enabled || access === null || live.mediaSwitching) return false;
2005
1995
  runSelectPresentation(selection);
2006
1996
  return true;
2007
1997
  }, [
2008
- credentials,
1998
+ access,
2009
1999
  enabled,
2010
2000
  live.mediaSwitching,
2011
- runSelectPresentation,
2012
- sessionId
2001
+ runSelectPresentation
2013
2002
  ]);
2014
2003
  const setWebRTCStreamSettings = useCallback((settings) => selectMediaStream({
2015
2004
  kind: "webrtc",
@@ -2061,6 +2050,7 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
2061
2050
  viewportAutoSizeDefault,
2062
2051
  captured,
2063
2052
  recordings: live.recordings,
2053
+ canRecord,
2064
2054
  recordingBusy,
2065
2055
  remoteCursorEnabled: live.presentation?.cursorVisible ?? true,
2066
2056
  collaboration: live.collaboration,
@@ -2130,7 +2120,7 @@ function errorMessage(cause, fallback) {
2130
2120
  }
2131
2121
  //#endregion
2132
2122
  //#region src/runtime.ts
2133
- const makeApertureRuntime = (options = {}) => ManagedRuntime.make(Layer.mergeAll(apiClientLayer.pipe(Layer.provide(options.baseUrl ? baseUrlLayer(options.baseUrl) : Layer.empty), Layer.provide(FetchHttpClient.layer)), Layer.succeed(HttpClient.TracerPropagationEnabled, false)));
2123
+ const makeApertureRuntime = (options = {}) => ManagedRuntime.make(Layer.mergeAll(FetchHttpClient.layer, apiClientLayer.pipe(Layer.provide(options.baseUrl ? baseUrlLayer(options.baseUrl) : Layer.empty), Layer.provide(FetchHttpClient.layer)), Layer.succeed(HttpClient.TracerPropagationEnabled, false)));
2134
2124
  //#endregion
2135
2125
  //#region src/provider.tsx
2136
2126
  function ApertureProvider({ baseUrl, children }) {
@@ -2150,55 +2140,75 @@ function ApertureProvider({ baseUrl, children }) {
2150
2140
  }) : null;
2151
2141
  }
2152
2142
  //#endregion
2153
- //#region src/shared-session.ts
2154
- function parseShareToken(token) {
2143
+ //#region src/session.ts
2144
+ /** Convert an editor/viewer share link into direct session access. */
2145
+ function shareSessionAccess(token, baseUrl) {
2155
2146
  const sessionId = token.slice(4, 40);
2156
- const role = token.startsWith("ape_") ? "editor" : token.startsWith("apv_") ? "viewer" : null;
2157
- if (!role || token[40] !== "_" || !sessionId || !token.slice(41)) return null;
2147
+ if (!(token.startsWith("ape_") || token.startsWith("apv_")) || token[40] !== "_" || token.length <= 41) return null;
2158
2148
  return {
2159
- token,
2149
+ kind: "direct",
2150
+ baseUrl,
2160
2151
  sessionId,
2161
- role
2152
+ credentials: {
2153
+ kind: "bearer",
2154
+ token: Redacted.make(token),
2155
+ authorityType: null,
2156
+ tenantId: null,
2157
+ selectedTenantId: null
2158
+ }
2162
2159
  };
2163
2160
  }
2164
2161
  const emptyIceServers = [];
2165
- function useSharedSession({ token, displayName, onNotice }) {
2166
- const share = useMemo(() => parseShareToken(token), [token]);
2167
- const credentials = useMemo(() => share && {
2168
- kind: "bearer",
2169
- token: Redacted.make(share.token),
2170
- authorityType: null,
2171
- tenantId: null,
2172
- selectedTenantId: null
2173
- }, [share]);
2174
- const [state, setState] = useState({ kind: "loading" });
2175
- useFork(() => share && credentials ? Effect.sync(() => setState({ kind: "loading" })).pipe(Effect.andThen(SessionsApi.use((sessions) => sessions.getBrowserStatus(credentials, share.sessionId))), Effect.match({
2176
- onFailure: (error) => setState({ kind: error.status === 410 ? "expired" : "unavailable" }),
2177
- onSuccess: (browser) => setState(browser.sessionId === share.sessionId ? {
2178
- kind: "ready",
2179
- browser
2180
- } : { kind: "unavailable" })
2181
- })) : void 0, [credentials, share]);
2182
- const browser = state.kind === "ready" ? state.browser : null;
2162
+ function failedStatus(httpStatus) {
2163
+ if (httpStatus === 401 || httpStatus === 403) return "denied";
2164
+ if (httpStatus === 410) return "expired";
2165
+ return "unavailable";
2166
+ }
2167
+ function useSession({ access, displayName, onNotice }) {
2168
+ const [state, setState] = useState({
2169
+ access: null,
2170
+ status: "loading",
2171
+ browser: null
2172
+ });
2173
+ useFork(() => {
2174
+ if (access === null) return;
2175
+ setState({
2176
+ access,
2177
+ status: "loading",
2178
+ browser: null
2179
+ });
2180
+ return getSessionStatus(access).pipe(Effect.match({
2181
+ onFailure: (error) => setState({
2182
+ access,
2183
+ status: failedStatus(error.status),
2184
+ browser: null
2185
+ }),
2186
+ onSuccess: (browser) => setState(access.kind === "direct" && browser.sessionId !== access.sessionId ? {
2187
+ access,
2188
+ status: "unavailable",
2189
+ browser: null
2190
+ } : {
2191
+ access,
2192
+ status: "ready",
2193
+ browser
2194
+ })
2195
+ }));
2196
+ }, [access]);
2197
+ const browser = state.access === access ? state.browser : null;
2183
2198
  const control = useBrowserControl({
2184
- sessionId: share?.sessionId ?? null,
2185
- credentials: browser ? credentials : null,
2199
+ access: browser === null ? null : access,
2186
2200
  displayName,
2187
- sessionToken: share?.token,
2188
- collaborationRole: share?.role ?? "viewer",
2189
- enabled: browser !== null,
2190
2201
  webrtcProducerSupported: browser?.media.mode === "auto" && browser.media.webrtcProducer,
2191
2202
  webrtcIceServers: browser?.media.iceServers ?? emptyIceServers,
2192
2203
  onNotice
2193
2204
  });
2194
2205
  return {
2195
- status: share ? state.kind : "invalid",
2196
- share,
2206
+ status: access === null ? "invalid" : state.access !== access ? "loading" : state.status,
2197
2207
  browser,
2198
2208
  control
2199
2209
  };
2200
2210
  }
2201
2211
  //#endregion
2202
- export { makeApertureRuntime as a, collaborationPaintLifetimeMs as c, RuntimeProviderMissingError as d, useBaseUrl as f, useRuntime as g, useRunApi as h, ApertureProvider as i, useLiveSession as l, useFork as m, parseShareToken as n, useBrowserControl as o, useEffectCallback as p, useSharedSession as r, SessionViewport as s, LiveSessionError$1 as t, RuntimeProvider as u };
2212
+ export { makeApertureRuntime as a, collaborationPaintLifetimeMs as c, RuntimeProviderMissingError as d, useBaseUrl as f, useRuntime as g, useRunApi as h, ApertureProvider as i, useLiveSession as l, useFork as m, shareSessionAccess as n, useBrowserControl as o, useEffectCallback as p, useSession as r, SessionViewport as s, LiveSessionError$1 as t, RuntimeProvider as u };
2203
2213
 
2204
- //# sourceMappingURL=headless-CJ5KaGbF.js.map
2214
+ //# sourceMappingURL=headless-Dz39dQkW.js.map