1import { ManagedRelay } from "@t3tools/client-runtime/relay";
2import * as Effect from "effect/Effect";
3import * as Schema from "effect/Schema";
4import * as SecureStore from "expo-secure-store";
5
6const MANAGED_RELAY_TOKEN_CACHE_KEY = "t3code.cloud.relay-access-tokens";
7const MANAGED_RELAY_TOKEN_CACHE_VERSION = 1;
8
9const ManagedRelayAccessTokenCacheEntrySchema = Schema.Struct({
10 accountId: Schema.String,
11 clientId: Schema.Literals(["t3-mobile", "t3-web"]),
12 relayUrl: Schema.String,
13 thumbprint: Schema.String,
14 scopes: Schema.Array(
15 Schema.Literals(["environment:connect", "environment:status", "mobile:registration"]),
16 ),
17 accessToken: Schema.String,
18 expiresAtMillis: Schema.Number,
19});
20
21const ManagedRelayAccessTokenCacheSchema = Schema.fromJsonString(
22 Schema.Struct({
23 version: Schema.Literal(MANAGED_RELAY_TOKEN_CACHE_VERSION),
24 entries: Schema.Array(ManagedRelayAccessTokenCacheEntrySchema),
25 }),
26);
27
28const decodeManagedRelayAccessTokenCache = Schema.decodeUnknownEffect(
29 ManagedRelayAccessTokenCacheSchema,
30);
31const encodeManagedRelayAccessTokenCache = Schema.encodeEffect(ManagedRelayAccessTokenCacheSchema);
32
33export class ManagedRelayTokenStoreError extends Schema.TaggedError<ManagedRelayTokenStoreError>()(
34 "ManagedRelayTokenStoreError",
35 {
36 operation: Schema.Literals(["read", "decode", "encode", "write", "clear"]),
37 storageKey: Schema.String,
38 cause: Schema.Defect(),
39 },
40) {
41 override get message(): string {
42 return `Managed relay token store operation "${this.operation}" failed for key "${this.storageKey}".`;
43 }
44}
45
46function logStoreFailure(error: ManagedRelayTokenStoreError) {
47 return Effect.logWarning("Managed relay token store operation failed.", {
48 errorTag: error._tag,
49 operation: error.operation,
50 storageKey: error.storageKey,
51 cause: error,
52 });
53}
54
55const loadManagedRelayAccessTokens = Effect.tryPromise({
56 try: () => SecureStore.getItemAsync(MANAGED_RELAY_TOKEN_CACHE_KEY),
57 catch: (cause) =>
58 new ManagedRelayTokenStoreError({
59 operation: "read",
60 storageKey: MANAGED_RELAY_TOKEN_CACHE_KEY,
61 cause,
62 }),
63}).pipe(
64 Effect.flatMap((encoded) =>
65 encoded === null
66 ? Effect.succeed<ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry>>([])
67 : decodeManagedRelayAccessTokenCache(encoded).pipe(
68 Effect.map((cache) => cache.entries),
69 Effect.mapError(
70 (cause) =>
71 new ManagedRelayTokenStoreError({
72 operation: "decode",
73 storageKey: MANAGED_RELAY_TOKEN_CACHE_KEY,
74 cause,
75 }),
76 ),
77 ),
78 ),
79);
80
81const saveManagedRelayAccessTokens = (
82 entries: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry>,
83) =>
84 encodeManagedRelayAccessTokenCache({
85 version: MANAGED_RELAY_TOKEN_CACHE_VERSION,
86 entries,
87 }).pipe(
88 Effect.mapError(
89 (cause) =>
90 new ManagedRelayTokenStoreError({
91 operation: "encode",
92 storageKey: MANAGED_RELAY_TOKEN_CACHE_KEY,
93 cause,
94 }),
95 ),
96 Effect.flatMap((encoded) =>
97 Effect.tryPromise({
98 try: () => SecureStore.setItemAsync(MANAGED_RELAY_TOKEN_CACHE_KEY, encoded),
99 catch: (cause) =>
100 new ManagedRelayTokenStoreError({
101 operation: "write",
102 storageKey: MANAGED_RELAY_TOKEN_CACHE_KEY,
103 cause,
104 }),
105 }),
106 ),
107 );
108
109const clearManagedRelayAccessTokens = Effect.tryPromise({
110 try: () => SecureStore.deleteItemAsync(MANAGED_RELAY_TOKEN_CACHE_KEY),
111 catch: (cause) =>
112 new ManagedRelayTokenStoreError({
113 operation: "clear",
114 storageKey: MANAGED_RELAY_TOKEN_CACHE_KEY,
115 cause,
116 }),
117});
118
119export const managedRelayAccessTokenStore: ManagedRelay.ManagedRelayAccessTokenStore = {
120 load: loadManagedRelayAccessTokens.pipe(
121 Effect.tapError(logStoreFailure),
122 Effect.orElseSucceed(() => []),
123 Effect.withSpan("mobile.managedRelayTokenStore.load"),
124 ),
125 save: Effect.fn("mobile.managedRelayTokenStore.save")((entries) =>
126 saveManagedRelayAccessTokens(entries).pipe(Effect.tapError(logStoreFailure), Effect.ignore),
127 ),
128 clear: clearManagedRelayAccessTokens.pipe(
129 Effect.tapError(logStoreFailure),
130 Effect.ignore,
131 Effect.withSpan("mobile.managedRelayTokenStore.clear"),
132 ),
133};