@serviceme/devtools-core 0.3.1 → 0.3.3

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/auth.mjs CHANGED
@@ -144,6 +144,18 @@ var AccessControl = class {
144
144
  }
145
145
  };
146
146
 
147
+ // src/logger.ts
148
+ var noopLogger = {
149
+ debug() {
150
+ },
151
+ info() {
152
+ },
153
+ warn() {
154
+ },
155
+ error() {
156
+ }
157
+ };
158
+
147
159
  // src/auth/AuthStateManager.ts
148
160
  import { EventEmitter } from "events";
149
161
  var AuthStateManager = class {
@@ -306,6 +318,7 @@ var AuthCore = class {
306
318
  this.state = opts.stateManager ?? new AuthStateManager();
307
319
  this.tokenStore = opts.tokenStore;
308
320
  this.accessControl = opts.accessControl;
321
+ this.logger = opts.logger ?? noopLogger;
309
322
  }
310
323
  /** Snapshot of every account, the active provider, and the last error. */
311
324
  status() {
@@ -327,16 +340,28 @@ var AuthCore = class {
327
340
  */
328
341
  async login(provider, ui, shouldContinue) {
329
342
  const providerImpl = this.registry.get(provider);
343
+ this.logger.info("[AuthCore] Starting device-flow login", { provider });
330
344
  try {
331
345
  const initial = await providerImpl.requestDeviceFlow();
332
346
  await ui(initial);
347
+ if (!initial.deviceCode) {
348
+ this.logger.error("[AuthCore] Provider did not return deviceCode", provider);
349
+ throw new Error("Auth provider did not return deviceCode for device flow completion");
350
+ }
333
351
  const session = await providerImpl.completeDeviceFlow(
334
- initial.userCode ? initial.deviceCode ?? "" : "",
335
- shouldContinue
352
+ initial.deviceCode,
353
+ shouldContinue,
354
+ initial.pollIntervalMs
336
355
  );
337
- return await this.persistSession(providerImpl, session);
356
+ const result = await this.persistSession(providerImpl, session);
357
+ this.logger.info("[AuthCore] Device-flow login completed", { provider });
358
+ return result;
338
359
  } catch (err) {
339
- this.state.recordError(err instanceof Error ? err.message : String(err));
360
+ const message = err instanceof Error ? err.message : String(err);
361
+ this.logger.error("[AuthCore] Device-flow login failed", message, {
362
+ provider
363
+ });
364
+ this.state.recordError(message);
340
365
  throw err;
341
366
  }
342
367
  }
@@ -361,7 +386,10 @@ var AuthCore = class {
361
386
  if (!provider) return null;
362
387
  const account = this.state.getActiveAccount();
363
388
  if (!account) return null;
364
- const envelope = await this.tokenStore.get({ provider, accountId: account.id });
389
+ const envelope = await this.tokenStore.get({
390
+ provider,
391
+ accountId: account.id
392
+ });
365
393
  if (!envelope) return null;
366
394
  return { provider, account, token: envelope.token };
367
395
  }
@@ -371,9 +399,15 @@ var AuthCore = class {
371
399
  * every account.
372
400
  */
373
401
  async logout(provider) {
402
+ this.logger.info("[AuthCore] Logout requested", {
403
+ provider: provider ?? "all"
404
+ });
374
405
  if (!provider) {
375
406
  for (const account of this.state.listAccounts()) {
376
- await this.tokenStore.delete({ provider: account.provider, accountId: account.id });
407
+ await this.tokenStore.delete({
408
+ provider: account.provider,
409
+ accountId: account.id
410
+ });
377
411
  this.state.removeAccount(account.provider, account.id);
378
412
  }
379
413
  this.state.clearAll();
@@ -448,6 +482,10 @@ var AuthCore = class {
448
482
  avatarUrl: meta.avatarUrl,
449
483
  expiresAt: meta.expiresAt ?? expiresAt
450
484
  };
485
+ this.logger.debug("[AuthCore] Persisting session", {
486
+ provider: meta.provider,
487
+ accountId: meta.id
488
+ });
451
489
  await this.tokenStore.set({ provider: meta.provider, accountId: meta.id }, session.token, {
452
490
  expiresAt
453
491
  });
@@ -550,11 +588,13 @@ var GitHubAuthProvider = class {
550
588
  userUrl: config.userUrl ?? DEFAULT_USER_URL,
551
589
  scope: config.scope ?? DEFAULT_SCOPE,
552
590
  minPollIntervalMs: config.minPollIntervalMs ?? 1e3,
553
- maxPollIntervalMs: config.maxPollIntervalMs ?? 15e3,
591
+ maxPollIntervalMs: config.maxPollIntervalMs ?? 6e4,
554
592
  maxWaitMs: config.maxWaitMs,
555
593
  fetchImpl: config.fetchImpl ?? fetch,
556
594
  deviceCodeRetryBaseDelayMs: config.deviceCodeRetryBaseDelayMs ?? DEFAULT_DEVICE_CODE_RETRY_BASE_DELAY_MS
557
595
  };
596
+ this.logger = config.logger ?? noopLogger;
597
+ this.sleepImpl = config.sleepImpl ?? sleep;
558
598
  }
559
599
  async requestDeviceFlow(opts) {
560
600
  const body = JSON.stringify({
@@ -563,6 +603,10 @@ var GitHubAuthProvider = class {
563
603
  });
564
604
  let lastNetworkError;
565
605
  for (let attempt = 1; attempt <= DEVICE_CODE_MAX_ATTEMPTS; attempt++) {
606
+ this.logger.debug("[GitHubAuthProvider] Requesting device code", {
607
+ attempt,
608
+ maxAttempts: DEVICE_CODE_MAX_ATTEMPTS
609
+ });
566
610
  let resp;
567
611
  try {
568
612
  resp = await this.cfg.fetchImpl(this.cfg.deviceCodeUrl, {
@@ -576,45 +620,99 @@ var GitHubAuthProvider = class {
576
620
  });
577
621
  } catch (error) {
578
622
  if (!isTransientNetworkError(error) || attempt === DEVICE_CODE_MAX_ATTEMPTS) {
623
+ this.logger.error(
624
+ "[GitHubAuthProvider] Device-code request failed (non-retryable)",
625
+ error instanceof Error ? error.message : String(error),
626
+ { attempt }
627
+ );
579
628
  throw error;
580
629
  }
630
+ const delayMs = this.cfg.deviceCodeRetryBaseDelayMs * 2 ** (attempt - 1);
631
+ this.logger.warn(
632
+ "[GitHubAuthProvider] Transient network error requesting device code, retrying",
633
+ {
634
+ attempt,
635
+ delayMs,
636
+ error: error instanceof Error ? error.message : String(error)
637
+ }
638
+ );
581
639
  lastNetworkError = error;
582
- await sleep(this.cfg.deviceCodeRetryBaseDelayMs * 2 ** (attempt - 1));
640
+ await this.sleepImpl(delayMs);
583
641
  continue;
584
642
  }
585
643
  if (!resp.ok) {
644
+ this.logger.error(
645
+ "[GitHubAuthProvider] Device-code request rejected by GitHub",
646
+ `HTTP ${resp.status}`
647
+ );
586
648
  throw new Error(`GitHub device-code request failed: HTTP ${resp.status}`);
587
649
  }
588
650
  const data = await resp.json();
589
651
  if (!data.device_code || !data.user_code || !data.verification_uri) {
652
+ this.logger.error(
653
+ "[GitHubAuthProvider] Device-code response missing required fields",
654
+ JSON.stringify(Object.keys(data))
655
+ );
590
656
  throw new Error("GitHub device-code response missing required fields");
591
657
  }
658
+ this.logger.info("[GitHubAuthProvider] Device code obtained", {
659
+ userCode: data.user_code,
660
+ verificationUri: data.verification_uri,
661
+ expiresInSec: data.expires_in,
662
+ pollIntervalSec: data.interval
663
+ });
592
664
  return {
593
665
  provider: this.providerId,
666
+ deviceCode: data.device_code,
594
667
  userCode: data.user_code,
595
668
  verificationUrl: data.verification_uri,
596
669
  expiresAt: Date.now() + data.expires_in * 1e3,
670
+ pollIntervalMs: data.interval * 1e3,
597
671
  message: `Open ${data.verification_uri} and enter ${data.user_code}`
598
672
  };
599
673
  }
600
674
  throw lastNetworkError ?? new Error("GitHub device-code request failed");
601
675
  }
602
- async completeDeviceFlow(deviceCode, shouldContinue) {
676
+ async completeDeviceFlow(deviceCode, shouldContinue, initialPollIntervalMs) {
603
677
  const start = Date.now();
604
- let pollIntervalMs = this.cfg.minPollIntervalMs;
678
+ let pollIntervalMs = Math.max(this.cfg.minPollIntervalMs, initialPollIntervalMs ?? 0);
605
679
  let consecutiveSlowDown = 0;
680
+ let pollCount = 0;
681
+ this.logger.info("[GitHubAuthProvider] Starting device-flow polling", {
682
+ initialPollIntervalMs: pollIntervalMs,
683
+ maxWaitMs: this.cfg.maxWaitMs
684
+ });
606
685
  while (true) {
607
686
  if (shouldContinue && !shouldContinue()) {
687
+ this.logger.info("[GitHubAuthProvider] Device flow cancelled by caller", {
688
+ elapsedMs: Date.now() - start,
689
+ pollCount
690
+ });
608
691
  throw new Error("GitHub device flow cancelled by caller");
609
692
  }
610
693
  const elapsed = Date.now() - start;
611
694
  if (this.cfg.maxWaitMs !== void 0 && elapsed > this.cfg.maxWaitMs) {
695
+ this.logger.warn("[GitHubAuthProvider] Device flow exceeded max wait time", {
696
+ elapsedMs: elapsed,
697
+ maxWaitMs: this.cfg.maxWaitMs,
698
+ pollCount
699
+ });
612
700
  throw new Error("GitHub device flow exceeded max wait time");
613
701
  }
614
- await sleep(pollIntervalMs);
702
+ await this.sleepImpl(pollIntervalMs);
615
703
  if (shouldContinue && !shouldContinue()) {
704
+ this.logger.info("[GitHubAuthProvider] Device flow cancelled by caller", {
705
+ elapsedMs: Date.now() - start,
706
+ pollCount
707
+ });
616
708
  throw new Error("GitHub device flow cancelled by caller");
617
709
  }
710
+ pollCount++;
711
+ this.logger.debug("[GitHubAuthProvider] Polling for authorization", {
712
+ pollCount,
713
+ elapsedMs: Date.now() - start,
714
+ pollIntervalMs
715
+ });
618
716
  let resp;
619
717
  try {
620
718
  resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {
@@ -632,19 +730,59 @@ var GitHubAuthProvider = class {
632
730
  });
633
731
  } catch (error) {
634
732
  if (!isTransientNetworkError(error)) {
733
+ this.logger.error(
734
+ "[GitHubAuthProvider] Non-retryable error while polling token endpoint",
735
+ error instanceof Error ? error.message : String(error),
736
+ { pollCount }
737
+ );
635
738
  throw error;
636
739
  }
637
- pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
740
+ const nextPollIntervalMs = Math.min(
741
+ Math.round(pollIntervalMs * 1.5),
742
+ this.cfg.maxPollIntervalMs
743
+ );
744
+ this.logger.warn(
745
+ "[GitHubAuthProvider] Transient network error while polling, backing off",
746
+ {
747
+ pollCount,
748
+ error: error instanceof Error ? error.message : String(error),
749
+ previousPollIntervalMs: pollIntervalMs,
750
+ nextPollIntervalMs
751
+ }
752
+ );
753
+ pollIntervalMs = nextPollIntervalMs;
638
754
  continue;
639
755
  }
640
756
  if (!resp.ok) {
641
- pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
757
+ const nextPollIntervalMs = Math.min(
758
+ Math.round(pollIntervalMs * 1.5),
759
+ this.cfg.maxPollIntervalMs
760
+ );
761
+ this.logger.warn(
762
+ "[GitHubAuthProvider] Token endpoint returned non-OK status, backing off",
763
+ {
764
+ pollCount,
765
+ status: resp.status,
766
+ previousPollIntervalMs: pollIntervalMs,
767
+ nextPollIntervalMs
768
+ }
769
+ );
770
+ pollIntervalMs = nextPollIntervalMs;
642
771
  continue;
643
772
  }
644
773
  const data = await resp.json();
645
774
  if (data.access_token) {
775
+ this.logger.info("[GitHubAuthProvider] Authorization granted, fetching user profile", {
776
+ pollCount,
777
+ elapsedMs: Date.now() - start
778
+ });
646
779
  const rawUser = await this.fetchGitHubUser(data.access_token);
647
780
  const email = await this.resolveEmail(data.access_token, rawUser);
781
+ this.logger.info("[GitHubAuthProvider] Device-flow login completed", {
782
+ pollCount,
783
+ elapsedMs: Date.now() - start,
784
+ login: rawUser.login
785
+ });
648
786
  return {
649
787
  token: data.access_token,
650
788
  refreshToken: data.refresh_token,
@@ -659,22 +797,42 @@ var GitHubAuthProvider = class {
659
797
  };
660
798
  }
661
799
  if (data.error === "authorization_pending") {
800
+ this.logger.debug("[GitHubAuthProvider] Authorization still pending", {
801
+ pollCount,
802
+ elapsedMs: Date.now() - start
803
+ });
662
804
  continue;
663
805
  }
664
806
  if (data.error === "slow_down") {
665
807
  consecutiveSlowDown++;
666
- pollIntervalMs = Math.min(
667
- Math.round(pollIntervalMs * 1.5 + Math.min(consecutiveSlowDown * 500, 2e3)),
668
- this.cfg.maxPollIntervalMs
669
- );
808
+ const nextPollIntervalMs = Math.min(pollIntervalMs + 5e3, this.cfg.maxPollIntervalMs);
809
+ this.logger.warn("[GitHubAuthProvider] GitHub requested slower polling (slow_down)", {
810
+ pollCount,
811
+ consecutiveSlowDown,
812
+ previousPollIntervalMs: pollIntervalMs,
813
+ nextPollIntervalMs
814
+ });
815
+ pollIntervalMs = nextPollIntervalMs;
670
816
  continue;
671
817
  }
672
818
  if (data.error === "access_denied") {
819
+ this.logger.warn("[GitHubAuthProvider] User denied authorization", {
820
+ pollCount
821
+ });
673
822
  throw new Error("User denied authorization");
674
823
  }
675
824
  if (data.error === "expired_token") {
825
+ this.logger.warn(
826
+ "[GitHubAuthProvider] Device code expired before authorization completed",
827
+ { pollCount, elapsedMs: Date.now() - start }
828
+ );
676
829
  throw new Error("GitHub device code expired \u2014 restart the flow");
677
830
  }
831
+ this.logger.error(
832
+ "[GitHubAuthProvider] Unexpected device-flow error from token endpoint",
833
+ data.error ?? "unknown",
834
+ { pollCount }
835
+ );
678
836
  throw new Error(`GitHub device flow error: ${data.error ?? "unknown"}`);
679
837
  }
680
838
  }