@pelican-identity/auth-core 1.2.10 → 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
@@ -188,16 +188,32 @@ var StateMachine = class {
188
188
  var Transport = class {
189
189
  constructor(handlers) {
190
190
  this.reconnectAttempts = 0;
191
- this.maxReconnectAttempts = 5;
191
+ this.maxReconnectAttempts = 3;
192
+ // Reduced from 5 for mobile
192
193
  this.isExplicitlyClosed = false;
194
+ this.isReconnecting = false;
193
195
  this.handlers = handlers;
194
196
  }
197
+ /**
198
+ * Establish WebSocket connection
199
+ * @param url - WebSocket URL
200
+ */
195
201
  connect(url) {
196
202
  this.url = url;
197
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
+ }
198
213
  this.socket = new WebSocket(url);
199
214
  this.socket.onopen = () => {
200
215
  this.reconnectAttempts = 0;
216
+ this.isReconnecting = false;
201
217
  this.handlers.onOpen?.();
202
218
  };
203
219
  this.socket.onmessage = (e) => {
@@ -205,43 +221,103 @@ var Transport = class {
205
221
  const data = JSON.parse(e.data);
206
222
  this.handlers.onMessage?.(data);
207
223
  } catch (err) {
208
- console.error("Failed to parse message", err);
224
+ console.error("Failed to parse WebSocket message", err);
209
225
  }
210
226
  };
211
227
  this.socket.onerror = (e) => {
212
- this.handlers.onError?.(e);
228
+ if (!this.isReconnecting && !this.isExplicitlyClosed) {
229
+ this.handlers.onError?.(e);
230
+ }
213
231
  };
214
232
  this.socket.onclose = (e) => {
215
- this.handlers.onClose?.(e);
216
- if (!this.isExplicitlyClosed) {
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) {
217
241
  this.attemptReconnect();
218
242
  }
219
243
  };
220
244
  }
245
+ /**
246
+ * Attempt to reconnect with exponential backoff
247
+ */
221
248
  attemptReconnect() {
222
249
  if (this.reconnectAttempts >= this.maxReconnectAttempts) {
223
- console.error("\u274C Max reconnect attempts reached");
250
+ console.warn("Max WebSocket reconnect attempts reached");
224
251
  return;
225
252
  }
253
+ this.isReconnecting = true;
226
254
  this.reconnectAttempts++;
227
- const delay = Math.pow(2, this.reconnectAttempts) * 500;
228
- setTimeout(() => {
229
- if (this.url) this.connect(this.url);
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
+ }
230
263
  }, delay);
231
264
  }
265
+ /**
266
+ * Send a message through the WebSocket
267
+ * Queues message if connection is not open
268
+ */
232
269
  send(payload) {
233
270
  if (this.socket?.readyState === WebSocket.OPEN) {
234
- this.socket.send(JSON.stringify(payload));
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
+ );
235
281
  }
236
282
  }
283
+ /**
284
+ * Close the WebSocket connection
285
+ * Prevents automatic reconnection
286
+ */
237
287
  close() {
238
288
  this.isExplicitlyClosed = true;
239
- this.socket?.close();
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;
240
316
  }
241
317
  };
242
318
 
243
319
  // src/constants.ts
244
- var BASEURL = "https://identityapi.pelicanidentity.com";
320
+ var BASEURL = "http://192.168.1.9:8080";
245
321
 
246
322
  // src/engine/engine.ts
247
323
  var PelicanAuthentication = class {
@@ -250,6 +326,7 @@ var PelicanAuthentication = class {
250
326
  this.stateMachine = new StateMachine();
251
327
  this.sessionId = "";
252
328
  this.sessionKey = null;
329
+ this.useWebSocket = true;
253
330
  this.listeners = {};
254
331
  if (!config.publicKey) throw new Error("Missing publicKey");
255
332
  if (!config.projectId) throw new Error("Missing projectId");
@@ -262,104 +339,67 @@ var PelicanAuthentication = class {
262
339
  this.stateMachine.subscribe((s) => this.emit("state", s));
263
340
  this.attachVisibilityRecovery();
264
341
  }
265
- /* -------------------- public API -------------------- */
266
- /**
267
- * Subscribe to SDK events (qr, deeplink, success, error, state)
268
- * @returns Unsubscribe function
269
- */
342
+ /* -------------------- Public API -------------------- */
270
343
  on(event, cb) {
271
344
  var _a;
272
345
  (_a = this.listeners)[event] ?? (_a[event] = /* @__PURE__ */ new Set());
273
346
  this.listeners[event].add(cb);
274
347
  return () => this.listeners[event].delete(cb);
275
348
  }
276
- /**
277
- * Initializes the authentication flow.
278
- * Fetches a relay, establishes a WebSocket, and generates the E2EE session key.
279
- */
280
349
  async start() {
281
350
  if (this.stateMachine.current !== "idle") return;
282
351
  this.resetSession();
283
352
  clearAuthSession();
284
353
  this.stateMachine.transition("initializing");
285
354
  try {
286
- const relay = await this.fetchRelayUrl();
287
355
  this.sessionKey = this.crypto.generateSymmetricKey();
288
356
  this.sessionId = crypto.randomUUID() + crypto.randomUUID();
289
- this.transport = new Transport({
290
- onOpen: () => {
291
- this.transport.send({
292
- type: "register",
293
- sessionID: this.sessionId,
294
- ...this.config
295
- });
296
- this.stateMachine.transition("awaiting-pair");
297
- },
298
- onMessage: (msg) => this.handleMessage(msg),
299
- onError: () => this.fail(new Error("WebSocket connection failed"))
300
- });
301
- this.transport.connect(relay);
302
- await this.emitEntryPoint();
357
+ this.useWebSocket = this.shouldUseWebSocket();
358
+ if (this.useWebSocket) {
359
+ await this.startWebSocketFlow();
360
+ } else {
361
+ await this.startDeepLinkFlow();
362
+ }
303
363
  } catch (err) {
304
364
  this.fail(err instanceof Error ? err : new Error("Start failed"));
305
365
  }
306
366
  }
307
- /**
308
- * Manually stops the authentication process and closes connections.
309
- */
310
367
  stop() {
311
368
  this.terminate(false);
312
369
  }
313
- /* -------------------- internals -------------------- */
314
- /** Fetches the WebSocket Relay URL from the backend */
315
- async fetchRelayUrl() {
316
- const { publicKey, projectId, authType } = this.config;
317
- const res = await fetch(
318
- `${BASEURL}/relay?public_key=${publicKey}&auth_type=${authType}&project_id=${projectId}`
319
- );
320
- if (!res.ok) {
321
- const error = await res.text();
322
- throw new Error(error);
323
- }
324
- const json = await res.json();
325
- return json.relay_url;
370
+ destroy() {
371
+ this.detachVisibilityRecovery();
372
+ this.clearBackupCheck();
373
+ this.transport?.close();
374
+ this.transport = void 0;
375
+ this.listeners = {};
326
376
  }
327
- /**
328
- * Decides whether to show a QR code (Desktop) or a Deep Link (Mobile).
329
- */
330
- async emitEntryPoint() {
331
- const payload = {
332
- sessionID: this.sessionId,
333
- sessionKey: this.sessionKey,
334
- publicKey: this.config.publicKey,
335
- authType: this.config.authType,
336
- projectId: this.config.projectId,
337
- url: window.location.href
338
- };
339
- const shouldUseQR = this.config.forceQRCode || !/Android|iPhone|iPad/i.test(navigator.userAgent);
340
- if (!shouldUseQR && this.sessionKey) {
341
- storeAuthSession(this.sessionId, this.sessionKey, 5 * 6e4);
342
- this.emit(
343
- "deeplink",
344
- `pelicanvault://auth/deep-link?sessionID=${encodeURIComponent(
345
- this.sessionId
346
- )}&sessionKey=${encodeURIComponent(
347
- this.sessionKey
348
- )}&publicKey=${encodeURIComponent(this.config.publicKey)}&authType=${this.config.authType}&projectId=${encodeURIComponent(
349
- this.config.projectId
350
- )}&url=${encodeURIComponent(window.location.href)}`
351
- );
352
- } else {
353
- const qr = await QRCode__default.default.toDataURL(JSON.stringify(payload), {
354
- type: "image/png",
355
- scale: 3,
356
- color: { light: "#ffffff", dark: "#424242ff" }
357
- });
358
- this.emit("qr", qr);
359
- }
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();
360
401
  }
361
- /** Main WebSocket message router */
362
- handleMessage(msg) {
402
+ handleWebSocketMessage(msg) {
363
403
  switch (msg.type) {
364
404
  case "paired":
365
405
  this.stateMachine.transition("paired");
@@ -374,7 +414,7 @@ var PelicanAuthentication = class {
374
414
  this.handleAuthSuccess(msg);
375
415
  return;
376
416
  case "phone-terminated":
377
- this.fail(new Error("Authenticating device terminated the connection"));
417
+ this.fail(new Error("Authentication cancelled on device"));
378
418
  this.restartIfContinuous();
379
419
  return;
380
420
  case "confirmed":
@@ -383,9 +423,6 @@ var PelicanAuthentication = class {
383
423
  return;
384
424
  }
385
425
  }
386
- /**
387
- * Decrypts the identity payload received from the phone using the session key.
388
- */
389
426
  handleAuthSuccess(msg) {
390
427
  if (!this.sessionKey || !msg.cipher || !msg.nonce) {
391
428
  this.fail(new Error("Invalid authentication payload"));
@@ -414,40 +451,89 @@ var PelicanAuthentication = class {
414
451
  this.restartIfContinuous();
415
452
  }
416
453
  }
417
- /**
418
- * Logic to handle users returning to the browser tab after using the mobile app.
419
- * Checks the server for a completed session that might have finished while in background.
420
- */
421
- async getCachedEntry(cached) {
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");
422
473
  try {
423
474
  const res = await fetch(
424
475
  `${BASEURL}/session?session_id=${cached.sessionId}`
425
476
  );
426
- if (!res.ok) throw new Error("Invalid session");
477
+ if (!res.ok) return;
427
478
  const data = await res.json();
428
479
  const decrypted = this.crypto.decryptSymmetric({
429
480
  encrypted: { cipher: data.cipher, nonce: data.nonce },
430
481
  keyString: cached.sessionKey
431
482
  });
432
483
  if (!decrypted) {
433
- this.fail(new Error("Invalid session data"));
434
- this.restartIfContinuous();
484
+ console.warn("Failed to decrypt session");
435
485
  return;
436
486
  }
437
487
  const result = JSON.parse(decrypted);
438
- this.emit("success", result);
488
+ this.clearBackupCheck();
439
489
  clearAuthSession();
440
- } catch {
441
- this.fail(new Error("Session recovery failed"));
442
- this.restartIfContinuous();
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);
443
502
  }
444
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 -------------------- */
445
532
  attachVisibilityRecovery() {
446
533
  this.visibilityHandler = async () => {
447
534
  if (document.visibilityState !== "visible") return;
448
- const cached = getAuthSession();
449
- if (!cached) return;
450
- await this.getCachedEntry(cached);
535
+ if (this.useWebSocket) return;
536
+ await this.checkSession();
451
537
  };
452
538
  document.addEventListener("visibilitychange", this.visibilityHandler);
453
539
  }
@@ -457,18 +543,34 @@ var PelicanAuthentication = class {
457
543
  this.visibilityHandler = void 0;
458
544
  }
459
545
  }
460
- /** Cleans up the current session state */
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
+ }
461
559
  terminate(success) {
462
- if (!success) {
463
- this.transport?.send({
464
- type: "client-terminated",
465
- sessionID: this.sessionId,
466
- projectId: this.config.projectId,
467
- publicKey: this.config.publicKey,
468
- authType: this.config.authType
469
- });
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;
470
573
  }
471
- this.transport?.close();
472
574
  clearAuthSession();
473
575
  this.resetSession();
474
576
  if (!this.config.continuousMode) {
@@ -477,9 +579,11 @@ var PelicanAuthentication = class {
477
579
  this.stateMachine.transition(success ? "confirmed" : "idle");
478
580
  }
479
581
  restartIfContinuous() {
480
- this.stateMachine.transition("idle");
481
- this.transport?.close();
482
582
  if (!this.config.continuousMode) return;
583
+ this.clearBackupCheck();
584
+ this.transport?.close();
585
+ this.transport = void 0;
586
+ this.stateMachine.transition("idle");
483
587
  setTimeout(() => {
484
588
  this.start();
485
589
  }, 150);
@@ -488,12 +592,6 @@ var PelicanAuthentication = class {
488
592
  this.sessionId = "";
489
593
  this.sessionKey = null;
490
594
  }
491
- /** Completely destroys the instance and removes all listeners */
492
- destroy() {
493
- this.detachVisibilityRecovery();
494
- this.transport?.close();
495
- this.listeners = {};
496
- }
497
595
  emit(event, payload) {
498
596
  this.listeners[event]?.forEach((cb) => cb(payload));
499
597
  }