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
90const decodeLegacyCatalogJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
91
92export const migrateLegacyConnectionCatalog = Effect.fn(
93 "mobile.connectionMigration.migrateCatalog",
94)(function* (raw: string) {
95 const parsed = yield* decodeLegacyCatalogJson(raw).pipe(
96 Effect.mapError(
97 (cause) =>
98 new LegacyConnectionMigrationError({
99 message: `Could not parse the legacy mobile connection catalog: ${String(cause)}`,
100 }),
101 ),
102 );
103 const legacy = yield* decodeLegacyConnectionDocument(parsed).pipe(
104 Effect.mapError(
105 (cause) =>
106 new LegacyConnectionMigrationError({
107 message: `Could not decode the legacy mobile connection catalog: ${String(cause)}`,
108 }),
109 ),
110 );
111
112 return (legacy.connections ?? []).reduce(migrateConnection, EMPTY_CONNECTION_CATALOG_DOCUMENT);
113});