@serviceme/devtools-core 0.3.2 → 0.3.4

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.js CHANGED
@@ -181,6 +181,18 @@ var AccessControl = class {
181
181
  }
182
182
  };
183
183
 
184
+ // src/logger.ts
185
+ var noopLogger = {
186
+ debug() {
187
+ },
188
+ info() {
189
+ },
190
+ warn() {
191
+ },
192
+ error() {
193
+ }
194
+ };
195
+
184
196
  // src/auth/AuthStateManager.ts
185
197
  var import_node_events = require("events");
186
198
  var AuthStateManager = class {
@@ -343,6 +355,7 @@ var AuthCore = class {
343
355
  this.state = opts.stateManager ?? new AuthStateManager();
344
356
  this.tokenStore = opts.tokenStore;
345
357
  this.accessControl = opts.accessControl;
358
+ this.logger = opts.logger ?? noopLogger;
346
359
  }
347
360
  /** Snapshot of every account, the active provider, and the last error. */
348
361
  status() {
@@ -364,16 +377,28 @@ var AuthCore = class {
364
377
  */
365
378
  async login(provider, ui, shouldContinue) {
366
379
  const providerImpl = this.registry.get(provider);
380
+ this.logger.info("[AuthCore] Starting device-flow login", { provider });
367
381
  try {
368
382
  const initial = await providerImpl.requestDeviceFlow();
369
383
  await ui(initial);
384
+ if (!initial.deviceCode) {
385
+ this.logger.error("[AuthCore] Provider did not return deviceCode", provider);
386
+ throw new Error("Auth provider did not return deviceCode for device flow completion");
387
+ }
370
388
  const session = await providerImpl.completeDeviceFlow(
371
- initial.userCode ? initial.deviceCode ?? "" : "",
372
- shouldContinue
389
+ initial.deviceCode,
390
+ shouldContinue,
391
+ initial.pollIntervalMs
373
392
  );
374
- return await this.persistSession(providerImpl, session);
393
+ const result = await this.persistSession(providerImpl, session);
394
+ this.logger.info("[AuthCore] Device-flow login completed", { provider });
395
+ return result;
375
396
  } catch (err) {
376
- this.state.recordError(err instanceof Error ? err.message : String(err));
397
+ const message = err instanceof Error ? err.message : String(err);
398
+ this.logger.error("[AuthCore] Device-flow login failed", message, {
399
+ provider
400
+ });
401
+ this.state.recordError(message);
377
402
  throw err;
378
403
  }
379
404
  }
@@ -398,7 +423,10 @@ var AuthCore = class {
398
423
  if (!provider) return null;
399
424
  const account = this.state.getActiveAccount();
400
425
  if (!account) return null;
401
- const envelope = await this.tokenStore.get({ provider, accountId: account.id });
426
+ const envelope = await this.tokenStore.get({
427
+ provider,
428
+ accountId: account.id
429
+ });
402
430
  if (!envelope) return null;
403
431
  return { provider, account, token: envelope.token };
404
432
  }
@@ -408,9 +436,15 @@ var AuthCore = class {
408
436
  * every account.
409
437
  */
410
438
  async logout(provider) {
439
+ this.logger.info("[AuthCore] Logout requested", {
440
+ provider: provider ?? "all"
441
+ });
411
442
  if (!provider) {
412
443
  for (const account of this.state.listAccounts()) {
413
- await this.tokenStore.delete({ provider: account.provider, accountId: account.id });
444
+ await this.tokenStore.delete({
445
+ provider: account.provider,
446
+ accountId: account.id
447
+ });
414
448
  this.state.removeAccount(account.provider, account.id);
415
449
  }
416
450
  this.state.clearAll();
@@ -485,6 +519,10 @@ var AuthCore = class {
485
519
  avatarUrl: meta.avatarUrl,
486
520
  expiresAt: meta.expiresAt ?? expiresAt
487
521
  };
522
+ this.logger.debug("[AuthCore] Persisting session", {
523
+ provider: meta.provider,
524
+ accountId: meta.id
525
+ });
488
526
  await this.tokenStore.set({ provider: meta.provider, accountId: meta.id }, session.token, {
489
527
  expiresAt
490
528
  });
@@ -587,11 +625,13 @@ var GitHubAuthProvider = class {
587
625
  userUrl: config.userUrl ?? DEFAULT_USER_URL,
588
626
  scope: config.scope ?? DEFAULT_SCOPE,
589
627
  minPollIntervalMs: config.minPollIntervalMs ?? 1e3,
590
- maxPollIntervalMs: config.maxPollIntervalMs ?? 15e3,
628
+ maxPollIntervalMs: config.maxPollIntervalMs ?? 6e4,
591
629
  maxWaitMs: config.maxWaitMs,
592
630
  fetchImpl: config.fetchImpl ?? fetch,
593
631
  deviceCodeRetryBaseDelayMs: config.deviceCodeRetryBaseDelayMs ?? DEFAULT_DEVICE_CODE_RETRY_BASE_DELAY_MS
594
632
  };
633
+ this.logger = config.logger ?? noopLogger;
634
+ this.sleepImpl = config.sleepImpl ?? sleep;
595
635
  }
596
636
  async requestDeviceFlow(opts) {
597
637
  const body = JSON.stringify({
@@ -600,6 +640,10 @@ var GitHubAuthProvider = class {
600
640
  });
601
641
  let lastNetworkError;
602
642
  for (let attempt = 1; attempt <= DEVICE_CODE_MAX_ATTEMPTS; attempt++) {
643
+ this.logger.debug("[GitHubAuthProvider] Requesting device code", {
644
+ attempt,
645
+ maxAttempts: DEVICE_CODE_MAX_ATTEMPTS
646
+ });
603
647
  let resp;
604
648
  try {
605
649
  resp = await this.cfg.fetchImpl(this.cfg.deviceCodeUrl, {
@@ -613,45 +657,99 @@ var GitHubAuthProvider = class {
613
657
  });
614
658
  } catch (error) {
615
659
  if (!isTransientNetworkError(error) || attempt === DEVICE_CODE_MAX_ATTEMPTS) {
660
+ this.logger.error(
661
+ "[GitHubAuthProvider] Device-code request failed (non-retryable)",
662
+ error instanceof Error ? error.message : String(error),
663
+ { attempt }
664
+ );
616
665
  throw error;
617
666
  }
667
+ const delayMs = this.cfg.deviceCodeRetryBaseDelayMs * 2 ** (attempt - 1);
668
+ this.logger.warn(
669
+ "[GitHubAuthProvider] Transient network error requesting device code, retrying",
670
+ {
671
+ attempt,
672
+ delayMs,
673
+ error: error instanceof Error ? error.message : String(error)
674
+ }
675
+ );
618
676
  lastNetworkError = error;
619
- await sleep(this.cfg.deviceCodeRetryBaseDelayMs * 2 ** (attempt - 1));
677
+ await this.sleepImpl(delayMs);
620
678
  continue;
621
679
  }
622
680
  if (!resp.ok) {
681
+ this.logger.error(
682
+ "[GitHubAuthProvider] Device-code request rejected by GitHub",
683
+ `HTTP ${resp.status}`
684
+ );
623
685
  throw new Error(`GitHub device-code request failed: HTTP ${resp.status}`);
624
686
  }
625
687
  const data = await resp.json();
626
688
  if (!data.device_code || !data.user_code || !data.verification_uri) {
689
+ this.logger.error(
690
+ "[GitHubAuthProvider] Device-code response missing required fields",
691
+ JSON.stringify(Object.keys(data))
692
+ );
627
693
  throw new Error("GitHub device-code response missing required fields");
628
694
  }
695
+ this.logger.info("[GitHubAuthProvider] Device code obtained", {
696
+ userCode: data.user_code,
697
+ verificationUri: data.verification_uri,
698
+ expiresInSec: data.expires_in,
699
+ pollIntervalSec: data.interval
700
+ });
629
701
  return {
630
702
  provider: this.providerId,
703
+ deviceCode: data.device_code,
631
704
  userCode: data.user_code,
632
705
  verificationUrl: data.verification_uri,
633
706
  expiresAt: Date.now() + data.expires_in * 1e3,
707
+ pollIntervalMs: data.interval * 1e3,
634
708
  message: `Open ${data.verification_uri} and enter ${data.user_code}`
635
709
  };
636
710
  }
637
711
  throw lastNetworkError ?? new Error("GitHub device-code request failed");
638
712
  }
639
- async completeDeviceFlow(deviceCode, shouldContinue) {
713
+ async completeDeviceFlow(deviceCode, shouldContinue, initialPollIntervalMs) {
640
714
  const start = Date.now();
641
- let pollIntervalMs = this.cfg.minPollIntervalMs;
715
+ let pollIntervalMs = Math.max(this.cfg.minPollIntervalMs, initialPollIntervalMs ?? 0);
642
716
  let consecutiveSlowDown = 0;
717
+ let pollCount = 0;
718
+ this.logger.info("[GitHubAuthProvider] Starting device-flow polling", {
719
+ initialPollIntervalMs: pollIntervalMs,
720
+ maxWaitMs: this.cfg.maxWaitMs
721
+ });
643
722
  while (true) {
644
723
  if (shouldContinue && !shouldContinue()) {
724
+ this.logger.info("[GitHubAuthProvider] Device flow cancelled by caller", {
725
+ elapsedMs: Date.now() - start,
726
+ pollCount
727
+ });
645
728
  throw new Error("GitHub device flow cancelled by caller");
646
729
  }
647
730
  const elapsed = Date.now() - start;
648
731
  if (this.cfg.maxWaitMs !== void 0 && elapsed > this.cfg.maxWaitMs) {
732
+ this.logger.warn("[GitHubAuthProvider] Device flow exceeded max wait time", {
733
+ elapsedMs: elapsed,
734
+ maxWaitMs: this.cfg.maxWaitMs,
735
+ pollCount
736
+ });
649
737
  throw new Error("GitHub device flow exceeded max wait time");
650
738
  }
651
- await sleep(pollIntervalMs);
739
+ await this.sleepImpl(pollIntervalMs);
652
740
  if (shouldContinue && !shouldContinue()) {
741
+ this.logger.info("[GitHubAuthProvider] Device flow cancelled by caller", {
742
+ elapsedMs: Date.now() - start,
743
+ pollCount
744
+ });
653
745
  throw new Error("GitHub device flow cancelled by caller");
654
746
  }
747
+ pollCount++;
748
+ this.logger.debug("[GitHubAuthProvider] Polling for authorization", {
749
+ pollCount,
750
+ elapsedMs: Date.now() - start,
751
+ pollIntervalMs
752
+ });
655
753
  let resp;
656
754
  try {
657
755
  resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {
@@ -669,19 +767,59 @@ var GitHubAuthProvider = class {
669
767
  });
670
768
  } catch (error) {
671
769
  if (!isTransientNetworkError(error)) {
770
+ this.logger.error(
771
+ "[GitHubAuthProvider] Non-retryable error while polling token endpoint",
772
+ error instanceof Error ? error.message : String(error),
773
+ { pollCount }
774
+ );
672
775
  throw error;
673
776
  }
674
- pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
777
+ const nextPollIntervalMs = Math.min(
778
+ Math.round(pollIntervalMs * 1.5),
779
+ this.cfg.maxPollIntervalMs
780
+ );
781
+ this.logger.warn(
782
+ "[GitHubAuthProvider] Transient network error while polling, backing off",
783
+ {
784
+ pollCount,
785
+ error: error instanceof Error ? error.message : String(error),
786
+ previousPollIntervalMs: pollIntervalMs,
787
+ nextPollIntervalMs
788
+ }
789
+ );
790
+ pollIntervalMs = nextPollIntervalMs;
675
791
  continue;
676
792
  }
677
793
  if (!resp.ok) {
678
- pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
794
+ const nextPollIntervalMs = Math.min(
795
+ Math.round(pollIntervalMs * 1.5),
796
+ this.cfg.maxPollIntervalMs
797
+ );
798
+ this.logger.warn(
799
+ "[GitHubAuthProvider] Token endpoint returned non-OK status, backing off",
800
+ {
801
+ pollCount,
802
+ status: resp.status,
803
+ previousPollIntervalMs: pollIntervalMs,
804
+ nextPollIntervalMs
805
+ }
806
+ );
807
+ pollIntervalMs = nextPollIntervalMs;
679
808
  continue;
680
809
  }
681
810
  const data = await resp.json();
682
811
  if (data.access_token) {
812
+ this.logger.info("[GitHubAuthProvider] Authorization granted, fetching user profile", {
813
+ pollCount,
814
+ elapsedMs: Date.now() - start
815
+ });
683
816
  const rawUser = await this.fetchGitHubUser(data.access_token);
684
817
  const email = await this.resolveEmail(data.access_token, rawUser);
818
+ this.logger.info("[GitHubAuthProvider] Device-flow login completed", {
819
+ pollCount,
820
+ elapsedMs: Date.now() - start,
821
+ login: rawUser.login
822
+ });
685
823
  return {
686
824
  token: data.access_token,
687
825
  refreshToken: data.refresh_token,
@@ -696,22 +834,42 @@ var GitHubAuthProvider = class {
696
834
  };
697
835
  }
698
836
  if (data.error === "authorization_pending") {
837
+ this.logger.debug("[GitHubAuthProvider] Authorization still pending", {
838
+ pollCount,
839
+ elapsedMs: Date.now() - start
840
+ });
699
841
  continue;
700
842
  }
701
843
  if (data.error === "slow_down") {
702
844
  consecutiveSlowDown++;
703
- pollIntervalMs = Math.min(
704
- Math.round(pollIntervalMs * 1.5 + Math.min(consecutiveSlowDown * 500, 2e3)),
705
- this.cfg.maxPollIntervalMs
706
- );
845
+ const nextPollIntervalMs = Math.min(pollIntervalMs + 5e3, this.cfg.maxPollIntervalMs);
846
+ this.logger.warn("[GitHubAuthProvider] GitHub requested slower polling (slow_down)", {
847
+ pollCount,
848
+ consecutiveSlowDown,
849
+ previousPollIntervalMs: pollIntervalMs,
850
+ nextPollIntervalMs
851
+ });
852
+ pollIntervalMs = nextPollIntervalMs;
707
853
  continue;
708
854
  }
709
855
  if (data.error === "access_denied") {
856
+ this.logger.warn("[GitHubAuthProvider] User denied authorization", {
857
+ pollCount
858
+ });
710
859
  throw new Error("User denied authorization");
711
860
  }
712
861
  if (data.error === "expired_token") {
862
+ this.logger.warn(
863
+ "[GitHubAuthProvider] Device code expired before authorization completed",
864
+ { pollCount, elapsedMs: Date.now() - start }
865
+ );
713
866
  throw new Error("GitHub device code expired \u2014 restart the flow");
714
867
  }
868
+ this.logger.error(
869
+ "[GitHubAuthProvider] Unexpected device-flow error from token endpoint",
870
+ data.error ?? "unknown",
871
+ { pollCount }
872
+ );
715
873
  throw new Error(`GitHub device flow error: ${data.error ?? "unknown"}`);
716
874
  }
717
875
  }