| ... | @@ -1,5 +1,8 @@ | ... | @@ -1,5 +1,8 @@ |
| 1 | import { EnvironmentId } from "@t3tools/contracts"; | 1 | import { EnvironmentId } from "@t3tools/contracts"; |
| 2 | import { RelayEnvironmentStatusScope } from "@t3tools/contracts/relay"; | 2 | import { |
| | 3 | RelayEnvironmentConnectScope, |
| | 4 | RelayEnvironmentStatusScope, |
| | 5 | } from "@t3tools/contracts/relay"; |
| 3 | import { describe, expect, it } from "@effect/vitest"; | 6 | import { describe, expect, it } from "@effect/vitest"; |
| 4 | import * as Duration from "effect/Duration"; | 7 | import * as Duration from "effect/Duration"; |
| 5 | import * as Effect from "effect/Effect"; | 8 | import * as Effect from "effect/Effect"; |
| ... | @@ -190,6 +193,102 @@ describe("ManagedRelayClient", () => { | ... | @@ -190,6 +193,102 @@ describe("ManagedRelayClient", () => { |
| 190 | }).pipe(Effect.provide(managedRelayTestLayer(fetchFn))); | 193 | }).pipe(Effect.provide(managedRelayTestLayer(fetchFn))); |
| 191 | }); | 194 | }); |
| 192 | | 195 | |
| | 196 | it.effect("uses a cached token while another scope waits for an exchange", () => { |
| | 197 | const exchangeStarted = Promise.withResolvers<void>(); |
| | 198 | const releaseExchange = Promise.withResolvers<void>(); |
| | 199 | let tokenExchangeCount = 0; |
| | 200 | const statusTokens: Array<string | null> = []; |
| | 201 | let persistedTokens: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry> = [ |
| | 202 | { |
| | 203 | accountId: "user-1", |
| | 204 | clientId: "t3-mobile", |
| | 205 | relayUrl: "https://relay.example.test", |
| | 206 | thumbprint: "client-thumbprint", |
| | 207 | scopes: [RelayEnvironmentStatusScope], |
| | 208 | accessToken: "cached-status-token", |
| | 209 | expiresAtMillis: Number.MAX_SAFE_INTEGER, |
| | 210 | }, |
| | 211 | ]; |
| | 212 | const accessTokenStore: ManagedRelay.ManagedRelayAccessTokenStore = { |
| | 213 | load: Effect.sync(() => persistedTokens), |
| | 214 | save: (entries) => |
| | 215 | Effect.sync(() => { |
| | 216 | persistedTokens = entries; |
| | 217 | }), |
| | 218 | clear: Effect.sync(() => { |
| | 219 | persistedTokens = []; |
| | 220 | }), |
| | 221 | }; |
| | 222 | const fetchFn = ((input, init) => { |
| | 223 | if (String(input).endsWith("/v1/client/dpop-token")) { |
| | 224 | tokenExchangeCount += 1; |
| | 225 | exchangeStarted.resolve(); |
| | 226 | return releaseExchange.promise.then(() => |
| | 227 | Response.json({ |
| | 228 | access_token: "expanded-scope-token", |
| | 229 | issued_token_type: "urn:ietf:params:oauth:token-type:access_token", |
| | 230 | token_type: "DPoP", |
| | 231 | expires_in: 1_800, |
| | 232 | scope: `${RelayEnvironmentStatusScope} ${RelayEnvironmentConnectScope}`, |
| | 233 | }), |
| | 234 | ); |
| | 235 | } |
| | 236 | statusTokens.push(new Headers(init?.headers).get("authorization")); |
| | 237 | return Promise.resolve( |
| | 238 | Response.json({ |
| | 239 | environmentId: "env-1", |
| | 240 | endpoint: { |
| | 241 | httpBaseUrl: "https://desktop.example.test/", |
| | 242 | wsBaseUrl: "wss://desktop.example.test/ws", |
| | 243 | providerKind: "cloudflare_tunnel", |
| | 244 | }, |
| | 245 | status: "online", |
| | 246 | checkedAt: "2026-09-04T00:00:00.000Z", |
| | 247 | }), |
| | 248 | ); |
| | 249 | }) satisfies typeof globalThis.fetch; |
| | 250 | |
| | 251 | return Effect.gen(function* () { |
| | 252 | const relayClient = yield* ManagedRelay.ManagedRelayClient; |
| | 253 | const statusInput = { |
| | 254 | clerkToken: clerkToken("user-1", "session-1"), |
| | 255 | scopes: [RelayEnvironmentStatusScope], |
| | 256 | environmentId: EnvironmentId.make("env-1"), |
| | 257 | } as const; |
| | 258 | const expandedScopeInput = { |
| | 259 | ...statusInput, |
| | 260 | scopes: [RelayEnvironmentStatusScope, RelayEnvironmentConnectScope], |
| | 261 | } as const; |
| | 262 | const firstMiss = yield* relayClient |
| | 263 | .getEnvironmentStatus(expandedScopeInput) |
| | 264 | .pipe(Effect.forkChild); |
| | 265 | yield* Effect.promise(() => exchangeStarted.promise); |
| | 266 | const secondMiss = yield* relayClient |
| | 267 | .getEnvironmentStatus(expandedScopeInput) |
| | 268 | .pipe(Effect.forkChild); |
| | 269 | |
| | 270 | const status = yield* relayClient.getEnvironmentStatus(statusInput); |
| | 271 | expect(status.status).toBe("online"); |
| | 272 | expect(statusTokens).toEqual(["DPoP cached-status-token"]); |
| | 273 | |
| | 274 | releaseExchange.resolve(); |
| | 275 | yield* Fiber.join(firstMiss); |
| | 276 | yield* Fiber.join(secondMiss); |
| | 277 | expect(tokenExchangeCount).toBe(1); |
| | 278 | expect(persistedTokens.map((token) => token.accessToken)).toEqual([ |
| | 279 | "cached-status-token", |
| | 280 | "expanded-scope-token", |
| | 281 | ]); |
| | 282 | yield* relayClient.resetTokenCache; |
| | 283 | expect(persistedTokens).toEqual([]); |
| | 284 | yield* relayClient.getEnvironmentStatus(expandedScopeInput); |
| | 285 | expect(tokenExchangeCount).toBe(2); |
| | 286 | }).pipe( |
| | 287 | Effect.ensuring(Effect.sync(() => releaseExchange.resolve())), |
| | 288 | Effect.provide(managedRelayTestLayer(fetchFn, undefined, accessTokenStore)), |
| | 289 | ); |
| | 290 | }); |
| | 291 | |
| 193 | it.effect("reuses a persisted token across runtimes and Clerk session token rotation", () => { | 292 | it.effect("reuses a persisted token across runtimes and Clerk session token rotation", () => { |
| 194 | let tokenExchangeCount = 0; | 293 | let tokenExchangeCount = 0; |
| 195 | let persistedTokens: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry> = []; | 294 | let persistedTokens: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry> = []; |