1import {
2 BearerConnectionCredential,
3 BearerConnectionProfile,
4 BearerConnectionRegistration,
5 RelayConnectionRegistration,
6 RelayConnectionTarget,
7 BearerConnectionTarget,
8} from "@t3tools/client-runtime/connection";
9import {
10 type ConnectionCatalogDocument,
11 EMPTY_CONNECTION_CATALOG_DOCUMENT,
12 registerConnectionInCatalog,
13} from "@t3tools/client-runtime/platform";
14import { EnvironmentId } from "@t3tools/contracts";
15import * as Effect from "effect/Effect";
16import * as Schema from "effect/Schema";
17
18const LegacySavedRemoteConnection = Schema.Struct({
19 environmentId: EnvironmentId,
20 environmentLabel: Schema.String,
21 pairingUrl: Schema.String,
22 displayUrl: Schema.String,
23 httpBaseUrl: Schema.String,
24 wsBaseUrl: Schema.String,
25 bearerToken: Schema.NullOr(Schema.String),
26 authenticationMethod: Schema.optionalKey(Schema.Literals(["bearer", "dpop"])),
27 dpopAccessToken: Schema.optionalKey(Schema.String),
28 relayManaged: Schema.optionalKey(Schema.Literal(true)),
29});
30
31const LegacyConnectionDocument = Schema.Struct({
32 connections: Schema.optionalKey(Schema.Array(LegacySavedRemoteConnection)),
33});
34const decodeLegacyConnectionDocument = Schema.decodeUnknownEffect(LegacyConnectionDocument);
35
36export class LegacyConnectionMigrationError extends Schema.TaggedError<LegacyConnectionMigrationError>()(
37 "LegacyConnectionMigrationError",
38 {
39 message: Schema.String,
40 },
41) {}
42
43function isRelayManaged(connection: typeof LegacySavedRemoteConnection.Type): boolean {
44 return connection.relayManaged === true || connection.authenticationMethod === "dpop";
45}
46
47function migrateConnection(
48 document: ConnectionCatalogDocument,
49 connection: typeof LegacySavedRemoteConnection.Type,
50): ConnectionCatalogDocument {
51 if (isRelayManaged(connection)) {
52 return registerConnectionInCatalog(
53 document,
54 new RelayConnectionRegistration({
55 target: new RelayConnectionTarget({
56 environmentId: connection.environmentId,
57 label: connection.environmentLabel,
58 }),
59 }),
60 );
61 }
62
63 if (connection.bearerToken === null || connection.bearerToken.trim() === "") {
64 return document;
65 }
66
67 const connectionId = `bearer:${connection.environmentId}`;
68 return registerConnectionInCatalog(
69 document,
70 new BearerConnectionRegistration({
71 target: new BearerConnectionTarget({
72 environmentId: connection.environmentId,
73 label: connection.environmentLabel,
74 connectionId,
75 }),
76 profile: new BearerConnectionProfile({
77 connectionId,
78 environmentId: connection.environmentId,
79 label: connection.environmentLabel,
80 httpBaseUrl: connection.httpBaseUrl,
81 wsBaseUrl: connection.wsBaseUrl,
82 }),
83 credential: new BearerConnectionCredential({
84 token: connection.bearerToken,
85 }),
86 }),
87 );
88}
89
90export const migrateLegacyConnectionCatalog = Effect.fn(
91 "mobile.connectionMigration.migrateCatalog",
92)(function* (raw: string) {
93 const parsed = yield* Effect.try({
94 try: () => JSON.parse(raw) as unknown,
95 catch: (cause) =>
96 new LegacyConnectionMigrationError({
97 message: `Could not parse the legacy mobile connection catalog: ${String(cause)}`,
98 }),
99 });
100 const legacy = yield* decodeLegacyConnectionDocument(parsed).pipe(
101 Effect.mapError(
102 (cause) =>
103 new LegacyConnectionMigrationError({
104 message: `Could not decode the legacy mobile connection catalog: ${String(cause)}`,
105 }),
106 ),
107 );
108
109 return (legacy.connections ?? []).reduce(migrateConnection, EMPTY_CONNECTION_CATALOG_DOCUMENT);
110});