@aperture-browser/session-relay 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Taras Fomin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @aperture-browser/session-relay
2
+
3
+ Relay Aperture session access through your application's backend. Your application authenticates the browser and decides which session it may access; Aperture credentials stay on the backend.
4
+
5
+ ```sh
6
+ npm install @aperture-browser/session-relay effect @effect/platform-node
7
+ ```
8
+
9
+ ## Mount the relay
10
+
11
+ `sessionRelay` registers four GET routes on an Effect `HttpRouter` below `<prefix>/sessions/:sessionId`: `/browser/status`, `/session`, `/webrtc/signal`, and `/recordings/:recordingId/content`. `/session` and `/webrtc/signal` upgrade to WebSockets. The Node adapter supplies upstream HTTP and WebSocket clients; it does not start a server.
12
+
13
+ ```ts
14
+ import * as Layer from "effect/Layer";
15
+ import { sessionRelay, type SessionRelayOptions } from "@aperture-browser/session-relay";
16
+ import { nodeRelayLayer } from "@aperture-browser/session-relay/node";
17
+
18
+ export function relayRoutes(authorize: SessionRelayOptions["authorize"]) {
19
+ return sessionRelay({
20
+ prefix: "/aperture",
21
+ publicOrigin: "https://app.example.com",
22
+ apertureBaseUrl: "https://aperture.example.com",
23
+ authorize,
24
+ }).pipe(Layer.provide(nodeRelayLayer));
25
+ }
26
+ ```
27
+
28
+ Merge the returned layer with your app's other routes and serve it with `HttpRouter.serve` and `NodeHttpServer`. Existing Node HTTP servers can use `NodeHttpServer.makeHandler` and `makeUpgradeHandler` for the corresponding request and upgrade events. Configure the server's WebSocket `maxPayload` for your expected raster frames, for example 16 MiB. Your ingress must support persistent WebSocket upgrades.
29
+
30
+ ## Authorize every request
31
+
32
+ The callback receives the original `HttpServerRequest` and the requested session identifier. Read your app's login cookie, verify the user's session access, and return an Effect containing:
33
+
34
+ ```ts
35
+ import * as Effect from "effect/Effect";
36
+ import * as Redacted from "effect/Redacted";
37
+ import { RelayDenied, type RelayGrant } from "@aperture-browser/session-relay";
38
+
39
+ // After your application's authentication and authorization have succeeded:
40
+ export function grantSession(
41
+ sessionId: string,
42
+ capability: Redacted.Redacted<string>,
43
+ signal: AbortSignal,
44
+ ): Effect.Effect<RelayGrant> {
45
+ return Effect.succeed({ sessionId, capability, signal });
46
+ }
47
+
48
+ // Return these from the authorization callback for login/access failures:
49
+ export const signInRequired = Effect.fail(new RelayDenied({ status: 401 }));
50
+ export const accessDenied = Effect.fail(new RelayDenied({ status: 403 }));
51
+ ```
52
+
53
+ Select the session's `collaboration.editorToken` or `collaboration.viewerToken` from your server-side `ApertureClient.sessions.getSession` result according to the app user's permissions. An `aps_` owner token or account API token is rejected as a relay grant. The browser cannot select its role. Aperture enforces that role on both WebSockets and direct WebRTC data channels.
54
+
55
+ The requested identifier is one path segment containing letters, digits, underscores, or hyphens. It may be an app-owned alias; the callback maps it to the actual Aperture `sessionId`. Every status request, fallback connection, and reconnect calls the callback again. Authentication callbacks may depend on Effect services; supply those services to the served router.
56
+
57
+ Own an `AbortController` for each access lifetime. Return its signal for all connections belonging to that access. Abort it when login expires, the user logs out, or their access is revoked or downgraded. This closes active signaling/session connections and their upstream peers. Reject later callback invocations until the user has access again. Use a fresh controller for a newly granted lifetime. Merely checking login when a socket opens does not revoke an already connected browser.
58
+
59
+ ## Connect the browser
60
+
61
+ ```tsx
62
+ import { ApertureSession } from "@aperture-browser/session-react";
63
+ import "@aperture-browser/session-react/styles.css";
64
+
65
+ const access = { kind: "relay", baseUrl: "/aperture", sessionId: "your-session-id" } as const;
66
+
67
+ export function Browser() {
68
+ return <ApertureSession access={access} className="h-full" />;
69
+ }
70
+ ```
71
+
72
+ For custom elements, assign the same object to `element.access`. Headless React uses `useSession({ access })`. Keep the access object stable across renders. No token or role is supplied to the browser; same-origin cookies authenticate it to the consumer app.
73
+
74
+ ## Connection behavior
75
+
76
+ - Status and both WebSockets use the relay mount. WebRTC video and data channels connect directly to Aperture, subject to its ICE/TURN connectivity. Raster fallback travels through the relay.
77
+ - Editors download stopped recordings through the relay. Aperture rejects viewer grants, and aborting the access signal ends a download in progress. The file is streamed, not buffered.
78
+ - The relay checks browser Origin against the configured public origin before authentication. It constructs upstream requests afresh, sending only the selected capability and session subprotocol. Browser cookies, Origin, authorization, and tenant headers are not forwarded. `embed_allowed_origins` needs no entry for the app.
79
+ - Upstream status authorization is checked before each WebSocket upgrade. Denials become close codes 4401/4403; missing or expired sessions use 4404/4410. All four stop client retries. Restore login/access before calling `reconnect()` explicitly. Transient transport failures retain normal fallback and reconnect behavior.
80
+ - Message boundaries and text/binary framing are preserved. Effect sockets apply backpressure; either connection closing disposes the other. No session protocol interpretation or input filtering happens in the relay.
81
+ - HTTP responses are not cacheable. The upstream HTTP client must not follow redirects or inject unrelated credentials; `nodeRelayLayer` supplies an undecorated client.
82
+
83
+ Only viewer/editor live-session access and recording downloads are exposed. Session management, raw CDP, and files are separate backend integrations.
@@ -0,0 +1,34 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Redacted from "effect/Redacted";
3
+ import * as Schema from "effect/Schema";
4
+ import * as HttpClient from "effect/unstable/http/HttpClient";
5
+ import * as HttpRouter from "effect/unstable/http/HttpRouter";
6
+ import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
7
+ import * as Socket from "effect/unstable/socket/Socket";
8
+
9
+ //#region src/relay.d.ts
10
+ declare const RelayDenied_base: Schema.Class<RelayDenied, Schema.TaggedStruct<"RelayDenied", {
11
+ readonly status: Schema.Literals<readonly [401, 403]>;
12
+ }>, import("effect/Cause").YieldableError>;
13
+ declare class RelayDenied extends RelayDenied_base {}
14
+ /** A consumer-authorized connection. Aborting the signal revokes active access. */
15
+ interface RelayGrant {
16
+ readonly sessionId: string;
17
+ /** An editor (ape_) or viewer (apv_) capability, never an owner or account token. */
18
+ readonly capability: Redacted.Redacted<string>;
19
+ readonly signal: AbortSignal;
20
+ }
21
+ interface SessionRelayOptions<R = never> {
22
+ readonly prefix: string;
23
+ readonly publicOrigin: string;
24
+ readonly apertureBaseUrl: string;
25
+ readonly authorize: (request: HttpServerRequest.HttpServerRequest, sessionId: string) => Effect.Effect<RelayGrant, RelayDenied, R>;
26
+ }
27
+ /**
28
+ * Registers session status, the session and signaling WebSockets, and recording downloads;
29
+ * the consumer owns login.
30
+ */
31
+ declare function sessionRelay<R>(options: SessionRelayOptions<R>): import("effect/Layer").Layer<never, never, HttpClient.HttpClient | Socket.WebSocketConstructor | HttpRouter.HttpRouter | HttpRouter.Request<"Error", import("effect/unstable/http/HttpServerError").HttpServerError> | HttpRouter.Request<"Error", Socket.SocketError> | Exclude<HttpRouter.Request.From<"Requires", Exclude<Exclude<R, import("effect/Scope").Scope>, HttpRouter.Provided>>, import("effect/Scope").Scope>>;
32
+ //#endregion
33
+ export { RelayDenied, RelayGrant, SessionRelayOptions, sessionRelay };
34
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,194 @@
1
+ import * as Data from "effect/Data";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Redacted from "effect/Redacted";
4
+ import * as Schema from "effect/Schema";
5
+ import * as Stream from "effect/Stream";
6
+ import * as HttpClient from "effect/unstable/http/HttpClient";
7
+ import * as HttpRouter from "effect/unstable/http/HttpRouter";
8
+ import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
9
+ import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
10
+ import * as Socket from "effect/unstable/socket/Socket";
11
+ //#region src/relay.ts
12
+ const protocol = "aperture-session.v1";
13
+ const pathParams = Schema.Struct({ sessionId: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)) });
14
+ var RelayDenied = class extends Schema.TaggedError()("RelayDenied", { status: Schema.Literals([401, 403]) }) {};
15
+ const isTerminalStatus = (status) => status === 401 || status === 403 || status === 404 || status === 410;
16
+ const recordingParams = Schema.Struct({ recordingId: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)) });
17
+ /** Ends a handler early with a finished response. */
18
+ var Respond = class extends Data.TaggedError("Respond") {};
19
+ const respond = (response) => Effect.fail(new Respond({ response }));
20
+ /**
21
+ * Registers session status, the session and signaling WebSockets, and recording downloads;
22
+ * the consumer owns login.
23
+ */
24
+ function sessionRelay(options) {
25
+ const publicOrigin = new URL(options.publicOrigin).origin;
26
+ const upstream = parseUpstream(options.apertureBaseUrl);
27
+ if (!options.prefix.startsWith("/") || /[?#]/.test(options.prefix)) throw new Error("relay prefix must be an absolute path");
28
+ const prefix = options.prefix.replace(/\/$/, "");
29
+ const upstreamURL = (sessionId, route) => {
30
+ const url = new URL(upstream);
31
+ url.pathname = `${upstream.pathname.replace(/\/$/, "")}/sessions/${encodeURIComponent(sessionId)}/${route}`;
32
+ return url;
33
+ };
34
+ return HttpRouter.use(Effect.fn("sessionRelay.routes")(function* (router) {
35
+ const http = yield* HttpClient.HttpClient;
36
+ const makeSocket = yield* Socket.WebSocketConstructor;
37
+ const upstreamGet = (url, capability) => http.get(url.toString(), { headers: { Authorization: `Bearer ${capability}` } }).pipe(Effect.timeout("15 seconds"), Effect.provideService(HttpClient.TracerPropagationEnabled, false));
38
+ /** Checks the browser request and asks the consumer for a grant. */
39
+ const admit = Effect.fnUntraced(function* (websocket) {
40
+ const request = yield* HttpServerRequest.HttpServerRequest;
41
+ const origin = request.headers.origin;
42
+ if (origin !== publicOrigin && (websocket || origin !== void 0)) return yield* respond(relayError(403, "origin_denied", "Request origin is not allowed"));
43
+ if (websocket && request.headers["sec-websocket-protocol"] !== protocol) return yield* respond(relayError(400, "invalid_protocol", "Session subprotocol is required"));
44
+ const { sessionId } = yield* HttpRouter.schemaPathParams(pathParams);
45
+ const grant = yield* options.authorize(request, sessionId);
46
+ const capability = Redacted.value(grant.capability);
47
+ if (!capability.startsWith("ape_") && !capability.startsWith("apv_")) return yield* respond(relayError(500, "invalid_relay_grant", "Relay requires an editor or viewer capability"));
48
+ if (grant.signal.aborted) return yield* respond(yield* rejectAccess(request, websocket, 403));
49
+ return {
50
+ request,
51
+ grant,
52
+ capability
53
+ };
54
+ });
55
+ /** Reads upstream session status, turning refusals into the browser's terminal responses. */
56
+ const sessionStatus = Effect.fnUntraced(function* (admitted, websocket) {
57
+ const { request, grant, capability } = admitted;
58
+ const response = yield* upstreamGet(upstreamURL(grant.sessionId, "browser/status"), capability);
59
+ if (isTerminalStatus(response.status)) return yield* respond(yield* rejectAccess(request, websocket, response.status));
60
+ if (response.status !== 200) {
61
+ const status = response.status >= 400 ? response.status : 502;
62
+ return yield* respond(relayError(status, "session_unavailable", "Upstream session is unavailable"));
63
+ }
64
+ const body = yield* response.arrayBuffer.pipe(Effect.timeout("15 seconds"));
65
+ if (grant.signal.aborted) return yield* respond(yield* rejectAccess(request, websocket, 403));
66
+ return body;
67
+ });
68
+ const status = Effect.gen(function* () {
69
+ const body = yield* sessionStatus(yield* admit(false), false);
70
+ return HttpServerResponse.uint8Array(new Uint8Array(body), {
71
+ contentType: "application/json",
72
+ headers: { "cache-control": "no-store" }
73
+ });
74
+ });
75
+ const socket = (route) => Effect.gen(function* () {
76
+ const admitted = yield* admit(true);
77
+ yield* sessionStatus(admitted, true);
78
+ const url = upstreamURL(admitted.grant.sessionId, route);
79
+ url.protocol = upstream.protocol === "https:" ? "wss:" : "ws:";
80
+ yield* relaySocket(admitted.request, url, admitted.capability, admitted.grant.signal).pipe(Effect.provideService(Socket.WebSocketConstructor, makeSocket));
81
+ return HttpServerResponse.empty();
82
+ });
83
+ const recording = Effect.gen(function* () {
84
+ const { request, grant, capability } = yield* admit(false);
85
+ const { recordingId } = yield* HttpRouter.schemaPathParams(recordingParams);
86
+ const url = upstreamURL(grant.sessionId, `recordings/${encodeURIComponent(recordingId)}/content`);
87
+ const response = yield* upstreamGet(url, capability);
88
+ if (response.status === 404) return relayError(404, "recording_not_found", "Recording not found");
89
+ if (response.status === 409) return relayError(409, "recording_not_ready", "Recording is not ready for download");
90
+ if (isTerminalStatus(response.status)) return yield* rejectAccess(request, false, response.status);
91
+ if (response.status !== 200) return relayError(502, "upstream_unavailable", "Aperture is unavailable");
92
+ const headers = { "cache-control": "no-store" };
93
+ const disposition = response.headers["content-disposition"];
94
+ if (disposition !== void 0) headers["content-disposition"] = disposition;
95
+ const contentLength = Number(response.headers["content-length"]);
96
+ return HttpServerResponse.stream(response.stream.pipe(Stream.interruptWhen(revoked(grant.signal))), {
97
+ contentType: response.headers["content-type"] ?? "application/octet-stream",
98
+ contentLength: Number.isSafeInteger(contentLength) ? contentLength : void 0,
99
+ headers
100
+ });
101
+ });
102
+ const routes = [
103
+ {
104
+ path: "browser/status",
105
+ websocket: false,
106
+ handler: status
107
+ },
108
+ {
109
+ path: "session",
110
+ websocket: true,
111
+ handler: socket("session")
112
+ },
113
+ {
114
+ path: "webrtc/signal",
115
+ websocket: true,
116
+ handler: socket("webrtc/signal")
117
+ },
118
+ {
119
+ path: "recordings/:recordingId/content",
120
+ websocket: false,
121
+ handler: recording
122
+ }
123
+ ];
124
+ const prefixed = router.prefixed(prefix);
125
+ for (const { path, websocket, handler } of routes) yield* prefixed.add("GET", `/sessions/:sessionId/${path}`, handler.pipe(Effect.catchTags({
126
+ Respond: (early) => Effect.succeed(early.response),
127
+ RelayDenied: (error) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => rejectAccess(request, websocket, error.status)),
128
+ SchemaError: () => Effect.succeed(relayError(400, "invalid_path", "Invalid session or recording identifier")),
129
+ TimeoutError: () => Effect.succeed(relayError(504, "upstream_timeout", "Aperture did not respond in time")),
130
+ HttpClientError: () => Effect.succeed(relayError(502, "upstream_unavailable", "Aperture is unavailable"))
131
+ }), Effect.scoped));
132
+ }));
133
+ }
134
+ function parseUpstream(apertureBaseUrl) {
135
+ const upstream = new URL(apertureBaseUrl);
136
+ if (upstream.protocol !== "http:" && upstream.protocol !== "https:") throw new Error("apertureBaseUrl must use HTTP or HTTPS");
137
+ if (upstream.username !== "" || upstream.password !== "" || upstream.search !== "" || upstream.hash !== "") throw new Error("apertureBaseUrl must not contain credentials, a query, or a fragment");
138
+ return upstream;
139
+ }
140
+ function relayError(status, code, message) {
141
+ return HttpServerResponse.jsonUnsafe({ error: {
142
+ code,
143
+ message
144
+ } }, {
145
+ status,
146
+ headers: { "cache-control": "no-store" }
147
+ });
148
+ }
149
+ const rejectAccess = Effect.fn("sessionRelay.rejectAccess")(function* (request, websocket, status) {
150
+ const denied = status === 401 || status === 403;
151
+ if (!websocket) return denied ? relayError(status, "access_denied", "Session access denied") : relayError(status, "session_unavailable", "Session unavailable");
152
+ const socket = yield* request.upgrade;
153
+ yield* socket.reader;
154
+ yield* (yield* socket.writer).write(new Socket.CloseEvent(4e3 + status, denied ? "Session access denied" : "Session unavailable"));
155
+ return HttpServerResponse.empty();
156
+ });
157
+ /** Upgrades the browser request and pipes it to Aperture until either side closes or access is revoked. */
158
+ const relaySocket = Effect.fn("sessionRelay.relaySocket")(function* (request, url, capability, signal) {
159
+ const downstream = yield* request.upgrade;
160
+ const writer = yield* downstream.writer;
161
+ const upstream = yield* Socket.makeWebSocket(url.toString(), {
162
+ protocols: [protocol, `authorization.bearer.${capability}`],
163
+ openTimeout: "10 seconds",
164
+ highWaterMark: 1024 * 1024
165
+ });
166
+ const closeRevoked = Effect.gen(function* () {
167
+ yield* revoked(signal);
168
+ yield* writer.write(new Socket.CloseEvent(4403, "Session access revoked"));
169
+ });
170
+ yield* bridge(downstream, upstream).pipe(Effect.raceFirst(closeRevoked), Effect.catchTag("SocketError", (error) => {
171
+ const { reason } = error;
172
+ const code = reason._tag === "SocketCloseError" && (reason.code === 1e3 || reason.code === 1001) ? reason.code : 1011;
173
+ return writer.write(new Socket.CloseEvent(code, "Session transport closed")).pipe(Effect.catchTag("SocketError", () => Effect.void));
174
+ }));
175
+ });
176
+ const bridge = Effect.fn("sessionRelay.bridge")(function* (downstream, upstream) {
177
+ const [client, server] = yield* Effect.all([downstream.reader, upstream.reader], { concurrency: "unbounded" });
178
+ const [toClient, toServer] = yield* Effect.all([downstream.writer, upstream.writer]);
179
+ yield* Effect.raceFirst(client.pull.pipe(Effect.flatMap(toServer.writeAll), Effect.forever), server.pull.pipe(Effect.flatMap(toClient.writeAll), Effect.forever));
180
+ });
181
+ /** AbortSignal is the consumer's per-user/per-session revocation boundary. */
182
+ const revoked = (signal) => Effect.callback((resume) => {
183
+ if (signal.aborted) {
184
+ resume(Effect.void);
185
+ return;
186
+ }
187
+ const abort = () => resume(Effect.void);
188
+ signal.addEventListener("abort", abort, { once: true });
189
+ return Effect.sync(() => signal.removeEventListener("abort", abort));
190
+ });
191
+ //#endregion
192
+ export { RelayDenied, sessionRelay };
193
+
194
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/relay.ts"],"sourcesContent":["import * as Data from \"effect/Data\";\nimport * as Effect from \"effect/Effect\";\nimport * as Redacted from \"effect/Redacted\";\nimport * as Schema from \"effect/Schema\";\nimport * as Stream from \"effect/Stream\";\nimport * as HttpClient from \"effect/unstable/http/HttpClient\";\nimport * as HttpRouter from \"effect/unstable/http/HttpRouter\";\nimport * as HttpServerRequest from \"effect/unstable/http/HttpServerRequest\";\nimport * as HttpServerResponse from \"effect/unstable/http/HttpServerResponse\";\nimport * as Socket from \"effect/unstable/socket/Socket\";\n\nconst protocol = \"aperture-session.v1\";\nconst pathParams = Schema.Struct({\n sessionId: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)),\n});\n\nexport class RelayDenied extends Schema.TaggedError<RelayDenied>()(\"RelayDenied\", {\n status: Schema.Literals([401, 403]),\n}) {}\n\n/** A consumer-authorized connection. Aborting the signal revokes active access. */\nexport interface RelayGrant {\n readonly sessionId: string;\n /** An editor (ape_) or viewer (apv_) capability, never an owner or account token. */\n readonly capability: Redacted.Redacted<string>;\n readonly signal: AbortSignal;\n}\n\nexport interface SessionRelayOptions<R = never> {\n readonly prefix: string;\n readonly publicOrigin: string;\n readonly apertureBaseUrl: string;\n readonly authorize: (\n request: HttpServerRequest.HttpServerRequest,\n sessionId: string,\n ) => Effect.Effect<RelayGrant, RelayDenied, R>;\n}\n\ntype SocketRoute = \"session\" | \"webrtc/signal\";\n\n/** Statuses the browser treats as final: it stops retrying until reconnected explicitly. */\ntype TerminalStatus = 401 | 403 | 404 | 410;\nconst isTerminalStatus = (status: number): status is TerminalStatus =>\n status === 401 || status === 403 || status === 404 || status === 410;\n\nconst recordingParams = Schema.Struct({\n recordingId: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)),\n});\n\n/** Ends a handler early with a finished response. */\nclass Respond extends Data.TaggedError(\"Respond\")<{\n readonly response: HttpServerResponse.HttpServerResponse;\n}> {}\n\nconst respond = (response: HttpServerResponse.HttpServerResponse) =>\n Effect.fail(new Respond({ response }));\n\n/**\n * Registers session status, the session and signaling WebSockets, and recording downloads;\n * the consumer owns login.\n */\nexport function sessionRelay<R>(options: SessionRelayOptions<R>) {\n const publicOrigin = new URL(options.publicOrigin).origin;\n const upstream = parseUpstream(options.apertureBaseUrl);\n if (!options.prefix.startsWith(\"/\") || /[?#]/.test(options.prefix)) {\n throw new Error(\"relay prefix must be an absolute path\");\n }\n const prefix = options.prefix.replace(/\\/$/, \"\");\n\n const upstreamURL = (sessionId: string, route: string) => {\n const url = new URL(upstream);\n url.pathname = `${upstream.pathname.replace(/\\/$/, \"\")}/sessions/${encodeURIComponent(sessionId)}/${route}`;\n return url;\n };\n\n return HttpRouter.use(\n Effect.fn(\"sessionRelay.routes\")(function* (router) {\n const http = yield* HttpClient.HttpClient;\n const makeSocket = yield* Socket.WebSocketConstructor;\n\n const upstreamGet = (url: URL, capability: string) =>\n http\n .get(url.toString(), { headers: { Authorization: `Bearer ${capability}` } })\n .pipe(\n Effect.timeout(\"15 seconds\"),\n Effect.provideService(HttpClient.TracerPropagationEnabled, false),\n );\n\n /** Checks the browser request and asks the consumer for a grant. */\n const admit = Effect.fnUntraced(function* (websocket: boolean) {\n const request = yield* HttpServerRequest.HttpServerRequest;\n // Browsers always send Origin on WebSocket upgrades, but may omit it on same-origin GETs.\n const origin = request.headers.origin;\n if (origin !== publicOrigin && (websocket || origin !== undefined)) {\n return yield* respond(relayError(403, \"origin_denied\", \"Request origin is not allowed\"));\n }\n if (websocket && request.headers[\"sec-websocket-protocol\"] !== protocol) {\n return yield* respond(\n relayError(400, \"invalid_protocol\", \"Session subprotocol is required\"),\n );\n }\n\n const { sessionId } = yield* HttpRouter.schemaPathParams(pathParams);\n const grant = yield* options.authorize(request, sessionId);\n const capability = Redacted.value(grant.capability);\n if (!capability.startsWith(\"ape_\") && !capability.startsWith(\"apv_\")) {\n return yield* respond(\n relayError(500, \"invalid_relay_grant\", \"Relay requires an editor or viewer capability\"),\n );\n }\n if (grant.signal.aborted) {\n return yield* respond(yield* rejectAccess(request, websocket, 403));\n }\n return { request, grant, capability };\n });\n\n /** Reads upstream session status, turning refusals into the browser's terminal responses. */\n const sessionStatus = Effect.fnUntraced(function* (\n admitted: Effect.Success<ReturnType<typeof admit>>,\n websocket: boolean,\n ) {\n const { request, grant, capability } = admitted;\n const response = yield* upstreamGet(\n upstreamURL(grant.sessionId, \"browser/status\"),\n capability,\n );\n if (isTerminalStatus(response.status)) {\n return yield* respond(yield* rejectAccess(request, websocket, response.status));\n }\n if (response.status !== 200) {\n const status = response.status >= 400 ? response.status : 502;\n return yield* respond(\n relayError(status, \"session_unavailable\", \"Upstream session is unavailable\"),\n );\n }\n const body = yield* response.arrayBuffer.pipe(Effect.timeout(\"15 seconds\"));\n if (grant.signal.aborted) {\n return yield* respond(yield* rejectAccess(request, websocket, 403));\n }\n return body;\n });\n\n const status = Effect.gen(function* () {\n const body = yield* sessionStatus(yield* admit(false), false);\n return HttpServerResponse.uint8Array(new Uint8Array(body), {\n contentType: \"application/json\",\n headers: { \"cache-control\": \"no-store\" },\n });\n });\n\n // Check upstream authorization before upgrading: browser WebSockets cannot expose HTTP status.\n const socket = (route: SocketRoute) =>\n Effect.gen(function* () {\n const admitted = yield* admit(true);\n yield* sessionStatus(admitted, true);\n const url = upstreamURL(admitted.grant.sessionId, route);\n url.protocol = upstream.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n yield* relaySocket(\n admitted.request,\n url,\n admitted.capability,\n admitted.grant.signal,\n ).pipe(Effect.provideService(Socket.WebSocketConstructor, makeSocket));\n return HttpServerResponse.empty();\n });\n\n // Aperture itself restricts recordings to editor capabilities.\n const recording = Effect.gen(function* () {\n const { request, grant, capability } = yield* admit(false);\n const { recordingId } = yield* HttpRouter.schemaPathParams(recordingParams);\n const url = upstreamURL(\n grant.sessionId,\n `recordings/${encodeURIComponent(recordingId)}/content`,\n );\n const response = yield* upstreamGet(url, capability);\n if (response.status === 404) {\n return relayError(404, \"recording_not_found\", \"Recording not found\");\n }\n if (response.status === 409) {\n return relayError(409, \"recording_not_ready\", \"Recording is not ready for download\");\n }\n if (isTerminalStatus(response.status)) {\n return yield* rejectAccess(request, false, response.status);\n }\n if (response.status !== 200) {\n return relayError(502, \"upstream_unavailable\", \"Aperture is unavailable\");\n }\n const headers: Record<string, string> = { \"cache-control\": \"no-store\" };\n const disposition = response.headers[\"content-disposition\"];\n if (disposition !== undefined) headers[\"content-disposition\"] = disposition;\n const contentLength = Number(response.headers[\"content-length\"]);\n return HttpServerResponse.stream(\n response.stream.pipe(Stream.interruptWhen(revoked(grant.signal))),\n {\n contentType: response.headers[\"content-type\"] ?? \"application/octet-stream\",\n contentLength: Number.isSafeInteger(contentLength) ? contentLength : undefined,\n headers,\n },\n );\n });\n\n const routes = [\n { path: \"browser/status\", websocket: false, handler: status },\n { path: \"session\", websocket: true, handler: socket(\"session\") },\n { path: \"webrtc/signal\", websocket: true, handler: socket(\"webrtc/signal\") },\n { path: \"recordings/:recordingId/content\", websocket: false, handler: recording },\n ];\n const prefixed = router.prefixed(prefix);\n for (const { path, websocket, handler } of routes) {\n yield* prefixed.add(\n \"GET\",\n `/sessions/:sessionId/${path}`,\n handler.pipe(\n Effect.catchTags({\n Respond: (early) => Effect.succeed(early.response),\n RelayDenied: (error) =>\n Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) =>\n rejectAccess(request, websocket, error.status),\n ),\n SchemaError: () =>\n Effect.succeed(\n relayError(400, \"invalid_path\", \"Invalid session or recording identifier\"),\n ),\n TimeoutError: () =>\n Effect.succeed(\n relayError(504, \"upstream_timeout\", \"Aperture did not respond in time\"),\n ),\n HttpClientError: () =>\n Effect.succeed(relayError(502, \"upstream_unavailable\", \"Aperture is unavailable\")),\n }),\n Effect.scoped,\n ),\n );\n }\n }),\n );\n}\n\nfunction parseUpstream(apertureBaseUrl: string): URL {\n const upstream = new URL(apertureBaseUrl);\n if (upstream.protocol !== \"http:\" && upstream.protocol !== \"https:\") {\n throw new Error(\"apertureBaseUrl must use HTTP or HTTPS\");\n }\n if (\n upstream.username !== \"\" ||\n upstream.password !== \"\" ||\n upstream.search !== \"\" ||\n upstream.hash !== \"\"\n ) {\n throw new Error(\"apertureBaseUrl must not contain credentials, a query, or a fragment\");\n }\n return upstream;\n}\n\nfunction relayError(status: number, code: string, message: string) {\n return HttpServerResponse.jsonUnsafe(\n { error: { code, message } },\n { status, headers: { \"cache-control\": \"no-store\" } },\n );\n}\n\nconst rejectAccess = Effect.fn(\"sessionRelay.rejectAccess\")(function* (\n request: HttpServerRequest.HttpServerRequest,\n websocket: boolean,\n status: TerminalStatus,\n) {\n const denied = status === 401 || status === 403;\n if (!websocket) {\n return denied\n ? relayError(status, \"access_denied\", \"Session access denied\")\n : relayError(status, \"session_unavailable\", \"Session unavailable\");\n }\n const socket = yield* request.upgrade;\n // Acquiring the reader installs the close listeners before sending a close frame.\n yield* socket.reader;\n const writer = yield* socket.writer;\n yield* writer.write(\n new Socket.CloseEvent(4000 + status, denied ? \"Session access denied\" : \"Session unavailable\"),\n );\n return HttpServerResponse.empty();\n});\n\n/** Upgrades the browser request and pipes it to Aperture until either side closes or access is revoked. */\nconst relaySocket = Effect.fn(\"sessionRelay.relaySocket\")(function* (\n request: HttpServerRequest.HttpServerRequest,\n url: URL,\n capability: string,\n signal: AbortSignal,\n) {\n const downstream = yield* request.upgrade;\n const writer = yield* downstream.writer;\n const upstream = yield* Socket.makeWebSocket(url.toString(), {\n protocols: [protocol, `authorization.bearer.${capability}`],\n openTimeout: \"10 seconds\",\n highWaterMark: 1024 * 1024,\n });\n\n const closeRevoked = Effect.gen(function* () {\n yield* revoked(signal);\n yield* writer.write(new Socket.CloseEvent(4403, \"Session access revoked\"));\n });\n\n yield* bridge(downstream, upstream).pipe(\n Effect.raceFirst(closeRevoked),\n Effect.catchTag(\"SocketError\", (error) => {\n // Only clean closes pass through; anything else tells the browser to retry.\n const { reason } = error;\n const code =\n reason._tag === \"SocketCloseError\" && (reason.code === 1000 || reason.code === 1001)\n ? reason.code\n : 1011;\n return writer\n .write(new Socket.CloseEvent(code, \"Session transport closed\"))\n .pipe(Effect.catchTag(\"SocketError\", () => Effect.void));\n }),\n );\n});\n\nconst bridge = Effect.fn(\"sessionRelay.bridge\")(function* (\n downstream: Socket.Socket,\n upstream: Socket.Socket,\n) {\n const [client, server] = yield* Effect.all([downstream.reader, upstream.reader], {\n concurrency: \"unbounded\",\n });\n const [toClient, toServer] = yield* Effect.all([downstream.writer, upstream.writer]);\n yield* Effect.raceFirst(\n client.pull.pipe(Effect.flatMap(toServer.writeAll), Effect.forever),\n server.pull.pipe(Effect.flatMap(toClient.writeAll), Effect.forever),\n );\n});\n\n/** AbortSignal is the consumer's per-user/per-session revocation boundary. */\nconst revoked = (signal: AbortSignal): Effect.Effect<void> =>\n Effect.callback((resume) => {\n if (signal.aborted) {\n resume(Effect.void);\n return;\n }\n const abort = () => resume(Effect.void);\n signal.addEventListener(\"abort\", abort, { once: true });\n return Effect.sync(() => signal.removeEventListener(\"abort\", abort));\n });\n"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import * as Layer from "effect/Layer";
2
+
3
+ //#region src/node.d.ts
4
+ /** Supply to sessionRelay in a Node HttpRouter application. Does not start a server. */
5
+ declare const nodeRelayLayer: Layer.Layer<import("effect/unstable/http/HttpClient").HttpClient | import("effect/unstable/socket/Socket").WebSocketConstructor, never, never>;
6
+ //#endregion
7
+ export { nodeRelayLayer };
8
+ //# sourceMappingURL=node.d.mts.map
package/dist/node.mjs ADDED
@@ -0,0 +1,10 @@
1
+ import * as Layer from "effect/Layer";
2
+ import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
3
+ import * as NodeSocket from "@effect/platform-node/NodeSocket";
4
+ //#region src/node.ts
5
+ /** Supply to sessionRelay in a Node HttpRouter application. Does not start a server. */
6
+ const nodeRelayLayer = Layer.mergeAll(NodeHttpClient.layerNodeHttp, NodeSocket.layerWebSocketConstructorWS);
7
+ //#endregion
8
+ export { nodeRelayLayer };
9
+
10
+ //# sourceMappingURL=node.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.mjs","names":[],"sources":["../src/node.ts"],"sourcesContent":["import * as Layer from \"effect/Layer\";\nimport * as NodeHttpClient from \"@effect/platform-node/NodeHttpClient\";\nimport * as NodeSocket from \"@effect/platform-node/NodeSocket\";\n\n/** Supply to sessionRelay in a Node HttpRouter application. Does not start a server. */\nexport const nodeRelayLayer = Layer.mergeAll(\n NodeHttpClient.layerNodeHttp,\n NodeSocket.layerWebSocketConstructorWS,\n);\n"],"mappings":";;;;;AAKA,MAAa,iBAAiB,MAAM,SAClC,eAAe,eACf,WAAW,2BACb"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@aperture-browser/session-relay",
3
+ "version": "0.0.0-nightly.88.6472b75ebdd9",
4
+ "description": "Authenticated HTTP and WebSocket relay for Aperture sessions, controlled by the consuming backend.",
5
+ "homepage": "https://github.com/tarik02/aperture#readme",
6
+ "bugs": "https://github.com/tarik02/aperture/issues",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/tarik02/aperture.git",
11
+ "directory": "packages/session-relay"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.mts",
21
+ "default": "./dist/index.mjs"
22
+ },
23
+ "./node": {
24
+ "types": "./dist/node.d.mts",
25
+ "default": "./dist/node.mjs"
26
+ }
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "devDependencies": {
32
+ "@effect/platform-node": "4.0.0-rc.117",
33
+ "effect": "4.0.0-rc.117",
34
+ "vite-plus": "0.2.4"
35
+ },
36
+ "peerDependencies": {
37
+ "@effect/platform-node": "^4.0.0-rc.117",
38
+ "effect": "^4.0.0-rc.117"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "@effect/platform-node": {
42
+ "optional": true
43
+ }
44
+ },
45
+ "scripts": {
46
+ "build": "vp pack",
47
+ "typecheck": "tsgo --noEmit"
48
+ }
49
+ }