@aperture-browser/session-react 0.0.0-nightly.87.b249cb4c5698 → 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);
@@ -1818,13 +1813,12 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
1818
1813
  setViewportState(createBrowserViewport(live.mediaSize, live.mediaSize.deviceScaleFactor));
1819
1814
  }, [live.mediaSize]);
1820
1815
  const sendInput = useCallback((message) => {
1821
- if (!enabled || !sessionId || !credentials) return false;
1816
+ if (!enabled || access === null) return false;
1822
1817
  return live.sendBrowserInput(message, inputDimensionsRef.current);
1823
1818
  }, [
1824
- credentials,
1819
+ access,
1825
1820
  enabled,
1826
- live,
1827
- sessionId
1821
+ live
1828
1822
  ]);
1829
1823
  const activateTarget = useCallback((targetId) => {
1830
1824
  live.selectTarget(targetId);
@@ -1972,19 +1966,18 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
1972
1966
  recordingBusy,
1973
1967
  runStartRecording
1974
1968
  ]);
1975
- 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 }) => {
1976
1970
  const recording = live.recordings.find((candidate) => candidate.recordingId === recordingId);
1977
- return downloadBlob(blob, filename ?? `${sessionId}-${recording?.targetId ?? "target"}-${recordingId}.webm`);
1978
- }), 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]);
1979
1973
  const stopRecording = useCallback((recordingId) => {
1980
- if (!sessionId || !credentials || recordingBusy) return;
1974
+ if (access === null || recordingBusy) return;
1981
1975
  setRecordingBusy(true);
1982
- runStopRecording(credentials, sessionId, recordingId);
1976
+ runStopRecording(access, recordingId);
1983
1977
  }, [
1984
- credentials,
1978
+ access,
1985
1979
  recordingBusy,
1986
- runStopRecording,
1987
- sessionId
1980
+ runStopRecording
1988
1981
  ]);
1989
1982
  const runCancelRecording = useEffectCallback((recordingId) => settleRecording(live.request("recording.cancel", { recordingId }).pipe(Effect.andThen(notify("success", "Recording stopped"))), "Recording failed to stop"), [live]);
1990
1983
  const cancelRecording = useCallback((recordingId) => {
@@ -1994,23 +1987,18 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
1994
1987
  }, [recordingBusy, runCancelRecording]);
1995
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]);
1996
1989
  const setRemoteCursorEnabled = useCallback((visible) => {
1997
- if (sessionId && credentials) runSetRemoteCursor(visible);
1998
- }, [
1999
- credentials,
2000
- runSetRemoteCursor,
2001
- sessionId
2002
- ]);
1990
+ if (access !== null) runSetRemoteCursor(visible);
1991
+ }, [access, runSetRemoteCursor]);
2003
1992
  const runSelectPresentation = useEffectCallback((selection) => live.selectPresentation(selection).pipe(Effect.catch((error) => notify("error", errorMessage(error, "Presentation could not be updated")))), [live]);
2004
1993
  const selectMediaStream = useCallback((selection) => {
2005
- if (!enabled || !sessionId || !credentials || live.mediaSwitching) return false;
1994
+ if (!enabled || access === null || live.mediaSwitching) return false;
2006
1995
  runSelectPresentation(selection);
2007
1996
  return true;
2008
1997
  }, [
2009
- credentials,
1998
+ access,
2010
1999
  enabled,
2011
2000
  live.mediaSwitching,
2012
- runSelectPresentation,
2013
- sessionId
2001
+ runSelectPresentation
2014
2002
  ]);
2015
2003
  const setWebRTCStreamSettings = useCallback((settings) => selectMediaStream({
2016
2004
  kind: "webrtc",
@@ -2132,7 +2120,7 @@ function errorMessage(cause, fallback) {
2132
2120
  }
2133
2121
  //#endregion
2134
2122
  //#region src/runtime.ts
2135
- 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)));
2136
2124
  //#endregion
2137
2125
  //#region src/provider.tsx
2138
2126
  function ApertureProvider({ baseUrl, children }) {
@@ -2152,55 +2140,75 @@ function ApertureProvider({ baseUrl, children }) {
2152
2140
  }) : null;
2153
2141
  }
2154
2142
  //#endregion
2155
- //#region src/shared-session.ts
2156
- function parseShareToken(token) {
2143
+ //#region src/session.ts
2144
+ /** Convert an editor/viewer share link into direct session access. */
2145
+ function shareSessionAccess(token, baseUrl) {
2157
2146
  const sessionId = token.slice(4, 40);
2158
- const role = token.startsWith("ape_") ? "editor" : token.startsWith("apv_") ? "viewer" : null;
2159
- 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;
2160
2148
  return {
2161
- token,
2149
+ kind: "direct",
2150
+ baseUrl,
2162
2151
  sessionId,
2163
- role
2152
+ credentials: {
2153
+ kind: "bearer",
2154
+ token: Redacted.make(token),
2155
+ authorityType: null,
2156
+ tenantId: null,
2157
+ selectedTenantId: null
2158
+ }
2164
2159
  };
2165
2160
  }
2166
2161
  const emptyIceServers = [];
2167
- function useSharedSession({ token, displayName, onNotice }) {
2168
- const share = useMemo(() => parseShareToken(token), [token]);
2169
- const credentials = useMemo(() => share && {
2170
- kind: "bearer",
2171
- token: Redacted.make(share.token),
2172
- authorityType: null,
2173
- tenantId: null,
2174
- selectedTenantId: null
2175
- }, [share]);
2176
- const [state, setState] = useState({ kind: "loading" });
2177
- useFork(() => share && credentials ? Effect.sync(() => setState({ kind: "loading" })).pipe(Effect.andThen(SessionsApi.use((sessions) => sessions.getBrowserStatus(credentials, share.sessionId))), Effect.match({
2178
- onFailure: (error) => setState({ kind: error.status === 410 ? "expired" : "unavailable" }),
2179
- onSuccess: (browser) => setState(browser.sessionId === share.sessionId ? {
2180
- kind: "ready",
2181
- browser
2182
- } : { kind: "unavailable" })
2183
- })) : void 0, [credentials, share]);
2184
- 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;
2185
2198
  const control = useBrowserControl({
2186
- sessionId: share?.sessionId ?? null,
2187
- credentials: browser ? credentials : null,
2199
+ access: browser === null ? null : access,
2188
2200
  displayName,
2189
- sessionToken: share?.token,
2190
- collaborationRole: share?.role ?? "viewer",
2191
- enabled: browser !== null,
2192
2201
  webrtcProducerSupported: browser?.media.mode === "auto" && browser.media.webrtcProducer,
2193
2202
  webrtcIceServers: browser?.media.iceServers ?? emptyIceServers,
2194
2203
  onNotice
2195
2204
  });
2196
2205
  return {
2197
- status: share ? state.kind : "invalid",
2198
- share,
2206
+ status: access === null ? "invalid" : state.access !== access ? "loading" : state.status,
2199
2207
  browser,
2200
2208
  control
2201
2209
  };
2202
2210
  }
2203
2211
  //#endregion
2204
- 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 };
2205
2213
 
2206
- //# sourceMappingURL=headless-BctsPMrj.js.map
2214
+ //# sourceMappingURL=headless-Dz39dQkW.js.map