authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 20:20:12-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 20:20:12-07:00
log2a29de7502cdd02686bda7d12fb2671060fd1979
tree407720a2cc764f8010f61b9dffdb8ead453e847c
parent8c1605b38327176bae94c33bbc6de86e8b09fe02
signature Signed by PGP key B5690EEEBB952194

[codex] Structure primary auth validation failures (#3471)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 146 insertions(+), 81 deletions(-)

apps/web/src/authBootstrap.test.ts+76-26
...@@ -71,6 +71,18 @@ function installTestBrowser(url: string) {...@@ -71,6 +71,18 @@ function installTestBrowser(url: string) {
71 return testWindow;71 return testWindow;
72}72}
7373
74function installDesktopBootstrap() {
75 const testWindow = installTestBrowser("http://localhost/");
76 testWindow.desktopBridge = {
77 getLocalEnvironmentBootstrap: () => ({
78 label: "Local environment",
79 httpBaseUrl: "http://localhost:3773",
80 wsBaseUrl: "ws://localhost:3773",
81 bootstrapToken: "desktop-bootstrap-token",
82 }),
83 } as DesktopBridge;
84}
85
74function sequence<A>(...values: ReadonlyArray<A>) {86function sequence<A>(...values: ReadonlyArray<A>) {
75 let index = 0;87 let index = 0;
76 return () => values[Math.min(index++, values.length - 1)]!;88 return () => values[Math.min(index++, values.length - 1)]!;
...@@ -131,15 +143,7 @@ describe("resolveInitialServerAuthGateState", () => {...@@ -131,15 +143,7 @@ describe("resolveInitialServerAuthGateState", () => {
131 browserSession: () => Effect.succeed(browserSession(["orchestration:read", "access:write"])),143 browserSession: () => Effect.succeed(browserSession(["orchestration:read", "access:write"])),
132 });144 });
133145
134 const testWindow = installTestBrowser("http://localhost/");146 installDesktopBootstrap();
135 testWindow.desktopBridge = {
136 getLocalEnvironmentBootstrap: () => ({
137 label: "Local environment",
138 httpBaseUrl: "http://localhost:3773",
139 wsBaseUrl: "ws://localhost:3773",
140 bootstrapToken: "desktop-bootstrap-token",
141 }),
142 } as DesktopBridge;
143147
144 const { resolveInitialServerAuthGateState } = await import("./environments/primary");148 const { resolveInitialServerAuthGateState } = await import("./environments/primary");
145149
...@@ -289,6 +293,23 @@ describe("resolveInitialServerAuthGateState", () => {...@@ -289,6 +293,23 @@ describe("resolveInitialServerAuthGateState", () => {
289 expect(testApi.calls.session).toBe(2);293 expect(testApi.calls.session).toBe(2);
290 });294 });
291295
296 it("rejects a blank pairing token with a structured validation error", async () => {
297 const { PrimaryEnvironmentPairingCredentialRequiredError, submitServerAuthCredential } =
298 await import("./environments/primary/auth");
299
300 const error = await submitServerAuthCredential(" ").then(
301 () => null,
302 (failure: unknown) => failure,
303 );
304
305 expect(error).toBeInstanceOf(PrimaryEnvironmentPairingCredentialRequiredError);
306 expect(error).toMatchObject({
307 _tag: "PrimaryEnvironmentPairingCredentialRequiredError",
308 providedLength: 3,
309 message: "Enter a pairing token to continue.",
310 });
311 });
312
292 it("surfaces a friendly error message when an invalid pairing token is submitted", async () => {313 it("surfaces a friendly error message when an invalid pairing token is submitted", async () => {
293 const cause = new EnvironmentAuthInvalidError({314 const cause = new EnvironmentAuthInvalidError({
294 code: "auth_invalid",315 code: "auth_invalid",
...@@ -299,7 +320,7 @@ describe("resolveInitialServerAuthGateState", () => {...@@ -299,7 +320,7 @@ describe("resolveInitialServerAuthGateState", () => {
299 browserSession: () => Effect.fail(cause),320 browserSession: () => Effect.fail(cause),
300 });321 });
301322
302 const { isPrimaryEnvironmentRequestError, submitServerAuthCredential } =323 const { isPrimaryEnvironmentPairingCredentialRejectedError, submitServerAuthCredential } =
303 await import("./environments/primary");324 await import("./environments/primary");
304325
305 const error = await submitServerAuthCredential("bad-token").then(326 const error = await submitServerAuthCredential("bad-token").then(
...@@ -307,14 +328,13 @@ describe("resolveInitialServerAuthGateState", () => {...@@ -307,14 +328,13 @@ describe("resolveInitialServerAuthGateState", () => {
307 (failure: unknown) => failure,328 (failure: unknown) => failure,
308 );329 );
309 expect(error).toMatchObject({330 expect(error).toMatchObject({
310 _tag: "PrimaryEnvironmentRequestError",331 _tag: "PrimaryEnvironmentPairingCredentialRejectedError",
311 operation: "exchange-bootstrap-credential",332 providedLength: 9,
312 status: 401,333 message: "Invalid pairing token. Check the token and try again.",
313 detail: "Invalid pairing token. Check the token and try again.",
314 });334 });
315 expect(isPrimaryEnvironmentRequestError(error)).toBe(true);335 expect(isPrimaryEnvironmentPairingCredentialRejectedError(error)).toBe(true);
316 if (!isPrimaryEnvironmentRequestError(error)) {336 if (!isPrimaryEnvironmentPairingCredentialRejectedError(error)) {
317 throw new Error("Expected a structured primary environment request error.");337 throw new Error("Expected a structured rejected pairing credential error.");
318 }338 }
319 expect(error.cause).toMatchObject({339 expect(error.cause).toMatchObject({
320 _tag: "EnvironmentAuthInvalidError",340 _tag: "EnvironmentAuthInvalidError",
...@@ -325,6 +345,22 @@ describe("resolveInitialServerAuthGateState", () => {...@@ -325,6 +345,22 @@ describe("resolveInitialServerAuthGateState", () => {
325 expect(testApi.calls.browserSession).toEqual([{ credential: "bad-token" }]);345 expect(testApi.calls.browserSession).toEqual([{ credential: "bad-token" }]);
326 });346 });
327347
348 it("derives primary request messages from structural request context", async () => {
349 const cause = new Error("private transport detail");
350 const { PrimaryEnvironmentRequestError } = await import("./environments/primary");
351 const error = PrimaryEnvironmentRequestError.fromCause({
352 operation: "list-pairing-links",
353 cause,
354 });
355
356 expect(error.status).toBe(500);
357 expect(error.cause).toBe(cause);
358 expect(error.message).toBe(
359 "Primary environment request failed during list-pairing-links (HTTP 500).",
360 );
361 expect(error.message).not.toContain(cause.message);
362 });
363
328 it("waits for the authenticated session to become observable after silent desktop bootstrap", async () => {364 it("waits for the authenticated session to become observable after silent desktop bootstrap", async () => {
329 vi.useFakeTimers();365 vi.useFakeTimers();
330 const nextSession = sequence(366 const nextSession = sequence(
...@@ -337,15 +373,7 @@ describe("resolveInitialServerAuthGateState", () => {...@@ -337,15 +373,7 @@ describe("resolveInitialServerAuthGateState", () => {
337 browserSession: () => Effect.succeed(browserSession(["orchestration:read", "access:write"])),373 browserSession: () => Effect.succeed(browserSession(["orchestration:read", "access:write"])),
338 });374 });
339375
340 const testWindow = installTestBrowser("http://localhost/");376 installDesktopBootstrap();
341 testWindow.desktopBridge = {
342 getLocalEnvironmentBootstrap: () => ({
343 label: "Local environment",
344 httpBaseUrl: "http://localhost:3773",
345 wsBaseUrl: "ws://localhost:3773",
346 bootstrapToken: "desktop-bootstrap-token",
347 }),
348 } as DesktopBridge;
349377
350 const { resolveInitialServerAuthGateState } = await import("./environments/primary");378 const { resolveInitialServerAuthGateState } = await import("./environments/primary");
351379
...@@ -356,6 +384,28 @@ describe("resolveInitialServerAuthGateState", () => {...@@ -356,6 +384,28 @@ describe("resolveInitialServerAuthGateState", () => {
356 expect(testApi.calls.session).toBe(3);384 expect(testApi.calls.session).toBe(3);
357 });385 });
358386
387 it("preserves the timeout message when a bootstrapped session never becomes observable", async () => {
388 vi.useFakeTimers();
389 const testApi = await installAuthApi({
390 session: () => unauthenticatedSession(DESKTOP_AUTH),
391 browserSession: () => Effect.succeed(browserSession(["orchestration:read", "access:write"])),
392 });
393
394 installDesktopBootstrap();
395
396 const { resolveInitialServerAuthGateState } = await import("./environments/primary");
397
398 const gateStatePromise = resolveInitialServerAuthGateState();
399 await vi.advanceTimersByTimeAsync(2_000);
400
401 await expect(gateStatePromise).resolves.toEqual({
402 status: "requires-auth",
403 auth: DESKTOP_AUTH,
404 errorMessage: "Timed out waiting for authenticated session after bootstrap.",
405 });
406 expect(testApi.calls.browserSession).toEqual([{ credential: "desktop-bootstrap-token" }]);
407 });
408
359 it("memoizes the authenticated gate state after the first successful read", async () => {409 it("memoizes the authenticated gate state after the first successful read", async () => {
360 const testApi = await installAuthApi({410 const testApi = await installAuthApi({
361 session: sequence(authenticatedSession(LOOPBACK_AUTH), unauthenticatedSession(LOOPBACK_AUTH)),411 session: sequence(authenticatedSession(LOOPBACK_AUTH), unauthenticatedSession(LOOPBACK_AUTH)),
apps/web/src/environments/primary/auth.ts+68-54
...@@ -40,7 +40,6 @@ export class PrimaryEnvironmentRequestError extends Schema.TaggedErrorClass<Prim...@@ -40,7 +40,6 @@ export class PrimaryEnvironmentRequestError extends Schema.TaggedErrorClass<Prim
40 {40 {
41 operation: PrimaryEnvironmentRequestOperation,41 operation: PrimaryEnvironmentRequestOperation,
42 status: Schema.Number,42 status: Schema.Number,
43 detail: Schema.String,
44 pairingLinkId: Schema.optional(Schema.String),43 pairingLinkId: Schema.optional(Schema.String),
45 sessionId: Schema.optional(Schema.String),44 sessionId: Schema.optional(Schema.String),
46 cause: Schema.Defect(),45 cause: Schema.Defect(),
...@@ -49,17 +48,13 @@ export class PrimaryEnvironmentRequestError extends Schema.TaggedErrorClass<Prim...@@ -49,17 +48,13 @@ export class PrimaryEnvironmentRequestError extends Schema.TaggedErrorClass<Prim
49 static fromCause(input: {48 static fromCause(input: {
50 readonly operation: PrimaryEnvironmentRequestOperation;49 readonly operation: PrimaryEnvironmentRequestOperation;
51 readonly cause: unknown;50 readonly cause: unknown;
52 readonly fallbackMessage: (status: number) => string;
53 readonly formatDetail?: (detail: string, status: number) => string;
54 readonly pairingLinkId?: string;51 readonly pairingLinkId?: string;
55 readonly sessionId?: string;52 readonly sessionId?: string;
56 }): PrimaryEnvironmentRequestError {53 }): PrimaryEnvironmentRequestError {
57 const status = readHttpApiStatus(input.cause) ?? 500;54 const status = readHttpApiStatus(input.cause) ?? 500;
58 const rawDetail = readHttpApiErrorMessage(input.cause, input.fallbackMessage(status));
59 return new PrimaryEnvironmentRequestError({55 return new PrimaryEnvironmentRequestError({
60 operation: input.operation,56 operation: input.operation,
61 status,57 status,
62 detail: input.formatDetail?.(rawDetail, status) ?? rawDetail,
63 ...(input.pairingLinkId !== undefined ? { pairingLinkId: input.pairingLinkId } : {}),58 ...(input.pairingLinkId !== undefined ? { pairingLinkId: input.pairingLinkId } : {}),
64 ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),59 ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
65 cause: input.cause,60 cause: input.cause,
...@@ -67,11 +62,59 @@ export class PrimaryEnvironmentRequestError extends Schema.TaggedErrorClass<Prim...@@ -67,11 +62,59 @@ export class PrimaryEnvironmentRequestError extends Schema.TaggedErrorClass<Prim
67 }62 }
6863
69 override get message(): string {64 override get message(): string {
70 return this.detail;65 return `Primary environment request failed during ${this.operation} (HTTP ${this.status}).`;
71 }66 }
72}67}
7368
74export const isPrimaryEnvironmentRequestError = Schema.is(PrimaryEnvironmentRequestError);69export const isPrimaryEnvironmentRequestError = Schema.is(PrimaryEnvironmentRequestError);
70
71export class PrimaryEnvironmentPairingCredentialRejectedError extends Schema.TaggedErrorClass<PrimaryEnvironmentPairingCredentialRejectedError>()(
72 "PrimaryEnvironmentPairingCredentialRejectedError",
73 {
74 providedLength: Schema.Number,
75 cause: Schema.Defect(),
76 },
77) {
78 override get message(): string {
79 return "Invalid pairing token. Check the token and try again.";
80 }
81}
82
83export const isPrimaryEnvironmentPairingCredentialRejectedError = Schema.is(
84 PrimaryEnvironmentPairingCredentialRejectedError,
85);
86
87export class PrimaryEnvironmentAuthSessionTimeoutError extends Schema.TaggedErrorClass<PrimaryEnvironmentAuthSessionTimeoutError>()(
88 "PrimaryEnvironmentAuthSessionTimeoutError",
89 {
90 timeoutMs: Schema.Number,
91 elapsedMs: Schema.Number,
92 },
93) {
94 override get message(): string {
95 return "Timed out waiting for authenticated session after bootstrap.";
96 }
97}
98
99export const isPrimaryEnvironmentAuthSessionTimeoutError = Schema.is(
100 PrimaryEnvironmentAuthSessionTimeoutError,
101);
102
103export class PrimaryEnvironmentPairingCredentialRequiredError extends Schema.TaggedErrorClass<PrimaryEnvironmentPairingCredentialRequiredError>()(
104 "PrimaryEnvironmentPairingCredentialRequiredError",
105 {
106 providedLength: Schema.Number,
107 },
108) {
109 override get message(): string {
110 return "Enter a pairing token to continue.";
111 }
112}
113
114export const isPrimaryEnvironmentPairingCredentialRequiredError = Schema.is(
115 PrimaryEnvironmentPairingCredentialRequiredError,
116);
117
75const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError);118const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError);
76119
77export interface ServerPairingLinkRecord {120export interface ServerPairingLinkRecord {
...@@ -151,7 +194,6 @@ export async function fetchSessionState(): Promise<AuthSessionState> {...@@ -151,7 +194,6 @@ export async function fetchSessionState(): Promise<AuthSessionState> {
151 throw PrimaryEnvironmentRequestError.fromCause({194 throw PrimaryEnvironmentRequestError.fromCause({
152 operation: "fetch-session-state",195 operation: "fetch-session-state",
153 cause: error,196 cause: error,
154 fallbackMessage: (status) => `Failed to load server auth session state (${status}).`,
155 });197 });
156 }198 }
157 });199 });
...@@ -180,42 +222,6 @@ function readEnvironmentHttpErrorStatus(error: EnvironmentHttpCommonErrorType):...@@ -180,42 +222,6 @@ function readEnvironmentHttpErrorStatus(error: EnvironmentHttpCommonErrorType):
180 }222 }
181}223}
182224
183function readHttpApiErrorMessage(error: unknown, fallbackMessage: string): string {
184 if (!isEnvironmentHttpCommonError(error)) {
185 return fallbackMessage;
186 }
187 switch (error._tag) {
188 case "EnvironmentAuthInvalidError":
189 return error.reason === "missing_credential"
190 ? "Authentication required."
191 : "Invalid bootstrap credential.";
192 case "EnvironmentRequestInvalidError":
193 return error.reason === "invalid_scope"
194 ? "Requested token scope is invalid."
195 : "Requested scope exceeds the bootstrap credential grant.";
196 case "EnvironmentScopeRequiredError":
197 return `The authenticated token is missing required scope: ${error.requiredScope}.`;
198 case "EnvironmentOperationForbiddenError":
199 return "This operation is not allowed for the current session.";
200 case "EnvironmentInternalError":
201 return fallbackMessage;
202 }
203}
204
205const INVALID_BOOTSTRAP_CREDENTIAL_MESSAGES = new Set([
206 "Invalid bootstrap credential.",
207 "Unknown bootstrap credential.",
208]);
209
210function toFriendlyBootstrapErrorMessage(status: number, message: string): string {
211 const trimmedMessage = message.trim();
212 if (status === 401 && INVALID_BOOTSTRAP_CREDENTIAL_MESSAGES.has(trimmedMessage)) {
213 return "Invalid pairing token. Check the token and try again.";
214 }
215
216 return trimmedMessage;
217}
218
219async function exchangeBootstrapCredential(credential: string): Promise<AuthBrowserSessionResult> {225async function exchangeBootstrapCredential(credential: string): Promise<AuthBrowserSessionResult> {
220 return retryTransientBootstrap(async () => {226 return retryTransientBootstrap(async () => {
221 try {227 try {
...@@ -225,11 +231,19 @@ async function exchangeBootstrapCredential(credential: string): Promise<AuthBrow...@@ -225,11 +231,19 @@ async function exchangeBootstrapCredential(credential: string): Promise<AuthBrow
225 ),231 ),
226 );232 );
227 } catch (error) {233 } catch (error) {
234 if (
235 isEnvironmentHttpCommonError(error) &&
236 error._tag === "EnvironmentAuthInvalidError" &&
237 error.reason === "invalid_credential"
238 ) {
239 throw new PrimaryEnvironmentPairingCredentialRejectedError({
240 providedLength: credential.length,
241 cause: error,
242 });
243 }
228 throw PrimaryEnvironmentRequestError.fromCause({244 throw PrimaryEnvironmentRequestError.fromCause({
229 operation: "exchange-bootstrap-credential",245 operation: "exchange-bootstrap-credential",
230 cause: error,246 cause: error,
231 fallbackMessage: (status) => `Failed to bootstrap auth session (${status}).`,
232 formatDetail: (detail, status) => toFriendlyBootstrapErrorMessage(status, detail),
233 });247 });
234 }248 }
235 });249 });
...@@ -244,8 +258,12 @@ async function waitForAuthenticatedSessionAfterBootstrap(): Promise<AuthSessionS...@@ -244,8 +258,12 @@ async function waitForAuthenticatedSessionAfterBootstrap(): Promise<AuthSessionS
244 return session;258 return session;
245 }259 }
246260
247 if (Date.now() - startedAt >= AUTH_SESSION_ESTABLISH_TIMEOUT_MS) {261 const elapsedMs = Date.now() - startedAt;
248 throw new Error("Timed out waiting for authenticated session after bootstrap.");262 if (elapsedMs >= AUTH_SESSION_ESTABLISH_TIMEOUT_MS) {
263 throw new PrimaryEnvironmentAuthSessionTimeoutError({
264 timeoutMs: AUTH_SESSION_ESTABLISH_TIMEOUT_MS,
265 elapsedMs,
266 });
249 }267 }
250268
251 await waitForBootstrapRetry(AUTH_SESSION_ESTABLISH_STEP_MS);269 await waitForBootstrapRetry(AUTH_SESSION_ESTABLISH_STEP_MS);
...@@ -323,7 +341,9 @@ async function bootstrapServerAuth(): Promise<ServerAuthGateState> {...@@ -323,7 +341,9 @@ async function bootstrapServerAuth(): Promise<ServerAuthGateState> {
323export async function submitServerAuthCredential(credential: string): Promise<void> {341export async function submitServerAuthCredential(credential: string): Promise<void> {
324 const trimmedCredential = credential.trim();342 const trimmedCredential = credential.trim();
325 if (!trimmedCredential) {343 if (!trimmedCredential) {
326 throw new Error("Enter a pairing token to continue.");344 throw new PrimaryEnvironmentPairingCredentialRequiredError({
345 providedLength: credential.length,
346 });
327 }347 }
328348
329 resolvedAuthenticatedGateState = null;349 resolvedAuthenticatedGateState = null;
...@@ -355,7 +375,6 @@ export async function createServerPairingCredential(input?: {...@@ -355,7 +375,6 @@ export async function createServerPairingCredential(input?: {
355 throw PrimaryEnvironmentRequestError.fromCause({375 throw PrimaryEnvironmentRequestError.fromCause({
356 operation: "create-pairing-credential",376 operation: "create-pairing-credential",
357 cause: error,377 cause: error,
358 fallbackMessage: (status) => `Failed to create pairing credential (${status}).`,
359 });378 });
360 }379 }
361}380}
...@@ -396,7 +415,6 @@ export async function listServerPairingLinks(): Promise<ReadonlyArray<ServerPair...@@ -396,7 +415,6 @@ export async function listServerPairingLinks(): Promise<ReadonlyArray<ServerPair
396 throw PrimaryEnvironmentRequestError.fromCause({415 throw PrimaryEnvironmentRequestError.fromCause({
397 operation: "list-pairing-links",416 operation: "list-pairing-links",
398 cause: error,417 cause: error,
399 fallbackMessage: (status) => `Failed to load pairing links (${status}).`,
400 });418 });
401 }419 }
402}420}
...@@ -413,7 +431,6 @@ export async function revokeServerPairingLink(id: string): Promise<void> {...@@ -413,7 +431,6 @@ export async function revokeServerPairingLink(id: string): Promise<void> {
413 operation: "revoke-pairing-link",431 operation: "revoke-pairing-link",
414 pairingLinkId: id,432 pairingLinkId: id,
415 cause: error,433 cause: error,
416 fallbackMessage: (status) => `Failed to revoke pairing link (${status}).`,
417 });434 });
418 }435 }
419}436}
...@@ -446,7 +463,6 @@ export async function listServerClientSessions(): Promise<...@@ -446,7 +463,6 @@ export async function listServerClientSessions(): Promise<
446 throw PrimaryEnvironmentRequestError.fromCause({463 throw PrimaryEnvironmentRequestError.fromCause({
447 operation: "list-client-sessions",464 operation: "list-client-sessions",
448 cause: error,465 cause: error,
449 fallbackMessage: (status) => `Failed to load paired clients (${status}).`,
450 });466 });
451 }467 }
452}468}
...@@ -465,7 +481,6 @@ export async function revokeServerClientSession(sessionId: AuthSessionId): Promi...@@ -465,7 +481,6 @@ export async function revokeServerClientSession(sessionId: AuthSessionId): Promi
465 operation: "revoke-client-session",481 operation: "revoke-client-session",
466 sessionId,482 sessionId,
467 cause: error,483 cause: error,
468 fallbackMessage: (status) => `Failed to revoke client session (${status}).`,
469 });484 });
470 }485 }
471}486}
...@@ -482,7 +497,6 @@ export async function revokeOtherServerClientSessions(): Promise<number> {...@@ -482,7 +497,6 @@ export async function revokeOtherServerClientSessions(): Promise<number> {
482 throw PrimaryEnvironmentRequestError.fromCause({497 throw PrimaryEnvironmentRequestError.fromCause({
483 operation: "revoke-other-client-sessions",498 operation: "revoke-other-client-sessions",
484 cause: error,499 cause: error,
485 fallbackMessage: (status) => `Failed to revoke other client sessions (${status}).`,
486 });500 });
487 }501 }
488}502}
apps/web/src/environments/primary/context.ts-1
...@@ -46,7 +46,6 @@ async function fetchPrimaryEnvironmentDescriptor(): Promise<ExecutionEnvironment...@@ -46,7 +46,6 @@ async function fetchPrimaryEnvironmentDescriptor(): Promise<ExecutionEnvironment
46 throw PrimaryEnvironmentRequestError.fromCause({46 throw PrimaryEnvironmentRequestError.fromCause({
47 operation: "fetch-environment-descriptor",47 operation: "fetch-environment-descriptor",
48 cause: error,48 cause: error,
49 fallbackMessage: (status) => `Failed to load server environment descriptor (${status}).`,
50 });49 });
51 }50 }
5251
apps/web/src/environments/primary/index.ts+2
...@@ -16,10 +16,12 @@ export {...@@ -16,10 +16,12 @@ export {
16export {16export {
17 createServerPairingCredential,17 createServerPairingCredential,
18 fetchSessionState,18 fetchSessionState,
19 isPrimaryEnvironmentPairingCredentialRejectedError,
19 isPrimaryEnvironmentRequestError,20 isPrimaryEnvironmentRequestError,
20 listServerClientSessions,21 listServerClientSessions,
21 listServerPairingLinks,22 listServerPairingLinks,
22 peekPairingTokenFromUrl,23 peekPairingTokenFromUrl,
24 PrimaryEnvironmentPairingCredentialRejectedError,
23 PrimaryEnvironmentRequestError,25 PrimaryEnvironmentRequestError,
24 resolveInitialServerAuthGateState,26 resolveInitialServerAuthGateState,
25 revokeOtherServerClientSessions,27 revokeOtherServerClientSessions,