1import { describe, expect, it } from "@effect/vitest";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Fiber from "effect/Fiber";
5import * as Schema from "effect/Schema";
6import * as TestClock from "effect/testing/TestClock";
7
8import { EnvironmentAuthInvalidError } from "@t3tools/contracts";
9import {
10 appendClientConnectionParams,
11 bootstrapRemoteBearerSession,
12 exchangeRemoteDpopAccessToken,
13 fetchRemoteSessionState,
14 issueRemoteDpopWebSocketTicket,
15 issueRemoteWebSocketTicket,
16 RemoteEnvironmentAuthInvalidJsonError,
17 RemoteEnvironmentAuthTimeoutError,
18 resolveRemoteWebSocketConnectionUrl,
19 resolveRemoteDpopWebSocketConnectionUrl,
20} from "./remote.ts";
21import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts";
22import * as RpcHttp from "../rpc/http.ts";
23
24const isEnvironmentAuthInvalidError = Schema.is(EnvironmentAuthInvalidError);
25
26type FetchCall = readonly [input: RequestInfo | URL, init: RequestInit];
27
28const recordedFetch = (...responses: ReadonlyArray<Response>) => {
29 const calls: Array<FetchCall> = [];
30 let responseIndex = 0;
31 const fetchFn = ((input, init) => {
32 calls.push([input, init ?? {}]);
33 const response = responses[responseIndex++];
34 if (!response) {
35 return Promise.reject(new Error("Unexpected fetch call"));
36 }
37 return Promise.resolve(response);
38 }) satisfies typeof fetch;
39
40 return { fetchFn, calls };
41};
42
43const hangingFetch = () => {
44 const calls: Array<FetchCall> = [];
45 const fetchFn = ((input, init) => {
46 calls.push([input, init ?? {}]);
47 return new Promise<Response>(() => undefined);
48 }) satisfies typeof fetch;
49
50 return { fetchFn, calls };
51};
52
53const provideRemoteHttp = (fetchFn: typeof fetch) =>
54 Effect.provide(RpcHttp.layerRemoteHttpClient(fetchFn));
55
56const expectFetchCall = (
57 calls: ReadonlyArray<FetchCall>,
58 index: number,
59 expected: {
60 readonly url: string;
61 readonly method: string;
62 readonly headers?: Record<string, string>;
63 readonly body?: string;
64 },
65): void => {
66 const call = calls[index - 1];
67 expect(call).toBeDefined();
68 if (!call) {
69 return;
70 }
71
72 const [url, init] = call;
73 expect(String(url)).toBe(expected.url);
74 expect(init).toEqual(
75 expect.objectContaining({
76 method: expected.method,
77 }),
78 );
79 expect(init.headers).toEqual(expect.objectContaining(expected.headers ?? {}));
80
81 if ("body" in expected) {
82 const body = init.body;
83 if (typeof body === "string") {
84 expect(body).toBe(expected.body);
85 } else if (body instanceof Uint8Array) {
86 expect(new TextDecoder().decode(body)).toBe(expected.body);
87 } else {
88 throw new Error("Expected fetch request body");
89 }
90 }
91};
92
93describe("remote environment authorization", () => {
94 it.effect("keeps prefixed descriptor, auth, proof, and socket URLs on one environment", () =>
95 Effect.gen(function* () {
96 const base = "https://hub.example/prefix/environments/one/";
97 const ticket = () =>
98 Response.json({ ticket: "ws-ticket", expiresAt: "2026-05-01T12:05:00.000Z" });
99 const fetch = recordedFetch(
100 Response.json({
101 environmentId: "one",
102 label: "One",
103 platform: { os: "linux", arch: "x64" },
104 serverVersion: "test",
105 capabilities: { repositoryIdentity: true },
106 }),
107 Response.json({
108 access_token: "access",
109 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
110 token_type: "DPoP",
111 expires_in: 3600,
112 scope: "orchestration:read",
113 }),
114 ticket(),
115 ticket(),
116 );
117 yield* fetchRemoteEnvironmentDescriptor({ httpBaseUrl: base }).pipe(
118 provideRemoteHttp(fetch.fetchFn),
119 );
120 yield* exchangeRemoteDpopAccessToken({
121 httpBaseUrl: base,
122 credential: "bootstrap",
123 dpopProof: "proof",
124 }).pipe(provideRemoteHttp(fetch.fetchFn));
125 const bearerSocket = yield* resolveRemoteWebSocketConnectionUrl({
126 httpBaseUrl: base,
127 wsBaseUrl: base.replace("https:", "wss:"),
128 bearerToken: "bearer",
129 }).pipe(provideRemoteHttp(fetch.fetchFn));
130 const dpopSocket = yield* resolveRemoteDpopWebSocketConnectionUrl({
131 httpBaseUrl: base,
132 wsBaseUrl: base.replace("https:", "wss:"),
133 accessToken: "access",
134 dpopProof: "proof",
135 }).pipe(provideRemoteHttp(fetch.fetchFn));
136 expect(fetch.calls.map(([url]) => String(url))).toEqual([
137 `${base}.well-known/t3/environment`,
138 `${base}oauth/token`,
139 `${base}api/auth/websocket-ticket`,
140 `${base}api/auth/websocket-ticket`,
141 ]);
142 const urls = RpcHttp.makeEnvironmentHttpApiUrlBuilder(base);
143 expect(urls.auth.token()).toBe(`${base}oauth/token`);
144 expect(urls.auth.webSocketTicket()).toBe(`${base}api/auth/websocket-ticket`);
145 expect(bearerSocket).toBe("wss://hub.example/prefix/environments/one/ws?wsTicket=ws-ticket");
146 expect(dpopSocket).toBe(bearerSocket);
147 }),
148 );
149
150 it.effect("bootstraps bearer auth against a remote backend", () =>
151 Effect.gen(function* () {
152 const fetch = recordedFetch(
153 Response.json(
154 {
155 access_token: "bearer-token",
156 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
157 token_type: "Bearer",
158 expires_in: 3600,
159 scope:
160 "orchestration:read orchestration:operate terminal:operate review:write relay:read",
161 },
162 { status: 200 },
163 ),
164 );
165
166 const result = yield* bootstrapRemoteBearerSession({
167 httpBaseUrl: "https://remote.example.com/",
168 credential: "pairing-token",
169 }).pipe(provideRemoteHttp(fetch.fetchFn));
170
171 expect(result).toMatchObject({
172 token_type: "Bearer",
173 access_token: "bearer-token",
174 scope: "orchestration:read orchestration:operate terminal:operate review:write relay:read",
175 });
176 expectFetchCall(fetch.calls, 1, {
177 url: "https://remote.example.com/oauth/token",
178 method: "POST",
179 headers: {
180 "content-type": "application/x-www-form-urlencoded",
181 },
182 body: "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&subject_token=pairing-token&subject_token_type=urn%3At3%3Aparams%3Aoauth%3Atoken-type%3Aenvironment-bootstrap&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token",
183 });
184 }),
185 );
186
187 it.effect("exchanges managed credentials and admits websocket requests with DPoP", () =>
188 Effect.gen(function* () {
189 const fetch = recordedFetch(
190 Response.json({
191 access_token: "dpop-access-token",
192 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
193 token_type: "DPoP",
194 expires_in: 3600,
195 scope: "orchestration:read orchestration:operate terminal:operate review:write",
196 }),
197 Response.json({
198 ticket: "ws-ticket",
199 expiresAt: "2026-05-01T12:05:00.000Z",
200 }),
201 );
202
203 const token = yield* exchangeRemoteDpopAccessToken({
204 httpBaseUrl: "https://remote.example.com/",
205 credential: "one-time-credential",
206 dpopProof: "token-proof",
207 clientMetadata: {
208 label: "T3 Code Mobile",
209 deviceType: "mobile",
210 os: "iOS",
211 },
212 }).pipe(provideRemoteHttp(fetch.fetchFn));
213 yield* issueRemoteDpopWebSocketTicket({
214 httpBaseUrl: "https://remote.example.com/",
215 accessToken: token.access_token,
216 dpopProof: "resource-proof",
217 }).pipe(provideRemoteHttp(fetch.fetchFn));
218
219 expectFetchCall(fetch.calls, 1, {
220 url: "https://remote.example.com/oauth/token",
221 method: "POST",
222 headers: { dpop: "token-proof", "content-type": "application/x-www-form-urlencoded" },
223 body: "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&subject_token=one-time-credential&subject_token_type=urn%3At3%3Aparams%3Aoauth%3Atoken-type%3Aenvironment-bootstrap&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&client_label=T3+Code+Mobile&client_device_type=mobile&client_os=iOS",
224 });
225 expectFetchCall(fetch.calls, 2, {
226 url: "https://remote.example.com/api/auth/websocket-ticket",
227 method: "POST",
228 headers: {
229 authorization: "DPoP dpop-access-token",
230 dpop: "resource-proof",
231 },
232 });
233 }),
234 );
235
236 it.effect("submits optional client display metadata during bearer token exchange", () =>
237 Effect.gen(function* () {
238 const fetch = recordedFetch(
239 Response.json(
240 {
241 access_token: "bearer-token",
242 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
243 token_type: "Bearer",
244 expires_in: 3600,
245 scope:
246 "orchestration:read orchestration:operate terminal:operate review:write relay:read",
247 },
248 { status: 200 },
249 ),
250 );
251
252 yield* bootstrapRemoteBearerSession({
253 httpBaseUrl: "https://remote.example.com/",
254 credential: "pairing-token",
255 clientMetadata: {
256 label: "T3 Code Mobile",
257 deviceType: "mobile",
258 os: "iOS",
259 },
260 }).pipe(provideRemoteHttp(fetch.fetchFn));
261
262 expectFetchCall(fetch.calls, 1, {
263 url: "https://remote.example.com/oauth/token",
264 method: "POST",
265 body: "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&subject_token=pairing-token&subject_token_type=urn%3At3%3Aparams%3Aoauth%3Atoken-type%3Aenvironment-bootstrap&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&client_label=T3+Code+Mobile&client_device_type=mobile&client_os=iOS",
266 });
267 }),
268 );
269
270 it.effect("keeps OS sentinels in telemetry but out of display metadata", () =>
271 Effect.gen(function* () {
272 const tokenResponse = () =>
273 Response.json(
274 {
275 access_token: "bearer-token",
276 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
277 token_type: "Bearer",
278 expires_in: 3600,
279 scope: "orchestration:read",
280 },
281 { status: 200 },
282 );
283 const fetch = recordedFetch(tokenResponse(), tokenResponse());
284
285 for (const os of ["unknown", "other"] as const) {
286 yield* bootstrapRemoteBearerSession({
287 httpBaseUrl: "https://remote.example.com/",
288 credential: "pairing-token",
289 clientMetadata: {
290 label: "T3 Code Web",
291 deviceType: "desktop",
292 os,
293 },
294 }).pipe(provideRemoteHttp(fetch.fetchFn));
295 }
296
297 for (const [, init] of fetch.calls) {
298 expect(String(init.body)).not.toContain("client_os=");
299 }
300
301 const websocketUrl = new URL("wss://remote.example.com/ws");
302 appendClientConnectionParams(websocketUrl, {
303 surface: "web",
304 deviceType: "desktop",
305 os: "unknown",
306 });
307 expect(websocketUrl.searchParams.get("clientOs")).toBeNull();
308 }),
309 );
310
311 it.effect("allows a client to explicitly narrow a pairing grant", () =>
312 Effect.gen(function* () {
313 const fetch = recordedFetch(
314 Response.json(
315 {
316 access_token: "read-only-token",
317 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
318 token_type: "Bearer",
319 expires_in: 3600,
320 scope: "orchestration:read",
321 },
322 { status: 200 },
323 ),
324 );
325
326 yield* bootstrapRemoteBearerSession({
327 httpBaseUrl: "https://remote.example.com/",
328 credential: "pairing-token",
329 scopes: ["orchestration:read"],
330 }).pipe(provideRemoteHttp(fetch.fetchFn));
331
332 expectFetchCall(fetch.calls, 1, {
333 url: "https://remote.example.com/oauth/token",
334 method: "POST",
335 body: "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&subject_token=pairing-token&subject_token_type=urn%3At3%3Aparams%3Aoauth%3Atoken-type%3Aenvironment-bootstrap&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=orchestration%3Aread",
336 });
337 }),
338 );
339
340 it.effect("loads remote session state and websocket tickets over bearer auth", () =>
341 Effect.gen(function* () {
342 const fetch = recordedFetch(
343 Response.json(
344 {
345 environmentId: "environment-remote",
346 label: "Remote environment",
347 platform: {
348 os: "linux",
349 arch: "x64",
350 },
351 serverVersion: "0.0.0-test",
352 capabilities: {
353 repositoryIdentity: true,
354 },
355 },
356 { status: 200 },
357 ),
358 Response.json(
359 {
360 authenticated: true,
361 auth: {
362 policy: "remote-reachable",
363 bootstrapMethods: ["one-time-token"],
364 sessionMethods: ["browser-session-cookie", "bearer-access-token"],
365 sessionCookieName: "t3_session",
366 },
367 scopes: [
368 "orchestration:read",
369 "orchestration:operate",
370 "terminal:operate",
371 "review:write",
372 "relay:read",
373 ],
374 sessionMethod: "bearer-access-token",
375 expiresAt: "2026-05-01T12:00:00.000Z",
376 },
377 { status: 200 },
378 ),
379 Response.json(
380 {
381 ticket: "ws-ticket",
382 expiresAt: "2026-05-01T12:05:00.000Z",
383 },
384 { status: 200 },
385 ),
386 );
387
388 const environment = yield* fetchRemoteEnvironmentDescriptor({
389 httpBaseUrl: "https://remote.example.com/",
390 }).pipe(provideRemoteHttp(fetch.fetchFn));
391 expect(environment).toMatchObject({
392 environmentId: "environment-remote",
393 label: "Remote environment",
394 });
395
396 const session = yield* fetchRemoteSessionState({
397 httpBaseUrl: "https://remote.example.com/",
398 bearerToken: "bearer-token",
399 }).pipe(provideRemoteHttp(fetch.fetchFn));
400 expect(session).toMatchObject({
401 authenticated: true,
402 scopes: [
403 "orchestration:read",
404 "orchestration:operate",
405 "terminal:operate",
406 "review:write",
407 "relay:read",
408 ],
409 });
410
411 const ticket = yield* issueRemoteWebSocketTicket({
412 httpBaseUrl: "https://remote.example.com/",
413 bearerToken: "bearer-token",
414 }).pipe(provideRemoteHttp(fetch.fetchFn));
415 expect(ticket).toMatchObject({
416 ticket: "ws-ticket",
417 });
418
419 expectFetchCall(fetch.calls, 1, {
420 url: "https://remote.example.com/.well-known/t3/environment",
421 method: "GET",
422 });
423 expectFetchCall(fetch.calls, 2, {
424 url: "https://remote.example.com/api/auth/session",
425 method: "GET",
426 headers: {
427 authorization: "Bearer bearer-token",
428 },
429 });
430 expectFetchCall(fetch.calls, 3, {
431 url: "https://remote.example.com/api/auth/websocket-ticket",
432 method: "POST",
433 headers: {
434 authorization: "Bearer bearer-token",
435 },
436 });
437 }),
438 );
439
440 it.effect("fails hung fetch requests on the configured timeout", () =>
441 Effect.gen(function* () {
442 const fetch = hangingFetch();
443
444 const errorFiber = yield* fetchRemoteEnvironmentDescriptor({
445 httpBaseUrl: "http://remote.example.com/",
446 timeoutMs: 25,
447 }).pipe(provideRemoteHttp(fetch.fetchFn), Effect.flip, Effect.forkScoped);
448 yield* Effect.yieldNow;
449 yield* TestClock.adjust(Duration.millis(25));
450 const error = yield* Fiber.join(errorFiber);
451
452 expect(error).toBeInstanceOf(RemoteEnvironmentAuthTimeoutError);
453 expect(error.message).toBe(
454 "Remote environment endpoint http://remote.example.com/.well-known/t3/environment timed out after 25ms.",
455 );
456 }).pipe(Effect.provide(TestClock.layer())),
457 );
458
459 it.effect("revives declared typed errors from remote auth failures", () =>
460 Effect.gen(function* () {
461 const fetch = recordedFetch(
462 Response.json(
463 {
464 _tag: "EnvironmentAuthInvalidError",
465 code: "auth_invalid",
466 reason: "missing_credential",
467 traceId: "trace-auth-test",
468 },
469 { status: 401 },
470 ),
471 );
472
473 const error = yield* issueRemoteWebSocketTicket({
474 httpBaseUrl: "https://remote.example.com/",
475 bearerToken: "expired-token",
476 }).pipe(provideRemoteHttp(fetch.fetchFn), Effect.flip);
477
478 expect(isEnvironmentAuthInvalidError(error)).toBe(true);
479 if (isEnvironmentAuthInvalidError(error)) {
480 expect(error.reason).toBe("missing_credential");
481 expect(error.traceId).toBe("trace-auth-test");
482 }
483 }),
484 );
485
486 it.effect("classifies malformed successful remote auth responses as invalid responses", () =>
487 Effect.gen(function* () {
488 const fetch = recordedFetch(
489 Response.json(
490 {
491 access_token: "",
492 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
493 token_type: "Bearer",
494 expires_in: 3600,
495 scope:
496 "orchestration:read orchestration:operate terminal:operate review:write relay:read",
497 },
498 { status: 200 },
499 ),
500 );
501
502 const error = yield* bootstrapRemoteBearerSession({
503 httpBaseUrl: "https://remote.example.com/",
504 credential: "pairing-token",
505 }).pipe(provideRemoteHttp(fetch.fetchFn), Effect.flip);
506
507 expect(error).toBeInstanceOf(RemoteEnvironmentAuthInvalidJsonError);
508 expect(error.message).toBe(
509 "Remote environment endpoint returned an invalid response from https://remote.example.com/oauth/token.",
510 );
511 }),
512 );
513
514 it.effect("mints a websocket url that targets the rpc route with a short-lived ticket", () =>
515 Effect.gen(function* () {
516 const fetch = recordedFetch(
517 Response.json(
518 {
519 ticket: "ws-ticket",
520 expiresAt: "2026-05-01T12:05:00.000Z",
521 },
522 { status: 200 },
523 ),
524 );
525
526 const url = yield* resolveRemoteWebSocketConnectionUrl({
527 wsBaseUrl: "wss://remote.example.com/",
528 httpBaseUrl: "https://remote.example.com/",
529 bearerToken: "bearer-token",
530 clientMetadata: {
531 surface: "mobile",
532 appVersion: "1.2.3",
533 deviceType: "mobile",
534 os: "Android",
535 },
536 }).pipe(provideRemoteHttp(fetch.fetchFn));
537
538 expect(url).toBe(
539 "wss://remote.example.com/ws?wsTicket=ws-ticket&clientSurface=mobile&clientAppVersion=1.2.3",
540 );
541 }),
542 );
543});