@aperture-browser/live-session 0.0.0-init.0 → 0.0.0-nightly.100.78b4e7ef6edc

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.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import * as Effect from "effect/Effect";
3
3
  import * as Schema from "effect/Schema";
4
- import { Recording, resolveTenantHeader } from "@aperture-browser/api-client";
4
+ import { BrowserStatus, Recording, contentDispositionFilename, resolveTenantHeader, toApiRequestError } from "@aperture-browser/api-client";
5
5
  import * as Data from "effect/Data";
6
6
  import * as Deferred from "effect/Deferred";
7
7
  import "effect/Fiber";
8
8
  import * as FiberSet from "effect/FiberSet";
9
9
  import * as Option from "effect/Option";
10
+ import * as Redacted from "effect/Redacted";
11
+ import * as HttpClient from "effect/http/HttpClient";
12
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
13
+ import * as HttpClientResponse from "effect/http/HttpClientResponse";
10
14
  //#region src/input-keycodes.ts
11
15
  const evdevKeycodeByCode = {
12
16
  Escape: 1,
@@ -315,6 +319,8 @@ const Target = Schema.Struct({
315
319
  title: Schema.String,
316
320
  url: Schema.String,
317
321
  loading: Schema.Boolean,
322
+ canGoBack: Schema.optionalKey(Schema.Boolean),
323
+ canGoForward: Schema.optionalKey(Schema.Boolean),
318
324
  viewport: Schema.optionalKey(Schema.Struct({
319
325
  width: positive,
320
326
  height: positive,
@@ -357,7 +363,9 @@ const Snapshot = Schema.Struct({
357
363
  targets: arrayOrEmpty(Target),
358
364
  participants: arrayOrEmpty(Participant),
359
365
  recordings: arrayOrEmpty(Recording),
360
- presentation: Presentation
366
+ presentation: Presentation,
367
+ viewportOwnerClientId: Schema.optionalKey(Schema.String),
368
+ autoSize: Schema.optionalKey(Schema.Boolean)
361
369
  });
362
370
  const PresenceState = Schema.Struct({
363
371
  type: Schema.Literal("presence.state"),
@@ -368,6 +376,11 @@ const InputState = Schema.Struct({
368
376
  holderClientId: Schema.optionalKey(Schema.String),
369
377
  mode: Schema.optionalKey(LeaseMode)
370
378
  });
379
+ const ViewportState = Schema.Struct({
380
+ type: Schema.Literal("viewport.state"),
381
+ viewportOwnerClientId: Schema.optionalKey(Schema.String),
382
+ autoSize: Schema.Boolean
383
+ });
371
384
  const Cursor = Schema.Struct({
372
385
  type: Schema.Literal("presence.cursor"),
373
386
  clientId: Schema.String,
@@ -429,8 +442,11 @@ const CommandResult = Schema.Struct({
429
442
  "page.reload.result",
430
443
  "page.stop-loading.result",
431
444
  "viewport.set.result",
445
+ "viewport.auto-size.set.result",
446
+ "viewport.owner.claim.result",
432
447
  "presentation.quality.set.result",
433
448
  "presentation.cursor.set.result",
449
+ "automation.pacing.set.result",
434
450
  "recording.start.result",
435
451
  "recording.stop.result",
436
452
  "recording.cancel.result"
@@ -447,6 +463,7 @@ const LiveSessionServerMessage = Schema.Union([
447
463
  Snapshot,
448
464
  PresenceState,
449
465
  InputState,
466
+ ViewportState,
450
467
  Cursor,
451
468
  CursorClear,
452
469
  Paint,
@@ -524,6 +541,73 @@ function mapClientToViewport(clientX, clientY, rect, viewportWidth, viewportHeig
524
541
  };
525
542
  }
526
543
  //#endregion
544
+ //#region src/access.ts
545
+ /** Resolve a path under the access mount, including relative relay mounts. */
546
+ function mountURL(access, path) {
547
+ const base = new URL(access.baseUrl ?? "/", window.location.href);
548
+ if (base.protocol !== "http:" && base.protocol !== "https:") throw new Error("session base URL must use HTTP or HTTPS");
549
+ if (access.kind === "relay" && base.origin !== window.location.origin) throw new Error("session relay must use the page's origin");
550
+ base.pathname = `${base.pathname.replace(/\/$/, "")}/${path}`;
551
+ base.search = "";
552
+ base.hash = "";
553
+ return base;
554
+ }
555
+ /** Resolve all session endpoints under the same mount. */
556
+ function sessionURL(access, route) {
557
+ return mountURL(access, `sessions/${encodeURIComponent(access.sessionId)}/${route}`);
558
+ }
559
+ function sessionWebSocketURL(access, route) {
560
+ const url = sessionURL(access, route);
561
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
562
+ return url.toString();
563
+ }
564
+ /** Credentials the browser sends itself; relay access is authenticated by the consumer backend instead. */
565
+ function directAuthorization(access) {
566
+ if (access.kind !== "direct") return {
567
+ token: void 0,
568
+ tenantId: void 0
569
+ };
570
+ const { credentials } = access;
571
+ return {
572
+ token: credentials.kind === "bearer" ? Redacted.value(credentials.token) : void 0,
573
+ tenantId: resolveTenantHeader(credentials, "tenant-scoped")
574
+ };
575
+ }
576
+ function sessionProtocols(access) {
577
+ const { token, tenantId } = directAuthorization(access);
578
+ const protocols = [LIVE_SESSION_PROTOCOL];
579
+ if (token !== void 0) protocols.push(`authorization.bearer.${token}`);
580
+ if (tenantId !== void 0) protocols.push(`x-aperture-tenant-id.${tenantId}`);
581
+ return protocols;
582
+ }
583
+ /** GETs a session route with the credentials this access sends itself. */
584
+ function sessionGet(access, url) {
585
+ const { token, tenantId } = directAuthorization(access);
586
+ const headers = {};
587
+ if (token !== void 0) headers.Authorization = `Bearer ${token}`;
588
+ if (tenantId !== void 0) headers["X-Aperture-Tenant-Id"] = tenantId;
589
+ return Effect.flatMap(HttpClient.HttpClient, (http) => HttpClient.filterStatusOk(http).get(url.toString(), { headers })).pipe(Effect.provideService(FetchHttpClient.RequestInit, {
590
+ credentials: token === void 0 ? "same-origin" : "omit",
591
+ redirect: "error",
592
+ cache: "no-store"
593
+ }));
594
+ }
595
+ const getSessionStatus = Effect.fn("getSessionStatus")(function* (access) {
596
+ return yield* sessionGet(access, sessionURL(access, "browser/status")).pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(BrowserStatus)), toApiRequestError);
597
+ });
598
+ const downloadSessionRecording = Effect.fn("downloadSessionRecording")(function* (access, recordingId) {
599
+ return yield* sessionGet(access, sessionURL(access, `recordings/${encodeURIComponent(recordingId)}/content`)).pipe(Effect.flatMap((response) => Effect.map(response.arrayBuffer, (body) => ({
600
+ blob: new Blob([body], { type: response.headers["content-type"] ?? "" }),
601
+ filename: contentDispositionFilename(response.headers["content-disposition"])
602
+ }))), toApiRequestError);
603
+ });
604
+ /** A JPEG of one tab, authorized through the relay, session access, or account API. */
605
+ const getTargetThumbnail = Effect.fn("getTargetThumbnail")(function* (access, targetId) {
606
+ const route = `targets/${encodeURIComponent(targetId)}/thumbnail`;
607
+ const token = directAuthorization(access).token;
608
+ return yield* sessionGet(access, access.kind === "relay" || token !== void 0 && (token.startsWith("aps_") || token.startsWith("ape_") || token.startsWith("apv_")) ? sessionURL(access, route) : mountURL(access, `api/sessions/${encodeURIComponent(access.sessionId)}/${route}`)).pipe(Effect.flatMap((response) => Effect.map(response.arrayBuffer, (body) => new Blob([body], { type: "image/jpeg" }))), toApiRequestError);
609
+ });
610
+ //#endregion
527
611
  //#region src/connection.ts
528
612
  var connection_exports = /* @__PURE__ */ __exportAll({
529
613
  LiveSessionError: () => LiveSessionError,
@@ -577,6 +661,7 @@ const make = Effect.fnUntraced(function* (options) {
577
661
  let preferredTransport = options.webrtcSupported ? "webrtc" : "websocket";
578
662
  let transportRequest = null;
579
663
  let disposed = false;
664
+ let terminalError = null;
580
665
  let retryTimer = null;
581
666
  let webrtcRetryMs = 1e3;
582
667
  let nextRequestId = 0;
@@ -587,7 +672,8 @@ const make = Effect.fnUntraced(function* (options) {
587
672
  const pendingCommands = /* @__PURE__ */ new Map();
588
673
  const hello = () => ({
589
674
  type: "session.hello",
590
- ...identity ?? options.identity
675
+ ...identity ?? options.identity,
676
+ ...options.autoSize === void 0 ? {} : { autoSize: options.autoSize() }
591
677
  });
592
678
  const transportCallbacks = {
593
679
  hello,
@@ -625,14 +711,11 @@ const make = Effect.fnUntraced(function* (options) {
625
711
  else startWebSocket();
626
712
  };
627
713
  const startWebRTC = () => {
628
- if (disposed || candidate) return;
714
+ if (disposed || terminalError !== null || candidate) return;
629
715
  let transport;
630
716
  try {
631
717
  transport = new WebRTCSessionTransport({
632
- baseUrl: options.baseUrl,
633
- sessionId: options.sessionId,
634
- credentials: options.credentials,
635
- sessionToken: options.sessionToken,
718
+ access: options.access,
636
719
  iceServers: options.iceServers,
637
720
  callbacks: transportCallbacks,
638
721
  fork
@@ -652,14 +735,11 @@ const make = Effect.fnUntraced(function* (options) {
652
735
  });
653
736
  };
654
737
  const startWebSocket = () => {
655
- if (disposed || candidate) return;
738
+ if (disposed || terminalError !== null || candidate) return;
656
739
  let transport;
657
740
  try {
658
741
  transport = new WebSocketSessionTransport({
659
- baseUrl: options.baseUrl,
660
- sessionId: options.sessionId,
661
- credentials: options.credentials,
662
- sessionToken: options.sessionToken,
742
+ access: options.access,
663
743
  callbacks: transportCallbacks,
664
744
  fork
665
745
  });
@@ -709,8 +789,27 @@ const make = Effect.fnUntraced(function* (options) {
709
789
  if (transport.kind === "websocket" && preferredTransport === "webrtc" && options.webrtcSupported) scheduleWebRTCRetry();
710
790
  if (previous && previous !== transport) previous.close();
711
791
  };
792
+ /** Closes every transport and clears what they presented; the connection itself stays usable. */
793
+ const stopTransports = (reason) => {
794
+ clearRetry();
795
+ dropCandidate();
796
+ active?.close();
797
+ active = null;
798
+ rejectTransportRequest(reason);
799
+ rejectPending(reason);
800
+ callbacks.onFrame(null);
801
+ callbacks.onStream(null);
802
+ callbacks.onTransport(null);
803
+ };
712
804
  const transportFailed = (transport, error) => {
713
805
  if (disposed) return;
806
+ if (error.terminalReason !== void 0 && (candidate === transport || active === transport)) {
807
+ terminalError = error;
808
+ stopTransports(error.message);
809
+ callbacks.onPhase("error");
810
+ callbacks.onError(error.message);
811
+ return;
812
+ }
714
813
  if (candidate === transport) {
715
814
  dropCandidate();
716
815
  candidateFailed(transport.kind, error);
@@ -791,21 +890,14 @@ const make = Effect.fnUntraced(function* (options) {
791
890
  };
792
891
  const close = () => {
793
892
  disposed = true;
794
- clearRetry();
795
- dropCandidate();
796
- active?.close();
797
- active = null;
798
- rejectTransportRequest("live session connection closed");
799
- rejectPending("live session connection closed");
800
- callbacks.onFrame(null);
801
- callbacks.onStream(null);
802
- callbacks.onTransport(null);
893
+ stopTransports("live session connection closed");
803
894
  };
804
895
  yield* Effect.addFinalizer(() => Effect.sync(close));
805
896
  callbacks.onPhase("connecting");
806
897
  startPreferredTransport();
807
898
  return {
808
899
  reconnect: Effect.sync(() => {
900
+ terminalError = null;
809
901
  clearRetry();
810
902
  dropCandidate();
811
903
  active?.close();
@@ -813,9 +905,10 @@ const make = Effect.fnUntraced(function* (options) {
813
905
  rejectTransportRequest("live session transport replaced");
814
906
  rejectPending("live session transport replaced");
815
907
  callbacks.onPhase("connecting");
816
- startPreferredTransport();
908
+ startWebSocket();
817
909
  }),
818
910
  selectTransport: (kind) => Effect.suspend(() => {
911
+ if (terminalError !== null) return Effect.fail(terminalError);
819
912
  if (kind === "webrtc" && !options.webrtcSupported) return Effect.fail(new LiveSessionError({ message: "WebRTC presentation is unavailable" }));
820
913
  preferredTransport = kind;
821
914
  clearRetry();
@@ -867,7 +960,7 @@ var WebSocketSessionTransport = class {
867
960
  constructor(options) {
868
961
  this.callbacks = options.callbacks;
869
962
  this.fork = options.fork;
870
- this.socket = new WebSocket(sessionWebSocketURL(options.baseUrl, options.sessionId), sessionProtocols(options.credentials, options.sessionToken));
963
+ this.socket = new WebSocket(sessionWebSocketURL(options.access, "session"), sessionProtocols(options.access));
871
964
  this.socket.binaryType = "blob";
872
965
  }
873
966
  connect() {
@@ -888,8 +981,8 @@ var WebSocketSessionTransport = class {
888
981
  }
889
982
  }
890
983
  });
891
- this.socket.addEventListener("close", () => {
892
- if (!this.closed) this.callbacks.failed(this, new LiveSessionError({ message: "WebSocket session transport closed" }));
984
+ this.socket.addEventListener("close", (event) => {
985
+ if (!this.closed) this.callbacks.failed(this, transportError("WebSocket session transport closed", event.code));
893
986
  });
894
987
  this.socket.addEventListener("error", () => {
895
988
  if (!this.closed) this.callbacks.failed(this, new LiveSessionError({ message: "WebSocket session transport failed" }));
@@ -969,8 +1062,9 @@ var WebRTCSessionTransport = class {
969
1062
  this.callbacks = options.callbacks;
970
1063
  this.fork = options.fork;
971
1064
  this.connection = new RTCPeerConnection({ iceServers: options.iceServers.map((server) => ({
972
- ...server,
973
- urls: [...server.urls]
1065
+ urls: [...server.urls],
1066
+ username: server.username,
1067
+ credential: server.credential === void 0 ? void 0 : Redacted.value(server.credential)
974
1068
  })) });
975
1069
  this.reliable = this.connection.createDataChannel("application", { ordered: true });
976
1070
  this.realtime = this.connection.createDataChannel("application-realtime", {
@@ -978,7 +1072,7 @@ var WebRTCSessionTransport = class {
978
1072
  maxRetransmits: 0
979
1073
  });
980
1074
  this.connection.addTransceiver("video", { direction: "recvonly" });
981
- this.signal = new WebSocket(signalWebSocketURL(options.baseUrl, options.sessionId), sessionProtocols(options.credentials, options.sessionToken));
1075
+ this.signal = new WebSocket(sessionWebSocketURL(options.access, "webrtc/signal"), sessionProtocols(options.access));
982
1076
  }
983
1077
  connect() {
984
1078
  this.reliable.addEventListener("open", () => {
@@ -1019,7 +1113,7 @@ var WebRTCSessionTransport = class {
1019
1113
  this.fork(this.sendOffer().pipe(Effect.catchTag("LiveSessionError", (error) => Effect.sync(() => this.fail(error.message)))));
1020
1114
  });
1021
1115
  this.signal.addEventListener("message", (event) => this.handleSignal(event));
1022
- this.signal.addEventListener("close", () => this.fail("WebRTC signaling closed"));
1116
+ this.signal.addEventListener("close", (event) => this.fail("WebRTC signaling closed", event.code));
1023
1117
  this.signal.addEventListener("error", () => this.fail("WebRTC signaling failed"));
1024
1118
  }
1025
1119
  sendReliable(message) {
@@ -1106,10 +1200,21 @@ var WebRTCSessionTransport = class {
1106
1200
  maybeReady() {
1107
1201
  if (this.snapshotReceived && this.stream && this.streamReady) this.callbacks.ready(this);
1108
1202
  }
1109
- fail(message) {
1110
- if (!this.closed) this.callbacks.failed(this, new LiveSessionError({ message }));
1203
+ fail(message, closeCode = 0) {
1204
+ if (!this.closed) this.callbacks.failed(this, transportError(message, closeCode));
1111
1205
  }
1112
1206
  };
1207
+ function transportError(message, closeCode) {
1208
+ if (closeCode === 4401 || closeCode === 4403) return new LiveSessionError({
1209
+ message: "Session access denied. Sign in or request access before reconnecting.",
1210
+ terminalReason: "access_denied"
1211
+ });
1212
+ if (closeCode === 4404 || closeCode === 4410) return new LiveSessionError({
1213
+ message: "Session is no longer available.",
1214
+ terminalReason: "session_unavailable"
1215
+ });
1216
+ return new LiveSessionError({ message });
1217
+ }
1113
1218
  function decodeRasterFrame(packet) {
1114
1219
  return Effect.gen(function* () {
1115
1220
  if (packet.size < 4) return Option.none();
@@ -1125,26 +1230,7 @@ function decodeRasterFrame(packet) {
1125
1230
  }));
1126
1231
  }).pipe(Effect.orElseSucceed(() => Option.none()));
1127
1232
  }
1128
- function sessionWebSocketURL(baseUrl, sessionId) {
1129
- return webSocketURL(baseUrl, `/sessions/${encodeURIComponent(sessionId)}/session`);
1130
- }
1131
- function signalWebSocketURL(baseUrl, sessionId) {
1132
- return webSocketURL(baseUrl, `/sessions/${encodeURIComponent(sessionId)}/webrtc/signal`);
1133
- }
1134
- function webSocketURL(baseUrl, path) {
1135
- const url = new URL(path, baseUrl ?? window.location.href);
1136
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1137
- return url.toString();
1138
- }
1139
- function sessionProtocols(credentials, sessionToken) {
1140
- const protocols = [LIVE_SESSION_PROTOCOL];
1141
- if (sessionToken) protocols.push(`authorization.bearer.${sessionToken}`);
1142
- else if (credentials.kind === "bearer") protocols.push(`authorization.bearer.${credentials.token}`);
1143
- const tenantId = resolveTenantHeader(credentials, "tenant-scoped");
1144
- if (tenantId) protocols.push(`x-aperture-tenant-id.${tenantId}`);
1145
- return protocols;
1146
- }
1147
1233
  //#endregion
1148
- export { DEFAULT_VIEWPORT, LIVE_SESSION_PROTOCOL, connection_exports as LiveSessionConnection, LiveSessionError, LiveSessionServerMessage, RasterFrameHeader, STALE_FRAME_MS, VIEWPORT_DEVICE_SCALE_FACTORS, VIEWPORT_PRESETS, computeRenderMetrics, createViewportPreset, decodeServerMessage, evdevKeycodeByCode, formatViewportScale, isPrintableKey, keyboardInputMessage, keyboardModifiers, mapClientToViewport, shouldForwardBrowserShortcut, strictParseOptions, windowsVirtualKeyCodeForCodeOrKey };
1234
+ export { DEFAULT_VIEWPORT, LIVE_SESSION_PROTOCOL, connection_exports as LiveSessionConnection, LiveSessionError, LiveSessionServerMessage, RasterFrameHeader, STALE_FRAME_MS, VIEWPORT_DEVICE_SCALE_FACTORS, VIEWPORT_PRESETS, computeRenderMetrics, createViewportPreset, decodeServerMessage, downloadSessionRecording, evdevKeycodeByCode, formatViewportScale, getSessionStatus, getTargetThumbnail, isPrintableKey, keyboardInputMessage, keyboardModifiers, mapClientToViewport, sessionProtocols, sessionURL, sessionWebSocketURL, shouldForwardBrowserShortcut, strictParseOptions, windowsVirtualKeyCodeForCodeOrKey };
1149
1235
 
1150
1236
  //# sourceMappingURL=index.js.map