Authenticate browser surface relay peers

This commit is contained in:
Timothy J. Aveni
2026-06-21 21:06:03 -07:00
parent 2cdd578848
commit 990c18f00b
10 changed files with 54 additions and 18 deletions
+2 -1
View File
@@ -60,7 +60,8 @@ type ProducerHandlers = {
export declare class BrowserSurfaceRelayProducerClient { export declare class BrowserSurfaceRelayProducerClient {
#private; #private;
readonly relayUrl: string; readonly relayUrl: string;
constructor(relayUrl?: string); readonly producerToken: string;
constructor(relayUrl?: string, producerToken?: string);
connect(): Promise<void>; connect(): Promise<void>;
close(): void; close(): void;
attach(stateContextId: string, surfaceId: string, handlers?: ProducerHandlers): Promise<void>; attach(stateContextId: string, surfaceId: string, handlers?: ProducerHandlers): Promise<void>;
+1 -1
View File
File diff suppressed because one or more lines are too long
+5 -2
View File
@@ -126,11 +126,13 @@ const parseProducerMessage = (value) => {
}; };
export class BrowserSurfaceRelayProducerClient { export class BrowserSurfaceRelayProducerClient {
relayUrl; relayUrl;
producerToken;
#socket = null; #socket = null;
#ready = null; #ready = null;
#handlers = new Map(); #handlers = new Map();
constructor(relayUrl = DEFAULT_SURFACE_RELAY_URL) { constructor(relayUrl = DEFAULT_SURFACE_RELAY_URL, producerToken = "") {
this.relayUrl = relayUrl; this.relayUrl = relayUrl;
this.producerToken = producerToken;
} }
async connect() { async connect() {
if (this.#ready) { if (this.#ready) {
@@ -216,6 +218,7 @@ export class BrowserSurfaceRelayProducerClient {
type: "browser-surface-producer-attach", type: "browser-surface-producer-attach",
stateContextId, stateContextId,
surfaceId, surfaceId,
producerToken: this.producerToken,
}); });
} }
async publishProps(stateContextId, surfaceId, props) { async publishProps(stateContextId, surfaceId, props) {
@@ -301,7 +304,7 @@ export const createBrowserSurfaceController = ({ relaySchema, surfaceId, bundleD
bundleDir, bundleDir,
entryPoint, entryPoint,
})); }));
const producer = new BrowserSurfaceRelayProducerClient(registration.relayUrl); const producer = new BrowserSurfaceRelayProducerClient(registration.relayUrl, registration.producerToken);
const instance = { const instance = {
ctx, ctx,
stateContextId: ctx.stateContext, stateContextId: ctx.stateContext,
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -10,6 +10,7 @@ type BrowserSurfaceSchemaLike = PackageSchema & {
type EmbeddedSurfaceHostProps = { type EmbeddedSurfaceHostProps = {
surface: EmbeddedSurfaceRef; surface: EmbeddedSurfaceRef;
relayUrl?: string; relayUrl?: string;
relaySessionToken?: string;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
fallback?: ReactNode; fallback?: ReactNode;
@@ -19,15 +20,16 @@ export type EmbeddedSurfaceComponentProps<SurfaceId extends string = string> = {
stateContextId: string; stateContextId: string;
surfaceId?: SurfaceId; surfaceId?: SurfaceId;
relayUrl?: string; relayUrl?: string;
relaySessionToken?: string;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
fallback?: ReactNode; fallback?: ReactNode;
onError?: (error: Error) => void; onError?: (error: Error) => void;
}; };
export declare const EmbeddedSurface: ({ surface, relayUrl, className, style, fallback, onError, }: EmbeddedSurfaceHostProps) => import("react/jsx-runtime").JSX.Element; export declare const EmbeddedSurface: ({ surface, relayUrl, relaySessionToken, className, style, fallback, onError, }: EmbeddedSurfaceHostProps) => import("react/jsx-runtime").JSX.Element;
export declare const createEmbeddedSurfaceComponent: <Target extends string | BrowserSurfaceSchemaLike, SurfaceId extends string = string>(target: Target, options?: { export declare const createEmbeddedSurfaceComponent: <Target extends string | BrowserSurfaceSchemaLike, SurfaceId extends string = string>(target: Target, options?: {
surfaceId?: SurfaceId; surfaceId?: SurfaceId;
relayUrl?: string; relayUrl?: string;
}) => ({ stateContextId, surfaceId, relayUrl, className, style, fallback, onError, }: EmbeddedSurfaceComponentProps<SurfaceId>) => import("react/jsx-runtime").JSX.Element; }) => ({ stateContextId, surfaceId, relayUrl, relaySessionToken, className, style, fallback, onError, }: EmbeddedSurfaceComponentProps<SurfaceId>) => import("react/jsx-runtime").JSX.Element;
export {}; export {};
//# sourceMappingURL=browserSurfaceEmbedded.d.ts.map //# sourceMappingURL=browserSurfaceEmbedded.d.ts.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"browserSurfaceEmbedded.d.ts","sourceRoot":"","sources":["../../src/browserSurfaceEmbedded.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AAMnC,KAAK,wBAAwB,GAAG,aAAa,GAAG;IAC9C,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,CAAC;CACH,CAAC;AA6HF,KAAK,wBAAwB,GAAG;IAC9B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,6BAA6B,CAAC,SAAS,SAAS,MAAM,GAAG,MAAM,IAAI;IAC7E,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAmUF,eAAO,MAAM,eAAe,GAAI,6DAO7B,wBAAwB,4CA6L1B,CAAC;AAEF,eAAO,MAAM,8BAA8B,GACzC,MAAM,SAAS,MAAM,GAAG,wBAAwB,EAChD,SAAS,SAAS,MAAM,GAAG,MAAM,EAEjC,QAAQ,MAAM,EACd,UAAU;IACR,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,MAOO,+EAQH,6BAA6B,CAAC,SAAS,CAAC,4CAa9C,CAAC"} {"version":3,"file":"browserSurfaceEmbedded.d.ts","sourceRoot":"","sources":["../../src/browserSurfaceEmbedded.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AAMnC,KAAK,wBAAwB,GAAG,aAAa,GAAG;IAC9C,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,CAAC;CACH,CAAC;AA8HF,KAAK,wBAAwB,GAAG;IAC9B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,6BAA6B,CAAC,SAAS,SAAS,MAAM,GAAG,MAAM,IAAI;IAC7E,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAqUF,eAAO,MAAM,eAAe,GAAI,gFAQ7B,wBAAwB,4CAoM1B,CAAC;AAEF,eAAO,MAAM,8BAA8B,GACzC,MAAM,SAAS,MAAM,GAAG,wBAAwB,EAChD,SAAS,SAAS,MAAM,GAAG,MAAM,EAEjC,QAAQ,MAAM,EACd,UAAU;IACR,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,MAOO,kGASH,6BAA6B,CAAC,SAAS,CAAC,4CAc9C,CAAC"}
+13 -6
View File
@@ -188,7 +188,7 @@ class BrowserSurfaceRelayClient {
} }
return null; return null;
} }
async attach(stateContextId, surfaceId, hostSessionId, handler) { async attach(stateContextId, surfaceId, hostSessionId, sessionToken, handler) {
const key = attachmentKey(stateContextId, surfaceId, hostSessionId); const key = attachmentKey(stateContextId, surfaceId, hostSessionId);
const existing = this.#findAttachment(stateContextId, surfaceId); const existing = this.#findAttachment(stateContextId, surfaceId);
if (existing) { if (existing) {
@@ -221,6 +221,7 @@ class BrowserSurfaceRelayClient {
stateContextId, stateContextId,
surfaceId, surfaceId,
hostSessionId, hostSessionId,
sessionToken,
}); });
return async () => { return async () => {
this.#attachments.delete(key); this.#attachments.delete(key);
@@ -282,7 +283,7 @@ const releaseSharedBrowserSurfaceRelayClient = (relayUrl) => {
entry.client.close(); entry.client.close();
sharedBrowserSurfaceRelayClients.delete(relayUrl); sharedBrowserSurfaceRelayClients.delete(relayUrl);
}; };
export const EmbeddedSurface = ({ surface, relayUrl, className, style, fallback = null, onError, }) => { export const EmbeddedSurface = ({ surface, relayUrl, relaySessionToken = "", className, style, fallback = null, onError, }) => {
const hostRef = useRef(null); const hostRef = useRef(null);
const unsubscribeRef = useRef(null); const unsubscribeRef = useRef(null);
const mountedHandleRef = useRef(null); const mountedHandleRef = useRef(null);
@@ -396,7 +397,7 @@ export const EmbeddedSurface = ({ surface, relayUrl, className, style, fallback
const hostSessionId = createBrowserSurfaceHostSessionId(); const hostSessionId = createBrowserSurfaceHostSessionId();
hostSessionIdRef.current = hostSessionId; hostSessionIdRef.current = hostSessionId;
void relayClient void relayClient
.attach(surface.stateContextId, surface.surfaceId, hostSessionId, async (message) => { .attach(surface.stateContextId, surface.surfaceId, hostSessionId, relaySessionToken, async (message) => {
try { try {
await handleRelayMessage(message); await handleRelayMessage(message);
} }
@@ -423,7 +424,13 @@ export const EmbeddedSurface = ({ surface, relayUrl, className, style, fallback
hostSessionIdRef.current = ""; hostSessionIdRef.current = "";
clearMountedSurface(); clearMountedSurface();
}; };
}, [relayClient, surface.packageName, surface.stateContextId, surface.surfaceId]); }, [
relayClient,
relaySessionToken,
surface.packageName,
surface.stateContextId,
surface.surfaceId,
]);
useEffect(() => { useEffect(() => {
return () => { return () => {
releaseSharedBrowserSurfaceRelayClient(resolvedRelayUrl); releaseSharedBrowserSurfaceRelayClient(resolvedRelayUrl);
@@ -436,9 +443,9 @@ export const EmbeddedSurface = ({ surface, relayUrl, className, style, fallback
}; };
export const createEmbeddedSurfaceComponent = (target, options) => { export const createEmbeddedSurfaceComponent = (target, options) => {
const defaultSurfaceId = getEmbeddedSurfaceId(target, options?.surfaceId); const defaultSurfaceId = getEmbeddedSurfaceId(target, options?.surfaceId);
return ({ stateContextId, surfaceId, relayUrl, className, style, fallback, onError, }) => (_jsx(EmbeddedSurface, { surface: createEmbeddedSurfaceRef(target, { return ({ stateContextId, surfaceId, relayUrl, relaySessionToken, className, style, fallback, onError, }) => (_jsx(EmbeddedSurface, { surface: createEmbeddedSurfaceRef(target, {
stateContextId, stateContextId,
surfaceId: surfaceId ?? defaultSurfaceId, surfaceId: surfaceId ?? defaultSurfaceId,
}), relayUrl: relayUrl ?? options?.relayUrl, className: className, style: style, fallback: fallback, onError: onError })); }), relayUrl: relayUrl ?? options?.relayUrl, relaySessionToken: relaySessionToken, className: className, style: style, fallback: fallback, onError: onError }));
}; };
//# sourceMappingURL=browserSurfaceEmbedded.js.map //# sourceMappingURL=browserSurfaceEmbedded.js.map
File diff suppressed because one or more lines are too long
+10 -2
View File
@@ -278,6 +278,7 @@ type ProducerClientMessage =
type: "browser-surface-producer-attach"; type: "browser-surface-producer-attach";
stateContextId: string; stateContextId: string;
surfaceId: string; surfaceId: string;
producerToken: string;
} }
| { | {
type: "browser-surface-producer-detach"; type: "browser-surface-producer-detach";
@@ -319,13 +320,15 @@ const parseProducerMessage = (value: unknown): ProducerMessage | null => {
export class BrowserSurfaceRelayProducerClient { export class BrowserSurfaceRelayProducerClient {
readonly relayUrl: string; readonly relayUrl: string;
readonly producerToken: string;
#socket: WebSocket | null = null; #socket: WebSocket | null = null;
#ready: Promise<void> | null = null; #ready: Promise<void> | null = null;
#handlers = new Map<string, ProducerHandlers>(); #handlers = new Map<string, ProducerHandlers>();
constructor(relayUrl = DEFAULT_SURFACE_RELAY_URL) { constructor(relayUrl = DEFAULT_SURFACE_RELAY_URL, producerToken = "") {
this.relayUrl = relayUrl; this.relayUrl = relayUrl;
this.producerToken = producerToken;
} }
async connect() { async connect() {
@@ -426,6 +429,7 @@ export class BrowserSurfaceRelayProducerClient {
type: "browser-surface-producer-attach", type: "browser-surface-producer-attach",
stateContextId, stateContextId,
surfaceId, surfaceId,
producerToken: this.producerToken,
}); });
} }
@@ -482,6 +486,7 @@ export type BrowserSurfaceInstance<State> = Omit<
type RelayRegistration = { type RelayRegistration = {
relayUrl: string; relayUrl: string;
producerToken: string;
}; };
export type BrowserSurfaceHostError = { export type BrowserSurfaceHostError = {
@@ -645,7 +650,10 @@ export const createBrowserSurfaceController = <State, Props, Action>({
bundleDir, bundleDir,
entryPoint, entryPoint,
})) as RelayRegistration; })) as RelayRegistration;
const producer = new BrowserSurfaceRelayProducerClient(registration.relayUrl); const producer = new BrowserSurfaceRelayProducerClient(
registration.relayUrl,
registration.producerToken,
);
const instance: BrowserSurfaceInstanceInternal<State> = { const instance: BrowserSurfaceInstanceInternal<State> = {
ctx, ctx,
+16 -1
View File
@@ -113,6 +113,7 @@ type BrowserSurfaceRelayClientMessage =
stateContextId: string; stateContextId: string;
surfaceId: string; surfaceId: string;
hostSessionId: string; hostSessionId: string;
sessionToken: string;
} }
| { | {
type: "browser-surface-host-detach"; type: "browser-surface-host-detach";
@@ -151,6 +152,7 @@ type SharedBrowserSurfaceRelayClientEntry = {
type EmbeddedSurfaceHostProps = { type EmbeddedSurfaceHostProps = {
surface: EmbeddedSurfaceRef; surface: EmbeddedSurfaceRef;
relayUrl?: string; relayUrl?: string;
relaySessionToken?: string;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
fallback?: ReactNode; fallback?: ReactNode;
@@ -161,6 +163,7 @@ export type EmbeddedSurfaceComponentProps<SurfaceId extends string = string> = {
stateContextId: string; stateContextId: string;
surfaceId?: SurfaceId; surfaceId?: SurfaceId;
relayUrl?: string; relayUrl?: string;
relaySessionToken?: string;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
fallback?: ReactNode; fallback?: ReactNode;
@@ -375,6 +378,7 @@ class BrowserSurfaceRelayClient {
stateContextId: string, stateContextId: string,
surfaceId: string, surfaceId: string,
hostSessionId: string, hostSessionId: string,
sessionToken: string,
handler: (message: BrowserSurfaceRelayServerMessage) => void | Promise<void>, handler: (message: BrowserSurfaceRelayServerMessage) => void | Promise<void>,
) { ) {
const key = attachmentKey(stateContextId, surfaceId, hostSessionId); const key = attachmentKey(stateContextId, surfaceId, hostSessionId);
@@ -409,6 +413,7 @@ class BrowserSurfaceRelayClient {
stateContextId, stateContextId,
surfaceId, surfaceId,
hostSessionId, hostSessionId,
sessionToken,
}); });
return async () => { return async () => {
this.#attachments.delete(key); this.#attachments.delete(key);
@@ -491,6 +496,7 @@ const releaseSharedBrowserSurfaceRelayClient = (relayUrl: string) => {
export const EmbeddedSurface = ({ export const EmbeddedSurface = ({
surface, surface,
relayUrl, relayUrl,
relaySessionToken = "",
className, className,
style, style,
fallback = null, fallback = null,
@@ -643,6 +649,7 @@ export const EmbeddedSurface = ({
surface.stateContextId, surface.stateContextId,
surface.surfaceId, surface.surfaceId,
hostSessionId, hostSessionId,
relaySessionToken,
async (message) => { async (message) => {
try { try {
await handleRelayMessage(message); await handleRelayMessage(message);
@@ -671,7 +678,13 @@ export const EmbeddedSurface = ({
hostSessionIdRef.current = ""; hostSessionIdRef.current = "";
clearMountedSurface(); clearMountedSurface();
}; };
}, [relayClient, surface.packageName, surface.stateContextId, surface.surfaceId]); }, [
relayClient,
relaySessionToken,
surface.packageName,
surface.stateContextId,
surface.surfaceId,
]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -705,6 +718,7 @@ export const createEmbeddedSurfaceComponent = <
stateContextId, stateContextId,
surfaceId, surfaceId,
relayUrl, relayUrl,
relaySessionToken,
className, className,
style, style,
fallback, fallback,
@@ -716,6 +730,7 @@ export const createEmbeddedSurfaceComponent = <
surfaceId: surfaceId ?? defaultSurfaceId, surfaceId: surfaceId ?? defaultSurfaceId,
})} })}
relayUrl={relayUrl ?? options?.relayUrl} relayUrl={relayUrl ?? options?.relayUrl}
relaySessionToken={relaySessionToken}
className={className} className={className}
style={style} style={style}
fallback={fallback} fallback={fallback}