1import { bootstrapRemoteBearerSession } from "@t3tools/client-runtime/authorization";
2import type { RemoteEnvironmentRequestError } from "@t3tools/client-runtime/rpc";
3import { PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";
4import { currentDesktopBootstrapToken } from "@t3tools/shared/desktopBootstrapToken";
5import * as Clock from "effect/Clock";
6import * as Context from "effect/Context";
7import * as Duration from "effect/Duration";
8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";
10import * as Option from "effect/Option";
11import * as Ref from "effect/Ref";
12import * as Schedule from "effect/Schedule";
13import * as Schema from "effect/Schema";
14import * as Semaphore from "effect/Semaphore";
15import * as HttpClient from "effect/http/HttpClient";
16
17import * as DesktopBackendPool from "./DesktopBackendPool.ts";
18
19// A loopback /oauth/token exchange can fail transiently while the backend
20// settles (a 502-504, a refused or reset connection, a timeout). Retry those
21// for as long as the renderer's own bootstrap does; a rejected credential or a
22// server error is final.
23const BOOTSTRAP_TRANSIENT_RETRY_TIMEOUT = Duration.seconds(15);
24const BOOTSTRAP_TRANSIENT_RETRY_INTERVAL = Duration.millis(500);
25const TRANSIENT_BOOTSTRAP_STATUS_CODES = new Set([502, 503, 504]);
26
27const isTransientBearerBootstrapError = (error: RemoteEnvironmentRequestError): boolean => {
28 switch (error._tag) {
29 case "RemoteEnvironmentAuthFetchError":
30 case "RemoteEnvironmentAuthTimeoutError":
31 return true;
32 case "RemoteEnvironmentAuthUndeclaredStatusError":
33 return TRANSIENT_BOOTSTRAP_STATUS_CODES.has(error.status);
34 default:
35 return false;
36 }
37};
38
39export class DesktopLocalEnvironmentAuthBackendNotConfiguredError extends Schema.TaggedError<DesktopLocalEnvironmentAuthBackendNotConfiguredError>()(
40 "DesktopLocalEnvironmentAuthBackendNotConfiguredError",
41 {},
42) {
43 override get message(): string {
44 return "Local backend is not configured.";
45 }
46}
47
48export class DesktopLocalEnvironmentAuthSessionBootstrapError extends Schema.TaggedError<DesktopLocalEnvironmentAuthSessionBootstrapError>()(
49 "DesktopLocalEnvironmentAuthSessionBootstrapError",
50 { cause: Schema.Defect() },
51) {
52 override get message(): string {
53 return "Failed to create the local desktop bearer session.";
54 }
55}
56
57export const DesktopLocalEnvironmentAuthError = Schema.Union([
58 DesktopLocalEnvironmentAuthBackendNotConfiguredError,
59 DesktopLocalEnvironmentAuthSessionBootstrapError,
60]);
61export type DesktopLocalEnvironmentAuthError = typeof DesktopLocalEnvironmentAuthError.Type;
62
63export class DesktopLocalEnvironmentAuth extends Context.Service<
64 DesktopLocalEnvironmentAuth,
65 {
66 readonly getBearerToken: Effect.Effect<string, DesktopLocalEnvironmentAuthError>;
67 }
68>()("@t3tools/desktop/backend/DesktopLocalEnvironmentAuth") {}
69
70/** @public Service construction is part of the canonical Effect module API. */
71export const make = Effect.gen(function* () {
72 const pool = yield* DesktopBackendPool.DesktopBackendPool;
73 const httpClient = yield* HttpClient.HttpClient;
74 const tokenRef = yield* Ref.make(Option.none<string>());
75 const mutex = yield* Semaphore.make(1);
76
77 const getBearerToken = mutex
78 .withPermits(1)(
79 Effect.gen(function* () {
80 const cached = yield* Ref.get(tokenRef);
81 if (Option.isSome(cached)) {
82 return cached.value;
83 }
84
85 const instances = yield* pool.list;
86 const primary = instances.find((instance) => instance.id === PRIMARY_LOCAL_ENVIRONMENT_ID);
87 const configOption = primary === undefined ? Option.none() : yield* primary.currentConfig;
88 if (Option.isNone(configOption)) {
89 return yield* new DesktopLocalEnvironmentAuthBackendNotConfiguredError();
90 }
91 const config = configOption.value;
92 // A backend launched with the desktop secret accepts the current
93 // window's token, not the one frozen into its launch config; this
94 // exchange can run long after launch (e.g. after a suspend).
95 const secret = config.bootstrap.desktopBootstrapSecret;
96 const credential =
97 secret === undefined
98 ? config.bootstrap.desktopBootstrapToken
99 : currentDesktopBootstrapToken(secret, yield* Clock.currentTimeMillis);
100 if (!credential) {
101 return yield* new DesktopLocalEnvironmentAuthBackendNotConfiguredError();
102 }
103 const session = yield* bootstrapRemoteBearerSession({
104 httpBaseUrl: config.httpBaseUrl.href,
105 credential,
106 clientMetadata: {
107 label: "The Knife Desktop",
108 deviceType: "desktop",
109 },
110 }).pipe(
111 Effect.provideService(HttpClient.HttpClient, httpClient),
112 Effect.retry({
113 while: isTransientBearerBootstrapError,
114 schedule: Schedule.spaced(BOOTSTRAP_TRANSIENT_RETRY_INTERVAL),
115 }),
116 // Bounds the attempts and any request still in flight at the deadline.
117 Effect.timeout(BOOTSTRAP_TRANSIENT_RETRY_TIMEOUT),
118 Effect.mapError(
119 (cause) =>
120 new DesktopLocalEnvironmentAuthSessionBootstrapError({
121 cause,
122 }),
123 ),
124 );
125 yield* Ref.set(tokenRef, Option.some(session.access_token));
126 return session.access_token;
127 }),
128 )
129 .pipe(Effect.withSpan("desktop.localEnvironmentAuth.getBearerToken"));
130
131 return DesktopLocalEnvironmentAuth.of({ getBearerToken });
132});
133
134export const layer = Layer.effect(DesktopLocalEnvironmentAuth, make);