@pelican-identity/auth-core 1.2.9 → 1.2.11

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/dist/index.js CHANGED
@@ -1,8 +1,614 @@
1
- export * from "./types/types";
2
- export { PelicanAuthentication } from "./engine/engine";
3
- export { CryptoService } from "./utilities/crypto";
4
- export { StateMachine } from "./utilities/stateMachine";
5
- export { Transport } from "./utilities/transport";
6
- export * from "./utilities/storage";
7
- export { BASEURL } from "./constants";
1
+ 'use strict';
2
+
3
+ var nacl = require('tweetnacl');
4
+ var tweetnaclUtil = require('tweetnacl-util');
5
+ var QRCode = require('qrcode');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var nacl__default = /*#__PURE__*/_interopDefault(nacl);
10
+ var QRCode__default = /*#__PURE__*/_interopDefault(QRCode);
11
+
12
+ // src/utilities/crypto.ts
13
+ var CryptoService = class {
14
+ generateSymmetricKey() {
15
+ const key = nacl__default.default.randomBytes(32);
16
+ return tweetnaclUtil.encodeBase64(key);
17
+ }
18
+ /**
19
+ * Encrypt with symmetric key (secret-key encryption)
20
+ * Use this when both parties share the same secret key
21
+ * @param plaintext - Message to encrypt
22
+ * @param keyString - Symmetric key (base64)
23
+ * @returns Encrypted message with nonce
24
+ */
25
+ encryptSymmetric({
26
+ plaintext,
27
+ keyString
28
+ }) {
29
+ const key = tweetnaclUtil.decodeBase64(keyString);
30
+ const nonce = nacl__default.default.randomBytes(24);
31
+ const messageBytes = new TextEncoder().encode(plaintext);
32
+ const ciphertext = nacl__default.default.secretbox(messageBytes, nonce, key);
33
+ return {
34
+ cipher: tweetnaclUtil.encodeBase64(ciphertext),
35
+ nonce: tweetnaclUtil.encodeBase64(nonce)
36
+ };
37
+ }
38
+ /**
39
+ * Decrypt with symmetric key
40
+ * @param encrypted - Encrypted message with nonce
41
+ * @param keyString - Symmetric key (base64)
42
+ * @returns Decrypted plaintext
43
+ */
44
+ decryptSymmetric({
45
+ encrypted,
46
+ keyString
47
+ }) {
48
+ try {
49
+ const key = tweetnaclUtil.decodeBase64(keyString);
50
+ const ciphertextBytes = tweetnaclUtil.decodeBase64(encrypted.cipher);
51
+ const nonceBytes = tweetnaclUtil.decodeBase64(encrypted.nonce);
52
+ const decrypted = nacl__default.default.secretbox.open(ciphertextBytes, nonceBytes, key);
53
+ if (!decrypted) {
54
+ throw new Error("Decryption failed - invalid key or corrupted data");
55
+ }
56
+ const decoded = new TextDecoder().decode(decrypted);
57
+ return decoded;
58
+ } catch (error) {
59
+ console.error("Decryption failed", error);
60
+ return null;
61
+ }
62
+ }
63
+ };
64
+ var crypto_default = CryptoService;
65
+
66
+ // src/utilities/storage.ts
67
+ var AuthStorage = class {
68
+ constructor() {
69
+ this.prefix = "pelican_auth_";
70
+ this.defaultTTL = 5 * 60 * 1e3;
71
+ // 5 minutes
72
+ this.memoryCache = /* @__PURE__ */ new Map();
73
+ }
74
+ /**
75
+ * Store auth session with automatic cleanup
76
+ */
77
+ set(key, value, options = {}) {
78
+ const { ttlMs = this.defaultTTL, useSessionStorage = true } = options;
79
+ const expiresAt = Date.now() + ttlMs;
80
+ const data = { value, expiresAt };
81
+ if (useSessionStorage) {
82
+ try {
83
+ sessionStorage.setItem(`${this.prefix}${key}`, JSON.stringify(data));
84
+ } catch (error) {
85
+ console.warn("SessionStorage unavailable, using memory:", error);
86
+ this.setMemory(key, data);
87
+ }
88
+ } else {
89
+ this.setMemory(key, data);
90
+ }
91
+ }
92
+ /**
93
+ * Get stored value if not expired
94
+ */
95
+ get(key) {
96
+ try {
97
+ const stored = sessionStorage.getItem(`${this.prefix}${key}`);
98
+ if (stored) {
99
+ const data = JSON.parse(stored);
100
+ if (Date.now() > data.expiresAt) {
101
+ this.remove(key);
102
+ return null;
103
+ }
104
+ return data.value;
105
+ }
106
+ } catch (error) {
107
+ }
108
+ return this.getMemory(key);
109
+ }
110
+ /**
111
+ * Remove specific key
112
+ */
113
+ remove(key) {
114
+ try {
115
+ sessionStorage.removeItem(`${this.prefix}${key}`);
116
+ } catch (error) {
117
+ }
118
+ this.memoryCache.delete(key);
119
+ }
120
+ /**
121
+ * Clear all auth data
122
+ */
123
+ clear() {
124
+ try {
125
+ Object.keys(sessionStorage).forEach((key) => {
126
+ if (key.startsWith(this.prefix)) {
127
+ sessionStorage.removeItem(key);
128
+ }
129
+ });
130
+ } catch (error) {
131
+ }
132
+ this.memoryCache.clear();
133
+ }
134
+ // ========================================
135
+ // Memory cache helpers
136
+ // ========================================
137
+ setMemory(key, data) {
138
+ this.memoryCache.set(key, data);
139
+ const ttl = data.expiresAt - Date.now();
140
+ setTimeout(() => {
141
+ this.memoryCache.delete(key);
142
+ }, ttl);
143
+ }
144
+ getMemory(key) {
145
+ const data = this.memoryCache.get(key);
146
+ if (!data) return null;
147
+ if (Date.now() > data.expiresAt) {
148
+ this.memoryCache.delete(key);
149
+ return null;
150
+ }
151
+ return data.value;
152
+ }
153
+ };
154
+ var storage = new AuthStorage();
155
+ var storeAuthSession = (sessionId, sessionKey, ttlMs) => {
156
+ storage.set("session", { sessionId, sessionKey }, { ttlMs });
157
+ };
158
+ var getAuthSession = () => {
159
+ return storage.get("session");
160
+ };
161
+ var clearAuthSession = () => {
162
+ storage.remove("session");
163
+ };
164
+ var clearAllAuthData = () => {
165
+ storage.clear();
166
+ };
167
+
168
+ // src/utilities/stateMachine.ts
169
+ var StateMachine = class {
170
+ constructor() {
171
+ this.state = "idle";
172
+ this.listeners = /* @__PURE__ */ new Set();
173
+ }
174
+ get current() {
175
+ return this.state;
176
+ }
177
+ transition(next) {
178
+ this.state = next;
179
+ this.listeners.forEach((l) => l(next));
180
+ }
181
+ subscribe(fn) {
182
+ this.listeners.add(fn);
183
+ return () => this.listeners.delete(fn);
184
+ }
185
+ };
186
+
187
+ // src/utilities/transport.ts
188
+ var Transport = class {
189
+ constructor(handlers) {
190
+ this.reconnectAttempts = 0;
191
+ this.maxReconnectAttempts = 3;
192
+ // Reduced from 5 for mobile
193
+ this.isExplicitlyClosed = false;
194
+ this.isReconnecting = false;
195
+ this.handlers = handlers;
196
+ }
197
+ /**
198
+ * Establish WebSocket connection
199
+ * @param url - WebSocket URL
200
+ */
201
+ connect(url) {
202
+ this.url = url;
203
+ this.isExplicitlyClosed = false;
204
+ if (this.socket) {
205
+ this.socket.onclose = null;
206
+ this.socket.onerror = null;
207
+ this.socket.onmessage = null;
208
+ this.socket.onopen = null;
209
+ if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
210
+ this.socket.close();
211
+ }
212
+ }
213
+ this.socket = new WebSocket(url);
214
+ this.socket.onopen = () => {
215
+ this.reconnectAttempts = 0;
216
+ this.isReconnecting = false;
217
+ this.handlers.onOpen?.();
218
+ };
219
+ this.socket.onmessage = (e) => {
220
+ try {
221
+ const data = JSON.parse(e.data);
222
+ this.handlers.onMessage?.(data);
223
+ } catch (err) {
224
+ console.error("Failed to parse WebSocket message", err);
225
+ }
226
+ };
227
+ this.socket.onerror = (e) => {
228
+ if (!this.isReconnecting && !this.isExplicitlyClosed) {
229
+ this.handlers.onError?.(e);
230
+ }
231
+ };
232
+ this.socket.onclose = (e) => {
233
+ if (this.reconnectTimeout) {
234
+ clearTimeout(this.reconnectTimeout);
235
+ this.reconnectTimeout = void 0;
236
+ }
237
+ if (!this.isReconnecting) {
238
+ this.handlers.onClose?.(e);
239
+ }
240
+ if (!this.isExplicitlyClosed && !this.isReconnecting && this.reconnectAttempts < this.maxReconnectAttempts) {
241
+ this.attemptReconnect();
242
+ }
243
+ };
244
+ }
245
+ /**
246
+ * Attempt to reconnect with exponential backoff
247
+ */
248
+ attemptReconnect() {
249
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
250
+ console.warn("Max WebSocket reconnect attempts reached");
251
+ return;
252
+ }
253
+ this.isReconnecting = true;
254
+ this.reconnectAttempts++;
255
+ const delay = Math.min(Math.pow(2, this.reconnectAttempts - 1) * 500, 2e3);
256
+ console.log(
257
+ `Reconnecting WebSocket (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}) in ${delay}ms...`
258
+ );
259
+ this.reconnectTimeout = window.setTimeout(() => {
260
+ if (this.url && !this.isExplicitlyClosed) {
261
+ this.connect(this.url);
262
+ }
263
+ }, delay);
264
+ }
265
+ /**
266
+ * Send a message through the WebSocket
267
+ * Queues message if connection is not open
268
+ */
269
+ send(payload) {
270
+ if (this.socket?.readyState === WebSocket.OPEN) {
271
+ try {
272
+ this.socket.send(JSON.stringify(payload));
273
+ } catch (err) {
274
+ console.error("Failed to send WebSocket message:", err);
275
+ }
276
+ } else {
277
+ console.warn(
278
+ "WebSocket not open, message not sent:",
279
+ this.socket?.readyState
280
+ );
281
+ }
282
+ }
283
+ /**
284
+ * Close the WebSocket connection
285
+ * Prevents automatic reconnection
286
+ */
287
+ close() {
288
+ this.isExplicitlyClosed = true;
289
+ this.isReconnecting = false;
290
+ if (this.reconnectTimeout) {
291
+ clearTimeout(this.reconnectTimeout);
292
+ this.reconnectTimeout = void 0;
293
+ }
294
+ if (this.socket) {
295
+ this.socket.onclose = null;
296
+ this.socket.onerror = null;
297
+ this.socket.onmessage = null;
298
+ this.socket.onopen = null;
299
+ if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
300
+ this.socket.close();
301
+ }
302
+ this.socket = void 0;
303
+ }
304
+ }
305
+ /**
306
+ * Get current connection state
307
+ */
308
+ get readyState() {
309
+ return this.socket?.readyState;
310
+ }
311
+ /**
312
+ * Check if connection is open
313
+ */
314
+ get isOpen() {
315
+ return this.socket?.readyState === WebSocket.OPEN;
316
+ }
317
+ };
318
+
319
+ // src/constants.ts
320
+ var BASEURL = "http://192.168.1.9:8080";
321
+
322
+ // src/engine/engine.ts
323
+ var PelicanAuthentication = class {
324
+ constructor(config) {
325
+ this.crypto = new crypto_default();
326
+ this.stateMachine = new StateMachine();
327
+ this.sessionId = "";
328
+ this.sessionKey = null;
329
+ this.useWebSocket = true;
330
+ this.listeners = {};
331
+ if (!config.publicKey) throw new Error("Missing publicKey");
332
+ if (!config.projectId) throw new Error("Missing projectId");
333
+ if (!config.authType) throw new Error("Missing authType");
334
+ this.config = {
335
+ continuousMode: false,
336
+ forceQRCode: false,
337
+ ...config
338
+ };
339
+ this.stateMachine.subscribe((s) => this.emit("state", s));
340
+ this.attachVisibilityRecovery();
341
+ }
342
+ /* -------------------- Public API -------------------- */
343
+ on(event, cb) {
344
+ var _a;
345
+ (_a = this.listeners)[event] ?? (_a[event] = /* @__PURE__ */ new Set());
346
+ this.listeners[event].add(cb);
347
+ return () => this.listeners[event].delete(cb);
348
+ }
349
+ async start() {
350
+ if (this.stateMachine.current !== "idle") return;
351
+ this.resetSession();
352
+ clearAuthSession();
353
+ this.stateMachine.transition("initializing");
354
+ try {
355
+ this.sessionKey = this.crypto.generateSymmetricKey();
356
+ this.sessionId = crypto.randomUUID() + crypto.randomUUID();
357
+ this.useWebSocket = this.shouldUseWebSocket();
358
+ if (this.useWebSocket) {
359
+ await this.startWebSocketFlow();
360
+ } else {
361
+ await this.startDeepLinkFlow();
362
+ }
363
+ } catch (err) {
364
+ this.fail(err instanceof Error ? err : new Error("Start failed"));
365
+ }
366
+ }
367
+ stop() {
368
+ this.terminate(false);
369
+ }
370
+ destroy() {
371
+ this.detachVisibilityRecovery();
372
+ this.clearBackupCheck();
373
+ this.transport?.close();
374
+ this.transport = void 0;
375
+ this.listeners = {};
376
+ }
377
+ /* -------------------- Flow Selection -------------------- */
378
+ shouldUseWebSocket() {
379
+ return this.config.forceQRCode || !/Android|iPhone|iPad/i.test(navigator.userAgent);
380
+ }
381
+ /* -------------------- WebSocket Flow (QR Code) -------------------- */
382
+ async startWebSocketFlow() {
383
+ const relay = await this.fetchRelayUrl();
384
+ this.transport = new Transport({
385
+ onOpen: () => {
386
+ this.transport.send({
387
+ type: "register",
388
+ sessionID: this.sessionId,
389
+ ...this.config
390
+ });
391
+ this.stateMachine.transition("awaiting-pair");
392
+ },
393
+ onMessage: (msg) => this.handleWebSocketMessage(msg),
394
+ onError: (err) => {
395
+ console.error("WebSocket error:", err);
396
+ this.fail(new Error("WebSocket connection failed"));
397
+ }
398
+ });
399
+ this.transport.connect(relay);
400
+ await this.emitQRCode();
401
+ }
402
+ handleWebSocketMessage(msg) {
403
+ switch (msg.type) {
404
+ case "paired":
405
+ this.stateMachine.transition("paired");
406
+ this.transport.send({
407
+ type: "authenticate",
408
+ sessionID: this.sessionId,
409
+ ...this.config,
410
+ url: window.location.href
411
+ });
412
+ return;
413
+ case "phone-auth-success":
414
+ this.handleAuthSuccess(msg);
415
+ return;
416
+ case "phone-terminated":
417
+ this.fail(new Error("Authentication cancelled on device"));
418
+ this.restartIfContinuous();
419
+ return;
420
+ case "confirmed":
421
+ this.terminate(true);
422
+ this.restartIfContinuous();
423
+ return;
424
+ }
425
+ }
426
+ handleAuthSuccess(msg) {
427
+ if (!this.sessionKey || !msg.cipher || !msg.nonce) {
428
+ this.fail(new Error("Invalid authentication payload"));
429
+ this.restartIfContinuous();
430
+ return;
431
+ }
432
+ try {
433
+ const decrypted = this.crypto.decryptSymmetric({
434
+ encrypted: { cipher: msg.cipher, nonce: msg.nonce },
435
+ keyString: this.sessionKey
436
+ });
437
+ if (!decrypted) {
438
+ this.fail(new Error("Invalid authentication data"));
439
+ this.restartIfContinuous();
440
+ return;
441
+ }
442
+ const result = JSON.parse(decrypted);
443
+ this.emit("success", result);
444
+ this.stateMachine.transition("authenticated");
445
+ this.transport?.send({
446
+ type: "confirm",
447
+ sessionID: this.sessionId
448
+ });
449
+ } catch {
450
+ this.fail(new Error("Failed to decrypt authentication data"));
451
+ this.restartIfContinuous();
452
+ }
453
+ }
454
+ /* -------------------- Deep Link Flow (Mobile) -------------------- */
455
+ async startDeepLinkFlow() {
456
+ await this.fetchRelayUrl();
457
+ if (this.sessionKey) {
458
+ storeAuthSession(this.sessionId, this.sessionKey, 10 * 6e4);
459
+ }
460
+ await this.emitDeepLink();
461
+ this.stateMachine.transition("awaiting-pair");
462
+ }
463
+ clearBackupCheck() {
464
+ if (this.backupCheckTimeout) {
465
+ clearTimeout(this.backupCheckTimeout);
466
+ this.backupCheckTimeout = void 0;
467
+ }
468
+ }
469
+ async checkSession() {
470
+ const cached = getAuthSession();
471
+ if (!cached) return;
472
+ this.stateMachine.transition("awaiting-auth");
473
+ try {
474
+ const res = await fetch(
475
+ `${BASEURL}/session?session_id=${cached.sessionId}`
476
+ );
477
+ if (!res.ok) return;
478
+ const data = await res.json();
479
+ const decrypted = this.crypto.decryptSymmetric({
480
+ encrypted: { cipher: data.cipher, nonce: data.nonce },
481
+ keyString: cached.sessionKey
482
+ });
483
+ if (!decrypted) {
484
+ console.warn("Failed to decrypt session");
485
+ return;
486
+ }
487
+ const result = JSON.parse(decrypted);
488
+ this.clearBackupCheck();
489
+ clearAuthSession();
490
+ this.emit("success", result);
491
+ this.stateMachine.transition("authenticated");
492
+ if (this.config.continuousMode) {
493
+ setTimeout(() => {
494
+ this.stateMachine.transition("confirmed");
495
+ this.start();
496
+ }, 1500);
497
+ } else {
498
+ this.stateMachine.transition("confirmed");
499
+ }
500
+ } catch (err) {
501
+ console.debug("Session check failed:", err);
502
+ }
503
+ }
504
+ /* -------------------- Entry Point Generation -------------------- */
505
+ async emitQRCode() {
506
+ const payload = {
507
+ sessionID: this.sessionId,
508
+ sessionKey: this.sessionKey,
509
+ publicKey: this.config.publicKey,
510
+ authType: this.config.authType,
511
+ projectId: this.config.projectId,
512
+ url: window.location.href
513
+ };
514
+ const qr = await QRCode__default.default.toDataURL(JSON.stringify(payload), {
515
+ type: "image/png",
516
+ scale: 3,
517
+ color: { light: "#ffffff", dark: "#424242ff" }
518
+ });
519
+ this.emit("qr", qr);
520
+ }
521
+ async emitDeepLink() {
522
+ const deeplink = `pelicanvault://auth/deep-link?sessionID=${encodeURIComponent(
523
+ this.sessionId
524
+ )}&sessionKey=${encodeURIComponent(
525
+ this.sessionKey
526
+ )}&publicKey=${encodeURIComponent(this.config.publicKey)}&authType=${this.config.authType}&projectId=${encodeURIComponent(
527
+ this.config.projectId
528
+ )}&url=${encodeURIComponent(window.location.href)}`;
529
+ this.emit("deeplink", deeplink);
530
+ }
531
+ /* -------------------- Visibility Recovery -------------------- */
532
+ attachVisibilityRecovery() {
533
+ this.visibilityHandler = async () => {
534
+ if (document.visibilityState !== "visible") return;
535
+ if (this.useWebSocket) return;
536
+ await this.checkSession();
537
+ };
538
+ document.addEventListener("visibilitychange", this.visibilityHandler);
539
+ }
540
+ detachVisibilityRecovery() {
541
+ if (this.visibilityHandler) {
542
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
543
+ this.visibilityHandler = void 0;
544
+ }
545
+ }
546
+ /* -------------------- Helpers -------------------- */
547
+ async fetchRelayUrl() {
548
+ const { publicKey, projectId, authType } = this.config;
549
+ const res = await fetch(
550
+ `${BASEURL}/relay?public_key=${publicKey}&auth_type=${authType}&project_id=${projectId}`
551
+ );
552
+ if (!res.ok) {
553
+ const error = await res.text();
554
+ throw new Error(error);
555
+ }
556
+ const json = await res.json();
557
+ return json.relay_url;
558
+ }
559
+ terminate(success) {
560
+ this.clearBackupCheck();
561
+ if (this.transport) {
562
+ if (!success) {
563
+ this.transport.send({
564
+ type: "client-terminated",
565
+ sessionID: this.sessionId,
566
+ projectId: this.config.projectId,
567
+ publicKey: this.config.publicKey,
568
+ authType: this.config.authType
569
+ });
570
+ }
571
+ this.transport.close();
572
+ this.transport = void 0;
573
+ }
574
+ clearAuthSession();
575
+ this.resetSession();
576
+ if (!this.config.continuousMode) {
577
+ this.detachVisibilityRecovery();
578
+ }
579
+ this.stateMachine.transition(success ? "confirmed" : "idle");
580
+ }
581
+ restartIfContinuous() {
582
+ if (!this.config.continuousMode) return;
583
+ this.clearBackupCheck();
584
+ this.transport?.close();
585
+ this.transport = void 0;
586
+ this.stateMachine.transition("idle");
587
+ setTimeout(() => {
588
+ this.start();
589
+ }, 150);
590
+ }
591
+ resetSession() {
592
+ this.sessionId = "";
593
+ this.sessionKey = null;
594
+ }
595
+ emit(event, payload) {
596
+ this.listeners[event]?.forEach((cb) => cb(payload));
597
+ }
598
+ fail(err) {
599
+ this.emit("error", err);
600
+ this.stateMachine.transition("error");
601
+ }
602
+ };
603
+
604
+ exports.BASEURL = BASEURL;
605
+ exports.CryptoService = CryptoService;
606
+ exports.PelicanAuthentication = PelicanAuthentication;
607
+ exports.StateMachine = StateMachine;
608
+ exports.Transport = Transport;
609
+ exports.clearAllAuthData = clearAllAuthData;
610
+ exports.clearAuthSession = clearAuthSession;
611
+ exports.getAuthSession = getAuthSession;
612
+ exports.storeAuthSession = storeAuthSession;
613
+ //# sourceMappingURL=index.js.map
8
614
  //# sourceMappingURL=index.js.map