local-first-auth-import-export 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/index.cjs +560 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +396 -0
- package/dist/index.d.ts +396 -0
- package/dist/index.js +492 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +1056 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +400 -0
- package/dist/react.d.ts +400 -0
- package/dist/react.js +991 -0
- package/dist/react.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var EXPORT_FILE_TYPE = "local-first-auth:export";
|
|
3
|
+
var EXPORT_FILE_VERSION = 1;
|
|
4
|
+
|
|
5
|
+
// src/core/crypto.ts
|
|
6
|
+
import * as ed25519 from "@stablelib/ed25519";
|
|
7
|
+
import { encode as base58Encode } from "base58-universal";
|
|
8
|
+
import * as base64 from "base64-js";
|
|
9
|
+
var ED25519_MULTICODEC_PREFIX = new Uint8Array([237, 1]);
|
|
10
|
+
var SECRET_KEY_SIZE = 64;
|
|
11
|
+
var PUBLIC_KEY_SIZE = 32;
|
|
12
|
+
var base64url = {
|
|
13
|
+
encode: (input) => {
|
|
14
|
+
const base64String = base64.fromByteArray(input);
|
|
15
|
+
return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
16
|
+
},
|
|
17
|
+
decode: (input) => {
|
|
18
|
+
let base64String = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
19
|
+
const padding = (4 - base64String.length % 4) % 4;
|
|
20
|
+
base64String += "=".repeat(padding);
|
|
21
|
+
return base64.toByteArray(base64String);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
function createDidFromPublicKey(publicKey) {
|
|
25
|
+
const multicodecKey = new Uint8Array(ED25519_MULTICODEC_PREFIX.length + publicKey.length);
|
|
26
|
+
multicodecKey.set(ED25519_MULTICODEC_PREFIX);
|
|
27
|
+
multicodecKey.set(publicKey, ED25519_MULTICODEC_PREFIX.length);
|
|
28
|
+
const encoded = base58Encode(multicodecKey);
|
|
29
|
+
return `did:key:z${encoded}`;
|
|
30
|
+
}
|
|
31
|
+
function validatePrivateKey(privateKey) {
|
|
32
|
+
if (typeof privateKey !== "string" || privateKey.trim() === "") {
|
|
33
|
+
return { valid: false, error: "Private key must be a non-empty string" };
|
|
34
|
+
}
|
|
35
|
+
let bytes;
|
|
36
|
+
try {
|
|
37
|
+
bytes = base64.toByteArray(privateKey.trim());
|
|
38
|
+
} catch {
|
|
39
|
+
return { valid: false, error: "Private key is not valid base64" };
|
|
40
|
+
}
|
|
41
|
+
if (bytes.length !== SECRET_KEY_SIZE) {
|
|
42
|
+
return {
|
|
43
|
+
valid: false,
|
|
44
|
+
error: `Private key must decode to ${SECRET_KEY_SIZE} bytes, got ${bytes.length}`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { valid: true };
|
|
48
|
+
}
|
|
49
|
+
function deriveKeysFromPrivateKey(privateKey) {
|
|
50
|
+
const { valid, error } = validatePrivateKey(privateKey);
|
|
51
|
+
if (!valid) {
|
|
52
|
+
throw new Error(`Invalid private key: ${error}`);
|
|
53
|
+
}
|
|
54
|
+
const trimmed = privateKey.trim();
|
|
55
|
+
const secretKey = base64.toByteArray(trimmed);
|
|
56
|
+
const publicKey = ed25519.extractPublicKeyFromSecretKey(secretKey);
|
|
57
|
+
const did = createDidFromPublicKey(publicKey);
|
|
58
|
+
return {
|
|
59
|
+
did,
|
|
60
|
+
publicKey: base64.fromByteArray(publicKey),
|
|
61
|
+
privateKey: trimmed
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function decodePublicKey(publicKey) {
|
|
65
|
+
return base64.toByteArray(publicKey);
|
|
66
|
+
}
|
|
67
|
+
async function createJWT(payload, privateKey) {
|
|
68
|
+
const privateKeyBytes = base64.toByteArray(privateKey);
|
|
69
|
+
if (privateKeyBytes.length !== SECRET_KEY_SIZE) {
|
|
70
|
+
throw new Error("Invalid private key length. Expected 64 bytes.");
|
|
71
|
+
}
|
|
72
|
+
const header = {
|
|
73
|
+
alg: "EdDSA",
|
|
74
|
+
typ: "JWT"
|
|
75
|
+
};
|
|
76
|
+
const headerB64 = base64url.encode(new TextEncoder().encode(JSON.stringify(header)));
|
|
77
|
+
const payloadB64 = base64url.encode(new TextEncoder().encode(JSON.stringify(payload)));
|
|
78
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
79
|
+
const signingInputBytes = new TextEncoder().encode(signingInput);
|
|
80
|
+
const signature = ed25519.sign(privateKeyBytes, signingInputBytes);
|
|
81
|
+
const signatureB64 = base64url.encode(signature);
|
|
82
|
+
return `${signingInput}.${signatureB64}`;
|
|
83
|
+
}
|
|
84
|
+
function decodeJWT(jwt) {
|
|
85
|
+
const parts = jwt.split(".");
|
|
86
|
+
if (parts.length !== 3) {
|
|
87
|
+
throw new Error("Invalid JWT format. Expected 3 parts separated by dots.");
|
|
88
|
+
}
|
|
89
|
+
const [encodedHeader, encodedPayload, signature] = parts;
|
|
90
|
+
if (!encodedHeader || !encodedPayload || !signature) {
|
|
91
|
+
throw new Error("Invalid JWT format: missing parts");
|
|
92
|
+
}
|
|
93
|
+
const headerBytes = base64url.decode(encodedHeader);
|
|
94
|
+
const payloadBytes = base64url.decode(encodedPayload);
|
|
95
|
+
const header = JSON.parse(new TextDecoder().decode(headerBytes));
|
|
96
|
+
const payload = JSON.parse(new TextDecoder().decode(payloadBytes));
|
|
97
|
+
return { header, payload, signature };
|
|
98
|
+
}
|
|
99
|
+
function verifyJWT(jwt, publicKey) {
|
|
100
|
+
try {
|
|
101
|
+
const parts = jwt.split(".");
|
|
102
|
+
if (parts.length !== 3) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
106
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
107
|
+
const signingInputBytes = new TextEncoder().encode(signingInput);
|
|
108
|
+
const signatureBytes = base64url.decode(encodedSignature);
|
|
109
|
+
return ed25519.verify(publicKey, signingInputBytes, signatureBytes);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error("JWT verification error:", error);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/core/storage.ts
|
|
117
|
+
var STORAGE_KEYS = {
|
|
118
|
+
PROFILE: "local-first-auth:profile",
|
|
119
|
+
PRIVATE_KEY: "local-first-auth:privateKey"
|
|
120
|
+
};
|
|
121
|
+
function saveProfile(profile) {
|
|
122
|
+
try {
|
|
123
|
+
localStorage.setItem(STORAGE_KEYS.PROFILE, JSON.stringify(profile));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error("Failed to save profile:", error);
|
|
126
|
+
throw new Error("Failed to save profile to LocalStorage");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function getProfile() {
|
|
130
|
+
try {
|
|
131
|
+
const profileString = localStorage.getItem(STORAGE_KEYS.PROFILE);
|
|
132
|
+
if (!profileString) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return JSON.parse(profileString);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error("Failed to get profile:", error);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function savePrivateKey(privateKey) {
|
|
142
|
+
try {
|
|
143
|
+
localStorage.setItem(STORAGE_KEYS.PRIVATE_KEY, privateKey);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error("Failed to save private key:", error);
|
|
146
|
+
throw new Error("Failed to save private key to LocalStorage");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function getPrivateKey() {
|
|
150
|
+
try {
|
|
151
|
+
return localStorage.getItem(STORAGE_KEYS.PRIVATE_KEY);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error("Failed to get private key:", error);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function clearProfile() {
|
|
158
|
+
try {
|
|
159
|
+
localStorage.removeItem(STORAGE_KEYS.PROFILE);
|
|
160
|
+
localStorage.removeItem(STORAGE_KEYS.PRIVATE_KEY);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error("Failed to clear profile:", error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function hasProfile() {
|
|
166
|
+
return getProfile() !== null && getPrivateKey() !== null;
|
|
167
|
+
}
|
|
168
|
+
function getCurrentProfile() {
|
|
169
|
+
const storedProfile = getProfile();
|
|
170
|
+
const privateKey = getPrivateKey();
|
|
171
|
+
if (!storedProfile || !privateKey) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
did: storedProfile.did,
|
|
176
|
+
name: storedProfile.name,
|
|
177
|
+
socials: storedProfile.socials,
|
|
178
|
+
avatar: storedProfile.avatar
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/core/export.ts
|
|
183
|
+
function exportProfile() {
|
|
184
|
+
const profile = getProfile();
|
|
185
|
+
const privateKey = getPrivateKey();
|
|
186
|
+
if (!profile || !privateKey) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const { did, publicKey } = deriveKeysFromPrivateKey(privateKey);
|
|
190
|
+
return {
|
|
191
|
+
type: EXPORT_FILE_TYPE,
|
|
192
|
+
version: EXPORT_FILE_VERSION,
|
|
193
|
+
did,
|
|
194
|
+
publicKey,
|
|
195
|
+
privateKey,
|
|
196
|
+
name: profile.name,
|
|
197
|
+
socials: profile.socials ?? [],
|
|
198
|
+
avatar: profile.avatar ?? null,
|
|
199
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function exportProfileToJSON(pretty = true) {
|
|
203
|
+
const exported = exportProfile();
|
|
204
|
+
if (!exported) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
return JSON.stringify(exported, null, pretty ? 2 : 0);
|
|
208
|
+
}
|
|
209
|
+
function defaultExportFilename(did) {
|
|
210
|
+
const key = did.replace(/^did:key:/, "");
|
|
211
|
+
const shortKey = key.slice(0, 12).replace(/[^a-zA-Z0-9]/g, "");
|
|
212
|
+
return `local-first-auth-profile-${shortKey}.json`;
|
|
213
|
+
}
|
|
214
|
+
function downloadProfile(filename) {
|
|
215
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
216
|
+
console.warn("Cannot download profile: not in a browser environment");
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const exported = exportProfile();
|
|
220
|
+
if (!exported) {
|
|
221
|
+
console.warn("Cannot download profile: no profile found");
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const json = JSON.stringify(exported, null, 2);
|
|
225
|
+
const blob = new Blob([json], { type: "application/json" });
|
|
226
|
+
const url = URL.createObjectURL(blob);
|
|
227
|
+
const anchor = document.createElement("a");
|
|
228
|
+
anchor.href = url;
|
|
229
|
+
anchor.download = filename || defaultExportFilename(exported.did);
|
|
230
|
+
anchor.style.display = "none";
|
|
231
|
+
document.body.appendChild(anchor);
|
|
232
|
+
anchor.click();
|
|
233
|
+
document.body.removeChild(anchor);
|
|
234
|
+
URL.revokeObjectURL(url);
|
|
235
|
+
return exported;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/core/api.ts
|
|
239
|
+
var LocalFirstAuthAPI = class {
|
|
240
|
+
/**
|
|
241
|
+
* Get profile details as a signed JWT
|
|
242
|
+
*/
|
|
243
|
+
async getProfileDetails() {
|
|
244
|
+
const profile = getProfile();
|
|
245
|
+
const privateKey = getPrivateKey();
|
|
246
|
+
if (!profile || !privateKey) {
|
|
247
|
+
throw new Error("No profile found. User must create or import a profile first.");
|
|
248
|
+
}
|
|
249
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
250
|
+
const payload = {
|
|
251
|
+
iss: profile.did,
|
|
252
|
+
aud: window.location.origin,
|
|
253
|
+
iat: now,
|
|
254
|
+
exp: now + 120,
|
|
255
|
+
// 2 minutes
|
|
256
|
+
type: "localFirstAuth:profile:details",
|
|
257
|
+
data: {
|
|
258
|
+
did: profile.did,
|
|
259
|
+
name: profile.name,
|
|
260
|
+
socials: profile.socials || []
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
return createJWT(payload, privateKey);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get avatar as base64-encoded string in a signed JWT
|
|
267
|
+
*/
|
|
268
|
+
async getAvatar() {
|
|
269
|
+
const profile = getProfile();
|
|
270
|
+
const privateKey = getPrivateKey();
|
|
271
|
+
if (!profile || !privateKey) {
|
|
272
|
+
throw new Error("No profile found. User must create or import a profile first.");
|
|
273
|
+
}
|
|
274
|
+
if (!profile.avatar) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
278
|
+
const payload = {
|
|
279
|
+
iss: profile.did,
|
|
280
|
+
aud: window.location.origin,
|
|
281
|
+
iat: now,
|
|
282
|
+
exp: now + 120,
|
|
283
|
+
type: "localFirstAuth:avatar",
|
|
284
|
+
data: {
|
|
285
|
+
did: profile.did,
|
|
286
|
+
avatar: profile.avatar
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
return createJWT(payload, privateKey);
|
|
290
|
+
}
|
|
291
|
+
getAppDetails() {
|
|
292
|
+
return {
|
|
293
|
+
name: "Local First Auth",
|
|
294
|
+
version: "2.0.0",
|
|
295
|
+
platform: "browser",
|
|
296
|
+
supportedPermissions: ["profile"]
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async requestPermission(permission) {
|
|
300
|
+
if (permission === "profile") {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
console.warn(`Permission "${permission}" is not yet supported`);
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
close() {
|
|
307
|
+
console.log("close() called - no-op in web version");
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
function injectLocalFirstAuthAPI() {
|
|
311
|
+
if (typeof window === "undefined") {
|
|
312
|
+
console.warn("Cannot inject Local First Auth API: window is undefined (not in browser)");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (window.localFirstAuth) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
;
|
|
319
|
+
window.localFirstAuth = new LocalFirstAuthAPI();
|
|
320
|
+
}
|
|
321
|
+
function removeLocalFirstAuthAPI() {
|
|
322
|
+
if (typeof window === "undefined") {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
delete window.localFirstAuth;
|
|
326
|
+
}
|
|
327
|
+
function hasLocalFirstAuthAPI() {
|
|
328
|
+
return typeof window !== "undefined" && !!window.localFirstAuth;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/core/import.ts
|
|
332
|
+
function validateExportedProfile(input) {
|
|
333
|
+
const errors = [];
|
|
334
|
+
let value = input;
|
|
335
|
+
if (typeof value === "string") {
|
|
336
|
+
try {
|
|
337
|
+
value = JSON.parse(value);
|
|
338
|
+
} catch {
|
|
339
|
+
return { valid: false, errors: ["File is not valid JSON"] };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
343
|
+
return { valid: false, errors: ["Expected a JSON object"] };
|
|
344
|
+
}
|
|
345
|
+
const candidate = value;
|
|
346
|
+
if (candidate.type !== EXPORT_FILE_TYPE) {
|
|
347
|
+
errors.push(
|
|
348
|
+
`Not a Local First Auth profile export (expected type "${EXPORT_FILE_TYPE}", got ${JSON.stringify(candidate.type)})`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
if (candidate.version !== EXPORT_FILE_VERSION) {
|
|
352
|
+
errors.push(
|
|
353
|
+
`Unsupported export version: ${JSON.stringify(candidate.version)} (this package supports version ${EXPORT_FILE_VERSION})`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
const keyCheck = validatePrivateKey(candidate.privateKey);
|
|
357
|
+
if (!keyCheck.valid) {
|
|
358
|
+
errors.push(keyCheck.error);
|
|
359
|
+
return { valid: false, errors };
|
|
360
|
+
}
|
|
361
|
+
const derived = deriveKeysFromPrivateKey(candidate.privateKey);
|
|
362
|
+
if (typeof candidate.did !== "string" || candidate.did !== derived.did) {
|
|
363
|
+
errors.push(
|
|
364
|
+
`Profile file is inconsistent: "did" does not match the private key (expected ${derived.did})`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (typeof candidate.publicKey !== "string" || candidate.publicKey !== derived.publicKey) {
|
|
368
|
+
errors.push('Profile file is inconsistent: "publicKey" does not match the private key');
|
|
369
|
+
}
|
|
370
|
+
if (candidate.name !== void 0 && typeof candidate.name !== "string") {
|
|
371
|
+
errors.push('"name" must be a string');
|
|
372
|
+
}
|
|
373
|
+
if (candidate.socials !== void 0) {
|
|
374
|
+
if (!Array.isArray(candidate.socials)) {
|
|
375
|
+
errors.push('"socials" must be an array');
|
|
376
|
+
} else {
|
|
377
|
+
const malformed = candidate.socials.some(
|
|
378
|
+
(s) => typeof s !== "object" || s === null || typeof s.platform !== "string" || typeof s.handle !== "string"
|
|
379
|
+
);
|
|
380
|
+
if (malformed) {
|
|
381
|
+
errors.push('"socials" entries must each be { platform, handle }');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (candidate.avatar !== void 0 && candidate.avatar !== null && typeof candidate.avatar !== "string") {
|
|
386
|
+
errors.push('"avatar" must be a string or null');
|
|
387
|
+
}
|
|
388
|
+
if (errors.length > 0) {
|
|
389
|
+
return { valid: false, errors };
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
valid: true,
|
|
393
|
+
errors: [],
|
|
394
|
+
profile: candidate
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function parseExportedProfile(input) {
|
|
398
|
+
if (typeof input === "string") {
|
|
399
|
+
const trimmed = input.trim();
|
|
400
|
+
if (!trimmed) {
|
|
401
|
+
throw new Error("Nothing to import: input is empty");
|
|
402
|
+
}
|
|
403
|
+
const looksLikeJSON = trimmed.startsWith("{");
|
|
404
|
+
if (!looksLikeJSON) {
|
|
405
|
+
const keyCheck = validatePrivateKey(trimmed);
|
|
406
|
+
if (!keyCheck.valid) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`Could not import: input is neither a profile export file nor a valid private key (${keyCheck.error})`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const { did, publicKey, privateKey } = deriveKeysFromPrivateKey(trimmed);
|
|
412
|
+
return {
|
|
413
|
+
type: EXPORT_FILE_TYPE,
|
|
414
|
+
version: EXPORT_FILE_VERSION,
|
|
415
|
+
did,
|
|
416
|
+
publicKey,
|
|
417
|
+
privateKey
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const result = validateExportedProfile(input);
|
|
422
|
+
if (!result.valid || !result.profile) {
|
|
423
|
+
throw new Error(result.errors.join("; "));
|
|
424
|
+
}
|
|
425
|
+
return result.profile;
|
|
426
|
+
}
|
|
427
|
+
async function importProfile(input, options = {}) {
|
|
428
|
+
const exported = parseExportedProfile(input);
|
|
429
|
+
const { did, privateKey } = deriveKeysFromPrivateKey(exported.privateKey);
|
|
430
|
+
if (hasProfile() && !options.overwrite) {
|
|
431
|
+
const existing = getProfile();
|
|
432
|
+
if (existing && existing.did !== did) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
"A different profile already exists on this device. Pass { overwrite: true } to replace it \u2014 this permanently discards the existing private key."
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const profile = {
|
|
439
|
+
did,
|
|
440
|
+
name: exported.name || "anonymous",
|
|
441
|
+
socials: exported.socials ?? [],
|
|
442
|
+
avatar: exported.avatar ?? null
|
|
443
|
+
};
|
|
444
|
+
saveProfile({
|
|
445
|
+
did: profile.did,
|
|
446
|
+
name: profile.name,
|
|
447
|
+
socials: profile.socials,
|
|
448
|
+
avatar: profile.avatar
|
|
449
|
+
});
|
|
450
|
+
savePrivateKey(privateKey);
|
|
451
|
+
injectLocalFirstAuthAPI();
|
|
452
|
+
return profile;
|
|
453
|
+
}
|
|
454
|
+
async function importProfileFromFile(file, options = {}) {
|
|
455
|
+
const text = await file.text();
|
|
456
|
+
return importProfile(text, options);
|
|
457
|
+
}
|
|
458
|
+
export {
|
|
459
|
+
EXPORT_FILE_TYPE,
|
|
460
|
+
EXPORT_FILE_VERSION,
|
|
461
|
+
LocalFirstAuthAPI,
|
|
462
|
+
PUBLIC_KEY_SIZE,
|
|
463
|
+
SECRET_KEY_SIZE,
|
|
464
|
+
STORAGE_KEYS,
|
|
465
|
+
base64url,
|
|
466
|
+
clearProfile,
|
|
467
|
+
createDidFromPublicKey,
|
|
468
|
+
createJWT,
|
|
469
|
+
decodeJWT,
|
|
470
|
+
decodePublicKey,
|
|
471
|
+
defaultExportFilename,
|
|
472
|
+
deriveKeysFromPrivateKey,
|
|
473
|
+
downloadProfile,
|
|
474
|
+
exportProfile,
|
|
475
|
+
exportProfileToJSON,
|
|
476
|
+
getCurrentProfile,
|
|
477
|
+
getPrivateKey,
|
|
478
|
+
getProfile,
|
|
479
|
+
hasLocalFirstAuthAPI,
|
|
480
|
+
hasProfile,
|
|
481
|
+
importProfile,
|
|
482
|
+
importProfileFromFile,
|
|
483
|
+
injectLocalFirstAuthAPI,
|
|
484
|
+
parseExportedProfile,
|
|
485
|
+
removeLocalFirstAuthAPI,
|
|
486
|
+
savePrivateKey,
|
|
487
|
+
saveProfile,
|
|
488
|
+
validateExportedProfile,
|
|
489
|
+
validatePrivateKey,
|
|
490
|
+
verifyJWT
|
|
491
|
+
};
|
|
492
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/core/crypto.ts","../src/core/storage.ts","../src/core/export.ts","../src/core/api.ts","../src/core/import.ts"],"sourcesContent":["/**\n * Local First Auth Import/Export Types\n * Compatible with the Local First Auth Specification\n */\n\n// ============================================================================\n// Profile Types (shared with local-first-auth)\n// ============================================================================\n\nexport interface SocialLink {\n platform: SocialPlatform\n handle: string\n}\n\nexport type SocialPlatform =\n | 'INSTAGRAM'\n | 'X'\n | 'BLUESKY'\n | 'LINKEDIN'\n | 'YOUTUBE'\n | 'SPOTIFY'\n | 'TIKTOK'\n | 'SNAPCHAT'\n | 'GITHUB'\n | 'FACEBOOK'\n | 'REDDIT'\n | 'DISCORD'\n | 'TWITCH'\n | 'TELEGRAM'\n | 'PINTEREST'\n | 'TUMBLR'\n | 'SOUNDCLOUD'\n | 'BANDCAMP'\n | 'PATREON'\n | 'KO_FI'\n | 'MASTODON'\n | 'WEBSITE'\n | 'EMAIL'\n\nexport interface Profile {\n did: string\n name: string\n socials?: SocialLink[]\n avatar?: string | null\n}\n\nexport interface ProfileKeys {\n did: string\n privateKey: string // base64-encoded 64-byte Ed25519 secret key\n publicKey: string // base64-encoded 32-byte public key\n}\n\nexport interface StoredProfile {\n did: string\n name: string\n socials?: SocialLink[]\n avatar?: string | null\n}\n\n// ============================================================================\n// Export File Format\n// ============================================================================\n\n/**\n * The magic string identifying a Local First Auth export file.\n * Present as the `type` field so a consumer can cheaply confirm the format.\n */\nexport const EXPORT_FILE_TYPE = 'local-first-auth:export' as const\n\n/**\n * The current export file schema version.\n */\nexport const EXPORT_FILE_VERSION = 1 as const\n\n/**\n * A Local First Auth profile export.\n *\n * This is the exact shape of the exported `.json` file. It is self-identifying\n * (`type` + `version`) and internally consistent: `did` and `publicKey` MUST be\n * re-derivable from `privateKey`, which lets any consumer validate the file\n * without trusting its contents.\n *\n * SECURITY: this contains a plaintext private key. Anyone who has it can act as\n * the user. Never transmit or log it.\n */\nexport interface ExportedProfile {\n /** Fixed magic string identifying this as a Local First Auth export */\n type: typeof EXPORT_FILE_TYPE\n /** Schema version, for forward compatibility */\n version: typeof EXPORT_FILE_VERSION\n /** The user's DID (did:key:z...) */\n did: string\n /** base64-encoded 32-byte Ed25519 public key */\n publicKey: string\n /** base64-encoded 64-byte Ed25519 secret key */\n privateKey: string\n /** Display name */\n name?: string\n /** Social links */\n socials?: SocialLink[]\n /** Avatar as a data: URI */\n avatar?: string | null\n /** ISO 8601 timestamp of when the file was exported */\n exportedAt?: string\n}\n\n/**\n * Result of validating an untrusted value against the export file format.\n */\nexport interface ValidationResult {\n valid: boolean\n errors: string[]\n profile?: ExportedProfile\n}\n\n/**\n * Options for importing a profile.\n */\nexport interface ImportOptions {\n /**\n * Replace an existing profile with a different DID.\n * Defaults to false — importing over a different identity throws unless this\n * is explicitly set, to guard against accidentally destroying the user's keys.\n */\n overwrite?: boolean\n}\n\n// ============================================================================\n// Local First Auth API Types (from specification)\n// ============================================================================\n\nexport interface LocalFirstAuth {\n getProfileDetails(): Promise<string>\n getAvatar(): Promise<string | null>\n getAppDetails(): AppDetails\n requestPermission(permission: string): Promise<boolean>\n close(): void\n}\n\nexport interface AppDetails {\n name: string\n version: string\n platform: 'ios' | 'android' | 'browser'\n supportedPermissions: string[]\n}\n\n// ============================================================================\n// JWT Types\n// ============================================================================\n\nexport interface JWTHeader {\n alg: 'EdDSA'\n typ: 'JWT'\n}\n\nexport interface JWTPayload {\n iss: string // Issuer - user's DID\n aud: string // Audience - origin URL\n iat: number // Issued at timestamp\n exp: number // Expiration timestamp\n type: string // Operation type\n data?: any // Type-specific payload\n}\n\n// ============================================================================\n// Styling\n// ============================================================================\n\nexport interface CustomStyles {\n primaryColor?: string\n backgroundColor?: string\n textColor?: string\n borderRadius?: string\n fontFamily?: string\n buttonRadius?: string\n inputRadius?: string\n inputTextColor?: string\n inputBackgroundColor?: string\n\n /** Scale factor for button press animation on mobile (0-1). Default: 0.95 */\n mobileButtonPressScale?: number\n\n /** Color for mobile tap highlight. Default: 'transparent' */\n mobileTapHighlightColor?: string\n\n /** Enable safe area insets for iOS notch / Android nav bars. Default: true */\n useSafeAreaInsets?: boolean\n}\n\n// ============================================================================\n// Component Props (React)\n// ============================================================================\n\nexport interface ExportProfileProps {\n customStyles?: CustomStyles\n /** Called after the user downloads their profile backup */\n onExport?: (profile: ExportedProfile) => void\n /** Rendered when no profile exists. Defaults to a short message. */\n emptyState?: React.ReactNode\n}\n\nexport interface ImportProfileProps {\n customStyles?: CustomStyles\n /** Called with the imported profile on success */\n onComplete?: (profile: Profile) => void\n onBack?: () => void\n}\n\nexport interface ImportExportProps {\n customStyles?: CustomStyles\n onComplete?: (profile: Profile) => void\n onExport?: (profile: ExportedProfile) => void\n /** Which tab is shown first. Default: 'import' */\n defaultTab?: 'import' | 'export'\n /** Hide the import tab */\n skipImport?: boolean\n /** Hide the export tab */\n skipExport?: boolean\n}\n","/**\n * Cryptographic utilities for DID derivation and JWT signing.\n *\n * Mirrors local-first-auth's crypto layer so that keys/DIDs/JWTs produced here\n * are byte-for-byte compatible, and adds the primitives needed to reconstruct an\n * identity from an existing private key (rather than generating a new one).\n */\n\nimport * as ed25519 from '@stablelib/ed25519'\nimport { encode as base58Encode } from 'base58-universal'\nimport * as base64 from 'base64-js'\nimport type { JWTHeader, JWTPayload, ProfileKeys } from '../types'\n\n/**\n * Ed25519 multicodec prefix for did:key format\n * 0xed = 237 (Ed25519 public key)\n * 0x01 = 1 (key type identifier)\n */\nconst ED25519_MULTICODEC_PREFIX = new Uint8Array([0xed, 0x01])\n\n/** Ed25519 secret key size in bytes (32-byte seed + 32-byte public key) */\nexport const SECRET_KEY_SIZE = 64\n\n/** Ed25519 public key size in bytes */\nexport const PUBLIC_KEY_SIZE = 32\n\n/**\n * Helper to encode/decode base64url (RFC 4648)\n */\nexport const base64url = {\n encode: (input: Uint8Array): string => {\n const base64String = base64.fromByteArray(input)\n return base64String\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n },\n\n decode: (input: string): Uint8Array => {\n let base64String = input\n .replace(/-/g, '+')\n .replace(/_/g, '/')\n\n const padding = (4 - (base64String.length % 4)) % 4\n base64String += '='.repeat(padding)\n\n return base64.toByteArray(base64String)\n }\n}\n\n/**\n * Create a did:key DID from an Ed25519 public key\n *\n * @param publicKey - The Ed25519 public key as Uint8Array (32 bytes)\n * @returns The did:key formatted DID string\n */\nexport function createDidFromPublicKey(publicKey: Uint8Array): string {\n // Prepend multicodec prefix to public key\n const multicodecKey = new Uint8Array(ED25519_MULTICODEC_PREFIX.length + publicKey.length)\n multicodecKey.set(ED25519_MULTICODEC_PREFIX)\n multicodecKey.set(publicKey, ED25519_MULTICODEC_PREFIX.length)\n\n // Base58 encode and prepend 'z' for base58btc multibase encoding\n const encoded = base58Encode(multicodecKey)\n\n return `did:key:z${encoded}`\n}\n\n/**\n * Validate that a string is a usable Ed25519 private key.\n *\n * A valid key is base64 that decodes to exactly 64 bytes (the Ed25519 secret\n * key: a 32-byte seed followed by the 32-byte public key).\n */\nexport function validatePrivateKey(privateKey: unknown): { valid: boolean; error?: string } {\n if (typeof privateKey !== 'string' || privateKey.trim() === '') {\n return { valid: false, error: 'Private key must be a non-empty string' }\n }\n\n let bytes: Uint8Array\n try {\n bytes = base64.toByteArray(privateKey.trim())\n } catch {\n return { valid: false, error: 'Private key is not valid base64' }\n }\n\n if (bytes.length !== SECRET_KEY_SIZE) {\n return {\n valid: false,\n error: `Private key must decode to ${SECRET_KEY_SIZE} bytes, got ${bytes.length}`\n }\n }\n\n return { valid: true }\n}\n\n/**\n * Reconstruct a full identity (DID + public key) from an existing private key.\n *\n * This is the core of \"import\": an Ed25519 secret key already contains its\n * public key in the final 32 bytes, so the private key alone is enough to\n * recover the complete identity. No new keypair is generated.\n *\n * @param privateKey - base64-encoded 64-byte Ed25519 secret key\n * @returns The derived { did, publicKey, privateKey }\n * @throws If the private key is malformed\n */\nexport function deriveKeysFromPrivateKey(privateKey: string): ProfileKeys {\n const { valid, error } = validatePrivateKey(privateKey)\n if (!valid) {\n throw new Error(`Invalid private key: ${error}`)\n }\n\n const trimmed = privateKey.trim()\n const secretKey = base64.toByteArray(trimmed)\n\n // The public key is the last 32 bytes of the 64-byte secret key\n const publicKey = ed25519.extractPublicKeyFromSecretKey(secretKey)\n const did = createDidFromPublicKey(publicKey)\n\n return {\n did,\n publicKey: base64.fromByteArray(publicKey),\n privateKey: trimmed\n }\n}\n\n/**\n * Decode a base64-encoded public key back into bytes (useful for verifyJWT)\n */\nexport function decodePublicKey(publicKey: string): Uint8Array {\n return base64.toByteArray(publicKey)\n}\n\n/**\n * Create and sign a JWT using Ed25519\n *\n * @param payload - JWT payload containing claims\n * @param privateKey - base64-encoded 64-byte Ed25519 secret key\n * @returns Signed JWT string\n */\nexport async function createJWT(payload: JWTPayload, privateKey: string): Promise<string> {\n const privateKeyBytes = base64.toByteArray(privateKey)\n\n if (privateKeyBytes.length !== SECRET_KEY_SIZE) {\n throw new Error('Invalid private key length. Expected 64 bytes.')\n }\n\n const header: JWTHeader = {\n alg: 'EdDSA',\n typ: 'JWT',\n }\n\n const headerB64 = base64url.encode(new TextEncoder().encode(JSON.stringify(header)))\n const payloadB64 = base64url.encode(new TextEncoder().encode(JSON.stringify(payload)))\n\n const signingInput = `${headerB64}.${payloadB64}`\n const signingInputBytes = new TextEncoder().encode(signingInput)\n\n const signature = ed25519.sign(privateKeyBytes, signingInputBytes)\n const signatureB64 = base64url.encode(signature)\n\n return `${signingInput}.${signatureB64}`\n}\n\n/**\n * Decode a JWT (for debugging - does NOT verify the signature)\n */\nexport function decodeJWT(jwt: string): { header: JWTHeader; payload: JWTPayload; signature: string } {\n const parts = jwt.split('.')\n\n if (parts.length !== 3) {\n throw new Error('Invalid JWT format. Expected 3 parts separated by dots.')\n }\n\n const [encodedHeader, encodedPayload, signature] = parts\n\n if (!encodedHeader || !encodedPayload || !signature) {\n throw new Error('Invalid JWT format: missing parts')\n }\n\n const headerBytes = base64url.decode(encodedHeader)\n const payloadBytes = base64url.decode(encodedPayload)\n\n const header = JSON.parse(new TextDecoder().decode(headerBytes)) as JWTHeader\n const payload = JSON.parse(new TextDecoder().decode(payloadBytes)) as JWTPayload\n\n return { header, payload, signature }\n}\n\n/**\n * Verify a JWT signature against an Ed25519 public key\n *\n * @param jwt - The JWT string to verify\n * @param publicKey - The Ed25519 public key (32 bytes)\n */\nexport function verifyJWT(jwt: string, publicKey: Uint8Array): boolean {\n try {\n const parts = jwt.split('.')\n\n if (parts.length !== 3) {\n return false\n }\n\n const [encodedHeader, encodedPayload, encodedSignature] = parts\n\n const signingInput = `${encodedHeader}.${encodedPayload}`\n const signingInputBytes = new TextEncoder().encode(signingInput)\n\n const signatureBytes = base64url.decode(encodedSignature!)\n\n return ed25519.verify(publicKey, signingInputBytes, signatureBytes)\n } catch (error) {\n console.error('JWT verification error:', error)\n return false\n }\n}\n","/**\n * LocalStorage wrapper for profile storage.\n *\n * IMPORTANT: these storage keys are intentionally identical to the ones used by\n * the `local-first-auth` package. That is what makes the two interoperable — a\n * profile created by local-first-auth's <Onboarding> can be exported here, and a\n * profile imported here is immediately usable by local-first-auth's hooks and\n * window API. Do not namespace these differently.\n */\n\nimport type { Profile, StoredProfile } from '../types'\n\nexport const STORAGE_KEYS = {\n PROFILE: 'local-first-auth:profile',\n PRIVATE_KEY: 'local-first-auth:privateKey',\n} as const\n\n/**\n * Save profile to LocalStorage\n */\nexport function saveProfile(profile: StoredProfile): void {\n try {\n localStorage.setItem(STORAGE_KEYS.PROFILE, JSON.stringify(profile))\n } catch (error) {\n console.error('Failed to save profile:', error)\n throw new Error('Failed to save profile to LocalStorage')\n }\n}\n\n/**\n * Get profile from LocalStorage\n */\nexport function getProfile(): StoredProfile | null {\n try {\n const profileString = localStorage.getItem(STORAGE_KEYS.PROFILE)\n if (!profileString) {\n return null\n }\n\n return JSON.parse(profileString) as StoredProfile\n } catch (error) {\n console.error('Failed to get profile:', error)\n return null\n }\n}\n\n/**\n * Save private key to LocalStorage\n */\nexport function savePrivateKey(privateKey: string): void {\n try {\n localStorage.setItem(STORAGE_KEYS.PRIVATE_KEY, privateKey)\n } catch (error) {\n console.error('Failed to save private key:', error)\n throw new Error('Failed to save private key to LocalStorage')\n }\n}\n\n/**\n * Get private key from LocalStorage\n */\nexport function getPrivateKey(): string | null {\n try {\n return localStorage.getItem(STORAGE_KEYS.PRIVATE_KEY)\n } catch (error) {\n console.error('Failed to get private key:', error)\n return null\n }\n}\n\n/**\n * Clear all stored profile data\n */\nexport function clearProfile(): void {\n try {\n localStorage.removeItem(STORAGE_KEYS.PROFILE)\n localStorage.removeItem(STORAGE_KEYS.PRIVATE_KEY)\n } catch (error) {\n console.error('Failed to clear profile:', error)\n }\n}\n\n/**\n * Check if a profile exists in storage\n */\nexport function hasProfile(): boolean {\n return getProfile() !== null && getPrivateKey() !== null\n}\n\n/**\n * Get the current profile (requires both the profile and its private key)\n */\nexport function getCurrentProfile(): Profile | null {\n const storedProfile = getProfile()\n const privateKey = getPrivateKey()\n\n if (!storedProfile || !privateKey) {\n return null\n }\n\n return {\n did: storedProfile.did,\n name: storedProfile.name,\n socials: storedProfile.socials,\n avatar: storedProfile.avatar\n }\n}\n","/**\n * Export the current profile (including its keypair) as a portable JSON file.\n *\n * SECURITY: the exported file contains a plaintext private key. Anyone who has\n * it can act as the user. Surface a clear warning in your UI, and never send the\n * file to a server or log it.\n */\n\nimport {\n EXPORT_FILE_TYPE,\n EXPORT_FILE_VERSION,\n type ExportedProfile\n} from '../types'\nimport { getProfile, getPrivateKey } from './storage'\nimport { deriveKeysFromPrivateKey } from './crypto'\n\n/**\n * Build the exportable representation of the current profile.\n *\n * The DID and public key are re-derived from the stored private key rather than\n * read from storage, so the resulting file is always internally consistent.\n *\n * @returns The ExportedProfile, or null if there is no profile to export.\n */\nexport function exportProfile(): ExportedProfile | null {\n const profile = getProfile()\n const privateKey = getPrivateKey()\n\n if (!profile || !privateKey) {\n return null\n }\n\n // Derive rather than trust storage — guarantees did/publicKey match privateKey\n const { did, publicKey } = deriveKeysFromPrivateKey(privateKey)\n\n return {\n type: EXPORT_FILE_TYPE,\n version: EXPORT_FILE_VERSION,\n did,\n publicKey,\n privateKey,\n name: profile.name,\n socials: profile.socials ?? [],\n avatar: profile.avatar ?? null,\n exportedAt: new Date().toISOString()\n }\n}\n\n/**\n * Serialize the current profile to a JSON string.\n *\n * @param pretty - Pretty-print the JSON (default true)\n * @returns The JSON string, or null if there is no profile to export.\n */\nexport function exportProfileToJSON(pretty = true): string | null {\n const exported = exportProfile()\n\n if (!exported) {\n return null\n }\n\n return JSON.stringify(exported, null, pretty ? 2 : 0)\n}\n\n/**\n * Build a default filename for a profile backup, e.g.\n * `local-first-auth-profile-z6MkhaXg.json`\n */\nexport function defaultExportFilename(did: string): string {\n // did:key:z6Mk... -> z6Mk...; keep a short, filesystem-safe suffix\n const key = did.replace(/^did:key:/, '')\n const shortKey = key.slice(0, 12).replace(/[^a-zA-Z0-9]/g, '')\n return `local-first-auth-profile-${shortKey}.json`\n}\n\n/**\n * Download the current profile as a .json file in the browser.\n *\n * @param filename - Optional filename. Defaults to `local-first-auth-profile-<key>.json`\n * @returns The exported profile, or null if there was nothing to export.\n */\nexport function downloadProfile(filename?: string): ExportedProfile | null {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n console.warn('Cannot download profile: not in a browser environment')\n return null\n }\n\n const exported = exportProfile()\n\n if (!exported) {\n console.warn('Cannot download profile: no profile found')\n return null\n }\n\n const json = JSON.stringify(exported, null, 2)\n const blob = new Blob([json], { type: 'application/json' })\n const url = URL.createObjectURL(blob)\n\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = filename || defaultExportFilename(exported.did)\n anchor.style.display = 'none'\n\n document.body.appendChild(anchor)\n anchor.click()\n document.body.removeChild(anchor)\n\n URL.revokeObjectURL(url)\n\n return exported\n}\n","/**\n * Local First Auth API implementation.\n *\n * Implements the LocalFirstAuth interface from the Local First Auth Specification\n * and injects it at window.localFirstAuth. Mirrors local-first-auth's api.ts so an\n * imported profile behaves exactly like one created through onboarding.\n */\n\nimport type { LocalFirstAuth, AppDetails, JWTPayload } from '../types'\nimport { getProfile, getPrivateKey } from './storage'\nimport { createJWT } from './crypto'\n\nexport class LocalFirstAuthAPI implements LocalFirstAuth {\n /**\n * Get profile details as a signed JWT\n */\n async getProfileDetails(): Promise<string> {\n const profile = getProfile()\n const privateKey = getPrivateKey()\n\n if (!profile || !privateKey) {\n throw new Error('No profile found. User must create or import a profile first.')\n }\n\n const now = Math.floor(Date.now() / 1000)\n const payload: JWTPayload = {\n iss: profile.did,\n aud: window.location.origin,\n iat: now,\n exp: now + 120, // 2 minutes\n type: 'localFirstAuth:profile:details',\n data: {\n did: profile.did,\n name: profile.name,\n socials: profile.socials || []\n }\n }\n\n return createJWT(payload, privateKey)\n }\n\n /**\n * Get avatar as base64-encoded string in a signed JWT\n */\n async getAvatar(): Promise<string | null> {\n const profile = getProfile()\n const privateKey = getPrivateKey()\n\n if (!profile || !privateKey) {\n throw new Error('No profile found. User must create or import a profile first.')\n }\n\n if (!profile.avatar) {\n return null\n }\n\n const now = Math.floor(Date.now() / 1000)\n const payload: JWTPayload = {\n iss: profile.did,\n aud: window.location.origin,\n iat: now,\n exp: now + 120,\n type: 'localFirstAuth:avatar',\n data: {\n did: profile.did,\n avatar: profile.avatar\n }\n }\n\n return createJWT(payload, privateKey)\n }\n\n getAppDetails(): AppDetails {\n return {\n name: 'Local First Auth',\n version: '2.0.0',\n platform: 'browser',\n supportedPermissions: ['profile']\n }\n }\n\n async requestPermission(permission: string): Promise<boolean> {\n if (permission === 'profile') {\n return true\n }\n\n console.warn(`Permission \"${permission}\" is not yet supported`)\n return false\n }\n\n close(): void {\n console.log('close() called - no-op in web version')\n }\n}\n\n/**\n * Inject the Local First Auth API into the window object\n */\nexport function injectLocalFirstAuthAPI(): void {\n if (typeof window === 'undefined') {\n console.warn('Cannot inject Local First Auth API: window is undefined (not in browser)')\n return\n }\n\n if ((window as any).localFirstAuth) {\n return\n }\n\n ;(window as any).localFirstAuth = new LocalFirstAuthAPI()\n}\n\n/**\n * Remove the Local First Auth API from the window object\n */\nexport function removeLocalFirstAuthAPI(): void {\n if (typeof window === 'undefined') {\n return\n }\n\n delete (window as any).localFirstAuth\n}\n\n/**\n * Check if the Local First Auth API is available\n */\nexport function hasLocalFirstAuthAPI(): boolean {\n return typeof window !== 'undefined' && !!(window as any).localFirstAuth\n}\n","/**\n * Import an existing profile (public + private keypair) from an export file.\n *\n * Importing adopts an identity the user already has — no new keypair is minted.\n * The private key is the source of truth: the DID and public key are always\n * re-derived from it, never trusted from the file.\n */\n\nimport {\n EXPORT_FILE_TYPE,\n EXPORT_FILE_VERSION,\n type ExportedProfile,\n type ImportOptions,\n type Profile,\n type SocialLink,\n type ValidationResult\n} from '../types'\nimport { deriveKeysFromPrivateKey, validatePrivateKey } from './crypto'\nimport { getProfile, hasProfile, saveProfile, savePrivateKey } from './storage'\nimport { injectLocalFirstAuthAPI } from './api'\n\n/**\n * Validate an untrusted value against the Local First Auth export file format.\n *\n * Checks, in order:\n * 1. It parses as an object (a string is JSON.parse'd first)\n * 2. `type` is the `local-first-auth:export` magic string\n * 3. `version` is a supported schema version\n * 4. `privateKey` is base64 that decodes to exactly 64 bytes\n * 5. `did` / `publicKey` are re-derivable from `privateKey` (consistency check)\n * 6. `socials`, if present, is a well-formed array\n *\n * This never throws — inspect `valid` and `errors`.\n */\nexport function validateExportedProfile(input: unknown): ValidationResult {\n const errors: string[] = []\n\n // 1. Parse into an object\n let value: unknown = input\n\n if (typeof value === 'string') {\n try {\n value = JSON.parse(value)\n } catch {\n return { valid: false, errors: ['File is not valid JSON'] }\n }\n }\n\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return { valid: false, errors: ['Expected a JSON object'] }\n }\n\n const candidate = value as Record<string, unknown>\n\n // 2. Magic string\n if (candidate.type !== EXPORT_FILE_TYPE) {\n errors.push(\n `Not a Local First Auth profile export (expected type \"${EXPORT_FILE_TYPE}\", got ${JSON.stringify(candidate.type)})`\n )\n }\n\n // 3. Version\n if (candidate.version !== EXPORT_FILE_VERSION) {\n errors.push(\n `Unsupported export version: ${JSON.stringify(candidate.version)} (this package supports version ${EXPORT_FILE_VERSION})`\n )\n }\n\n // 4. Private key\n const keyCheck = validatePrivateKey(candidate.privateKey)\n if (!keyCheck.valid) {\n errors.push(keyCheck.error!)\n // Without a usable private key we cannot run the consistency check\n return { valid: false, errors }\n }\n\n // 5. Consistency: did/publicKey must be re-derivable from privateKey\n const derived = deriveKeysFromPrivateKey(candidate.privateKey as string)\n\n if (typeof candidate.did !== 'string' || candidate.did !== derived.did) {\n errors.push(\n `Profile file is inconsistent: \"did\" does not match the private key (expected ${derived.did})`\n )\n }\n\n if (typeof candidate.publicKey !== 'string' || candidate.publicKey !== derived.publicKey) {\n errors.push('Profile file is inconsistent: \"publicKey\" does not match the private key')\n }\n\n // 6. Optional fields\n if (candidate.name !== undefined && typeof candidate.name !== 'string') {\n errors.push('\"name\" must be a string')\n }\n\n if (candidate.socials !== undefined) {\n if (!Array.isArray(candidate.socials)) {\n errors.push('\"socials\" must be an array')\n } else {\n const malformed = candidate.socials.some(\n (s) =>\n typeof s !== 'object' ||\n s === null ||\n typeof (s as SocialLink).platform !== 'string' ||\n typeof (s as SocialLink).handle !== 'string'\n )\n if (malformed) {\n errors.push('\"socials\" entries must each be { platform, handle }')\n }\n }\n }\n\n if (\n candidate.avatar !== undefined &&\n candidate.avatar !== null &&\n typeof candidate.avatar !== 'string'\n ) {\n errors.push('\"avatar\" must be a string or null')\n }\n\n if (errors.length > 0) {\n return { valid: false, errors }\n }\n\n return {\n valid: true,\n errors: [],\n profile: candidate as unknown as ExportedProfile\n }\n}\n\n/**\n * Normalize any supported import input into a valid ExportedProfile.\n *\n * Accepts:\n * - an `ExportedProfile` object\n * - a JSON string of an export file\n * - a bare base64 private key (convenience — the envelope is synthesized)\n *\n * @throws Error with a human-readable message if the input is unusable.\n */\nexport function parseExportedProfile(input: string | ExportedProfile): ExportedProfile {\n // Convenience path: a bare private key with no envelope.\n // Detect it before JSON parsing — a raw key is never valid JSON object syntax.\n if (typeof input === 'string') {\n const trimmed = input.trim()\n\n if (!trimmed) {\n throw new Error('Nothing to import: input is empty')\n }\n\n const looksLikeJSON = trimmed.startsWith('{')\n\n if (!looksLikeJSON) {\n const keyCheck = validatePrivateKey(trimmed)\n if (!keyCheck.valid) {\n throw new Error(\n `Could not import: input is neither a profile export file nor a valid private key (${keyCheck.error})`\n )\n }\n\n const { did, publicKey, privateKey } = deriveKeysFromPrivateKey(trimmed)\n\n return {\n type: EXPORT_FILE_TYPE,\n version: EXPORT_FILE_VERSION,\n did,\n publicKey,\n privateKey\n }\n }\n }\n\n const result = validateExportedProfile(input)\n\n if (!result.valid || !result.profile) {\n throw new Error(result.errors.join('; '))\n }\n\n return result.profile\n}\n\n/**\n * Import a profile, adopting its existing identity.\n *\n * Saves the profile and private key to storage (the same keys local-first-auth\n * uses) and injects `window.localFirstAuth`, so the app is immediately\n * authenticated as the imported user.\n *\n * If a profile with a *different* DID already exists, this throws unless\n * `overwrite: true` is passed — importing would otherwise silently destroy the\n * user's existing keys.\n *\n * @param input - An export file (object or JSON string), or a bare private key\n * @param options - `{ overwrite }` to replace an existing, different identity\n */\nexport async function importProfile(\n input: string | ExportedProfile,\n options: ImportOptions = {}\n): Promise<Profile> {\n const exported = parseExportedProfile(input)\n\n // Re-derive rather than trusting the file's did/publicKey\n const { did, privateKey } = deriveKeysFromPrivateKey(exported.privateKey)\n\n if (hasProfile() && !options.overwrite) {\n const existing = getProfile()\n\n if (existing && existing.did !== did) {\n throw new Error(\n 'A different profile already exists on this device. Pass { overwrite: true } to replace it — this permanently discards the existing private key.'\n )\n }\n }\n\n const profile: Profile = {\n did,\n name: exported.name || 'anonymous',\n socials: exported.socials ?? [],\n avatar: exported.avatar ?? null\n }\n\n saveProfile({\n did: profile.did,\n name: profile.name,\n socials: profile.socials,\n avatar: profile.avatar\n })\n savePrivateKey(privateKey)\n\n injectLocalFirstAuthAPI()\n\n return profile\n}\n\n/**\n * Import a profile from a File (e.g. from an <input type=\"file\"> or a drop).\n */\nexport async function importProfileFromFile(\n file: File,\n options: ImportOptions = {}\n): Promise<Profile> {\n const text = await file.text()\n return importProfile(text, options)\n}\n"],"mappings":";AAmEO,IAAM,mBAAmB;AAKzB,IAAM,sBAAsB;;;AChEnC,YAAY,aAAa;AACzB,SAAS,UAAU,oBAAoB;AACvC,YAAY,YAAY;AAQxB,IAAM,4BAA4B,IAAI,WAAW,CAAC,KAAM,CAAI,CAAC;AAGtD,IAAM,kBAAkB;AAGxB,IAAM,kBAAkB;AAKxB,IAAM,YAAY;AAAA,EACvB,QAAQ,CAAC,UAA8B;AACrC,UAAM,eAAsB,qBAAc,KAAK;AAC/C,WAAO,aACJ,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,MAAM,EAAE;AAAA,EACrB;AAAA,EAEA,QAAQ,CAAC,UAA8B;AACrC,QAAI,eAAe,MAChB,QAAQ,MAAM,GAAG,EACjB,QAAQ,MAAM,GAAG;AAEpB,UAAM,WAAW,IAAK,aAAa,SAAS,KAAM;AAClD,oBAAgB,IAAI,OAAO,OAAO;AAElC,WAAc,mBAAY,YAAY;AAAA,EACxC;AACF;AAQO,SAAS,uBAAuB,WAA+B;AAEpE,QAAM,gBAAgB,IAAI,WAAW,0BAA0B,SAAS,UAAU,MAAM;AACxF,gBAAc,IAAI,yBAAyB;AAC3C,gBAAc,IAAI,WAAW,0BAA0B,MAAM;AAG7D,QAAM,UAAU,aAAa,aAAa;AAE1C,SAAO,YAAY,OAAO;AAC5B;AAQO,SAAS,mBAAmB,YAAyD;AAC1F,MAAI,OAAO,eAAe,YAAY,WAAW,KAAK,MAAM,IAAI;AAC9D,WAAO,EAAE,OAAO,OAAO,OAAO,yCAAyC;AAAA,EACzE;AAEA,MAAI;AACJ,MAAI;AACF,YAAe,mBAAY,WAAW,KAAK,CAAC;AAAA,EAC9C,QAAQ;AACN,WAAO,EAAE,OAAO,OAAO,OAAO,kCAAkC;AAAA,EAClE;AAEA,MAAI,MAAM,WAAW,iBAAiB;AACpC,WAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO,8BAA8B,eAAe,eAAe,MAAM,MAAM;AAAA,IACjF;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,KAAK;AACvB;AAaO,SAAS,yBAAyB,YAAiC;AACxE,QAAM,EAAE,OAAO,MAAM,IAAI,mBAAmB,UAAU;AACtD,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,wBAAwB,KAAK,EAAE;AAAA,EACjD;AAEA,QAAM,UAAU,WAAW,KAAK;AAChC,QAAM,YAAmB,mBAAY,OAAO;AAG5C,QAAM,YAAoB,sCAA8B,SAAS;AACjE,QAAM,MAAM,uBAAuB,SAAS;AAE5C,SAAO;AAAA,IACL;AAAA,IACA,WAAkB,qBAAc,SAAS;AAAA,IACzC,YAAY;AAAA,EACd;AACF;AAKO,SAAS,gBAAgB,WAA+B;AAC7D,SAAc,mBAAY,SAAS;AACrC;AASA,eAAsB,UAAU,SAAqB,YAAqC;AACxF,QAAM,kBAAyB,mBAAY,UAAU;AAErD,MAAI,gBAAgB,WAAW,iBAAiB;AAC9C,UAAM,IAAI,MAAM,gDAAgD;AAAA,EAClE;AAEA,QAAM,SAAoB;AAAA,IACxB,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AAEA,QAAM,YAAY,UAAU,OAAO,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,MAAM,CAAC,CAAC;AACnF,QAAM,aAAa,UAAU,OAAO,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,OAAO,CAAC,CAAC;AAErF,QAAM,eAAe,GAAG,SAAS,IAAI,UAAU;AAC/C,QAAM,oBAAoB,IAAI,YAAY,EAAE,OAAO,YAAY;AAE/D,QAAM,YAAoB,aAAK,iBAAiB,iBAAiB;AACjE,QAAM,eAAe,UAAU,OAAO,SAAS;AAE/C,SAAO,GAAG,YAAY,IAAI,YAAY;AACxC;AAKO,SAAS,UAAU,KAA4E;AACpG,QAAM,QAAQ,IAAI,MAAM,GAAG;AAE3B,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AAEA,QAAM,CAAC,eAAe,gBAAgB,SAAS,IAAI;AAEnD,MAAI,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,WAAW;AACnD,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AAEA,QAAM,cAAc,UAAU,OAAO,aAAa;AAClD,QAAM,eAAe,UAAU,OAAO,cAAc;AAEpD,QAAM,SAAS,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,WAAW,CAAC;AAC/D,QAAM,UAAU,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,YAAY,CAAC;AAEjE,SAAO,EAAE,QAAQ,SAAS,UAAU;AACtC;AAQO,SAAS,UAAU,KAAa,WAAgC;AACrE,MAAI;AACF,UAAM,QAAQ,IAAI,MAAM,GAAG;AAE3B,QAAI,MAAM,WAAW,GAAG;AACtB,aAAO;AAAA,IACT;AAEA,UAAM,CAAC,eAAe,gBAAgB,gBAAgB,IAAI;AAE1D,UAAM,eAAe,GAAG,aAAa,IAAI,cAAc;AACvD,UAAM,oBAAoB,IAAI,YAAY,EAAE,OAAO,YAAY;AAE/D,UAAM,iBAAiB,UAAU,OAAO,gBAAiB;AAEzD,WAAe,eAAO,WAAW,mBAAmB,cAAc;AAAA,EACpE,SAAS,OAAO;AACd,YAAQ,MAAM,2BAA2B,KAAK;AAC9C,WAAO;AAAA,EACT;AACF;;;AC5MO,IAAM,eAAe;AAAA,EAC1B,SAAS;AAAA,EACT,aAAa;AACf;AAKO,SAAS,YAAY,SAA8B;AACxD,MAAI;AACF,iBAAa,QAAQ,aAAa,SAAS,KAAK,UAAU,OAAO,CAAC;AAAA,EACpE,SAAS,OAAO;AACd,YAAQ,MAAM,2BAA2B,KAAK;AAC9C,UAAM,IAAI,MAAM,wCAAwC;AAAA,EAC1D;AACF;AAKO,SAAS,aAAmC;AACjD,MAAI;AACF,UAAM,gBAAgB,aAAa,QAAQ,aAAa,OAAO;AAC/D,QAAI,CAAC,eAAe;AAClB,aAAO;AAAA,IACT;AAEA,WAAO,KAAK,MAAM,aAAa;AAAA,EACjC,SAAS,OAAO;AACd,YAAQ,MAAM,0BAA0B,KAAK;AAC7C,WAAO;AAAA,EACT;AACF;AAKO,SAAS,eAAe,YAA0B;AACvD,MAAI;AACF,iBAAa,QAAQ,aAAa,aAAa,UAAU;AAAA,EAC3D,SAAS,OAAO;AACd,YAAQ,MAAM,+BAA+B,KAAK;AAClD,UAAM,IAAI,MAAM,4CAA4C;AAAA,EAC9D;AACF;AAKO,SAAS,gBAA+B;AAC7C,MAAI;AACF,WAAO,aAAa,QAAQ,aAAa,WAAW;AAAA,EACtD,SAAS,OAAO;AACd,YAAQ,MAAM,8BAA8B,KAAK;AACjD,WAAO;AAAA,EACT;AACF;AAKO,SAAS,eAAqB;AACnC,MAAI;AACF,iBAAa,WAAW,aAAa,OAAO;AAC5C,iBAAa,WAAW,aAAa,WAAW;AAAA,EAClD,SAAS,OAAO;AACd,YAAQ,MAAM,4BAA4B,KAAK;AAAA,EACjD;AACF;AAKO,SAAS,aAAsB;AACpC,SAAO,WAAW,MAAM,QAAQ,cAAc,MAAM;AACtD;AAKO,SAAS,oBAAoC;AAClD,QAAM,gBAAgB,WAAW;AACjC,QAAM,aAAa,cAAc;AAEjC,MAAI,CAAC,iBAAiB,CAAC,YAAY;AACjC,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,KAAK,cAAc;AAAA,IACnB,MAAM,cAAc;AAAA,IACpB,SAAS,cAAc;AAAA,IACvB,QAAQ,cAAc;AAAA,EACxB;AACF;;;AClFO,SAAS,gBAAwC;AACtD,QAAM,UAAU,WAAW;AAC3B,QAAM,aAAa,cAAc;AAEjC,MAAI,CAAC,WAAW,CAAC,YAAY;AAC3B,WAAO;AAAA,EACT;AAGA,QAAM,EAAE,KAAK,UAAU,IAAI,yBAAyB,UAAU;AAE9D,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,QAAQ;AAAA,IACd,SAAS,QAAQ,WAAW,CAAC;AAAA,IAC7B,QAAQ,QAAQ,UAAU;AAAA,IAC1B,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,EACrC;AACF;AAQO,SAAS,oBAAoB,SAAS,MAAqB;AAChE,QAAM,WAAW,cAAc;AAE/B,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,UAAU,UAAU,MAAM,SAAS,IAAI,CAAC;AACtD;AAMO,SAAS,sBAAsB,KAAqB;AAEzD,QAAM,MAAM,IAAI,QAAQ,aAAa,EAAE;AACvC,QAAM,WAAW,IAAI,MAAM,GAAG,EAAE,EAAE,QAAQ,iBAAiB,EAAE;AAC7D,SAAO,4BAA4B,QAAQ;AAC7C;AAQO,SAAS,gBAAgB,UAA2C;AACzE,MAAI,OAAO,WAAW,eAAe,OAAO,aAAa,aAAa;AACpE,YAAQ,KAAK,uDAAuD;AACpE,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,cAAc;AAE/B,MAAI,CAAC,UAAU;AACb,YAAQ,KAAK,2CAA2C;AACxD,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,KAAK,UAAU,UAAU,MAAM,CAAC;AAC7C,QAAM,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAC1D,QAAM,MAAM,IAAI,gBAAgB,IAAI;AAEpC,QAAM,SAAS,SAAS,cAAc,GAAG;AACzC,SAAO,OAAO;AACd,SAAO,WAAW,YAAY,sBAAsB,SAAS,GAAG;AAChE,SAAO,MAAM,UAAU;AAEvB,WAAS,KAAK,YAAY,MAAM;AAChC,SAAO,MAAM;AACb,WAAS,KAAK,YAAY,MAAM;AAEhC,MAAI,gBAAgB,GAAG;AAEvB,SAAO;AACT;;;AClGO,IAAM,oBAAN,MAAkD;AAAA;AAAA;AAAA;AAAA,EAIvD,MAAM,oBAAqC;AACzC,UAAM,UAAU,WAAW;AAC3B,UAAM,aAAa,cAAc;AAEjC,QAAI,CAAC,WAAW,CAAC,YAAY;AAC3B,YAAM,IAAI,MAAM,+DAA+D;AAAA,IACjF;AAEA,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,UAAM,UAAsB;AAAA,MAC1B,KAAK,QAAQ;AAAA,MACb,KAAK,OAAO,SAAS;AAAA,MACrB,KAAK;AAAA,MACL,KAAK,MAAM;AAAA;AAAA,MACX,MAAM;AAAA,MACN,MAAM;AAAA,QACJ,KAAK,QAAQ;AAAA,QACb,MAAM,QAAQ;AAAA,QACd,SAAS,QAAQ,WAAW,CAAC;AAAA,MAC/B;AAAA,IACF;AAEA,WAAO,UAAU,SAAS,UAAU;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAoC;AACxC,UAAM,UAAU,WAAW;AAC3B,UAAM,aAAa,cAAc;AAEjC,QAAI,CAAC,WAAW,CAAC,YAAY;AAC3B,YAAM,IAAI,MAAM,+DAA+D;AAAA,IACjF;AAEA,QAAI,CAAC,QAAQ,QAAQ;AACnB,aAAO;AAAA,IACT;AAEA,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,UAAM,UAAsB;AAAA,MAC1B,KAAK,QAAQ;AAAA,MACb,KAAK,OAAO,SAAS;AAAA,MACrB,KAAK;AAAA,MACL,KAAK,MAAM;AAAA,MACX,MAAM;AAAA,MACN,MAAM;AAAA,QACJ,KAAK,QAAQ;AAAA,QACb,QAAQ,QAAQ;AAAA,MAClB;AAAA,IACF;AAEA,WAAO,UAAU,SAAS,UAAU;AAAA,EACtC;AAAA,EAEA,gBAA4B;AAC1B,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,MACT,UAAU;AAAA,MACV,sBAAsB,CAAC,SAAS;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,MAAM,kBAAkB,YAAsC;AAC5D,QAAI,eAAe,WAAW;AAC5B,aAAO;AAAA,IACT;AAEA,YAAQ,KAAK,eAAe,UAAU,wBAAwB;AAC9D,WAAO;AAAA,EACT;AAAA,EAEA,QAAc;AACZ,YAAQ,IAAI,uCAAuC;AAAA,EACrD;AACF;AAKO,SAAS,0BAAgC;AAC9C,MAAI,OAAO,WAAW,aAAa;AACjC,YAAQ,KAAK,0EAA0E;AACvF;AAAA,EACF;AAEA,MAAK,OAAe,gBAAgB;AAClC;AAAA,EACF;AAEA;AAAC,EAAC,OAAe,iBAAiB,IAAI,kBAAkB;AAC1D;AAKO,SAAS,0BAAgC;AAC9C,MAAI,OAAO,WAAW,aAAa;AACjC;AAAA,EACF;AAEA,SAAQ,OAAe;AACzB;AAKO,SAAS,uBAAgC;AAC9C,SAAO,OAAO,WAAW,eAAe,CAAC,CAAE,OAAe;AAC5D;;;AC7FO,SAAS,wBAAwB,OAAkC;AACxE,QAAM,SAAmB,CAAC;AAG1B,MAAI,QAAiB;AAErB,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI;AACF,cAAQ,KAAK,MAAM,KAAK;AAAA,IAC1B,QAAQ;AACN,aAAO,EAAE,OAAO,OAAO,QAAQ,CAAC,wBAAwB,EAAE;AAAA,IAC5D;AAAA,EACF;AAEA,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG;AACvE,WAAO,EAAE,OAAO,OAAO,QAAQ,CAAC,wBAAwB,EAAE;AAAA,EAC5D;AAEA,QAAM,YAAY;AAGlB,MAAI,UAAU,SAAS,kBAAkB;AACvC,WAAO;AAAA,MACL,yDAAyD,gBAAgB,UAAU,KAAK,UAAU,UAAU,IAAI,CAAC;AAAA,IACnH;AAAA,EACF;AAGA,MAAI,UAAU,YAAY,qBAAqB;AAC7C,WAAO;AAAA,MACL,+BAA+B,KAAK,UAAU,UAAU,OAAO,CAAC,mCAAmC,mBAAmB;AAAA,IACxH;AAAA,EACF;AAGA,QAAM,WAAW,mBAAmB,UAAU,UAAU;AACxD,MAAI,CAAC,SAAS,OAAO;AACnB,WAAO,KAAK,SAAS,KAAM;AAE3B,WAAO,EAAE,OAAO,OAAO,OAAO;AAAA,EAChC;AAGA,QAAM,UAAU,yBAAyB,UAAU,UAAoB;AAEvE,MAAI,OAAO,UAAU,QAAQ,YAAY,UAAU,QAAQ,QAAQ,KAAK;AACtE,WAAO;AAAA,MACL,gFAAgF,QAAQ,GAAG;AAAA,IAC7F;AAAA,EACF;AAEA,MAAI,OAAO,UAAU,cAAc,YAAY,UAAU,cAAc,QAAQ,WAAW;AACxF,WAAO,KAAK,0EAA0E;AAAA,EACxF;AAGA,MAAI,UAAU,SAAS,UAAa,OAAO,UAAU,SAAS,UAAU;AACtE,WAAO,KAAK,yBAAyB;AAAA,EACvC;AAEA,MAAI,UAAU,YAAY,QAAW;AACnC,QAAI,CAAC,MAAM,QAAQ,UAAU,OAAO,GAAG;AACrC,aAAO,KAAK,4BAA4B;AAAA,IAC1C,OAAO;AACL,YAAM,YAAY,UAAU,QAAQ;AAAA,QAClC,CAAC,MACC,OAAO,MAAM,YACb,MAAM,QACN,OAAQ,EAAiB,aAAa,YACtC,OAAQ,EAAiB,WAAW;AAAA,MACxC;AACA,UAAI,WAAW;AACb,eAAO,KAAK,qDAAqD;AAAA,MACnE;AAAA,IACF;AAAA,EACF;AAEA,MACE,UAAU,WAAW,UACrB,UAAU,WAAW,QACrB,OAAO,UAAU,WAAW,UAC5B;AACA,WAAO,KAAK,mCAAmC;AAAA,EACjD;AAEA,MAAI,OAAO,SAAS,GAAG;AACrB,WAAO,EAAE,OAAO,OAAO,OAAO;AAAA,EAChC;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,IACP,QAAQ,CAAC;AAAA,IACT,SAAS;AAAA,EACX;AACF;AAYO,SAAS,qBAAqB,OAAkD;AAGrF,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,UAAU,MAAM,KAAK;AAE3B,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACrD;AAEA,UAAM,gBAAgB,QAAQ,WAAW,GAAG;AAE5C,QAAI,CAAC,eAAe;AAClB,YAAM,WAAW,mBAAmB,OAAO;AAC3C,UAAI,CAAC,SAAS,OAAO;AACnB,cAAM,IAAI;AAAA,UACR,qFAAqF,SAAS,KAAK;AAAA,QACrG;AAAA,MACF;AAEA,YAAM,EAAE,KAAK,WAAW,WAAW,IAAI,yBAAyB,OAAO;AAEvE,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,wBAAwB,KAAK;AAE5C,MAAI,CAAC,OAAO,SAAS,CAAC,OAAO,SAAS;AACpC,UAAM,IAAI,MAAM,OAAO,OAAO,KAAK,IAAI,CAAC;AAAA,EAC1C;AAEA,SAAO,OAAO;AAChB;AAgBA,eAAsB,cACpB,OACA,UAAyB,CAAC,GACR;AAClB,QAAM,WAAW,qBAAqB,KAAK;AAG3C,QAAM,EAAE,KAAK,WAAW,IAAI,yBAAyB,SAAS,UAAU;AAExE,MAAI,WAAW,KAAK,CAAC,QAAQ,WAAW;AACtC,UAAM,WAAW,WAAW;AAE5B,QAAI,YAAY,SAAS,QAAQ,KAAK;AACpC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAmB;AAAA,IACvB;AAAA,IACA,MAAM,SAAS,QAAQ;AAAA,IACvB,SAAS,SAAS,WAAW,CAAC;AAAA,IAC9B,QAAQ,SAAS,UAAU;AAAA,EAC7B;AAEA,cAAY;AAAA,IACV,KAAK,QAAQ;AAAA,IACb,MAAM,QAAQ;AAAA,IACd,SAAS,QAAQ;AAAA,IACjB,QAAQ,QAAQ;AAAA,EAClB,CAAC;AACD,iBAAe,UAAU;AAEzB,0BAAwB;AAExB,SAAO;AACT;AAKA,eAAsB,sBACpB,MACA,UAAyB,CAAC,GACR;AAClB,QAAM,OAAO,MAAM,KAAK,KAAK;AAC7B,SAAO,cAAc,MAAM,OAAO;AACpC;","names":[]}
|