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