| author | |
| committer | |
| log | c00e721c047ab0b832401196e64df26431fd9c98 |
| tree | e5f8101c265ec4d44d92e1559e3f3ca6ffd11c30 |
| parent | b0a3a504481ab4d8f2da0343e5a4258c10d03c73 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>29 files changed, 962 insertions(+), 765 deletions(-)
apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts+2-2| ... | ... | @@ -2,7 +2,7 @@ import { beforeEach, vi } from "vite-plus/test"; |
| 2 | 2 | import { describe, expect, it } from "@effect/vitest"; |
| 3 | 3 | import * as Effect from "effect/Effect"; |
| 4 | 4 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 5 | import { ManagedRelayClient } from "@t3tools/client-runtime/relay"; | |
| 5 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; | |
| 6 | 6 | import * as Layer from "effect/Layer"; |
| 7 | 7 | import { HttpClient } from "effect/unstable/http"; |
| 8 | 8 | |
| ... | ... | @@ -35,7 +35,7 @@ const connection: SavedRemoteConnection = { |
| 35 | 35 | }; |
| 36 | 36 | |
| 37 | 37 | const testLayer = Layer.mergeAll( |
| 38 | Layer.succeed(ManagedRelayClient, null as never), | |
| 38 | Layer.succeed(ManagedRelay.ManagedRelayClient, null as never), | |
| 39 | 39 | Layer.succeed( |
| 40 | 40 | HttpClient.HttpClient, |
| 41 | 41 | HttpClient.make(() => Effect.die("unexpected HTTP request")), |
apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts+2-2| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | 1 | import * as Effect from "effect/Effect"; |
| 2 | 2 | import { HttpClient } from "effect/unstable/http"; |
| 3 | import { ManagedRelayClient } from "@t3tools/client-runtime/relay"; | |
| 3 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; | |
| 4 | 4 | |
| 5 | 5 | import type { SavedRemoteConnection } from "../../lib/connection"; |
| 6 | 6 | import { savePreferencesPatch } from "../../lib/storage"; |
| ... | ... | @@ -11,7 +11,7 @@ export function setLiveActivityUpdatesEnabled(input: { |
| 11 | 11 | readonly enabled: boolean; |
| 12 | 12 | readonly clerkToken: string | null; |
| 13 | 13 | readonly connections: ReadonlyArray<SavedRemoteConnection>; |
| 14 | }): Effect.Effect<void, unknown, HttpClient.HttpClient | ManagedRelayClient> { | |
| 14 | }): Effect.Effect<void, unknown, HttpClient.HttpClient | ManagedRelay.ManagedRelayClient> { | |
| 15 | 15 | return Effect.gen(function* () { |
| 16 | 16 | yield* Effect.tryPromise({ |
| 17 | 17 | try: () => savePreferencesPatch({ liveActivitiesEnabled: input.enabled }), |
apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts+2-2| ... | ... | @@ -9,7 +9,7 @@ import * as Effect from "effect/Effect"; |
| 9 | 9 | import * as Exit from "effect/Exit"; |
| 10 | 10 | import * as Layer from "effect/Layer"; |
| 11 | 11 | import { FetchHttpClient } from "effect/unstable/http"; |
| 12 | import { type ManagedRelayClient } from "@t3tools/client-runtime/relay"; | |
| 12 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; | |
| 13 | 13 | |
| 14 | 14 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 15 | 15 | import { verifyDpopProof } from "@t3tools/shared/dpop"; |
| ... | ... | @@ -158,7 +158,7 @@ const runBackgroundOperations = Effect.fn("TestRemoteRegistration.runBackgroundO |
| 158 | 158 | } |
| 159 | 159 | idlePasses = 0; |
| 160 | 160 | const exit = yield* Effect.exit( |
| 161 | pending.operation as Effect.Effect<unknown, unknown, ManagedRelayClient>, | |
| 161 | pending.operation as Effect.Effect<unknown, unknown, ManagedRelay.ManagedRelayClient>, | |
| 162 | 162 | ); |
| 163 | 163 | yield* Effect.sync(() => { |
| 164 | 164 | pending.resolve(exit); |
apps/mobile/src/features/agent-awareness/remoteRegistration.ts+15-15| ... | ... | @@ -9,7 +9,7 @@ import { |
| 9 | 9 | type RelayLiveActivityRegistrationRequest, |
| 10 | 10 | } from "@t3tools/contracts/relay"; |
| 11 | 11 | import { findErrorTraceId } from "@t3tools/client-runtime/errors"; |
| 12 | import { ManagedRelayClient } from "@t3tools/client-runtime/relay"; | |
| 12 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; | |
| 13 | 13 | import { |
| 14 | 14 | isAtomCommandInterrupted, |
| 15 | 15 | settleAsyncResult, |
| ... | ... | @@ -175,7 +175,7 @@ const relayToken = Effect.gen(function* () { |
| 175 | 175 | function registerDeviceWithRelay( |
| 176 | 176 | body: RelayDeviceRegistrationRequest, |
| 177 | 177 | expectedGeneration: number, |
| 178 | ): Effect.Effect<void, unknown, ManagedRelayClient> { | |
| 178 | ): Effect.Effect<void, unknown, ManagedRelay.ManagedRelayClient> { | |
| 179 | 179 | return Effect.gen(function* () { |
| 180 | 180 | if (expectedGeneration !== deviceRegistrationGeneration) { |
| 181 | 181 | logRegistrationDebug("device registration cancelled before relay request", { |
| ... | ... | @@ -198,7 +198,7 @@ function registerDeviceWithRelay( |
| 198 | 198 | return; |
| 199 | 199 | } |
| 200 | 200 | |
| 201 | const client = yield* ManagedRelayClient; | |
| 201 | const client = yield* ManagedRelay.ManagedRelayClient; | |
| 202 | 202 | logRegistrationDebug("relay device registration request started", { |
| 203 | 203 | expectedGeneration, |
| 204 | 204 | }); |
| ... | ... | @@ -215,7 +215,7 @@ function registerDeviceWithRelay( |
| 215 | 215 | function unregisterDeviceWithRelay(input: { |
| 216 | 216 | readonly deviceId: string; |
| 217 | 217 | readonly tokenProvider: () => Promise<string | null>; |
| 218 | }): Effect.Effect<void, unknown, ManagedRelayClient> { | |
| 218 | }): Effect.Effect<void, unknown, ManagedRelay.ManagedRelayClient> { | |
| 219 | 219 | return Effect.gen(function* () { |
| 220 | 220 | if (!readRelayConfig()) return; |
| 221 | 221 | const token = yield* Effect.tryPromise({ |
| ... | ... | @@ -227,7 +227,7 @@ function unregisterDeviceWithRelay(input: { |
| 227 | 227 | return; |
| 228 | 228 | } |
| 229 | 229 | |
| 230 | const client = yield* ManagedRelayClient; | |
| 230 | const client = yield* ManagedRelay.ManagedRelayClient; | |
| 231 | 231 | yield* client.unregisterDevice({ |
| 232 | 232 | clerkToken: token, |
| 233 | 233 | deviceId: input.deviceId, |
| ... | ... | @@ -237,7 +237,7 @@ function unregisterDeviceWithRelay(input: { |
| 237 | 237 | |
| 238 | 238 | function registerLiveActivityWithRelay( |
| 239 | 239 | body: RelayLiveActivityRegistrationRequest, |
| 240 | ): Effect.Effect<boolean, unknown, ManagedRelayClient> { | |
| 240 | ): Effect.Effect<boolean, unknown, ManagedRelay.ManagedRelayClient> { | |
| 241 | 241 | return Effect.gen(function* () { |
| 242 | 242 | if (!readRelayConfig()) return false; |
| 243 | 243 | const token = yield* relayToken; |
| ... | ... | @@ -246,7 +246,7 @@ function registerLiveActivityWithRelay( |
| 246 | 246 | return false; |
| 247 | 247 | } |
| 248 | 248 | |
| 249 | const client = yield* ManagedRelayClient; | |
| 249 | const client = yield* ManagedRelay.ManagedRelayClient; | |
| 250 | 250 | yield* client.registerLiveActivity({ |
| 251 | 251 | clerkToken: token, |
| 252 | 252 | payload: body, |
| ... | ... | @@ -274,7 +274,7 @@ function logRegistrationDebug(context: string, details?: unknown): void { |
| 274 | 274 | } |
| 275 | 275 | |
| 276 | 276 | function runRegistrationInBackground( |
| 277 | operation: Effect.Effect<unknown, unknown, ManagedRelayClient>, | |
| 277 | operation: Effect.Effect<unknown, unknown, ManagedRelay.ManagedRelayClient>, | |
| 278 | 278 | context: string, |
| 279 | 279 | ): void { |
| 280 | 280 | void (async () => { |
| ... | ... | @@ -370,7 +370,7 @@ function enqueueDeviceRegistration(input: DeviceRegistrationInput, context: stri |
| 370 | 370 | function registerDevice( |
| 371 | 371 | input: DeviceRegistrationInput = {}, |
| 372 | 372 | expectedGeneration = deviceRegistrationGeneration, |
| 373 | ): Effect.Effect<void, unknown, ManagedRelayClient> { | |
| 373 | ): Effect.Effect<void, unknown, ManagedRelay.ManagedRelayClient> { | |
| 374 | 374 | return Effect.gen(function* () { |
| 375 | 375 | if (!canRegisterRemoteLiveActivities()) { |
| 376 | 376 | logRegistrationDebug("device registration skipped; platform does not support it"); |
| ... | ... | @@ -411,7 +411,7 @@ function registerDevice( |
| 411 | 411 | |
| 412 | 412 | function registerDeviceForCurrentUser( |
| 413 | 413 | pushToStartToken?: string, |
| 414 | ): Effect.Effect<void, unknown, ManagedRelayClient> { | |
| 414 | ): Effect.Effect<void, unknown, ManagedRelay.ManagedRelayClient> { | |
| 415 | 415 | return registerDevice(pushToStartToken ? { pushToStartToken } : undefined); |
| 416 | 416 | } |
| 417 | 417 | |
| ... | ... | @@ -485,7 +485,7 @@ export function unregisterAllAgentAwarenessConnections(): void { |
| 485 | 485 | export function refreshAgentAwarenessRegistration(): Effect.Effect< |
| 486 | 486 | void, |
| 487 | 487 | never, |
| 488 | ManagedRelayClient | |
| 488 | ManagedRelay.ManagedRelayClient | |
| 489 | 489 | > { |
| 490 | 490 | return registerDeviceForCurrentUser().pipe( |
| 491 | 491 | Effect.catch((error) => |
| ... | ... | @@ -515,7 +515,7 @@ export function __resetAgentAwarenessRemoteRegistrationForTest(): void { |
| 515 | 515 | |
| 516 | 516 | export function unregisterAgentAwarenessDeviceForCurrentUser( |
| 517 | 517 | tokenProvider: () => Promise<string | null>, |
| 518 | ): Effect.Effect<void, never, ManagedRelayClient> { | |
| 518 | ): Effect.Effect<void, never, ManagedRelay.ManagedRelayClient> { | |
| 519 | 519 | return Effect.gen(function* () { |
| 520 | 520 | const deviceId = yield* Effect.tryPromise({ |
| 521 | 521 | try: () => loadAgentAwarenessDeviceId(), |
| ... | ... | @@ -536,7 +536,7 @@ export function unregisterAgentAwarenessDeviceForCurrentUser( |
| 536 | 536 | |
| 537 | 537 | export function registerLiveActivityPushToken(input: { |
| 538 | 538 | readonly activity: LiveActivity<AgentActivityProps>; |
| 539 | }): Effect.Effect<boolean, unknown, ManagedRelayClient> { | |
| 539 | }): Effect.Effect<boolean, unknown, ManagedRelay.ManagedRelayClient> { | |
| 540 | 540 | return Effect.gen(function* () { |
| 541 | 541 | if (!canRegisterRemoteLiveActivities()) { |
| 542 | 542 | return false; |
| ... | ... | @@ -588,7 +588,7 @@ export function registerLiveActivityPushToken(input: { |
| 588 | 588 | |
| 589 | 589 | function registerLiveActivityPushTokenValue(input: { |
| 590 | 590 | readonly activityPushToken: string; |
| 591 | }): Effect.Effect<boolean, unknown, ManagedRelayClient> { | |
| 591 | }): Effect.Effect<boolean, unknown, ManagedRelay.ManagedRelayClient> { | |
| 592 | 592 | return Effect.gen(function* () { |
| 593 | 593 | const deviceId = yield* Effect.tryPromise({ |
| 594 | 594 | try: () => loadOrCreateAgentAwarenessDeviceId(), |
| ... | ... | @@ -624,7 +624,7 @@ function scheduleActiveLiveActivityRegistrationRetry(): void { |
| 624 | 624 | export function refreshActiveLiveActivityRemoteRegistration(): Effect.Effect< |
| 625 | 625 | void, |
| 626 | 626 | never, |
| 627 | ManagedRelayClient | |
| 627 | ManagedRelay.ManagedRelayClient | |
| 628 | 628 | > { |
| 629 | 629 | return Effect.gen(function* () { |
| 630 | 630 | if (!canRegisterRemoteLiveActivities() || !relayTokenProvider) { |
apps/mobile/src/features/cloud/CloudAuthProvider.tsx+2-2| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | 1 | import { ClerkProvider, useAuth } from "@clerk/expo"; |
| 2 | 2 | import { tokenCache } from "@clerk/expo/token-cache"; |
| 3 | import { ManagedRelayClient, setManagedRelaySession } from "@t3tools/client-runtime/relay"; | |
| 3 | import { ManagedRelay, setManagedRelaySession } from "@t3tools/client-runtime/relay"; | |
| 4 | 4 | import { |
| 5 | 5 | reportAtomCommandResult, |
| 6 | 6 | settleAsyncResult, |
| ... | ... | @@ -22,7 +22,7 @@ import { resolveCloudPublicConfig, resolveRelayClerkTokenOptions } from "./publi |
| 22 | 22 | function resetManagedRelayTokenCache() { |
| 23 | 23 | return settleAsyncResult(() => |
| 24 | 24 | runtime.runPromiseExit( |
| 25 | ManagedRelayClient.pipe(Effect.flatMap((client) => client.resetTokenCache)), | |
| 25 | ManagedRelay.ManagedRelayClient.pipe(Effect.flatMap((client) => client.resetTokenCache)), | |
| 26 | 26 | ), |
| 27 | 27 | ); |
| 28 | 28 | } |
apps/mobile/src/features/cloud/linkEnvironment.test.ts+9-9| ... | ... | @@ -4,11 +4,7 @@ import * as Effect from "effect/Effect"; |
| 4 | 4 | import * as Layer from "effect/Layer"; |
| 5 | 5 | import { EnvironmentId } from "@t3tools/contracts"; |
| 6 | 6 | import { RelayMobileClientId } from "@t3tools/contracts/relay"; |
| 7 | import { | |
| 8 | managedRelayClientLayer, | |
| 9 | ManagedRelayClient, | |
| 10 | ManagedRelayDpopSigner, | |
| 11 | } from "@t3tools/client-runtime/relay"; | |
| 7 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; | |
| 12 | 8 | import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc"; |
| 13 | 9 | import { HttpClient } from "effect/unstable/http"; |
| 14 | 10 | |
| ... | ... | @@ -62,8 +58,8 @@ const createProofMock = vi.fn( |
| 62 | 58 | Effect.succeed(`dpop:${input.method}:${input.url}`), |
| 63 | 59 | ); |
| 64 | 60 | const testDpopSignerLayer = Layer.succeed( |
| 65 | ManagedRelayDpopSigner, | |
| 66 | ManagedRelayDpopSigner.of({ | |
| 61 | ManagedRelay.ManagedRelayDpopSigner, | |
| 62 | ManagedRelay.ManagedRelayDpopSigner.of({ | |
| 67 | 63 | thumbprint: Effect.succeed("client-proof-key-thumbprint"), |
| 68 | 64 | createProof: (input) => createProofMock(input), |
| 69 | 65 | }), |
| ... | ... | @@ -73,7 +69,7 @@ function cloudClientLayer() { |
| 73 | 69 | const httpClientLayer = remoteHttpClientLayer((input, init) => globalThis.fetch(input, init)); |
| 74 | 70 | return Layer.mergeAll( |
| 75 | 71 | httpClientLayer, |
| 76 | managedRelayClientLayer({ | |
| 72 | ManagedRelay.layer({ | |
| 77 | 73 | relayUrl: "https://relay.example.test", |
| 78 | 74 | clientId: RelayMobileClientId, |
| 79 | 75 | }).pipe(Layer.provideMerge(testDpopSignerLayer), Layer.provide(httpClientLayer)), |
| ... | ... | @@ -81,7 +77,11 @@ function cloudClientLayer() { |
| 81 | 77 | } |
| 82 | 78 | |
| 83 | 79 | const withCloudServices = <A, E>( |
| 84 | effect: Effect.Effect<A, E, HttpClient.HttpClient | ManagedRelayClient | ManagedRelayDpopSigner>, | |
| 80 | effect: Effect.Effect< | |
| 81 | A, | |
| 82 | E, | |
| 83 | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | ManagedRelay.ManagedRelayDpopSigner | |
| 84 | >, | |
| 85 | 85 | ) => effect.pipe(Effect.provide(cloudClientLayer())); |
| 86 | 86 | |
| 87 | 87 | function validLinkProof() { |
apps/mobile/src/features/cloud/linkEnvironment.ts+22-20| ... | ... | @@ -25,11 +25,7 @@ import { |
| 25 | 25 | import { exchangeRemoteDpopAccessToken } from "@t3tools/client-runtime/authorization"; |
| 26 | 26 | import { fetchRemoteEnvironmentDescriptor } from "@t3tools/client-runtime/environment"; |
| 27 | 27 | import { findErrorTraceId } from "@t3tools/client-runtime/errors"; |
| 28 | import { | |
| 29 | ManagedRelayClient, | |
| 30 | type ManagedRelayClientError, | |
| 31 | ManagedRelayDpopSigner, | |
| 32 | } from "@t3tools/client-runtime/relay"; | |
| 28 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; | |
| 33 | 29 | import { makeEnvironmentHttpApiClient } from "@t3tools/client-runtime/rpc"; |
| 34 | 30 | |
| 35 | 31 | import { authClientMetadata } from "../../lib/authClientMetadata"; |
| ... | ... | @@ -156,13 +152,15 @@ function relayProtectedErrorMessage(error: RelayProtectedErrorType): string { |
| 156 | 152 | } |
| 157 | 153 | |
| 158 | 154 | function decodedRelayClientError(message: string) { |
| 159 | return (cause: ManagedRelayClientError) => { | |
| 160 | const relayError = cause.relayError; | |
| 155 | return (cause: ManagedRelay.ManagedRelayClientError) => { | |
| 156 | const relayError = | |
| 157 | cause._tag === "ManagedRelayRequestFailedError" ? cause.relayError : undefined; | |
| 158 | const traceId = cause._tag === "ManagedRelayRequestFailedError" ? cause.traceId : undefined; | |
| 161 | 159 | const detail = relayError ? relayProtectedErrorMessage(relayError) : null; |
| 162 | 160 | return new CloudEnvironmentLinkError({ |
| 163 | 161 | message: detail ? `${message}: ${detail}` : message, |
| 164 | 162 | cause, |
| 165 | ...(cause.traceId ? { traceId: cause.traceId } : {}), | |
| 163 | ...(traceId ? { traceId } : {}), | |
| 166 | 164 | }); |
| 167 | 165 | }; |
| 168 | 166 | } |
| ... | ... | @@ -261,7 +259,11 @@ function ensureConnectEndpointMatchesEnvironment(input: { |
| 261 | 259 | export function linkEnvironmentToCloud(input: { |
| 262 | 260 | readonly connection: SavedRemoteConnection; |
| 263 | 261 | readonly clerkToken: string; |
| 264 | }): Effect.Effect<void, CloudEnvironmentLinkError, HttpClient.HttpClient | ManagedRelayClient> { | |
| 262 | }): Effect.Effect< | |
| 263 | void, | |
| 264 | CloudEnvironmentLinkError, | |
| 265 | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | |
| 266 | > { | |
| 265 | 267 | return Effect.gen(function* () { |
| 266 | 268 | if (!input.connection.bearerToken) { |
| 267 | 269 | return yield* new CloudEnvironmentLinkError({ |
| ... | ... | @@ -270,7 +272,7 @@ export function linkEnvironmentToCloud(input: { |
| 270 | 272 | } |
| 271 | 273 | const localBearerToken = input.connection.bearerToken; |
| 272 | 274 | const relayUrl = yield* requireRelayUrl(); |
| 273 | const relayClient = yield* ManagedRelayClient; | |
| 275 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 274 | 276 | const deviceId = yield* Effect.tryPromise({ |
| 275 | 277 | try: () => loadOrCreateAgentAwarenessDeviceId(), |
| 276 | 278 | catch: cloudEnvironmentLinkError("Could not load the mobile device id."), |
| ... | ... | @@ -353,11 +355,11 @@ export function listCloudEnvironments(input: { |
| 353 | 355 | }): Effect.Effect< |
| 354 | 356 | ReadonlyArray<RelayClientEnvironmentRecord>, |
| 355 | 357 | CloudEnvironmentLinkError, |
| 356 | ManagedRelayClient | |
| 358 | ManagedRelay.ManagedRelayClient | |
| 357 | 359 | > { |
| 358 | 360 | return Effect.gen(function* () { |
| 359 | 361 | const relayUrl = yield* requireRelayUrl(); |
| 360 | const relayClient = yield* ManagedRelayClient; | |
| 362 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 361 | 363 | |
| 362 | 364 | return yield* relayClient |
| 363 | 365 | .listEnvironments({ |
| ... | ... | @@ -374,11 +376,11 @@ export function getCloudEnvironmentStatus(input: { |
| 374 | 376 | }): Effect.Effect< |
| 375 | 377 | RelayEnvironmentStatusResponseType, |
| 376 | 378 | CloudEnvironmentLinkError, |
| 377 | ManagedRelayClient | |
| 379 | ManagedRelay.ManagedRelayClient | |
| 378 | 380 | > { |
| 379 | 381 | return Effect.gen(function* () { |
| 380 | 382 | const relayUrl = yield* requireRelayUrl(); |
| 381 | const relayClient = yield* ManagedRelayClient; | |
| 383 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 382 | 384 | const status = yield* relayClient |
| 383 | 385 | .getEnvironmentStatus({ |
| 384 | 386 | clerkToken: input.clerkToken, |
| ... | ... | @@ -413,7 +415,7 @@ export function loadCloudEnvironmentStatuses(input: { |
| 413 | 415 | }): Effect.Effect< |
| 414 | 416 | ReadonlyArray<CloudEnvironmentRecordWithStatus>, |
| 415 | 417 | CloudEnvironmentLinkError, |
| 416 | ManagedRelayClient | |
| 418 | ManagedRelay.ManagedRelayClient | |
| 417 | 419 | > { |
| 418 | 420 | return Effect.forEach( |
| 419 | 421 | input.environments, |
| ... | ... | @@ -445,7 +447,7 @@ export function listCloudEnvironmentsWithStatus(input: { |
| 445 | 447 | }): Effect.Effect< |
| 446 | 448 | ReadonlyArray<CloudEnvironmentRecordWithStatus>, |
| 447 | 449 | CloudEnvironmentLinkError, |
| 448 | ManagedRelayClient | |
| 450 | ManagedRelay.ManagedRelayClient | |
| 449 | 451 | > { |
| 450 | 452 | return Effect.gen(function* () { |
| 451 | 453 | const environments = yield* listCloudEnvironments(input); |
| ... | ... | @@ -473,7 +475,7 @@ const connectRelayManagedEnvironment = Effect.fn("mobile.cloud.connectRelayManag |
| 473 | 475 | }) { |
| 474 | 476 | yield* Effect.annotateCurrentSpan({ "environment.id": input.environmentId }); |
| 475 | 477 | const relayUrl = yield* requireRelayUrl(); |
| 476 | const relayClient = yield* ManagedRelayClient; | |
| 478 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 477 | 479 | |
| 478 | 480 | const deviceId = yield* loadAgentAwarenessDeviceId(); |
| 479 | 481 | const connect = yield* relayClient |
| ... | ... | @@ -514,7 +516,7 @@ const connectRelayManagedEnvironment = Effect.fn("mobile.cloud.connectRelayManag |
| 514 | 516 | message: "Connected endpoint descriptor does not match the selected environment.", |
| 515 | 517 | }); |
| 516 | 518 | } |
| 517 | const signer = yield* ManagedRelayDpopSigner; | |
| 519 | const signer = yield* ManagedRelay.ManagedRelayDpopSigner; | |
| 518 | 520 | const bootstrapDpop = yield* signer |
| 519 | 521 | .createProof({ |
| 520 | 522 | method: "POST", |
| ... | ... | @@ -555,7 +557,7 @@ export function connectCloudEnvironment(input: { |
| 555 | 557 | }): Effect.Effect< |
| 556 | 558 | SavedRemoteConnection, |
| 557 | 559 | CloudEnvironmentLinkError, |
| 558 | HttpClient.HttpClient | ManagedRelayClient | ManagedRelayDpopSigner | |
| 560 | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | ManagedRelay.ManagedRelayDpopSigner | |
| 559 | 561 | > { |
| 560 | 562 | return connectRelayManagedEnvironment({ |
| 561 | 563 | clerkToken: input.clerkToken, |
| ... | ... | @@ -570,7 +572,7 @@ export function refreshCloudEnvironmentConnection(input: { |
| 570 | 572 | }): Effect.Effect< |
| 571 | 573 | SavedRemoteConnection, |
| 572 | 574 | CloudEnvironmentLinkError, |
| 573 | HttpClient.HttpClient | ManagedRelayClient | ManagedRelayDpopSigner | |
| 575 | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | ManagedRelay.ManagedRelayDpopSigner | |
| 574 | 576 | > { |
| 575 | 577 | return connectRelayManagedEnvironment({ |
| 576 | 578 | clerkToken: input.clerkToken, |
apps/mobile/src/features/cloud/managedRelayLayer.ts+35-19| ... | ... | @@ -1,8 +1,4 @@ |
| 1 | import { | |
| 2 | managedRelayClientLayer as makeManagedRelayClientLayer, | |
| 3 | ManagedRelayDpopSigner, | |
| 4 | ManagedRelayDpopSignerError, | |
| 5 | } from "@t3tools/client-runtime/relay"; | |
| 1 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; | |
| 6 | 2 | import { RelayMobileClientId } from "@t3tools/contracts/relay"; |
| 7 | 3 | import * as Crypto from "effect/Crypto"; |
| 8 | 4 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -12,34 +8,54 @@ import { createDpopProof, loadOrCreateDpopProofKeyPair } from "./dpop"; |
| 12 | 8 | import { managedRelayAccessTokenStore } from "./managedRelayTokenStore"; |
| 13 | 9 | |
| 14 | 10 | const relayDpopSignerLayer = Layer.effect( |
| 15 | ManagedRelayDpopSigner, | |
| 11 | ManagedRelay.ManagedRelayDpopSigner, | |
| 16 | 12 | Effect.gen(function* () { |
| 17 | 13 | const crypto = yield* Crypto.Crypto; |
| 18 | 14 | const loadProofKey = yield* Effect.cached( |
| 19 | 15 | loadOrCreateDpopProofKeyPair().pipe(Effect.provideService(Crypto.Crypto, crypto)), |
| 20 | 16 | ); |
| 21 | return ManagedRelayDpopSigner.of({ | |
| 17 | return ManagedRelay.ManagedRelayDpopSigner.of({ | |
| 22 | 18 | thumbprint: loadProofKey.pipe( |
| 23 | 19 | Effect.map((proofKey) => proofKey.thumbprint), |
| 24 | Effect.mapError((cause) => new ManagedRelayDpopSignerError({ cause })), | |
| 20 | Effect.mapError( | |
| 21 | (error) => | |
| 22 | new ManagedRelay.ManagedRelayDpopKeyLoadError({ | |
| 23 | keyStore: "expo-secure-store", | |
| 24 | cause: error, | |
| 25 | }), | |
| 26 | ), | |
| 25 | 27 | Effect.withSpan("mobile.managedRelayDpopSigner.loadThumbprint"), |
| 26 | 28 | ), |
| 27 | createProof: Effect.fn("mobile.managedRelayDpopSigner.createProof")( | |
| 28 | function* (input) { | |
| 29 | const proofKey = yield* loadProofKey; | |
| 30 | return yield* createDpopProof({ ...input, proofKey }).pipe( | |
| 31 | Effect.provideService(Crypto.Crypto, crypto), | |
| 32 | Effect.map((proof) => proof.proof), | |
| 33 | ); | |
| 34 | }, | |
| 35 | Effect.mapError((cause) => new ManagedRelayDpopSignerError({ cause })), | |
| 36 | ), | |
| 29 | createProof: Effect.fn("mobile.managedRelayDpopSigner.createProof")(function* (input) { | |
| 30 | const proofKey = yield* loadProofKey.pipe( | |
| 31 | Effect.mapError( | |
| 32 | (error) => | |
| 33 | new ManagedRelay.ManagedRelayDpopProofCreationError({ | |
| 34 | method: input.method, | |
| 35 | url: input.url, | |
| 36 | cause: error, | |
| 37 | }), | |
| 38 | ), | |
| 39 | ); | |
| 40 | return yield* createDpopProof({ ...input, proofKey }).pipe( | |
| 41 | Effect.provideService(Crypto.Crypto, crypto), | |
| 42 | Effect.map((proof) => proof.proof), | |
| 43 | Effect.mapError( | |
| 44 | (error) => | |
| 45 | new ManagedRelay.ManagedRelayDpopProofCreationError({ | |
| 46 | method: input.method, | |
| 47 | url: input.url, | |
| 48 | cause: error, | |
| 49 | }), | |
| 50 | ), | |
| 51 | ); | |
| 52 | }), | |
| 37 | 53 | }); |
| 38 | 54 | }), |
| 39 | 55 | ); |
| 40 | 56 | |
| 41 | 57 | export const managedRelayClientLayer = (relayUrl: string) => |
| 42 | makeManagedRelayClientLayer({ | |
| 58 | ManagedRelay.layer({ | |
| 43 | 59 | relayUrl, |
| 44 | 60 | clientId: RelayMobileClientId, |
| 45 | 61 | accessTokenStore: managedRelayAccessTokenStore, |
apps/mobile/src/features/cloud/managedRelayTokenStore.ts+6-7| ... | ... | @@ -1,7 +1,4 @@ |
| 1 | import { | |
| 2 | type ManagedRelayAccessTokenCacheEntry, | |
| 3 | type ManagedRelayAccessTokenStore, | |
| 4 | } from "@t3tools/client-runtime/relay"; | |
| 1 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; | |
| 5 | 2 | import * as Data from "effect/Data"; |
| 6 | 3 | import * as Effect from "effect/Effect"; |
| 7 | 4 | import * as Schema from "effect/Schema"; |
| ... | ... | @@ -60,7 +57,7 @@ const loadManagedRelayAccessTokens = Effect.tryPromise({ |
| 60 | 57 | }).pipe( |
| 61 | 58 | Effect.flatMap((encoded) => |
| 62 | 59 | encoded === null |
| 63 | ? Effect.succeed<ReadonlyArray<ManagedRelayAccessTokenCacheEntry>>([]) | |
| 60 | ? Effect.succeed<ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry>>([]) | |
| 64 | 61 | : decodeManagedRelayAccessTokenCache(encoded).pipe( |
| 65 | 62 | Effect.map((cache) => cache.entries), |
| 66 | 63 | Effect.mapError(storeError("Persisted relay access tokens are invalid.")), |
| ... | ... | @@ -68,7 +65,9 @@ const loadManagedRelayAccessTokens = Effect.tryPromise({ |
| 68 | 65 | ), |
| 69 | 66 | ); |
| 70 | 67 | |
| 71 | const saveManagedRelayAccessTokens = (entries: ReadonlyArray<ManagedRelayAccessTokenCacheEntry>) => | |
| 68 | const saveManagedRelayAccessTokens = ( | |
| 69 | entries: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry>, | |
| 70 | ) => | |
| 72 | 71 | encodeManagedRelayAccessTokenCache({ |
| 73 | 72 | version: MANAGED_RELAY_TOKEN_CACHE_VERSION, |
| 74 | 73 | entries, |
| ... | ... | @@ -87,7 +86,7 @@ const clearManagedRelayAccessTokens = Effect.tryPromise({ |
| 87 | 86 | catch: storeError("Could not clear persisted relay access tokens."), |
| 88 | 87 | }); |
| 89 | 88 | |
| 90 | export const managedRelayAccessTokenStore: ManagedRelayAccessTokenStore = { | |
| 89 | export const managedRelayAccessTokenStore: ManagedRelay.ManagedRelayAccessTokenStore = { | |
| 91 | 90 | load: loadManagedRelayAccessTokens.pipe( |
| 92 | 91 | Effect.tapError(logStoreFailure("load")), |
| 93 | 92 | Effect.orElseSucceed(() => []), |
apps/mobile/src/lib/runtime.ts+19-3| ... | ... | @@ -15,7 +15,17 @@ function configuredRelayUrl(): string { |
| 15 | 15 | |
| 16 | 16 | const httpClientLayer = remoteHttpClientLayer(fetch); |
| 17 | 17 | |
| 18 | export const runtimeLayer = Layer.merge( | |
| 18 | type RuntimeLayerSource = | |
| 19 | | ReturnType<typeof managedRelayClientLayer> | |
| 20 | | typeof Socket.layerWebSocketConstructorGlobal | |
| 21 | | typeof cryptoLayer | |
| 22 | | typeof httpClientLayer | |
| 23 | | typeof tracingLayer; | |
| 24 | ||
| 25 | export const runtimeLayer: Layer.Layer< | |
| 26 | Layer.Success<RuntimeLayerSource>, | |
| 27 | Layer.Error<RuntimeLayerSource> | |
| 28 | > = Layer.merge( | |
| 19 | 29 | managedRelayClientLayer(configuredRelayUrl()), |
| 20 | 30 | Socket.layerWebSocketConstructorGlobal, |
| 21 | 31 | ).pipe( |
| ... | ... | @@ -24,6 +34,12 @@ export const runtimeLayer = Layer.merge( |
| 24 | 34 | Layer.provideMerge(tracingLayer.pipe(Layer.provide(httpClientLayer))), |
| 25 | 35 | ); |
| 26 | 36 | |
| 27 | export const runtime = ManagedRuntime.make(runtimeLayer); | |
| 37 | export const runtime: ManagedRuntime.ManagedRuntime< | |
| 38 | Layer.Success<typeof runtimeLayer>, | |
| 39 | Layer.Error<typeof runtimeLayer> | |
| 40 | > = ManagedRuntime.make(runtimeLayer); | |
| 28 | 41 | |
| 29 | export const runtimeContextLayer = Layer.effectContext(runtime.contextEffect); | |
| 42 | export const runtimeContextLayer: Layer.Layer< | |
| 43 | Layer.Success<typeof runtimeLayer>, | |
| 44 | Layer.Error<typeof runtimeLayer> | |
| 45 | > = Layer.effectContext(runtime.contextEffect); |
apps/mobile/src/state/relay.ts+1-1| ... | ... | @@ -2,5 +2,5 @@ import { createRelayEnvironmentDiscoveryAtoms } from "@t3tools/client-runtime/st |
| 2 | 2 | |
| 3 | 3 | import { connectionAtomRuntime } from "../connection/runtime"; |
| 4 | 4 | |
| 5 | export const relayEnvironmentDiscovery = | |
| 5 | export const relayEnvironmentDiscovery: ReturnType<typeof createRelayEnvironmentDiscoveryAtoms> = | |
| 6 | 6 | createRelayEnvironmentDiscoveryAtoms(connectionAtomRuntime); |
apps/web/src/cloud/linkEnvironment.test.ts+9-9| ... | ... | @@ -21,11 +21,7 @@ import { |
| 21 | 21 | } from "@t3tools/client-runtime/connection"; |
| 22 | 22 | import { type RpcSession } from "@t3tools/client-runtime/rpc"; |
| 23 | 23 | import { EnvironmentRegistry } from "@t3tools/client-runtime/connection"; |
| 24 | import { | |
| 25 | managedRelayClientLayer, | |
| 26 | ManagedRelayClient, | |
| 27 | ManagedRelayDpopSigner, | |
| 28 | } from "@t3tools/client-runtime/relay"; | |
| 24 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; | |
| 29 | 25 | import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc"; |
| 30 | 26 | import { __resetDesktopPrimaryAuthForTests } from "../environments/primary/desktopAuth"; |
| 31 | 27 | |
| ... | ... | @@ -60,8 +56,8 @@ vi.mock("./relayClientInstallDialog", () => ({ |
| 60 | 56 | |
| 61 | 57 | const createProof = vi.fn(() => Effect.succeed("dpop-proof")); |
| 62 | 58 | const dpopSignerLayer = Layer.succeed( |
| 63 | ManagedRelayDpopSigner, | |
| 64 | ManagedRelayDpopSigner.of({ | |
| 59 | ManagedRelay.ManagedRelayDpopSigner, | |
| 60 | ManagedRelay.ManagedRelayDpopSigner.of({ | |
| 65 | 61 | thumbprint: Effect.succeed("thumbprint"), |
| 66 | 62 | createProof, |
| 67 | 63 | }), |
| ... | ... | @@ -71,7 +67,7 @@ function relayLayer() { |
| 71 | 67 | const http = remoteHttpClientLayer(globalThis.fetch); |
| 72 | 68 | return Layer.mergeAll( |
| 73 | 69 | http, |
| 74 | managedRelayClientLayer({ | |
| 70 | ManagedRelay.layer({ | |
| 75 | 71 | relayUrl: "https://relay.example.test", |
| 76 | 72 | clientId: RelayWebClientId, |
| 77 | 73 | }).pipe(Layer.provideMerge(dpopSignerLayer), Layer.provide(http)), |
| ... | ... | @@ -129,7 +125,11 @@ function services(options?: Parameters<typeof registryLayer>[0]) { |
| 129 | 125 | } |
| 130 | 126 | |
| 131 | 127 | function withServices<A, E>( |
| 132 | effect: Effect.Effect<A, E, HttpClient.HttpClient | ManagedRelayClient | EnvironmentRegistry>, | |
| 128 | effect: Effect.Effect< | |
| 129 | A, | |
| 130 | E, | |
| 131 | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | EnvironmentRegistry | |
| 132 | >, | |
| 133 | 133 | options?: Parameters<typeof registryLayer>[0], |
| 134 | 134 | ) { |
| 135 | 135 | return effect.pipe(Effect.provide(services(options))); |
apps/web/src/cloud/linkEnvironment.ts+18-12| ... | ... | @@ -25,7 +25,7 @@ import { |
| 25 | 25 | import { EnvironmentRegistry } from "@t3tools/client-runtime/connection"; |
| 26 | 26 | import { request, runStream } from "@t3tools/client-runtime/rpc"; |
| 27 | 27 | import { makeEnvironmentHttpApiClient } from "@t3tools/client-runtime/rpc"; |
| 28 | import { ManagedRelayClient, type ManagedRelayClientError } from "@t3tools/client-runtime/relay"; | |
| 28 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; | |
| 29 | 29 | |
| 30 | 30 | import { |
| 31 | 31 | readPrimaryEnvironmentDescriptor, |
| ... | ... | @@ -164,13 +164,15 @@ function relayProtectedErrorMessage(error: RelayProtectedErrorType): string { |
| 164 | 164 | } |
| 165 | 165 | |
| 166 | 166 | function decodedRelayClientError(message: string) { |
| 167 | return (cause: ManagedRelayClientError) => { | |
| 168 | const relayError = cause.relayError; | |
| 167 | return (cause: ManagedRelay.ManagedRelayClientError) => { | |
| 168 | const relayError = | |
| 169 | cause._tag === "ManagedRelayRequestFailedError" ? cause.relayError : undefined; | |
| 170 | const traceId = cause._tag === "ManagedRelayRequestFailedError" ? cause.traceId : undefined; | |
| 169 | 171 | const detail = relayError ? relayProtectedErrorMessage(relayError) : null; |
| 170 | 172 | return new CloudEnvironmentLinkError({ |
| 171 | 173 | message: detail ? `${message}: ${detail}` : message, |
| 172 | 174 | cause, |
| 173 | ...(cause.traceId ? { traceId: cause.traceId } : {}), | |
| 175 | ...(traceId ? { traceId } : {}), | |
| 174 | 176 | }); |
| 175 | 177 | }; |
| 176 | 178 | } |
| ... | ... | @@ -268,7 +270,7 @@ export function listManagedCloudEnvironments(input: { |
| 268 | 270 | }): Effect.Effect< |
| 269 | 271 | ReadonlyArray<RelayClientEnvironmentRecord>, |
| 270 | 272 | CloudEnvironmentLinkError, |
| 271 | ManagedRelayClient | |
| 273 | ManagedRelay.ManagedRelayClient | |
| 272 | 274 | > { |
| 273 | 275 | return Effect.gen(function* () { |
| 274 | 276 | const configuredRelayUrl = relayUrl(); |
| ... | ... | @@ -277,7 +279,7 @@ export function listManagedCloudEnvironments(input: { |
| 277 | 279 | message: "T3CODE_RELAY_URL is not configured.", |
| 278 | 280 | }); |
| 279 | 281 | } |
| 280 | const relayClient = yield* ManagedRelayClient; | |
| 282 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 281 | 283 | return yield* relayClient |
| 282 | 284 | .listEnvironments({ |
| 283 | 285 | clerkToken: input.clerkToken, |
| ... | ... | @@ -299,7 +301,7 @@ export function listCloudDevices(input: { |
| 299 | 301 | }): Effect.Effect< |
| 300 | 302 | ReadonlyArray<RelayClientDeviceRecord>, |
| 301 | 303 | CloudEnvironmentLinkError, |
| 302 | ManagedRelayClient | |
| 304 | ManagedRelay.ManagedRelayClient | |
| 303 | 305 | > { |
| 304 | 306 | return Effect.gen(function* () { |
| 305 | 307 | if (!relayUrl()) { |
| ... | ... | @@ -307,7 +309,7 @@ export function listCloudDevices(input: { |
| 307 | 309 | message: "T3CODE_RELAY_URL is not configured.", |
| 308 | 310 | }); |
| 309 | 311 | } |
| 310 | const relayClient = yield* ManagedRelayClient; | |
| 312 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 311 | 313 | return yield* relayClient.listDevices({ clerkToken: input.clerkToken }).pipe( |
| 312 | 314 | Effect.mapError( |
| 313 | 315 | (cause) => |
| ... | ... | @@ -351,7 +353,11 @@ export function updatePrimaryCloudPreferences(input: { |
| 351 | 353 | export function unlinkPrimaryEnvironmentFromCloud(input: { |
| 352 | 354 | readonly target: CloudLinkTarget; |
| 353 | 355 | readonly clerkToken: string | null; |
| 354 | }): Effect.Effect<void, CloudEnvironmentLinkError, HttpClient.HttpClient | ManagedRelayClient> { | |
| 356 | }): Effect.Effect< | |
| 357 | void, | |
| 358 | CloudEnvironmentLinkError, | |
| 359 | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | |
| 360 | > { | |
| 355 | 361 | return Effect.gen(function* () { |
| 356 | 362 | const client = yield* makeEnvironmentHttpApiClient(input.target.httpBaseUrl); |
| 357 | 363 | yield* client.connect |
| ... | ... | @@ -360,7 +366,7 @@ export function unlinkPrimaryEnvironmentFromCloud(input: { |
| 360 | 366 | |
| 361 | 367 | const configuredRelayUrl = relayUrl(); |
| 362 | 368 | if (configuredRelayUrl && input.clerkToken) { |
| 363 | const relayClient = yield* ManagedRelayClient; | |
| 369 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 364 | 370 | yield* relayClient |
| 365 | 371 | .unlinkEnvironment({ |
| 366 | 372 | clerkToken: input.clerkToken, |
| ... | ... | @@ -383,7 +389,7 @@ export function linkPrimaryEnvironmentToCloud(input: { |
| 383 | 389 | }): Effect.Effect< |
| 384 | 390 | void, |
| 385 | 391 | CloudEnvironmentLinkError, |
| 386 | EnvironmentRegistry | HttpClient.HttpClient | ManagedRelayClient | |
| 392 | EnvironmentRegistry | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | |
| 387 | 393 | > { |
| 388 | 394 | return Effect.gen(function* () { |
| 389 | 395 | const configuredRelayUrl = relayUrl(); |
| ... | ... | @@ -392,7 +398,7 @@ export function linkPrimaryEnvironmentToCloud(input: { |
| 392 | 398 | message: "T3CODE_RELAY_URL is not configured.", |
| 393 | 399 | }); |
| 394 | 400 | } |
| 395 | const relayClient = yield* ManagedRelayClient; | |
| 401 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 396 | 402 | const environmentClient = yield* makeEnvironmentHttpApiClient(input.target.httpBaseUrl); |
| 397 | 403 | yield* ensureRelayClientAvailable(EnvironmentId.make(input.target.environmentId)); |
| 398 | 404 |
apps/web/src/cloud/managedAuth.tsx+4-2| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { useAuth } from "@clerk/react"; |
| 2 | import { ManagedRelayClient, setManagedRelaySession } from "@t3tools/client-runtime/relay"; | |
| 2 | import { ManagedRelay, setManagedRelaySession } from "@t3tools/client-runtime/relay"; | |
| 3 | 3 | import { |
| 4 | 4 | reportAtomCommandResult, |
| 5 | 5 | settleAsyncResult, |
| ... | ... | @@ -64,7 +64,9 @@ export function ManagedRelayAuthProvider({ children }: { readonly children: Reac |
| 64 | 64 | removeRelayEnvironments(), |
| 65 | 65 | settleAsyncResult(() => |
| 66 | 66 | runtime.runPromiseExit( |
| 67 | ManagedRelayClient.pipe(Effect.flatMap((client) => client.resetTokenCache)), | |
| 67 | ManagedRelay.ManagedRelayClient.pipe( | |
| 68 | Effect.flatMap((client) => client.resetTokenCache), | |
| 69 | ), | |
| 68 | 70 | ), |
| 69 | 71 | ), |
| 70 | 72 | ]); |
apps/web/src/cloud/managedRelayLayer.ts+35-19| ... | ... | @@ -1,8 +1,4 @@ |
| 1 | import { | |
| 2 | managedRelayClientLayer as makeManagedRelayClientLayer, | |
| 3 | ManagedRelayDpopSigner, | |
| 4 | ManagedRelayDpopSignerError, | |
| 5 | } from "@t3tools/client-runtime/relay"; | |
| 1 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; | |
| 6 | 2 | import { RelayWebClientId } from "@t3tools/contracts/relay"; |
| 7 | 3 | import * as Crypto from "effect/Crypto"; |
| 8 | 4 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -18,7 +14,7 @@ import { |
| 18 | 14 | } from "./dpop"; |
| 19 | 15 | |
| 20 | 16 | export const relayDpopSignerLayer = Layer.effect( |
| 21 | ManagedRelayDpopSigner, | |
| 17 | ManagedRelay.ManagedRelayDpopSigner, | |
| 22 | 18 | Effect.gen(function* () { |
| 23 | 19 | const crypto = yield* Crypto.Crypto; |
| 24 | 20 | const keyLoadSemaphore = yield* Semaphore.make(1); |
| ... | ... | @@ -40,27 +36,47 @@ export const relayDpopSignerLayer = Layer.effect( |
| 40 | 36 | }), |
| 41 | 37 | ); |
| 42 | 38 | |
| 43 | return ManagedRelayDpopSigner.of({ | |
| 39 | return ManagedRelay.ManagedRelayDpopSigner.of({ | |
| 44 | 40 | thumbprint: loadOrCreateBrowserDpopKey.pipe( |
| 45 | 41 | Effect.map((proofKey) => proofKey.thumbprint), |
| 46 | Effect.mapError((cause) => new ManagedRelayDpopSignerError({ cause })), | |
| 42 | Effect.mapError( | |
| 43 | (error) => | |
| 44 | new ManagedRelay.ManagedRelayDpopKeyLoadError({ | |
| 45 | keyStore: "indexed-db", | |
| 46 | cause: error, | |
| 47 | }), | |
| 48 | ), | |
| 47 | 49 | Effect.withSpan("web.managedRelayDpopSigner.loadThumbprint"), |
| 48 | 50 | ), |
| 49 | createProof: Effect.fn("web.managedRelayDpopSigner.createProof")( | |
| 50 | function* (input) { | |
| 51 | const proofKey = yield* loadOrCreateBrowserDpopKey; | |
| 52 | return yield* createBrowserDpopProof({ ...input, proofKey }).pipe( | |
| 53 | Effect.provideService(Crypto.Crypto, crypto), | |
| 54 | Effect.map((proof) => proof.proof), | |
| 55 | ); | |
| 56 | }, | |
| 57 | Effect.mapError((cause) => new ManagedRelayDpopSignerError({ cause })), | |
| 58 | ), | |
| 51 | createProof: Effect.fn("web.managedRelayDpopSigner.createProof")(function* (input) { | |
| 52 | const proofKey = yield* loadOrCreateBrowserDpopKey.pipe( | |
| 53 | Effect.mapError( | |
| 54 | (error) => | |
| 55 | new ManagedRelay.ManagedRelayDpopProofCreationError({ | |
| 56 | method: input.method, | |
| 57 | url: input.url, | |
| 58 | cause: error, | |
| 59 | }), | |
| 60 | ), | |
| 61 | ); | |
| 62 | return yield* createBrowserDpopProof({ ...input, proofKey }).pipe( | |
| 63 | Effect.provideService(Crypto.Crypto, crypto), | |
| 64 | Effect.map((proof) => proof.proof), | |
| 65 | Effect.mapError( | |
| 66 | (error) => | |
| 67 | new ManagedRelay.ManagedRelayDpopProofCreationError({ | |
| 68 | method: input.method, | |
| 69 | url: input.url, | |
| 70 | cause: error, | |
| 71 | }), | |
| 72 | ), | |
| 73 | ); | |
| 74 | }), | |
| 59 | 75 | }); |
| 60 | 76 | }), |
| 61 | 77 | ); |
| 62 | 78 | |
| 63 | 79 | export const managedRelayClientLayer = (relayUrl: string) => |
| 64 | makeManagedRelayClientLayer({ relayUrl, clientId: RelayWebClientId }).pipe( | |
| 80 | ManagedRelay.layer({ relayUrl, clientId: RelayWebClientId }).pipe( | |
| 65 | 81 | Layer.provideMerge(relayDpopSignerLayer), |
| 66 | 82 | ); |
apps/web/src/cloud/managedRelayState.ts+5-3| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | 2 | import { |
| 3 | 3 | createManagedRelayQueryManager, |
| 4 | ManagedRelayClient, | |
| 4 | ManagedRelay, | |
| 5 | 5 | managedRelaySessionAtom, |
| 6 | 6 | readManagedRelaySnapshotState, |
| 7 | 7 | } from "@t3tools/client-runtime/relay"; |
| ... | ... | @@ -20,8 +20,10 @@ import { appAtomRegistry } from "../rpc/atomRegistry"; |
| 20 | 20 | |
| 21 | 21 | const managedRelayAtomRuntime = Atom.runtime( |
| 22 | 22 | Layer.effect( |
| 23 | ManagedRelayClient, | |
| 24 | runtime.contextEffect.pipe(Effect.map((context) => Context.get(context, ManagedRelayClient))), | |
| 23 | ManagedRelay.ManagedRelayClient, | |
| 24 | runtime.contextEffect.pipe( | |
| 25 | Effect.map((context) => Context.get(context, ManagedRelay.ManagedRelayClient)), | |
| 26 | ), | |
| 25 | 27 | ), |
| 26 | 28 | ); |
| 27 | 29 |
apps/web/src/lib/runtime.ts+19-3| ... | ... | @@ -24,6 +24,13 @@ const relayTracingLayer = makeRelayClientTracingLayer(resolveRelayTracingConfig( |
| 24 | 24 | client: typeof window !== "undefined" && window.desktopBridge ? "desktop" : "web", |
| 25 | 25 | }).pipe(Layer.provide(httpClientLayer)); |
| 26 | 26 | |
| 27 | type RuntimeLayerSource = | |
| 28 | | typeof httpClientLayer | |
| 29 | | typeof browserCryptoLayer | |
| 30 | | typeof Socket.layerWebSocketConstructorGlobal | |
| 31 | | typeof relayTracingLayer | |
| 32 | | ReturnType<typeof managedRelayClientLayer>; | |
| 33 | ||
| 27 | 34 | export const remoteHttpRuntime = ManagedRuntime.make(httpClientLayer); |
| 28 | 35 | |
| 29 | 36 | const primaryHttpRuntime = ManagedRuntime.make( |
| ... | ... | @@ -47,7 +54,10 @@ export function __setPrimaryHttpRunnerForTests(runner?: PrimaryHttpEffectRunner) |
| 47 | 54 | primaryHttpRunner = runner ?? livePrimaryHttpRunner; |
| 48 | 55 | } |
| 49 | 56 | |
| 50 | export const runtimeLayer = Layer.mergeAll( | |
| 57 | export const runtimeLayer: Layer.Layer< | |
| 58 | Layer.Success<RuntimeLayerSource>, | |
| 59 | Layer.Error<RuntimeLayerSource> | |
| 60 | > = Layer.mergeAll( | |
| 51 | 61 | httpClientLayer, |
| 52 | 62 | browserCryptoLayer, |
| 53 | 63 | Socket.layerWebSocketConstructorGlobal, |
| ... | ... | @@ -57,6 +67,12 @@ export const runtimeLayer = Layer.mergeAll( |
| 57 | 67 | ), |
| 58 | 68 | ); |
| 59 | 69 | |
| 60 | export const runtime = ManagedRuntime.make(runtimeLayer); | |
| 70 | export const runtime: ManagedRuntime.ManagedRuntime< | |
| 71 | Layer.Success<typeof runtimeLayer>, | |
| 72 | Layer.Error<typeof runtimeLayer> | |
| 73 | > = ManagedRuntime.make(runtimeLayer); | |
| 61 | 74 | |
| 62 | export const runtimeContextLayer = Layer.effectContext(runtime.contextEffect); | |
| 75 | export const runtimeContextLayer: Layer.Layer< | |
| 76 | Layer.Success<typeof runtimeLayer>, | |
| 77 | Layer.Error<typeof runtimeLayer> | |
| 78 | > = Layer.effectContext(runtime.contextEffect); |
apps/web/src/state/environments.ts+2-1| ... | ... | @@ -3,6 +3,7 @@ import { |
| 3 | 3 | connectionCatalogDisplayUrl, |
| 4 | 4 | type EnvironmentPresentation as BaseEnvironmentPresentation, |
| 5 | 5 | } from "@t3tools/client-runtime/connection"; |
| 6 | import { Discovery } from "@t3tools/client-runtime/relay"; | |
| 6 | 7 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 7 | 8 | import * as Option from "effect/Option"; |
| 8 | 9 | import { useMemo } from "react"; |
| ... | ... | @@ -81,7 +82,7 @@ export function useEnvironmentHttpBaseUrl(environmentId: EnvironmentId | null): |
| 81 | 82 | return Option.isSome(prepared) ? prepared.value.httpBaseUrl : null; |
| 82 | 83 | } |
| 83 | 84 | |
| 84 | export function useRelayEnvironmentDiscovery() { | |
| 85 | export function useRelayEnvironmentDiscovery(): Discovery.RelayEnvironmentDiscoveryState { | |
| 85 | 86 | return useAtomValue(relayEnvironmentDiscovery.stateValueAtom); |
| 86 | 87 | } |
| 87 | 88 |
apps/web/src/state/relay.ts+1-1| ... | ... | @@ -2,5 +2,5 @@ import { createRelayEnvironmentDiscoveryAtoms } from "@t3tools/client-runtime/st |
| 2 | 2 | |
| 3 | 3 | import { connectionAtomRuntime } from "../connection/runtime"; |
| 4 | 4 | |
| 5 | export const relayEnvironmentDiscovery = | |
| 5 | export const relayEnvironmentDiscovery: ReturnType<typeof createRelayEnvironmentDiscoveryAtoms> = | |
| 6 | 6 | createRelayEnvironmentDiscoveryAtoms(connectionAtomRuntime); |
packages/client-runtime/src/authorization/layer.test.ts-1| ... | ... | @@ -118,7 +118,6 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* ( |
| 118 | 118 | createProof: (proofInput) => |
| 119 | 119 | Ref.update(proofInputs, (current) => [...current, proofInput]).pipe( |
| 120 | 120 | Effect.as(`proof:${proofInput.url}`), |
| 121 | Effect.mapError((cause) => new ManagedRelay.ManagedRelayDpopSignerError({ cause })), | |
| 122 | 121 | ), |
| 123 | 122 | }); |
| 124 | 123 | const layer = RemoteEnvironmentAuthorization.layer.pipe( |
packages/client-runtime/src/connection/errors.ts+32-14| ... | ... | @@ -74,21 +74,39 @@ function relayProtectedError(error: RelayProtectedError): ConnectionAttemptError |
| 74 | 74 | } |
| 75 | 75 | |
| 76 | 76 | export function mapManagedRelayError(error: ManagedRelayClientError): ConnectionAttemptError { |
| 77 | if (error.relayError) { | |
| 78 | return relayProtectedError(error.relayError); | |
| 79 | } | |
| 80 | if (error.cause?._tag === "ManagedRelayRequestTimeoutError") { | |
| 81 | return new ConnectionTransientError({ | |
| 82 | reason: "timeout", | |
| 83 | detail: error.message, | |
| 84 | ...(error.traceId ? { traceId: error.traceId } : {}), | |
| 85 | }); | |
| 77 | switch (error._tag) { | |
| 78 | case "ManagedRelayRequestFailedError": | |
| 79 | if (error.relayError) { | |
| 80 | return relayProtectedError(error.relayError); | |
| 81 | } | |
| 82 | return new ConnectionTransientError({ | |
| 83 | reason: "relay-unavailable", | |
| 84 | detail: error.message, | |
| 85 | ...(error.traceId ? { traceId: error.traceId } : {}), | |
| 86 | }); | |
| 87 | case "ManagedRelayRequestTimeoutError": | |
| 88 | return new ConnectionTransientError({ | |
| 89 | reason: "timeout", | |
| 90 | detail: error.message, | |
| 91 | }); | |
| 92 | case "ManagedRelayUrlInvalidError": | |
| 93 | return new ConnectionBlockedError({ | |
| 94 | reason: "configuration", | |
| 95 | detail: error.message, | |
| 96 | }); | |
| 97 | case "ManagedRelayAccessTokenScopesUnexpectedError": | |
| 98 | return new ConnectionBlockedError({ | |
| 99 | reason: "permission", | |
| 100 | detail: error.message, | |
| 101 | }); | |
| 102 | case "ManagedRelayDpopKeyLoadError": | |
| 103 | case "ManagedRelayTokenProofCreationError": | |
| 104 | case "ManagedRelayRequestProofCreationError": | |
| 105 | return new ConnectionBlockedError({ | |
| 106 | reason: "authentication", | |
| 107 | detail: error.message, | |
| 108 | }); | |
| 86 | 109 | } |
| 87 | return new ConnectionTransientError({ | |
| 88 | reason: "relay-unavailable", | |
| 89 | detail: error.message, | |
| 90 | ...(error.traceId ? { traceId: error.traceId } : {}), | |
| 91 | }); | |
| 92 | 110 | } |
| 93 | 111 | |
| 94 | 112 | export function mapRemoteEnvironmentError( |
packages/client-runtime/src/connection/resolver.test.ts+3-5| ... | ... | @@ -446,11 +446,9 @@ describe("ConnectionResolver", () => { |
| 446 | 446 | const brokerLayer = yield* makeDependencies({ |
| 447 | 447 | connectEnvironment: () => |
| 448 | 448 | Effect.fail( |
| 449 | new ManagedRelay.ManagedRelayClientError({ | |
| 450 | message: "Relay timed out.", | |
| 451 | cause: new ManagedRelay.ManagedRelayRequestTimeoutError({ | |
| 452 | message: "Relay timed out.", | |
| 453 | }), | |
| 449 | new ManagedRelay.ManagedRelayRequestTimeoutError({ | |
| 450 | activity: "Relay environment connection", | |
| 451 | timeoutMs: ManagedRelay.MANAGED_RELAY_REQUEST_TIMEOUT_MS, | |
| 454 | 452 | }), |
| 455 | 453 | ), |
| 456 | 454 | }); |
packages/client-runtime/src/relay/discovery.test.ts+6-7| ... | ... | @@ -258,11 +258,9 @@ describe("RelayEnvironmentDiscovery", () => { |
| 258 | 258 | relayUrl: "https://relay.example.test", |
| 259 | 259 | listEnvironments: () => |
| 260 | 260 | Effect.fail( |
| 261 | new ManagedRelay.ManagedRelayClientError({ | |
| 262 | message: "Relay environment listing timed out.", | |
| 263 | cause: new ManagedRelay.ManagedRelayRequestTimeoutError({ | |
| 264 | message: "Relay environment listing timed out.", | |
| 265 | }), | |
| 261 | new ManagedRelay.ManagedRelayRequestTimeoutError({ | |
| 262 | activity: "Relay environment listing", | |
| 263 | timeoutMs: ManagedRelay.MANAGED_RELAY_REQUEST_TIMEOUT_MS, | |
| 266 | 264 | }), |
| 267 | 265 | ), |
| 268 | 266 | getEnvironmentStatus: () => Effect.die("unused"), |
| ... | ... | @@ -327,8 +325,9 @@ describe("RelayEnvironmentDiscovery", () => { |
| 327 | 325 | |
| 328 | 326 | yield* Ref.set( |
| 329 | 327 | harness.listFailure, |
| 330 | new ManagedRelay.ManagedRelayClientError({ | |
| 331 | message: "Relay environment listing failed.", | |
| 328 | new ManagedRelay.ManagedRelayRequestFailedError({ | |
| 329 | action: "list relay-managed environments", | |
| 330 | cause: new Error("Relay request failed."), | |
| 332 | 331 | }), |
| 333 | 332 | ); |
| 334 | 333 | yield* discovery.refresh; |
packages/client-runtime/src/relay/index.ts+2-2| ... | ... | @@ -1,3 +1,3 @@ |
| 1 | export * from "./discovery.ts"; | |
| 2 | export * from "./managedRelay.ts"; | |
| 1 | export * as Discovery from "./discovery.ts"; | |
| 2 | export * as ManagedRelay from "./managedRelay.ts"; | |
| 3 | 3 | export * from "./managedRelayState.ts"; |
packages/client-runtime/src/relay/managedRelay.test.ts+30-34| ... | ... | @@ -8,31 +8,24 @@ import * as Layer from "effect/Layer"; |
| 8 | 8 | import * as Tracer from "effect/Tracer"; |
| 9 | 9 | import * as TestClock from "effect/testing/TestClock"; |
| 10 | 10 | |
| 11 | import { | |
| 12 | MANAGED_RELAY_REQUEST_TIMEOUT_MS, | |
| 13 | ManagedRelayClient, | |
| 14 | ManagedRelayDpopSigner, | |
| 15 | managedRelayClientLayer, | |
| 16 | type ManagedRelayAccessTokenCacheEntry, | |
| 17 | type ManagedRelayAccessTokenStore, | |
| 18 | type ManagedRelayDpopProofInput, | |
| 19 | } from "./managedRelay.ts"; | |
| 11 | import * as ManagedRelay from "./managedRelay.ts"; | |
| 20 | 12 | import { remoteHttpClientLayer } from "../rpc/http.ts"; |
| 21 | 13 | |
| 22 | 14 | function managedRelayTestLayer( |
| 23 | 15 | fetchFn: typeof globalThis.fetch, |
| 24 | 16 | relayUrl = "https://relay.example.test", |
| 25 | accessTokenStore?: ManagedRelayAccessTokenStore, | |
| 17 | accessTokenStore?: ManagedRelay.ManagedRelayAccessTokenStore, | |
| 26 | 18 | ) { |
| 27 | 19 | const httpClientLayer = remoteHttpClientLayer(fetchFn); |
| 28 | 20 | const signerLayer = Layer.succeed( |
| 29 | ManagedRelayDpopSigner, | |
| 30 | ManagedRelayDpopSigner.of({ | |
| 21 | ManagedRelay.ManagedRelayDpopSigner, | |
| 22 | ManagedRelay.ManagedRelayDpopSigner.of({ | |
| 31 | 23 | thumbprint: Effect.succeed("client-thumbprint"), |
| 32 | createProof: (input: ManagedRelayDpopProofInput) => Effect.succeed(`proof:${input.url}`), | |
| 24 | createProof: (input: ManagedRelay.ManagedRelayDpopProofInput) => | |
| 25 | Effect.succeed(`proof:${input.url}`), | |
| 33 | 26 | }), |
| 34 | 27 | ); |
| 35 | return managedRelayClientLayer({ | |
| 28 | return ManagedRelay.layer({ | |
| 36 | 29 | relayUrl, |
| 37 | 30 | clientId: "t3-mobile", |
| 38 | 31 | ...(accessTokenStore ? { accessTokenStore } : {}), |
| ... | ... | @@ -90,7 +83,7 @@ describe("ManagedRelayClient", () => { |
| 90 | 83 | }) satisfies typeof globalThis.fetch; |
| 91 | 84 | |
| 92 | 85 | return Effect.gen(function* () { |
| 93 | const relayClient = yield* ManagedRelayClient; | |
| 86 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 94 | 87 | yield* relayClient.getEnvironmentStatus({ |
| 95 | 88 | clerkToken: clerkToken("user-1", "session-1"), |
| 96 | 89 | scopes: [RelayEnvironmentStatusScope], |
| ... | ... | @@ -124,13 +117,14 @@ describe("ManagedRelayClient", () => { |
| 124 | 117 | }) satisfies typeof globalThis.fetch; |
| 125 | 118 | |
| 126 | 119 | return Effect.gen(function* () { |
| 127 | const relayClient = yield* ManagedRelayClient; | |
| 120 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 128 | 121 | const error = yield* relayClient |
| 129 | 122 | .listEnvironments({ clerkToken: "clerk-token" }) |
| 130 | 123 | .pipe(Effect.flip); |
| 131 | 124 | |
| 132 | 125 | expect(error).toMatchObject({ |
| 133 | _tag: "ManagedRelayClientError", | |
| 126 | _tag: "ManagedRelayUrlInvalidError", | |
| 127 | relayUrl: "http://relay.example.test", | |
| 134 | 128 | message: "Relay URL must be a secure absolute HTTPS origin.", |
| 135 | 129 | }); |
| 136 | 130 | expect(requestCount).toBe(0); |
| ... | ... | @@ -175,7 +169,7 @@ describe("ManagedRelayClient", () => { |
| 175 | 169 | }) satisfies typeof globalThis.fetch; |
| 176 | 170 | |
| 177 | 171 | return Effect.gen(function* () { |
| 178 | const relayClient = yield* ManagedRelayClient; | |
| 172 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 179 | 173 | const statusInput = { |
| 180 | 174 | clerkToken: clerkToken("user-1", "session-1"), |
| 181 | 175 | scopes: [RelayEnvironmentStatusScope], |
| ... | ... | @@ -198,8 +192,8 @@ describe("ManagedRelayClient", () => { |
| 198 | 192 | |
| 199 | 193 | it.effect("reuses a persisted token across runtimes and Clerk session token rotation", () => { |
| 200 | 194 | let tokenExchangeCount = 0; |
| 201 | let persistedTokens: ReadonlyArray<ManagedRelayAccessTokenCacheEntry> = []; | |
| 202 | const accessTokenStore: ManagedRelayAccessTokenStore = { | |
| 195 | let persistedTokens: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry> = []; | |
| 196 | const accessTokenStore: ManagedRelay.ManagedRelayAccessTokenStore = { | |
| 203 | 197 | load: Effect.sync(() => persistedTokens), |
| 204 | 198 | save: (entries) => |
| 205 | 199 | Effect.sync(() => { |
| ... | ... | @@ -252,7 +246,7 @@ describe("ManagedRelayClient", () => { |
| 252 | 246 | |
| 253 | 247 | return Effect.gen(function* () { |
| 254 | 248 | yield* Effect.gen(function* () { |
| 255 | const relayClient = yield* ManagedRelayClient; | |
| 249 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 256 | 250 | yield* relayClient.getEnvironmentStatus(statusInput(clerkToken("user-1", "session-1"))); |
| 257 | 251 | }).pipe(Effect.provide(managedRelayTestLayer(fetchFn, undefined, accessTokenStore))); |
| 258 | 252 | |
| ... | ... | @@ -260,7 +254,7 @@ describe("ManagedRelayClient", () => { |
| 260 | 254 | expect(persistedTokens).toHaveLength(1); |
| 261 | 255 | |
| 262 | 256 | yield* Effect.gen(function* () { |
| 263 | const relayClient = yield* ManagedRelayClient; | |
| 257 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 264 | 258 | yield* relayClient.getEnvironmentStatus(statusInput(clerkToken("user-1", "session-2"))); |
| 265 | 259 | }).pipe(Effect.provide(managedRelayTestLayer(fetchFn, undefined, accessTokenStore))); |
| 266 | 260 | |
| ... | ... | @@ -271,7 +265,7 @@ describe("ManagedRelayClient", () => { |
| 271 | 265 | it.effect("refreshes a persisted DPoP token once when the relay rejects it", () => { |
| 272 | 266 | let tokenExchangeCount = 0; |
| 273 | 267 | const statusTokens: Array<string | null> = []; |
| 274 | let persistedTokens: ReadonlyArray<ManagedRelayAccessTokenCacheEntry> = [ | |
| 268 | let persistedTokens: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry> = [ | |
| 275 | 269 | { |
| 276 | 270 | accountId: "user-1", |
| 277 | 271 | clientId: "t3-mobile", |
| ... | ... | @@ -282,7 +276,7 @@ describe("ManagedRelayClient", () => { |
| 282 | 276 | expiresAtMillis: Number.MAX_SAFE_INTEGER, |
| 283 | 277 | }, |
| 284 | 278 | ]; |
| 285 | const accessTokenStore: ManagedRelayAccessTokenStore = { | |
| 279 | const accessTokenStore: ManagedRelay.ManagedRelayAccessTokenStore = { | |
| 286 | 280 | load: Effect.sync(() => persistedTokens), |
| 287 | 281 | save: (entries) => |
| 288 | 282 | Effect.sync(() => { |
| ... | ... | @@ -344,7 +338,7 @@ describe("ManagedRelayClient", () => { |
| 344 | 338 | }) satisfies typeof globalThis.fetch; |
| 345 | 339 | |
| 346 | 340 | return Effect.gen(function* () { |
| 347 | const relayClient = yield* ManagedRelayClient; | |
| 341 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 348 | 342 | const result = yield* relayClient.getEnvironmentStatus({ |
| 349 | 343 | clerkToken: clerkToken("user-1", "session-1"), |
| 350 | 344 | scopes: [RelayEnvironmentStatusScope], |
| ... | ... | @@ -363,8 +357,8 @@ describe("ManagedRelayClient", () => { |
| 363 | 357 | }); |
| 364 | 358 | |
| 365 | 359 | it.effect("does not persist tokens when the Clerk subject cannot be decoded", () => { |
| 366 | let persistedTokens: ReadonlyArray<ManagedRelayAccessTokenCacheEntry> = []; | |
| 367 | const accessTokenStore: ManagedRelayAccessTokenStore = { | |
| 360 | let persistedTokens: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry> = []; | |
| 361 | const accessTokenStore: ManagedRelay.ManagedRelayAccessTokenStore = { | |
| 368 | 362 | load: Effect.succeed([]), |
| 369 | 363 | save: (entries) => |
| 370 | 364 | Effect.sync(() => { |
| ... | ... | @@ -407,7 +401,7 @@ describe("ManagedRelayClient", () => { |
| 407 | 401 | }) satisfies typeof globalThis.fetch; |
| 408 | 402 | |
| 409 | 403 | return Effect.gen(function* () { |
| 410 | const relayClient = yield* ManagedRelayClient; | |
| 404 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 411 | 405 | yield* relayClient.getEnvironmentStatus({ |
| 412 | 406 | clerkToken: "not-a-jwt", |
| 413 | 407 | scopes: [RelayEnvironmentStatusScope], |
| ... | ... | @@ -423,17 +417,19 @@ describe("ManagedRelayClient", () => { |
| 423 | 417 | new Promise<Response>(() => undefined)) satisfies typeof globalThis.fetch; |
| 424 | 418 | |
| 425 | 419 | return Effect.gen(function* () { |
| 426 | const relayClient = yield* ManagedRelayClient; | |
| 420 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 427 | 421 | const errorFiber = yield* relayClient |
| 428 | 422 | .listEnvironments({ clerkToken: "clerk-token" }) |
| 429 | 423 | .pipe(Effect.flip, Effect.forkScoped); |
| 430 | 424 | |
| 431 | 425 | yield* Effect.yieldNow; |
| 432 | yield* TestClock.adjust(Duration.millis(MANAGED_RELAY_REQUEST_TIMEOUT_MS)); | |
| 426 | yield* TestClock.adjust(Duration.millis(ManagedRelay.MANAGED_RELAY_REQUEST_TIMEOUT_MS)); | |
| 433 | 427 | const error = yield* Fiber.join(errorFiber); |
| 434 | 428 | |
| 435 | 429 | expect(error).toMatchObject({ |
| 436 | _tag: "ManagedRelayClientError", | |
| 430 | _tag: "ManagedRelayRequestTimeoutError", | |
| 431 | activity: "Relay environment listing", | |
| 432 | timeoutMs: ManagedRelay.MANAGED_RELAY_REQUEST_TIMEOUT_MS, | |
| 437 | 433 | message: "Relay environment listing timed out.", |
| 438 | 434 | }); |
| 439 | 435 | }).pipe(Effect.provide(Layer.merge(TestClock.layer(), managedRelayTestLayer(fetchFn)))); |
| ... | ... | @@ -454,13 +450,13 @@ describe("ManagedRelayClient", () => { |
| 454 | 450 | )) satisfies typeof globalThis.fetch; |
| 455 | 451 | |
| 456 | 452 | return Effect.gen(function* () { |
| 457 | const relayClient = yield* ManagedRelayClient; | |
| 453 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 458 | 454 | const error = yield* relayClient |
| 459 | 455 | .listEnvironments({ clerkToken: "clerk-token" }) |
| 460 | 456 | .pipe(Effect.flip); |
| 461 | 457 | |
| 462 | 458 | expect(error).toMatchObject({ |
| 463 | _tag: "ManagedRelayClientError", | |
| 459 | _tag: "ManagedRelayRequestFailedError", | |
| 464 | 460 | traceId: "trace-managed-relay", |
| 465 | 461 | }); |
| 466 | 462 | }).pipe(Effect.provide(managedRelayTestLayer(fetchFn))); |
| ... | ... | @@ -499,7 +495,7 @@ describe("ManagedRelayClient", () => { |
| 499 | 495 | }) satisfies typeof globalThis.fetch; |
| 500 | 496 | |
| 501 | 497 | return Effect.gen(function* () { |
| 502 | const relayClient = yield* ManagedRelayClient; | |
| 498 | const relayClient = yield* ManagedRelay.ManagedRelayClient; | |
| 503 | 499 | const devices = yield* relayClient.listDevices({ clerkToken: "clerk-token" }); |
| 504 | 500 | expect(devices).toMatchObject([ |
| 505 | 501 | { |
packages/client-runtime/src/relay/managedRelay.ts+661-546| ... | ... | @@ -5,7 +5,7 @@ import { |
| 5 | 5 | type RelayClientDeviceRecord, |
| 6 | 6 | RelayConnectEnvironmentEndpoint, |
| 7 | 7 | type RelayDeviceRegistrationRequest, |
| 8 | type RelayDpopAccessTokenScope, | |
| 8 | RelayDpopAccessTokenScope, | |
| 9 | 9 | RelayDpopTokenExchangeGrantType, |
| 10 | 10 | type RelayEnvironmentConnectRequest, |
| 11 | 11 | type RelayEnvironmentConnectResponse, |
| ... | ... | @@ -33,58 +33,184 @@ import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; |
| 33 | 33 | import { normalizeSecureRelayUrl } from "@t3tools/shared/relayUrl"; |
| 34 | 34 | import * as Clock from "effect/Clock"; |
| 35 | 35 | import * as Context from "effect/Context"; |
| 36 | import * as Data from "effect/Data"; | |
| 37 | 36 | import * as Duration from "effect/Duration"; |
| 38 | 37 | import * as Effect from "effect/Effect"; |
| 39 | 38 | import * as Layer from "effect/Layer"; |
| 40 | 39 | import * as Option from "effect/Option"; |
| 41 | 40 | import * as Schema from "effect/Schema"; |
| 42 | 41 | import * as SynchronizedRef from "effect/SynchronizedRef"; |
| 43 | import { HttpClientError } from "effect/unstable/http"; | |
| 44 | import type { HttpMethod } from "effect/unstable/http/HttpMethod"; | |
| 42 | import * as HttpClientError from "effect/unstable/http/HttpClientError"; | |
| 43 | import type * as HttpMethod from "effect/unstable/http/HttpMethod"; | |
| 45 | 44 | import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"; |
| 46 | 45 | |
| 47 | 46 | export interface ManagedRelayDpopProofInput { |
| 48 | readonly method: HttpMethod; | |
| 47 | readonly method: HttpMethod.HttpMethod; | |
| 49 | 48 | readonly url: string; |
| 50 | 49 | readonly accessToken?: string; |
| 51 | 50 | } |
| 52 | 51 | |
| 53 | export class ManagedRelayDpopSignerError extends Data.TaggedError("ManagedRelayDpopSignerError")<{ | |
| 54 | readonly cause: unknown; | |
| 55 | }> {} | |
| 52 | export class ManagedRelayDpopKeyLoadError extends Schema.TaggedErrorClass<ManagedRelayDpopKeyLoadError>()( | |
| 53 | "ManagedRelayDpopKeyLoadError", | |
| 54 | { | |
| 55 | keyStore: Schema.Literals(["expo-secure-store", "indexed-db"]), | |
| 56 | cause: Schema.Defect(), | |
| 57 | }, | |
| 58 | ) { | |
| 59 | override get message(): string { | |
| 60 | return "Could not load relay DPoP proof key."; | |
| 61 | } | |
| 62 | } | |
| 63 | ||
| 64 | export class ManagedRelayDpopProofCreationError extends Schema.TaggedErrorClass<ManagedRelayDpopProofCreationError>()( | |
| 65 | "ManagedRelayDpopProofCreationError", | |
| 66 | { | |
| 67 | method: Schema.String, | |
| 68 | url: Schema.String, | |
| 69 | cause: Schema.Defect(), | |
| 70 | }, | |
| 71 | ) { | |
| 72 | override get message(): string { | |
| 73 | return `Could not create the relay DPoP proof for ${this.method} ${this.url}.`; | |
| 74 | } | |
| 75 | } | |
| 56 | 76 | |
| 57 | export class ManagedRelayRequestTimeoutError extends Data.TaggedError( | |
| 77 | export const ManagedRelayDpopSignerError = Schema.Union([ | |
| 78 | ManagedRelayDpopKeyLoadError, | |
| 79 | ManagedRelayDpopProofCreationError, | |
| 80 | ]); | |
| 81 | export type ManagedRelayDpopSignerError = typeof ManagedRelayDpopSignerError.Type; | |
| 82 | ||
| 83 | export const ManagedRelayRequestAction = Schema.Literals([ | |
| 84 | "exchange relay DPoP access token", | |
| 85 | "list relay-managed environments", | |
| 86 | "list relay client devices", | |
| 87 | "create relay environment link challenge", | |
| 88 | "link relay environment", | |
| 89 | "unlink relay environment", | |
| 90 | "get relay environment status", | |
| 91 | "connect relay environment", | |
| 92 | "register relay mobile device", | |
| 93 | "unregister relay mobile device", | |
| 94 | "register relay live activity", | |
| 95 | ]); | |
| 96 | export type ManagedRelayRequestAction = typeof ManagedRelayRequestAction.Type; | |
| 97 | ||
| 98 | export const ManagedRelayRequestActivity = Schema.Literals([ | |
| 99 | "Relay DPoP access token exchange", | |
| 100 | "Relay environment listing", | |
| 101 | "Relay client device listing", | |
| 102 | "Relay environment link challenge", | |
| 103 | "Relay environment linking", | |
| 104 | "Relay environment unlinking", | |
| 105 | "Relay environment status request", | |
| 106 | "Relay environment connection", | |
| 107 | "Relay mobile device registration", | |
| 108 | "Relay mobile device unregistration", | |
| 109 | "Relay Live Activity registration", | |
| 110 | ]); | |
| 111 | export type ManagedRelayRequestActivity = typeof ManagedRelayRequestActivity.Type; | |
| 112 | ||
| 113 | export class ManagedRelayRequestTimeoutError extends Schema.TaggedErrorClass<ManagedRelayRequestTimeoutError>()( | |
| 58 | 114 | "ManagedRelayRequestTimeoutError", |
| 59 | )<{ | |
| 60 | readonly message: string; | |
| 61 | }> {} | |
| 115 | { | |
| 116 | activity: ManagedRelayRequestActivity, | |
| 117 | timeoutMs: Schema.Number, | |
| 118 | }, | |
| 119 | ) { | |
| 120 | override get message(): string { | |
| 121 | return `${this.activity} timed out.`; | |
| 122 | } | |
| 123 | } | |
| 124 | ||
| 125 | export class ManagedRelayUrlInvalidError extends Schema.TaggedErrorClass<ManagedRelayUrlInvalidError>()( | |
| 126 | "ManagedRelayUrlInvalidError", | |
| 127 | { | |
| 128 | relayUrl: Schema.String, | |
| 129 | }, | |
| 130 | ) { | |
| 131 | override get message(): string { | |
| 132 | return "Relay URL must be a secure absolute HTTPS origin."; | |
| 133 | } | |
| 134 | } | |
| 135 | ||
| 136 | export class ManagedRelayRequestFailedError extends Schema.TaggedErrorClass<ManagedRelayRequestFailedError>()( | |
| 137 | "ManagedRelayRequestFailedError", | |
| 138 | { | |
| 139 | action: ManagedRelayRequestAction, | |
| 140 | cause: Schema.Defect(), | |
| 141 | relayError: Schema.optional(RelayProtectedError), | |
| 142 | traceId: Schema.optional(Schema.String), | |
| 143 | }, | |
| 144 | ) { | |
| 145 | override get message(): string { | |
| 146 | return `Could not ${this.action}.`; | |
| 147 | } | |
| 148 | } | |
| 149 | ||
| 150 | export class ManagedRelayAccessTokenScopesUnexpectedError extends Schema.TaggedErrorClass<ManagedRelayAccessTokenScopesUnexpectedError>()( | |
| 151 | "ManagedRelayAccessTokenScopesUnexpectedError", | |
| 152 | { | |
| 153 | requestedScopes: Schema.Array(RelayDpopAccessTokenScope), | |
| 154 | grantedScope: Schema.String, | |
| 155 | }, | |
| 156 | ) { | |
| 157 | override get message(): string { | |
| 158 | return "Relay granted unexpected DPoP access token scopes."; | |
| 159 | } | |
| 160 | } | |
| 161 | ||
| 162 | export class ManagedRelayTokenProofCreationError extends Schema.TaggedErrorClass<ManagedRelayTokenProofCreationError>()( | |
| 163 | "ManagedRelayTokenProofCreationError", | |
| 164 | { | |
| 165 | method: Schema.String, | |
| 166 | url: Schema.String, | |
| 167 | cause: Schema.Defect(), | |
| 168 | }, | |
| 169 | ) { | |
| 170 | override get message(): string { | |
| 171 | return "Could not create relay token DPoP proof."; | |
| 172 | } | |
| 173 | } | |
| 174 | ||
| 175 | export class ManagedRelayRequestProofCreationError extends Schema.TaggedErrorClass<ManagedRelayRequestProofCreationError>()( | |
| 176 | "ManagedRelayRequestProofCreationError", | |
| 177 | { | |
| 178 | method: Schema.String, | |
| 179 | url: Schema.String, | |
| 180 | cause: Schema.Defect(), | |
| 181 | }, | |
| 182 | ) { | |
| 183 | override get message(): string { | |
| 184 | return "Could not create relay request DPoP proof."; | |
| 185 | } | |
| 186 | } | |
| 187 | ||
| 188 | export const ManagedRelayClientError = Schema.Union([ | |
| 189 | ManagedRelayUrlInvalidError, | |
| 190 | ManagedRelayRequestFailedError, | |
| 191 | ManagedRelayRequestTimeoutError, | |
| 192 | ManagedRelayAccessTokenScopesUnexpectedError, | |
| 193 | ManagedRelayDpopKeyLoadError, | |
| 194 | ManagedRelayTokenProofCreationError, | |
| 195 | ManagedRelayRequestProofCreationError, | |
| 196 | ]); | |
| 197 | export type ManagedRelayClientError = typeof ManagedRelayClientError.Type; | |
| 62 | 198 | |
| 63 | 199 | type RelayHttpRequestError = |
| 64 | 200 | | RelayProtectedErrorType |
| 65 | 201 | | HttpClientError.HttpClientError |
| 66 | | Schema.SchemaError | |
| 67 | | ManagedRelayRequestTimeoutError; | |
| 68 | ||
| 69 | export interface ManagedRelayDpopSignerShape { | |
| 70 | readonly thumbprint: Effect.Effect<string, ManagedRelayDpopSignerError>; | |
| 71 | readonly createProof: ( | |
| 72 | input: ManagedRelayDpopProofInput, | |
| 73 | ) => Effect.Effect<string, ManagedRelayDpopSignerError>; | |
| 74 | } | |
| 202 | | Schema.SchemaError; | |
| 75 | 203 | |
| 76 | 204 | export class ManagedRelayDpopSigner extends Context.Service< |
| 77 | 205 | ManagedRelayDpopSigner, |
| 78 | ManagedRelayDpopSignerShape | |
| 206 | { | |
| 207 | readonly thumbprint: Effect.Effect<string, ManagedRelayDpopKeyLoadError>; | |
| 208 | readonly createProof: ( | |
| 209 | input: ManagedRelayDpopProofInput, | |
| 210 | ) => Effect.Effect<string, ManagedRelayDpopProofCreationError>; | |
| 211 | } | |
| 79 | 212 | >()("@t3tools/client-runtime/relay/managedRelay/ManagedRelayDpopSigner") {} |
| 80 | 213 | |
| 81 | export class ManagedRelayClientError extends Data.TaggedError("ManagedRelayClientError")<{ | |
| 82 | readonly message: string; | |
| 83 | readonly cause?: RelayHttpRequestError | ManagedRelayDpopSignerError; | |
| 84 | readonly relayError?: RelayProtectedErrorType; | |
| 85 | readonly traceId?: string; | |
| 86 | }> {} | |
| 87 | ||
| 88 | 214 | export const MANAGED_RELAY_REQUEST_TIMEOUT_MS = 10_000; |
| 89 | 215 | |
| 90 | 216 | export interface ManagedRelayAccessTokenCacheEntry { |
| ... | ... | @@ -115,100 +241,96 @@ export interface ManagedRelayClientLayerOptions { |
| 115 | 241 | readonly accessTokenStore?: ManagedRelayAccessTokenStore; |
| 116 | 242 | } |
| 117 | 243 | |
| 118 | export interface ManagedRelayClientShape { | |
| 119 | readonly relayUrl: string; | |
| 120 | readonly listEnvironments: (input: { | |
| 121 | readonly clerkToken: string; | |
| 122 | }) => Effect.Effect<ReadonlyArray<RelayClientEnvironmentRecord>, ManagedRelayClientError>; | |
| 123 | readonly listDevices: (input: { | |
| 124 | readonly clerkToken: string; | |
| 125 | }) => Effect.Effect<ReadonlyArray<RelayClientDeviceRecord>, ManagedRelayClientError>; | |
| 126 | readonly createEnvironmentLinkChallenge: (input: { | |
| 127 | readonly clerkToken: string; | |
| 128 | readonly payload: RelayEnvironmentLinkChallengeRequest; | |
| 129 | }) => Effect.Effect<RelayEnvironmentLinkChallengeResponse, ManagedRelayClientError>; | |
| 130 | readonly linkEnvironment: (input: { | |
| 131 | readonly clerkToken: string; | |
| 132 | readonly payload: RelayEnvironmentLinkRequest; | |
| 133 | }) => Effect.Effect<RelayEnvironmentLinkResponse, ManagedRelayClientError>; | |
| 134 | readonly unlinkEnvironment: (input: { | |
| 135 | readonly clerkToken: string; | |
| 136 | readonly environmentId: RelayClientEnvironmentRecord["environmentId"]; | |
| 137 | }) => Effect.Effect<RelayOkResponse, ManagedRelayClientError>; | |
| 138 | readonly getEnvironmentStatus: (input: { | |
| 139 | readonly clerkToken: string; | |
| 140 | readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>; | |
| 141 | readonly environmentId: RelayClientEnvironmentRecord["environmentId"]; | |
| 142 | }) => Effect.Effect<RelayEnvironmentStatusResponse, ManagedRelayClientError>; | |
| 143 | readonly connectEnvironment: (input: { | |
| 144 | readonly clerkToken: string; | |
| 145 | readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>; | |
| 146 | readonly environmentId: RelayClientEnvironmentRecord["environmentId"]; | |
| 147 | readonly deviceId?: string; | |
| 148 | }) => Effect.Effect<RelayEnvironmentConnectResponse, ManagedRelayClientError>; | |
| 149 | readonly registerDevice: (input: { | |
| 150 | readonly clerkToken: string; | |
| 151 | readonly payload: RelayDeviceRegistrationRequest; | |
| 152 | }) => Effect.Effect<RelayOkResponse, ManagedRelayClientError>; | |
| 153 | readonly unregisterDevice: (input: { | |
| 154 | readonly clerkToken: string; | |
| 155 | readonly deviceId: string; | |
| 156 | }) => Effect.Effect<RelayOkResponse, ManagedRelayClientError>; | |
| 157 | readonly registerLiveActivity: (input: { | |
| 158 | readonly clerkToken: string; | |
| 159 | readonly payload: RelayLiveActivityRegistrationRequest; | |
| 160 | }) => Effect.Effect<RelayOkResponse, ManagedRelayClientError>; | |
| 161 | readonly resetTokenCache: Effect.Effect<void>; | |
| 162 | } | |
| 163 | ||
| 164 | 244 | export class ManagedRelayClient extends Context.Service< |
| 165 | 245 | ManagedRelayClient, |
| 166 | ManagedRelayClientShape | |
| 246 | { | |
| 247 | readonly relayUrl: string; | |
| 248 | readonly listEnvironments: (input: { | |
| 249 | readonly clerkToken: string; | |
| 250 | }) => Effect.Effect<ReadonlyArray<RelayClientEnvironmentRecord>, ManagedRelayClientError>; | |
| 251 | readonly listDevices: (input: { | |
| 252 | readonly clerkToken: string; | |
| 253 | }) => Effect.Effect<ReadonlyArray<RelayClientDeviceRecord>, ManagedRelayClientError>; | |
| 254 | readonly createEnvironmentLinkChallenge: (input: { | |
| 255 | readonly clerkToken: string; | |
| 256 | readonly payload: RelayEnvironmentLinkChallengeRequest; | |
| 257 | }) => Effect.Effect<RelayEnvironmentLinkChallengeResponse, ManagedRelayClientError>; | |
| 258 | readonly linkEnvironment: (input: { | |
| 259 | readonly clerkToken: string; | |
| 260 | readonly payload: RelayEnvironmentLinkRequest; | |
| 261 | }) => Effect.Effect<RelayEnvironmentLinkResponse, ManagedRelayClientError>; | |
| 262 | readonly unlinkEnvironment: (input: { | |
| 263 | readonly clerkToken: string; | |
| 264 | readonly environmentId: RelayClientEnvironmentRecord["environmentId"]; | |
| 265 | }) => Effect.Effect<RelayOkResponse, ManagedRelayClientError>; | |
| 266 | readonly getEnvironmentStatus: (input: { | |
| 267 | readonly clerkToken: string; | |
| 268 | readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>; | |
| 269 | readonly environmentId: RelayClientEnvironmentRecord["environmentId"]; | |
| 270 | }) => Effect.Effect<RelayEnvironmentStatusResponse, ManagedRelayClientError>; | |
| 271 | readonly connectEnvironment: (input: { | |
| 272 | readonly clerkToken: string; | |
| 273 | readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>; | |
| 274 | readonly environmentId: RelayClientEnvironmentRecord["environmentId"]; | |
| 275 | readonly deviceId?: string; | |
| 276 | }) => Effect.Effect<RelayEnvironmentConnectResponse, ManagedRelayClientError>; | |
| 277 | readonly registerDevice: (input: { | |
| 278 | readonly clerkToken: string; | |
| 279 | readonly payload: RelayDeviceRegistrationRequest; | |
| 280 | }) => Effect.Effect<RelayOkResponse, ManagedRelayClientError>; | |
| 281 | readonly unregisterDevice: (input: { | |
| 282 | readonly clerkToken: string; | |
| 283 | readonly deviceId: string; | |
| 284 | }) => Effect.Effect<RelayOkResponse, ManagedRelayClientError>; | |
| 285 | readonly registerLiveActivity: (input: { | |
| 286 | readonly clerkToken: string; | |
| 287 | readonly payload: RelayLiveActivityRegistrationRequest; | |
| 288 | }) => Effect.Effect<RelayOkResponse, ManagedRelayClientError>; | |
| 289 | readonly resetTokenCache: Effect.Effect<void>; | |
| 290 | } | |
| 167 | 291 | >()("@t3tools/client-runtime/relay/managedRelay/ManagedRelayClient") {} |
| 168 | 292 | |
| 169 | 293 | const isRelayProtectedError = Schema.is(RelayProtectedError); |
| 170 | 294 | |
| 171 | function relayClientError(message: string, cause?: RelayHttpRequestError): ManagedRelayClientError { | |
| 172 | return new ManagedRelayClientError({ | |
| 173 | message, | |
| 174 | ...(cause === undefined ? {} : { cause }), | |
| 175 | }); | |
| 176 | } | |
| 177 | ||
| 178 | function relayLocalError( | |
| 179 | message: string, | |
| 180 | cause: ManagedRelayDpopSignerError, | |
| 181 | ): ManagedRelayClientError { | |
| 182 | return new ManagedRelayClientError({ message, cause }); | |
| 183 | } | |
| 184 | ||
| 185 | function relayRequestError(message: string) { | |
| 295 | function relayRequestError(action: ManagedRelayRequestAction) { | |
| 186 | 296 | return (cause: RelayHttpRequestError): ManagedRelayClientError => |
| 187 | new ManagedRelayClientError({ | |
| 188 | message, | |
| 297 | new ManagedRelayRequestFailedError({ | |
| 298 | action, | |
| 189 | 299 | cause, |
| 190 | 300 | ...(isRelayProtectedError(cause) ? { relayError: cause, traceId: cause.traceId } : {}), |
| 191 | 301 | }); |
| 192 | 302 | } |
| 193 | 303 | |
| 304 | function proofCreationErrorFields(error: ManagedRelayDpopProofCreationError) { | |
| 305 | return { | |
| 306 | method: error.method, | |
| 307 | url: error.url, | |
| 308 | cause: error, | |
| 309 | }; | |
| 310 | } | |
| 311 | ||
| 194 | 312 | function isRejectedDpopAccessToken(error: ManagedRelayClientError): boolean { |
| 195 | 313 | return ( |
| 314 | error._tag === "ManagedRelayRequestFailedError" && | |
| 196 | 315 | error.relayError?._tag === "RelayAuthInvalidError" && |
| 197 | 316 | error.relayError.reason === "invalid_bearer" |
| 198 | 317 | ); |
| 199 | 318 | } |
| 200 | 319 | |
| 201 | function timeoutRelayRequest(message: string) { | |
| 320 | function timeoutRelayRequest(activity: ManagedRelayRequestActivity) { | |
| 202 | 321 | return <A, E, R>( |
| 203 | request: Effect.Effect<A, E, R>, | |
| 322 | effect: Effect.Effect<A, E, R>, | |
| 204 | 323 | ): Effect.Effect<A, E | ManagedRelayClientError, R> => |
| 205 | request.pipe( | |
| 324 | effect.pipe( | |
| 206 | 325 | Effect.timeoutOption(Duration.millis(MANAGED_RELAY_REQUEST_TIMEOUT_MS)), |
| 207 | 326 | Effect.flatMap( |
| 208 | 327 | Option.match({ |
| 209 | 328 | onNone: () => |
| 210 | 329 | Effect.fail( |
| 211 | relayClientError(message, new ManagedRelayRequestTimeoutError({ message })), | |
| 330 | new ManagedRelayRequestTimeoutError({ | |
| 331 | activity, | |
| 332 | timeoutMs: MANAGED_RELAY_REQUEST_TIMEOUT_MS, | |
| 333 | }), | |
| 212 | 334 | ), |
| 213 | 335 | onSome: Effect.succeed, |
| 214 | 336 | }), |
| ... | ... | @@ -258,10 +380,10 @@ function dpopHeaders(authorization: ManagedRelayAuthorization) { |
| 258 | 380 | }; |
| 259 | 381 | } |
| 260 | 382 | |
| 261 | function disabledManagedRelayClient(relayUrl: string): ManagedRelayClientShape { | |
| 383 | function disabledManagedRelayClient(relayUrl: string): ManagedRelayClient["Service"] { | |
| 262 | 384 | const unavailable = (spanName: string) => |
| 263 | 385 | Effect.fn(spanName)(function* () { |
| 264 | return yield* relayClientError("Relay URL must be a secure absolute HTTPS origin."); | |
| 386 | return yield* new ManagedRelayUrlInvalidError({ relayUrl }); | |
| 265 | 387 | }); |
| 266 | 388 | return ManagedRelayClient.of({ |
| 267 | 389 | relayUrl, |
| ... | ... | @@ -283,482 +405,475 @@ function disabledManagedRelayClient(relayUrl: string): ManagedRelayClientShape { |
| 283 | 405 | }); |
| 284 | 406 | } |
| 285 | 407 | |
| 286 | export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) { | |
| 287 | return Layer.effect( | |
| 288 | ManagedRelayClient, | |
| 289 | Effect.gen(function* () { | |
| 290 | const relayUrl = normalizeSecureRelayUrl(options.relayUrl); | |
| 291 | if (relayUrl === null) { | |
| 292 | return disabledManagedRelayClient(options.relayUrl); | |
| 293 | } | |
| 294 | const signer = yield* ManagedRelayDpopSigner; | |
| 295 | const client = yield* HttpApiClient.make(RelayApi, { baseUrl: relayUrl }); | |
| 296 | const initialTokens = options.accessTokenStore ? yield* options.accessTokenStore.load : []; | |
| 297 | const cachedTokens = yield* SynchronizedRef.make< | |
| 298 | ReadonlyArray<ManagedRelayAccessTokenCacheEntry> | |
| 299 | >(initialTokens.filter((token) => token.clientId === options.clientId)); | |
| 300 | const urlBuilder = HttpApiClient.urlBuilder(RelayApi, { baseUrl: relayUrl }); | |
| 301 | ||
| 302 | type DpopProofTarget = Pick<ManagedRelayDpopProofInput, "method" | "url">; | |
| 303 | const dpopProofTargets = { | |
| 304 | exchangeAccessToken: (): DpopProofTarget => ({ | |
| 305 | method: RelayExchangeDpopAccessTokenEndpoint.method, | |
| 306 | url: urlBuilder.token.exchangeDpopAccessToken(), | |
| 307 | }), | |
| 308 | getEnvironmentStatus: ( | |
| 309 | environmentId: RelayClientEnvironmentRecord["environmentId"], | |
| 310 | ): DpopProofTarget => ({ | |
| 311 | method: RelayGetEnvironmentStatusEndpoint.method, | |
| 312 | url: urlBuilder.dpopClient.getEnvironmentStatus({ params: { environmentId } }), | |
| 313 | }), | |
| 314 | connectEnvironment: ( | |
| 315 | environmentId: RelayClientEnvironmentRecord["environmentId"], | |
| 316 | ): DpopProofTarget => ({ | |
| 317 | method: RelayConnectEnvironmentEndpoint.method, | |
| 318 | url: urlBuilder.dpopClient.connectEnvironment({ params: { environmentId } }), | |
| 319 | }), | |
| 320 | registerDevice: (): DpopProofTarget => ({ | |
| 321 | method: RelayRegisterDeviceEndpoint.method, | |
| 322 | url: urlBuilder.mobile.registerDevice(), | |
| 323 | }), | |
| 324 | unregisterDevice: (deviceId: string): DpopProofTarget => ({ | |
| 325 | method: RelayUnregisterDeviceEndpoint.method, | |
| 326 | url: urlBuilder.mobile.unregisterDevice({ params: { deviceId } }), | |
| 327 | }), | |
| 328 | registerLiveActivity: (): DpopProofTarget => ({ | |
| 329 | method: RelayRegisterLiveActivityEndpoint.method, | |
| 330 | url: urlBuilder.mobile.registerLiveActivity(), | |
| 331 | }), | |
| 332 | }; | |
| 408 | export const make = Effect.fn("ManagedRelayClient.make")(function* ( | |
| 409 | options: ManagedRelayClientLayerOptions, | |
| 410 | ) { | |
| 411 | const relayUrl = normalizeSecureRelayUrl(options.relayUrl); | |
| 412 | if (relayUrl === null) { | |
| 413 | return disabledManagedRelayClient(options.relayUrl); | |
| 414 | } | |
| 415 | const signer = yield* ManagedRelayDpopSigner; | |
| 416 | const client = yield* HttpApiClient.make(RelayApi, { baseUrl: relayUrl }); | |
| 417 | const initialTokens = options.accessTokenStore ? yield* options.accessTokenStore.load : []; | |
| 418 | const cachedTokens = yield* SynchronizedRef.make< | |
| 419 | ReadonlyArray<ManagedRelayAccessTokenCacheEntry> | |
| 420 | >(initialTokens.filter((token) => token.clientId === options.clientId)); | |
| 421 | const urlBuilder = HttpApiClient.urlBuilder(RelayApi, { baseUrl: relayUrl }); | |
| 422 | ||
| 423 | type DpopProofTarget = Pick<ManagedRelayDpopProofInput, "method" | "url">; | |
| 424 | const dpopProofTargets = { | |
| 425 | exchangeAccessToken: (): DpopProofTarget => ({ | |
| 426 | method: RelayExchangeDpopAccessTokenEndpoint.method, | |
| 427 | url: urlBuilder.token.exchangeDpopAccessToken(), | |
| 428 | }), | |
| 429 | getEnvironmentStatus: ( | |
| 430 | environmentId: RelayClientEnvironmentRecord["environmentId"], | |
| 431 | ): DpopProofTarget => ({ | |
| 432 | method: RelayGetEnvironmentStatusEndpoint.method, | |
| 433 | url: urlBuilder.dpopClient.getEnvironmentStatus({ params: { environmentId } }), | |
| 434 | }), | |
| 435 | connectEnvironment: ( | |
| 436 | environmentId: RelayClientEnvironmentRecord["environmentId"], | |
| 437 | ): DpopProofTarget => ({ | |
| 438 | method: RelayConnectEnvironmentEndpoint.method, | |
| 439 | url: urlBuilder.dpopClient.connectEnvironment({ params: { environmentId } }), | |
| 440 | }), | |
| 441 | registerDevice: (): DpopProofTarget => ({ | |
| 442 | method: RelayRegisterDeviceEndpoint.method, | |
| 443 | url: urlBuilder.mobile.registerDevice(), | |
| 444 | }), | |
| 445 | unregisterDevice: (deviceId: string): DpopProofTarget => ({ | |
| 446 | method: RelayUnregisterDeviceEndpoint.method, | |
| 447 | url: urlBuilder.mobile.unregisterDevice({ params: { deviceId } }), | |
| 448 | }), | |
| 449 | registerLiveActivity: (): DpopProofTarget => ({ | |
| 450 | method: RelayRegisterLiveActivityEndpoint.method, | |
| 451 | url: urlBuilder.mobile.registerLiveActivity(), | |
| 452 | }), | |
| 453 | }; | |
| 333 | 454 | |
| 334 | const exchangeAccessToken = Effect.fn("clientRuntime.managedRelay.exchangeAccessToken")( | |
| 335 | function* (input: { | |
| 336 | readonly clerkToken: string; | |
| 337 | readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>; | |
| 338 | }) { | |
| 339 | yield* Effect.annotateCurrentSpan({ | |
| 340 | "relay.client_id": options.clientId, | |
| 341 | "relay.scopes": input.scopes.join(" "), | |
| 342 | }); | |
| 343 | const proof = yield* signer | |
| 344 | .createProof(dpopProofTargets.exchangeAccessToken()) | |
| 345 | .pipe( | |
| 346 | Effect.mapError((cause) => | |
| 347 | relayLocalError("Could not create relay token DPoP proof.", cause), | |
| 348 | ), | |
| 349 | ); | |
| 350 | const response = yield* client.token | |
| 351 | .exchangeDpopAccessToken({ | |
| 352 | headers: { dpop: proof }, | |
| 353 | payload: { | |
| 354 | grant_type: RelayDpopTokenExchangeGrantType, | |
| 355 | subject_token: input.clerkToken, | |
| 356 | subject_token_type: RelayJwtSubjectTokenType, | |
| 357 | requested_token_type: RelayAccessTokenType, | |
| 358 | resource: relayUrl, | |
| 359 | scope: encodeOAuthScope(input.scopes), | |
| 360 | client_id: options.clientId, | |
| 361 | }, | |
| 362 | }) | |
| 363 | .pipe( | |
| 364 | Effect.mapError(relayRequestError("Could not exchange relay DPoP access token.")), | |
| 365 | timeoutRelayRequest("Relay DPoP access token exchange timed out."), | |
| 366 | ); | |
| 367 | if (!oauthScopeSetEquals(response.scope, input.scopes)) { | |
| 368 | return yield* relayClientError("Relay granted unexpected DPoP access token scopes."); | |
| 369 | } | |
| 370 | return response; | |
| 371 | }, | |
| 372 | ); | |
| 455 | const exchangeAccessToken = Effect.fn("clientRuntime.managedRelay.exchangeAccessToken")( | |
| 456 | function* (input: { | |
| 457 | readonly clerkToken: string; | |
| 458 | readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>; | |
| 459 | }) { | |
| 460 | yield* Effect.annotateCurrentSpan({ | |
| 461 | "relay.client_id": options.clientId, | |
| 462 | "relay.scopes": input.scopes.join(" "), | |
| 463 | }); | |
| 464 | const proof = yield* signer | |
| 465 | .createProof(dpopProofTargets.exchangeAccessToken()) | |
| 466 | .pipe( | |
| 467 | Effect.mapError( | |
| 468 | (error) => new ManagedRelayTokenProofCreationError(proofCreationErrorFields(error)), | |
| 469 | ), | |
| 470 | ); | |
| 471 | const response = yield* client.token | |
| 472 | .exchangeDpopAccessToken({ | |
| 473 | headers: { dpop: proof }, | |
| 474 | payload: { | |
| 475 | grant_type: RelayDpopTokenExchangeGrantType, | |
| 476 | subject_token: input.clerkToken, | |
| 477 | subject_token_type: RelayJwtSubjectTokenType, | |
| 478 | requested_token_type: RelayAccessTokenType, | |
| 479 | resource: relayUrl, | |
| 480 | scope: encodeOAuthScope(input.scopes), | |
| 481 | client_id: options.clientId, | |
| 482 | }, | |
| 483 | }) | |
| 484 | .pipe( | |
| 485 | Effect.mapError(relayRequestError("exchange relay DPoP access token")), | |
| 486 | timeoutRelayRequest("Relay DPoP access token exchange"), | |
| 487 | ); | |
| 488 | if (!oauthScopeSetEquals(response.scope, input.scopes)) { | |
| 489 | return yield* new ManagedRelayAccessTokenScopesUnexpectedError({ | |
| 490 | requestedScopes: input.scopes, | |
| 491 | grantedScope: response.scope, | |
| 492 | }); | |
| 493 | } | |
| 494 | return response; | |
| 495 | }, | |
| 496 | ); | |
| 373 | 497 | |
| 374 | const obtainAccessToken = Effect.fn("clientRuntime.managedRelay.obtainAccessToken")( | |
| 375 | function* (input: { | |
| 376 | readonly clerkToken: string; | |
| 377 | readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>; | |
| 378 | readonly thumbprint: string; | |
| 379 | }) { | |
| 380 | yield* Effect.annotateCurrentSpan({ | |
| 381 | "relay.client_id": options.clientId, | |
| 382 | "relay.scopes": input.scopes.join(" "), | |
| 383 | }); | |
| 384 | const nowMillis = yield* Clock.currentTimeMillis; | |
| 385 | const accountId = relayAccountId(input.clerkToken); | |
| 386 | if (Option.isNone(accountId)) { | |
| 387 | yield* Effect.annotateCurrentSpan({ | |
| 388 | "relay.token_cache.result": "bypass", | |
| 389 | "relay.token_cache.bypass_reason": "invalid_subject_token", | |
| 390 | }); | |
| 391 | const response = yield* exchangeAccessToken(input); | |
| 392 | return { | |
| 393 | accountId: "", | |
| 498 | const obtainAccessToken = Effect.fn("clientRuntime.managedRelay.obtainAccessToken")( | |
| 499 | function* (input: { | |
| 500 | readonly clerkToken: string; | |
| 501 | readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>; | |
| 502 | readonly thumbprint: string; | |
| 503 | }) { | |
| 504 | yield* Effect.annotateCurrentSpan({ | |
| 505 | "relay.client_id": options.clientId, | |
| 506 | "relay.scopes": input.scopes.join(" "), | |
| 507 | }); | |
| 508 | const nowMillis = yield* Clock.currentTimeMillis; | |
| 509 | const accountId = relayAccountId(input.clerkToken); | |
| 510 | if (Option.isNone(accountId)) { | |
| 511 | yield* Effect.annotateCurrentSpan({ | |
| 512 | "relay.token_cache.result": "bypass", | |
| 513 | "relay.token_cache.bypass_reason": "invalid_subject_token", | |
| 514 | }); | |
| 515 | const response = yield* exchangeAccessToken(input); | |
| 516 | return { | |
| 517 | accountId: "", | |
| 518 | clientId: options.clientId, | |
| 519 | relayUrl, | |
| 520 | thumbprint: input.thumbprint, | |
| 521 | scopes: input.scopes, | |
| 522 | accessToken: response.access_token, | |
| 523 | expiresAtMillis: nowMillis + response.expires_in * 1_000, | |
| 524 | } satisfies ManagedRelayAccessTokenCacheEntry; | |
| 525 | } | |
| 526 | return yield* SynchronizedRef.modifyEffect(cachedTokens, (tokens) => | |
| 527 | Effect.gen(function* () { | |
| 528 | const activeTokens = tokens.filter((token) => token.expiresAtMillis > nowMillis + 5_000); | |
| 529 | const cached = activeTokens.find((token) => | |
| 530 | tokenMatches(token, { | |
| 531 | accountId: accountId.value, | |
| 394 | 532 | clientId: options.clientId, |
| 395 | 533 | relayUrl, |
| 396 | 534 | thumbprint: input.thumbprint, |
| 397 | 535 | scopes: input.scopes, |
| 398 | accessToken: response.access_token, | |
| 399 | expiresAtMillis: nowMillis + response.expires_in * 1_000, | |
| 400 | } satisfies ManagedRelayAccessTokenCacheEntry; | |
| 536 | nowMillis, | |
| 537 | }), | |
| 538 | ); | |
| 539 | if (cached) { | |
| 540 | yield* Effect.annotateCurrentSpan({ | |
| 541 | "relay.token_cache.result": "hit", | |
| 542 | }); | |
| 543 | return [cached, activeTokens] as const; | |
| 401 | 544 | } |
| 402 | return yield* SynchronizedRef.modifyEffect(cachedTokens, (tokens) => | |
| 403 | Effect.gen(function* () { | |
| 404 | const activeTokens = tokens.filter( | |
| 405 | (token) => token.expiresAtMillis > nowMillis + 5_000, | |
| 406 | ); | |
| 407 | const cached = activeTokens.find((token) => | |
| 408 | tokenMatches(token, { | |
| 409 | accountId: accountId.value, | |
| 410 | clientId: options.clientId, | |
| 411 | relayUrl, | |
| 412 | thumbprint: input.thumbprint, | |
| 413 | scopes: input.scopes, | |
| 414 | nowMillis, | |
| 415 | }), | |
| 416 | ); | |
| 417 | if (cached) { | |
| 418 | yield* Effect.annotateCurrentSpan({ | |
| 419 | "relay.token_cache.result": "hit", | |
| 420 | }); | |
| 421 | return [cached, activeTokens] as const; | |
| 422 | } | |
| 423 | yield* Effect.annotateCurrentSpan({ | |
| 424 | "relay.token_cache.result": "miss", | |
| 425 | }); | |
| 426 | const response = yield* exchangeAccessToken(input); | |
| 427 | const next: ManagedRelayAccessTokenCacheEntry = { | |
| 428 | accountId: accountId.value, | |
| 429 | clientId: options.clientId, | |
| 430 | relayUrl, | |
| 431 | thumbprint: input.thumbprint, | |
| 432 | scopes: input.scopes, | |
| 433 | accessToken: response.access_token, | |
| 434 | expiresAtMillis: nowMillis + response.expires_in * 1_000, | |
| 435 | }; | |
| 436 | const nextTokens = [...activeTokens, next]; | |
| 437 | if (options.accessTokenStore) { | |
| 438 | yield* options.accessTokenStore.save(nextTokens); | |
| 545 | yield* Effect.annotateCurrentSpan({ | |
| 546 | "relay.token_cache.result": "miss", | |
| 547 | }); | |
| 548 | const response = yield* exchangeAccessToken(input); | |
| 549 | const next: ManagedRelayAccessTokenCacheEntry = { | |
| 550 | accountId: accountId.value, | |
| 551 | clientId: options.clientId, | |
| 552 | relayUrl, | |
| 553 | thumbprint: input.thumbprint, | |
| 554 | scopes: input.scopes, | |
| 555 | accessToken: response.access_token, | |
| 556 | expiresAtMillis: nowMillis + response.expires_in * 1_000, | |
| 557 | }; | |
| 558 | const nextTokens = [...activeTokens, next]; | |
| 559 | if (options.accessTokenStore) { | |
| 560 | yield* options.accessTokenStore.save(nextTokens); | |
| 561 | } | |
| 562 | return [next, nextTokens] as const; | |
| 563 | }), | |
| 564 | ).pipe(Effect.withSpan("clientRuntime.managedRelay.tokenCacheCriticalSection")); | |
| 565 | }, | |
| 566 | ); | |
| 567 | ||
| 568 | const authorize = Effect.fn("clientRuntime.managedRelay.authorize")(function* (input: { | |
| 569 | readonly clerkToken: string; | |
| 570 | readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>; | |
| 571 | readonly target: DpopProofTarget; | |
| 572 | }) { | |
| 573 | yield* Effect.annotateCurrentSpan({ | |
| 574 | "relay.client_id": options.clientId, | |
| 575 | "relay.scopes": input.scopes.join(" "), | |
| 576 | "http.request.method": input.target.method, | |
| 577 | "url.full": input.target.url, | |
| 578 | }); | |
| 579 | const thumbprint = yield* signer.thumbprint; | |
| 580 | const token = yield* obtainAccessToken({ | |
| 581 | clerkToken: input.clerkToken, | |
| 582 | scopes: input.scopes, | |
| 583 | thumbprint, | |
| 584 | }); | |
| 585 | const proof = yield* signer | |
| 586 | .createProof({ | |
| 587 | ...input.target, | |
| 588 | accessToken: token.accessToken, | |
| 589 | }) | |
| 590 | .pipe( | |
| 591 | Effect.mapError( | |
| 592 | (error) => new ManagedRelayRequestProofCreationError(proofCreationErrorFields(error)), | |
| 593 | ), | |
| 594 | ); | |
| 595 | return { accessToken: token.accessToken, proof, thumbprint }; | |
| 596 | }); | |
| 597 | ||
| 598 | const invalidateAccessToken = Effect.fn("clientRuntime.managedRelay.invalidateAccessToken")( | |
| 599 | function* (accessToken: string) { | |
| 600 | return yield* SynchronizedRef.modifyEffect(cachedTokens, (tokens) => { | |
| 601 | const nextTokens = tokens.filter((token) => token.accessToken !== accessToken); | |
| 602 | if (nextTokens.length === tokens.length) { | |
| 603 | return Effect.succeed([false, tokens] as const); | |
| 604 | } | |
| 605 | return ( | |
| 606 | options.accessTokenStore ? options.accessTokenStore.save(nextTokens) : Effect.void | |
| 607 | ).pipe(Effect.as([true, nextTokens] as const)); | |
| 608 | }); | |
| 609 | }, | |
| 610 | ); | |
| 611 | ||
| 612 | const runDpopRequest = <A>( | |
| 613 | input: { | |
| 614 | readonly clerkToken: string; | |
| 615 | readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>; | |
| 616 | readonly target: DpopProofTarget; | |
| 617 | }, | |
| 618 | request: ( | |
| 619 | authorization: ManagedRelayAuthorization, | |
| 620 | ) => Effect.Effect<A, ManagedRelayClientError>, | |
| 621 | ): Effect.Effect<A, ManagedRelayClientError> => { | |
| 622 | const attempt = (refreshRejectedToken: boolean): Effect.Effect<A, ManagedRelayClientError> => | |
| 623 | authorize(input).pipe( | |
| 624 | Effect.flatMap((authorization) => | |
| 625 | request(authorization).pipe( | |
| 626 | Effect.catch((error) => { | |
| 627 | if (!isRejectedDpopAccessToken(error)) { | |
| 628 | return Effect.fail(error); | |
| 439 | 629 | } |
| 440 | return [next, nextTokens] as const; | |
| 630 | return invalidateAccessToken(authorization.accessToken).pipe( | |
| 631 | Effect.tap((invalidated) => | |
| 632 | Effect.annotateCurrentSpan({ | |
| 633 | "relay.token_cache.invalidated": invalidated, | |
| 634 | "relay.token_cache.invalidation_reason": "invalid_bearer", | |
| 635 | "relay.token_cache.retry_after_invalidation": refreshRejectedToken, | |
| 636 | }), | |
| 637 | ), | |
| 638 | Effect.tap((invalidated) => | |
| 639 | invalidated && refreshRejectedToken | |
| 640 | ? Effect.logWarning( | |
| 641 | "Relay rejected a cached DPoP access token; refreshing it once.", | |
| 642 | ) | |
| 643 | : Effect.void, | |
| 644 | ), | |
| 645 | Effect.andThen(refreshRejectedToken ? attempt(false) : Effect.fail(error)), | |
| 646 | ); | |
| 441 | 647 | }), |
| 442 | ).pipe(Effect.withSpan("clientRuntime.managedRelay.tokenCacheCriticalSection")); | |
| 443 | }, | |
| 648 | ), | |
| 649 | ), | |
| 444 | 650 | ); |
| 651 | return attempt(true); | |
| 652 | }; | |
| 445 | 653 | |
| 446 | const authorize = Effect.fn("clientRuntime.managedRelay.authorize")(function* (input: { | |
| 447 | readonly clerkToken: string; | |
| 448 | readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>; | |
| 449 | readonly target: DpopProofTarget; | |
| 450 | }) { | |
| 451 | yield* Effect.annotateCurrentSpan({ | |
| 452 | "relay.client_id": options.clientId, | |
| 453 | "relay.scopes": input.scopes.join(" "), | |
| 454 | "http.request.method": input.target.method, | |
| 455 | "url.full": input.target.url, | |
| 456 | }); | |
| 457 | const thumbprint = yield* signer.thumbprint.pipe( | |
| 458 | Effect.mapError((cause) => | |
| 459 | relayLocalError("Could not load relay DPoP proof key.", cause), | |
| 460 | ), | |
| 461 | ); | |
| 462 | const token = yield* obtainAccessToken({ | |
| 463 | clerkToken: input.clerkToken, | |
| 464 | scopes: input.scopes, | |
| 465 | thumbprint, | |
| 466 | }); | |
| 467 | const proof = yield* signer | |
| 468 | .createProof({ | |
| 469 | ...input.target, | |
| 470 | accessToken: token.accessToken, | |
| 654 | const mobileRegistrationRequest = <A>( | |
| 655 | input: { | |
| 656 | readonly clerkToken: string; | |
| 657 | readonly target: DpopProofTarget; | |
| 658 | }, | |
| 659 | request: ( | |
| 660 | authorization: ManagedRelayAuthorization, | |
| 661 | ) => Effect.Effect<A, ManagedRelayClientError>, | |
| 662 | ) => | |
| 663 | runDpopRequest( | |
| 664 | { | |
| 665 | ...input, | |
| 666 | scopes: [RelayMobileRegistrationScope], | |
| 667 | }, | |
| 668 | request, | |
| 669 | ); | |
| 670 | ||
| 671 | return ManagedRelayClient.of({ | |
| 672 | relayUrl, | |
| 673 | listEnvironments: Effect.fnUntraced( | |
| 674 | function* (input) { | |
| 675 | return yield* client.client | |
| 676 | .listEnvironments({ headers: bearerHeaders(input.clerkToken) }) | |
| 677 | .pipe( | |
| 678 | Effect.map((response) => response.environments), | |
| 679 | Effect.mapError(relayRequestError("list relay-managed environments")), | |
| 680 | timeoutRelayRequest("Relay environment listing"), | |
| 681 | ); | |
| 682 | }, | |
| 683 | Effect.withSpan("clientRuntime.managedRelay.listEnvironments"), | |
| 684 | withRelayClientTracing, | |
| 685 | ), | |
| 686 | listDevices: Effect.fnUntraced( | |
| 687 | function* (input) { | |
| 688 | return yield* client.client | |
| 689 | .listDevices({ | |
| 690 | headers: bearerHeaders(input.clerkToken), | |
| 471 | 691 | }) |
| 472 | 692 | .pipe( |
| 473 | Effect.mapError((cause) => | |
| 474 | relayLocalError("Could not create relay request DPoP proof.", cause), | |
| 475 | ), | |
| 693 | Effect.map((response) => response.devices), | |
| 694 | Effect.mapError(relayRequestError("list relay client devices")), | |
| 695 | timeoutRelayRequest("Relay client device listing"), | |
| 476 | 696 | ); |
| 477 | return { accessToken: token.accessToken, proof, thumbprint }; | |
| 478 | }); | |
| 479 | ||
| 480 | const invalidateAccessToken = Effect.fn("clientRuntime.managedRelay.invalidateAccessToken")( | |
| 481 | function* (accessToken: string) { | |
| 482 | return yield* SynchronizedRef.modifyEffect(cachedTokens, (tokens) => { | |
| 483 | const nextTokens = tokens.filter((token) => token.accessToken !== accessToken); | |
| 484 | if (nextTokens.length === tokens.length) { | |
| 485 | return Effect.succeed([false, tokens] as const); | |
| 486 | } | |
| 487 | return ( | |
| 488 | options.accessTokenStore ? options.accessTokenStore.save(nextTokens) : Effect.void | |
| 489 | ).pipe(Effect.as([true, nextTokens] as const)); | |
| 490 | }); | |
| 491 | }, | |
| 492 | ); | |
| 493 | ||
| 494 | const runDpopRequest = <A>( | |
| 495 | input: { | |
| 496 | readonly clerkToken: string; | |
| 497 | readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>; | |
| 498 | readonly target: DpopProofTarget; | |
| 499 | }, | |
| 500 | request: ( | |
| 501 | authorization: ManagedRelayAuthorization, | |
| 502 | ) => Effect.Effect<A, ManagedRelayClientError>, | |
| 503 | ): Effect.Effect<A, ManagedRelayClientError> => { | |
| 504 | const attempt = ( | |
| 505 | refreshRejectedToken: boolean, | |
| 506 | ): Effect.Effect<A, ManagedRelayClientError> => | |
| 507 | authorize(input).pipe( | |
| 508 | Effect.flatMap((authorization) => | |
| 509 | request(authorization).pipe( | |
| 510 | Effect.catch((error) => { | |
| 511 | if (!isRejectedDpopAccessToken(error)) { | |
| 512 | return Effect.fail(error); | |
| 513 | } | |
| 514 | return invalidateAccessToken(authorization.accessToken).pipe( | |
| 515 | Effect.tap((invalidated) => | |
| 516 | Effect.annotateCurrentSpan({ | |
| 517 | "relay.token_cache.invalidated": invalidated, | |
| 518 | "relay.token_cache.invalidation_reason": "invalid_bearer", | |
| 519 | "relay.token_cache.retry_after_invalidation": refreshRejectedToken, | |
| 520 | }), | |
| 521 | ), | |
| 522 | Effect.tap((invalidated) => | |
| 523 | invalidated && refreshRejectedToken | |
| 524 | ? Effect.logWarning( | |
| 525 | "Relay rejected a cached DPoP access token; refreshing it once.", | |
| 526 | ) | |
| 527 | : Effect.void, | |
| 528 | ), | |
| 529 | Effect.andThen(refreshRejectedToken ? attempt(false) : Effect.fail(error)), | |
| 530 | ); | |
| 531 | }), | |
| 532 | ), | |
| 533 | ), | |
| 697 | }, | |
| 698 | Effect.withSpan("clientRuntime.managedRelay.listDevices"), | |
| 699 | withRelayClientTracing, | |
| 700 | ), | |
| 701 | createEnvironmentLinkChallenge: Effect.fnUntraced( | |
| 702 | function* (input) { | |
| 703 | return yield* client.client | |
| 704 | .createEnvironmentLinkChallenge({ | |
| 705 | headers: bearerHeaders(input.clerkToken), | |
| 706 | payload: input.payload, | |
| 707 | }) | |
| 708 | .pipe( | |
| 709 | Effect.mapError(relayRequestError("create relay environment link challenge")), | |
| 710 | timeoutRelayRequest("Relay environment link challenge"), | |
| 711 | ); | |
| 712 | }, | |
| 713 | Effect.withSpan("clientRuntime.managedRelay.createEnvironmentLinkChallenge"), | |
| 714 | withRelayClientTracing, | |
| 715 | ), | |
| 716 | linkEnvironment: Effect.fnUntraced( | |
| 717 | function* (input) { | |
| 718 | return yield* client.client | |
| 719 | .linkEnvironment({ | |
| 720 | headers: bearerHeaders(input.clerkToken), | |
| 721 | payload: input.payload, | |
| 722 | }) | |
| 723 | .pipe( | |
| 724 | Effect.mapError(relayRequestError("link relay environment")), | |
| 725 | timeoutRelayRequest("Relay environment linking"), | |
| 534 | 726 | ); |
| 535 | return attempt(true); | |
| 536 | }; | |
| 537 | ||
| 538 | const mobileRegistrationRequest = <A>( | |
| 539 | input: { | |
| 540 | readonly clerkToken: string; | |
| 541 | readonly target: DpopProofTarget; | |
| 542 | }, | |
| 543 | request: ( | |
| 544 | authorization: ManagedRelayAuthorization, | |
| 545 | ) => Effect.Effect<A, ManagedRelayClientError>, | |
| 546 | ) => | |
| 547 | runDpopRequest( | |
| 727 | }, | |
| 728 | Effect.withSpan("clientRuntime.managedRelay.linkEnvironment"), | |
| 729 | withRelayClientTracing, | |
| 730 | ), | |
| 731 | unlinkEnvironment: Effect.fnUntraced( | |
| 732 | function* (input) { | |
| 733 | return yield* client.client | |
| 734 | .unlinkEnvironment({ | |
| 735 | headers: bearerHeaders(input.clerkToken), | |
| 736 | params: { environmentId: input.environmentId }, | |
| 737 | }) | |
| 738 | .pipe( | |
| 739 | Effect.mapError(relayRequestError("unlink relay environment")), | |
| 740 | timeoutRelayRequest("Relay environment unlinking"), | |
| 741 | ); | |
| 742 | }, | |
| 743 | Effect.withSpan("clientRuntime.managedRelay.unlinkEnvironment"), | |
| 744 | withRelayClientTracing, | |
| 745 | ), | |
| 746 | getEnvironmentStatus: Effect.fnUntraced( | |
| 747 | function* (input) { | |
| 748 | yield* Effect.annotateCurrentSpan({ | |
| 749 | "environment.id": input.environmentId, | |
| 750 | }); | |
| 751 | return yield* runDpopRequest( | |
| 548 | 752 | { |
| 549 | ...input, | |
| 550 | scopes: [RelayMobileRegistrationScope], | |
| 753 | clerkToken: input.clerkToken, | |
| 754 | scopes: input.scopes, | |
| 755 | target: dpopProofTargets.getEnvironmentStatus(input.environmentId), | |
| 551 | 756 | }, |
| 552 | request, | |
| 553 | ); | |
| 554 | ||
| 555 | return ManagedRelayClient.of({ | |
| 556 | relayUrl, | |
| 557 | listEnvironments: Effect.fnUntraced( | |
| 558 | function* (input) { | |
| 559 | return yield* client.client | |
| 560 | .listEnvironments({ headers: bearerHeaders(input.clerkToken) }) | |
| 757 | (authorization) => | |
| 758 | client.dpopClient | |
| 759 | .getEnvironmentStatus({ | |
| 760 | headers: dpopHeaders(authorization), | |
| 761 | params: { environmentId: input.environmentId }, | |
| 762 | }) | |
| 561 | 763 | .pipe( |
| 562 | Effect.map((response) => response.environments), | |
| 563 | Effect.mapError(relayRequestError("Could not list relay-managed environments.")), | |
| 564 | timeoutRelayRequest("Relay environment listing timed out."), | |
| 565 | ); | |
| 764 | Effect.mapError(relayRequestError("get relay environment status")), | |
| 765 | timeoutRelayRequest("Relay environment status request"), | |
| 766 | ), | |
| 767 | ); | |
| 768 | }, | |
| 769 | Effect.withSpan("clientRuntime.managedRelay.getEnvironmentStatus"), | |
| 770 | withRelayClientTracing, | |
| 771 | ), | |
| 772 | connectEnvironment: Effect.fnUntraced( | |
| 773 | function* (input) { | |
| 774 | yield* Effect.annotateCurrentSpan({ | |
| 775 | "environment.id": input.environmentId, | |
| 776 | }); | |
| 777 | return yield* runDpopRequest( | |
| 778 | { | |
| 779 | clerkToken: input.clerkToken, | |
| 780 | scopes: input.scopes, | |
| 781 | target: dpopProofTargets.connectEnvironment(input.environmentId), | |
| 566 | 782 | }, |
| 567 | Effect.withSpan("clientRuntime.managedRelay.listEnvironments"), | |
| 568 | withRelayClientTracing, | |
| 569 | ), | |
| 570 | listDevices: Effect.fnUntraced( | |
| 571 | function* (input) { | |
| 572 | return yield* client.client | |
| 573 | .listDevices({ | |
| 574 | headers: bearerHeaders(input.clerkToken), | |
| 783 | (authorization) => { | |
| 784 | const payload: RelayEnvironmentConnectRequest = { | |
| 785 | ...(input.deviceId ? { deviceId: input.deviceId } : {}), | |
| 786 | clientKeyThumbprint: authorization.thumbprint, | |
| 787 | }; | |
| 788 | return client.dpopClient | |
| 789 | .connectEnvironment({ | |
| 790 | headers: dpopHeaders(authorization), | |
| 791 | params: { environmentId: input.environmentId }, | |
| 792 | payload, | |
| 575 | 793 | }) |
| 576 | 794 | .pipe( |
| 577 | Effect.map((response) => response.devices), | |
| 578 | Effect.mapError(relayRequestError("Could not list relay client devices.")), | |
| 579 | timeoutRelayRequest("Relay client device listing timed out."), | |
| 795 | Effect.mapError(relayRequestError("connect relay environment")), | |
| 796 | timeoutRelayRequest("Relay environment connection"), | |
| 580 | 797 | ); |
| 581 | 798 | }, |
| 582 | Effect.withSpan("clientRuntime.managedRelay.listDevices"), | |
| 583 | withRelayClientTracing, | |
| 584 | ), | |
| 585 | createEnvironmentLinkChallenge: Effect.fnUntraced( | |
| 586 | function* (input) { | |
| 587 | return yield* client.client | |
| 588 | .createEnvironmentLinkChallenge({ | |
| 589 | headers: bearerHeaders(input.clerkToken), | |
| 799 | ); | |
| 800 | }, | |
| 801 | Effect.withSpan("clientRuntime.managedRelay.connectEnvironment"), | |
| 802 | withRelayClientTracing, | |
| 803 | ), | |
| 804 | registerDevice: Effect.fnUntraced( | |
| 805 | function* (input) { | |
| 806 | return yield* mobileRegistrationRequest( | |
| 807 | { | |
| 808 | clerkToken: input.clerkToken, | |
| 809 | target: dpopProofTargets.registerDevice(), | |
| 810 | }, | |
| 811 | (authorization) => | |
| 812 | client.mobile | |
| 813 | .registerDevice({ | |
| 814 | headers: dpopHeaders(authorization), | |
| 590 | 815 | payload: input.payload, |
| 591 | 816 | }) |
| 592 | 817 | .pipe( |
| 593 | Effect.mapError( | |
| 594 | relayRequestError("Could not create relay environment link challenge."), | |
| 595 | ), | |
| 596 | timeoutRelayRequest("Relay environment link challenge timed out."), | |
| 597 | ); | |
| 818 | Effect.mapError(relayRequestError("register relay mobile device")), | |
| 819 | timeoutRelayRequest("Relay mobile device registration"), | |
| 820 | ), | |
| 821 | ); | |
| 822 | }, | |
| 823 | Effect.withSpan("clientRuntime.managedRelay.registerDevice"), | |
| 824 | withRelayClientTracing, | |
| 825 | ), | |
| 826 | unregisterDevice: Effect.fnUntraced( | |
| 827 | function* (input) { | |
| 828 | return yield* mobileRegistrationRequest( | |
| 829 | { | |
| 830 | clerkToken: input.clerkToken, | |
| 831 | target: dpopProofTargets.unregisterDevice(input.deviceId), | |
| 598 | 832 | }, |
| 599 | Effect.withSpan("clientRuntime.managedRelay.createEnvironmentLinkChallenge"), | |
| 600 | withRelayClientTracing, | |
| 601 | ), | |
| 602 | linkEnvironment: Effect.fnUntraced( | |
| 603 | function* (input) { | |
| 604 | return yield* client.client | |
| 605 | .linkEnvironment({ | |
| 606 | headers: bearerHeaders(input.clerkToken), | |
| 607 | payload: input.payload, | |
| 833 | (authorization) => | |
| 834 | client.mobile | |
| 835 | .unregisterDevice({ | |
| 836 | headers: dpopHeaders(authorization), | |
| 837 | params: { deviceId: input.deviceId }, | |
| 608 | 838 | }) |
| 609 | 839 | .pipe( |
| 610 | Effect.mapError(relayRequestError("Could not link relay environment.")), | |
| 611 | timeoutRelayRequest("Relay environment linking timed out."), | |
| 612 | ); | |
| 840 | Effect.mapError(relayRequestError("unregister relay mobile device")), | |
| 841 | timeoutRelayRequest("Relay mobile device unregistration"), | |
| 842 | ), | |
| 843 | ); | |
| 844 | }, | |
| 845 | Effect.withSpan("clientRuntime.managedRelay.unregisterDevice"), | |
| 846 | withRelayClientTracing, | |
| 847 | ), | |
| 848 | registerLiveActivity: Effect.fnUntraced( | |
| 849 | function* (input) { | |
| 850 | return yield* mobileRegistrationRequest( | |
| 851 | { | |
| 852 | clerkToken: input.clerkToken, | |
| 853 | target: dpopProofTargets.registerLiveActivity(), | |
| 613 | 854 | }, |
| 614 | Effect.withSpan("clientRuntime.managedRelay.linkEnvironment"), | |
| 615 | withRelayClientTracing, | |
| 616 | ), | |
| 617 | unlinkEnvironment: Effect.fnUntraced( | |
| 618 | function* (input) { | |
| 619 | return yield* client.client | |
| 620 | .unlinkEnvironment({ | |
| 621 | headers: bearerHeaders(input.clerkToken), | |
| 622 | params: { environmentId: input.environmentId }, | |
| 855 | (authorization) => | |
| 856 | client.mobile | |
| 857 | .registerLiveActivity({ | |
| 858 | headers: dpopHeaders(authorization), | |
| 859 | payload: input.payload, | |
| 623 | 860 | }) |
| 624 | 861 | .pipe( |
| 625 | Effect.mapError(relayRequestError("Could not unlink relay environment.")), | |
| 626 | timeoutRelayRequest("Relay environment unlinking timed out."), | |
| 627 | ); | |
| 628 | }, | |
| 629 | Effect.withSpan("clientRuntime.managedRelay.unlinkEnvironment"), | |
| 630 | withRelayClientTracing, | |
| 631 | ), | |
| 632 | getEnvironmentStatus: Effect.fnUntraced( | |
| 633 | function* (input) { | |
| 634 | yield* Effect.annotateCurrentSpan({ | |
| 635 | "environment.id": input.environmentId, | |
| 636 | }); | |
| 637 | return yield* runDpopRequest( | |
| 638 | { | |
| 639 | clerkToken: input.clerkToken, | |
| 640 | scopes: input.scopes, | |
| 641 | target: dpopProofTargets.getEnvironmentStatus(input.environmentId), | |
| 642 | }, | |
| 643 | (authorization) => | |
| 644 | client.dpopClient | |
| 645 | .getEnvironmentStatus({ | |
| 646 | headers: dpopHeaders(authorization), | |
| 647 | params: { environmentId: input.environmentId }, | |
| 648 | }) | |
| 649 | .pipe( | |
| 650 | Effect.mapError(relayRequestError("Could not get relay environment status.")), | |
| 651 | timeoutRelayRequest("Relay environment status request timed out."), | |
| 652 | ), | |
| 653 | ); | |
| 654 | }, | |
| 655 | Effect.withSpan("clientRuntime.managedRelay.getEnvironmentStatus"), | |
| 656 | withRelayClientTracing, | |
| 657 | ), | |
| 658 | connectEnvironment: Effect.fnUntraced( | |
| 659 | function* (input) { | |
| 660 | yield* Effect.annotateCurrentSpan({ | |
| 661 | "environment.id": input.environmentId, | |
| 662 | }); | |
| 663 | return yield* runDpopRequest( | |
| 664 | { | |
| 665 | clerkToken: input.clerkToken, | |
| 666 | scopes: input.scopes, | |
| 667 | target: dpopProofTargets.connectEnvironment(input.environmentId), | |
| 668 | }, | |
| 669 | (authorization) => { | |
| 670 | const payload: RelayEnvironmentConnectRequest = { | |
| 671 | ...(input.deviceId ? { deviceId: input.deviceId } : {}), | |
| 672 | clientKeyThumbprint: authorization.thumbprint, | |
| 673 | }; | |
| 674 | return client.dpopClient | |
| 675 | .connectEnvironment({ | |
| 676 | headers: dpopHeaders(authorization), | |
| 677 | params: { environmentId: input.environmentId }, | |
| 678 | payload, | |
| 679 | }) | |
| 680 | .pipe( | |
| 681 | Effect.mapError(relayRequestError("Could not connect relay environment.")), | |
| 682 | timeoutRelayRequest("Relay environment connection timed out."), | |
| 683 | ); | |
| 684 | }, | |
| 685 | ); | |
| 686 | }, | |
| 687 | Effect.withSpan("clientRuntime.managedRelay.connectEnvironment"), | |
| 688 | withRelayClientTracing, | |
| 689 | ), | |
| 690 | registerDevice: Effect.fnUntraced( | |
| 691 | function* (input) { | |
| 692 | return yield* mobileRegistrationRequest( | |
| 693 | { | |
| 694 | clerkToken: input.clerkToken, | |
| 695 | target: dpopProofTargets.registerDevice(), | |
| 696 | }, | |
| 697 | (authorization) => | |
| 698 | client.mobile | |
| 699 | .registerDevice({ | |
| 700 | headers: dpopHeaders(authorization), | |
| 701 | payload: input.payload, | |
| 702 | }) | |
| 703 | .pipe( | |
| 704 | Effect.mapError(relayRequestError("Could not register relay mobile device.")), | |
| 705 | timeoutRelayRequest("Relay mobile device registration timed out."), | |
| 706 | ), | |
| 707 | ); | |
| 708 | }, | |
| 709 | Effect.withSpan("clientRuntime.managedRelay.registerDevice"), | |
| 710 | withRelayClientTracing, | |
| 711 | ), | |
| 712 | unregisterDevice: Effect.fnUntraced( | |
| 713 | function* (input) { | |
| 714 | return yield* mobileRegistrationRequest( | |
| 715 | { | |
| 716 | clerkToken: input.clerkToken, | |
| 717 | target: dpopProofTargets.unregisterDevice(input.deviceId), | |
| 718 | }, | |
| 719 | (authorization) => | |
| 720 | client.mobile | |
| 721 | .unregisterDevice({ | |
| 722 | headers: dpopHeaders(authorization), | |
| 723 | params: { deviceId: input.deviceId }, | |
| 724 | }) | |
| 725 | .pipe( | |
| 726 | Effect.mapError(relayRequestError("Could not unregister relay mobile device.")), | |
| 727 | timeoutRelayRequest("Relay mobile device unregistration timed out."), | |
| 728 | ), | |
| 729 | ); | |
| 730 | }, | |
| 731 | Effect.withSpan("clientRuntime.managedRelay.unregisterDevice"), | |
| 732 | withRelayClientTracing, | |
| 733 | ), | |
| 734 | registerLiveActivity: Effect.fnUntraced( | |
| 735 | function* (input) { | |
| 736 | return yield* mobileRegistrationRequest( | |
| 737 | { | |
| 738 | clerkToken: input.clerkToken, | |
| 739 | target: dpopProofTargets.registerLiveActivity(), | |
| 740 | }, | |
| 741 | (authorization) => | |
| 742 | client.mobile | |
| 743 | .registerLiveActivity({ | |
| 744 | headers: dpopHeaders(authorization), | |
| 745 | payload: input.payload, | |
| 746 | }) | |
| 747 | .pipe( | |
| 748 | Effect.mapError(relayRequestError("Could not register relay live activity.")), | |
| 749 | timeoutRelayRequest("Relay Live Activity registration timed out."), | |
| 750 | ), | |
| 751 | ); | |
| 752 | }, | |
| 753 | Effect.withSpan("clientRuntime.managedRelay.registerLiveActivity"), | |
| 754 | withRelayClientTracing, | |
| 755 | ), | |
| 756 | resetTokenCache: SynchronizedRef.set(cachedTokens, []).pipe( | |
| 757 | Effect.andThen(options.accessTokenStore ? options.accessTokenStore.clear : Effect.void), | |
| 758 | Effect.withSpan("clientRuntime.managedRelay.resetTokenCache"), | |
| 759 | withRelayClientTracing, | |
| 760 | ), | |
| 761 | }); | |
| 762 | }), | |
| 763 | ); | |
| 764 | } | |
| 862 | Effect.mapError(relayRequestError("register relay live activity")), | |
| 863 | timeoutRelayRequest("Relay Live Activity registration"), | |
| 864 | ), | |
| 865 | ); | |
| 866 | }, | |
| 867 | Effect.withSpan("clientRuntime.managedRelay.registerLiveActivity"), | |
| 868 | withRelayClientTracing, | |
| 869 | ), | |
| 870 | resetTokenCache: SynchronizedRef.set(cachedTokens, []).pipe( | |
| 871 | Effect.andThen(options.accessTokenStore ? options.accessTokenStore.clear : Effect.void), | |
| 872 | Effect.withSpan("clientRuntime.managedRelay.resetTokenCache"), | |
| 873 | withRelayClientTracing, | |
| 874 | ), | |
| 875 | }); | |
| 876 | }); | |
| 877 | ||
| 878 | export const layer = (options: ManagedRelayClientLayerOptions) => | |
| 879 | Layer.effect(ManagedRelayClient, make(options)); |
packages/client-runtime/src/relay/managedRelayState.test.ts+7-10| ... | ... | @@ -12,11 +12,7 @@ import * as Stream from "effect/Stream"; |
| 12 | 12 | import { Atom, AtomRegistry } from "effect/unstable/reactivity"; |
| 13 | 13 | import { afterEach, vi } from "vite-plus/test"; |
| 14 | 14 | |
| 15 | import { | |
| 16 | ManagedRelayClient, | |
| 17 | ManagedRelayClientError, | |
| 18 | type ManagedRelayClientShape, | |
| 19 | } from "./managedRelay.ts"; | |
| 15 | import * as ManagedRelay from "./managedRelay.ts"; | |
| 20 | 16 | import { |
| 21 | 17 | createManagedRelayQueryManager, |
| 22 | 18 | createManagedRelaySession, |
| ... | ... | @@ -66,10 +62,10 @@ function resetRegistry() { |
| 66 | 62 | } |
| 67 | 63 | |
| 68 | 64 | function createManager( |
| 69 | overrides?: Partial<ManagedRelayClientShape>, | |
| 65 | overrides?: Partial<ManagedRelay.ManagedRelayClient["Service"]>, | |
| 70 | 66 | onQueryEvent?: (event: ManagedRelayQueryEvent) => void, |
| 71 | 67 | ) { |
| 72 | const client = ManagedRelayClient.of({ | |
| 68 | const client = ManagedRelay.ManagedRelayClient.of({ | |
| 73 | 69 | relayUrl: "https://relay.example.test", |
| 74 | 70 | listEnvironments: () => Effect.succeed([environment]), |
| 75 | 71 | listDevices: () => Effect.succeed([device]), |
| ... | ... | @@ -90,7 +86,7 @@ function createManager( |
| 90 | 86 | resetTokenCache: Effect.void, |
| 91 | 87 | ...overrides, |
| 92 | 88 | }); |
| 93 | const runtime = Atom.runtime(Layer.succeed(ManagedRelayClient, client)); | |
| 89 | const runtime = Atom.runtime(Layer.succeed(ManagedRelay.ManagedRelayClient, client)); | |
| 94 | 90 | return createManagedRelayQueryManager(runtime, { |
| 95 | 91 | staleTimeMs: 60_000, |
| 96 | 92 | ...(onQueryEvent ? { onQueryEvent } : {}), |
| ... | ... | @@ -363,8 +359,9 @@ describe("createManagedRelayQueryManager", () => { |
| 363 | 359 | const manager = createManager({ |
| 364 | 360 | getEnvironmentStatus: () => |
| 365 | 361 | Effect.fail( |
| 366 | new ManagedRelayClientError({ | |
| 367 | message: "Could not get relay environment status.", | |
| 362 | new ManagedRelay.ManagedRelayRequestFailedError({ | |
| 363 | action: "get relay environment status", | |
| 364 | cause: new Error("Relay request failed."), | |
| 368 | 365 | traceId: "trace-status", |
| 369 | 366 | }), |
| 370 | 367 | ), |
packages/client-runtime/src/relay/managedRelayState.ts+5-5| ... | ... | @@ -16,7 +16,7 @@ import * as Stream from "effect/Stream"; |
| 16 | 16 | import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; |
| 17 | 17 | |
| 18 | 18 | import { findErrorTraceId } from "../errors/errorTrace.ts"; |
| 19 | import { ManagedRelayClient } from "./managedRelay.ts"; | |
| 19 | import * as ManagedRelay from "./managedRelay.ts"; | |
| 20 | 20 | |
| 21 | 21 | const DEFAULT_STALE_TIME_MS = 15_000; |
| 22 | 22 | const DEFAULT_IDLE_TTL_MS = 5 * 60_000; |
| ... | ... | @@ -308,7 +308,7 @@ export function readManagedRelaySnapshotState<A>( |
| 308 | 308 | } |
| 309 | 309 | |
| 310 | 310 | export function createManagedRelayQueryManager( |
| 311 | runtime: Atom.AtomRuntime<ManagedRelayClient>, | |
| 311 | runtime: Atom.AtomRuntime<ManagedRelay.ManagedRelayClient>, | |
| 312 | 312 | options?: { |
| 313 | 313 | readonly staleTimeMs?: number; |
| 314 | 314 | readonly idleTtlMs?: number; |
| ... | ... | @@ -351,7 +351,7 @@ export function createManagedRelayQueryManager( |
| 351 | 351 | { ...base, stage: "clerk-token" }, |
| 352 | 352 | requireClerkToken(get, accountId), |
| 353 | 353 | ); |
| 354 | const relay = yield* ManagedRelayClient; | |
| 354 | const relay = yield* ManagedRelay.ManagedRelayClient; | |
| 355 | 355 | return yield* observe( |
| 356 | 356 | { ...base, stage: "relay-request" }, |
| 357 | 357 | relay.listEnvironments({ clerkToken }), |
| ... | ... | @@ -374,7 +374,7 @@ export function createManagedRelayQueryManager( |
| 374 | 374 | { ...base, stage: "clerk-token" }, |
| 375 | 375 | requireClerkToken(get, accountId), |
| 376 | 376 | ); |
| 377 | const relay = yield* ManagedRelayClient; | |
| 377 | const relay = yield* ManagedRelay.ManagedRelayClient; | |
| 378 | 378 | return yield* observe( |
| 379 | 379 | { ...base, stage: "relay-request" }, |
| 380 | 380 | relay.listDevices({ clerkToken }), |
| ... | ... | @@ -402,7 +402,7 @@ export function createManagedRelayQueryManager( |
| 402 | 402 | { ...base, stage: "clerk-token" }, |
| 403 | 403 | requireClerkToken(get, accountId), |
| 404 | 404 | ); |
| 405 | const relay = yield* ManagedRelayClient; | |
| 405 | const relay = yield* ManagedRelay.ManagedRelayClient; | |
| 406 | 406 | const status = yield* observe( |
| 407 | 407 | { ...base, stage: "relay-request" }, |
| 408 | 408 | relay.getEnvironmentStatus({ |
packages/client-runtime/src/state/relayDiscovery.ts+8-9| ... | ... | @@ -4,34 +4,33 @@ import * as Stream from "effect/Stream"; |
| 4 | 4 | import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 5 | 5 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; |
| 6 | 6 | |
| 7 | import { | |
| 8 | EMPTY_RELAY_ENVIRONMENT_DISCOVERY_STATE, | |
| 9 | RelayEnvironmentDiscovery, | |
| 10 | } from "../relay/discovery.ts"; | |
| 7 | import * as RelayEnvironmentDiscovery from "../relay/discovery.ts"; | |
| 11 | 8 | import { createRuntimeCommand } from "./runtime.ts"; |
| 12 | 9 | |
| 13 | 10 | export function createRelayEnvironmentDiscoveryAtoms<R, E>( |
| 14 | runtime: Atom.AtomRuntime<RelayEnvironmentDiscovery | R, E>, | |
| 11 | runtime: Atom.AtomRuntime<RelayEnvironmentDiscovery.RelayEnvironmentDiscovery | R, E>, | |
| 15 | 12 | ) { |
| 16 | 13 | const stateAtom = runtime.atom( |
| 17 | 14 | Stream.unwrap( |
| 18 | RelayEnvironmentDiscovery.pipe( | |
| 15 | RelayEnvironmentDiscovery.RelayEnvironmentDiscovery.pipe( | |
| 19 | 16 | Effect.map((discovery) => SubscriptionRef.changes(discovery.state)), |
| 20 | 17 | ), |
| 21 | 18 | ), |
| 22 | { initialValue: EMPTY_RELAY_ENVIRONMENT_DISCOVERY_STATE }, | |
| 19 | { initialValue: RelayEnvironmentDiscovery.EMPTY_RELAY_ENVIRONMENT_DISCOVERY_STATE }, | |
| 23 | 20 | ); |
| 24 | 21 | const stateValueAtom = Atom.make((get) => |
| 25 | 22 | Option.getOrElse( |
| 26 | 23 | AsyncResult.value(get(stateAtom)), |
| 27 | () => EMPTY_RELAY_ENVIRONMENT_DISCOVERY_STATE, | |
| 24 | () => RelayEnvironmentDiscovery.EMPTY_RELAY_ENVIRONMENT_DISCOVERY_STATE, | |
| 28 | 25 | ), |
| 29 | 26 | ).pipe(Atom.withLabel("relay-environment-discovery-value")); |
| 30 | 27 | const refresh = createRuntimeCommand(runtime, { |
| 31 | 28 | label: "relay-environment-discovery:refresh", |
| 32 | 29 | concurrency: { mode: "singleFlight", key: () => "refresh" }, |
| 33 | 30 | execute: (_input: void) => |
| 34 | RelayEnvironmentDiscovery.pipe(Effect.flatMap((discovery) => discovery.refresh)), | |
| 31 | RelayEnvironmentDiscovery.RelayEnvironmentDiscovery.pipe( | |
| 32 | Effect.flatMap((discovery) => discovery.refresh), | |
| 33 | ), | |
| 35 | 34 | }); |
| 36 | 35 | |
| 37 | 36 | return { |