forked from
tranquil.farm/tranquil-pds
Our Personal Data Server from scratch!
1import type {
2 AccountResult,
3 ExternalDidWebState,
4 RegistrationInfo,
5 RegistrationMode,
6 RegistrationStep,
7 SessionState,
8} from "./types.ts";
9
10const STORAGE_KEY = "tranquil_registration_state";
11const MAX_AGE_MS = 60 * 60 * 1000;
12
13interface StoredRegistrationState {
14 version: 1;
15 startedAt: string;
16 mode: RegistrationMode;
17 step: RegistrationStep;
18 pdsHostname: string;
19 info: RegistrationInfo;
20 externalDidWeb: StoredExternalDidWebState;
21 account: StoredAccountResult | null;
22 session: StoredSessionState | null;
23}
24
25interface StoredExternalDidWebState {
26 keyMode: "reserved" | "byod";
27 reservedSigningKey?: string;
28 byodPrivateKeyBase64?: string;
29 byodPublicKeyMultibase?: string;
30 initialDidDocument?: string;
31 updatedDidDocument?: string;
32}
33
34interface StoredAccountResult {
35 did: string;
36 handle: string;
37 setupToken?: string;
38 appPassword?: string;
39 appPasswordName?: string;
40}
41
42interface StoredSessionState {
43 accessJwt: string;
44 refreshJwt: string;
45}
46
47function uint8ArrayToBase64(arr: Uint8Array): string {
48 return btoa(Array.from(arr, (byte) => String.fromCharCode(byte)).join(""));
49}
50
51function base64ToUint8Array(base64: string): Uint8Array {
52 const binary = atob(base64);
53 return Uint8Array.from(binary, (char) => char.charCodeAt(0));
54}
55
56export function saveRegistrationState(
57 mode: RegistrationMode,
58 step: RegistrationStep,
59 pdsHostname: string,
60 info: RegistrationInfo,
61 externalDidWeb: ExternalDidWebState,
62 account: AccountResult | null,
63 session: SessionState | null,
64): void {
65 const stored: StoredRegistrationState = {
66 version: 1,
67 startedAt: new Date().toISOString(),
68 mode,
69 step,
70 pdsHostname,
71 info: { ...info, password: undefined },
72 externalDidWeb: {
73 keyMode: externalDidWeb.keyMode,
74 reservedSigningKey: externalDidWeb.reservedSigningKey,
75 byodPrivateKeyBase64: externalDidWeb.byodPrivateKey
76 ? uint8ArrayToBase64(externalDidWeb.byodPrivateKey)
77 : undefined,
78 byodPublicKeyMultibase: externalDidWeb.byodPublicKeyMultibase,
79 initialDidDocument: externalDidWeb.initialDidDocument,
80 updatedDidDocument: externalDidWeb.updatedDidDocument,
81 },
82 account: account
83 ? {
84 did: account.did,
85 handle: account.handle,
86 setupToken: account.setupToken,
87 appPassword: account.appPassword,
88 appPasswordName: account.appPasswordName,
89 }
90 : null,
91 session: session
92 ? {
93 accessJwt: session.accessJwt,
94 refreshJwt: session.refreshJwt,
95 }
96 : null,
97 };
98
99 try {
100 localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
101 } catch { /* localStorage unavailable */ }
102}
103
104export function loadRegistrationState(): {
105 mode: RegistrationMode;
106 step: RegistrationStep;
107 pdsHostname: string;
108 info: RegistrationInfo;
109 externalDidWeb: ExternalDidWebState;
110 account: AccountResult | null;
111 session: SessionState | null;
112} | null {
113 try {
114 const stored = localStorage.getItem(STORAGE_KEY);
115 if (!stored) return null;
116
117 const state = JSON.parse(stored) as StoredRegistrationState;
118
119 if (state.version !== 1) {
120 clearRegistrationState();
121 return null;
122 }
123
124 const startedAt = new Date(state.startedAt).getTime();
125 if (Date.now() - startedAt > MAX_AGE_MS) {
126 clearRegistrationState();
127 return null;
128 }
129
130 return {
131 mode: state.mode,
132 step: state.step,
133 pdsHostname: state.pdsHostname,
134 info: state.info,
135 externalDidWeb: {
136 keyMode: state.externalDidWeb.keyMode,
137 reservedSigningKey: state.externalDidWeb.reservedSigningKey,
138 byodPrivateKey: state.externalDidWeb.byodPrivateKeyBase64
139 ? base64ToUint8Array(state.externalDidWeb.byodPrivateKeyBase64)
140 : undefined,
141 byodPublicKeyMultibase: state.externalDidWeb.byodPublicKeyMultibase,
142 initialDidDocument: state.externalDidWeb.initialDidDocument,
143 updatedDidDocument: state.externalDidWeb.updatedDidDocument,
144 },
145 account: state.account
146 ? {
147 did: state.account.did as AccountResult["did"],
148 handle: state.account.handle as AccountResult["handle"],
149 setupToken: state.account.setupToken,
150 appPassword: state.account.appPassword,
151 appPasswordName: state.account.appPasswordName,
152 }
153 : null,
154 session: state.session
155 ? {
156 accessJwt: state.session.accessJwt as SessionState["accessJwt"],
157 refreshJwt: state.session.refreshJwt as SessionState["refreshJwt"],
158 }
159 : null,
160 };
161 } catch {
162 clearRegistrationState();
163 return null;
164 }
165}
166
167export function clearRegistrationState(): void {
168 try {
169 localStorage.removeItem(STORAGE_KEY);
170 } catch { /* localStorage unavailable */ }
171}
172
173export function hasPendingRegistration(): boolean {
174 const state = loadRegistrationState();
175 return state !== null && state.step !== "info" &&
176 state.step !== "redirect-to-dashboard";
177}
178
179export function getRegistrationResumeInfo(): {
180 mode: RegistrationMode;
181 handle: string;
182 step: RegistrationStep;
183 did?: string;
184} | null {
185 const state = loadRegistrationState();
186 if (
187 !state || state.step === "info" || state.step === "redirect-to-dashboard"
188 ) {
189 return null;
190 }
191
192 return {
193 mode: state.mode,
194 handle: state.info.handle,
195 step: state.step,
196 did: state.account?.did,
197 };
198}