react-native-nitro-auth 0.5.8 → 0.5.9

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +64 -50
  3. package/cpp/HybridAuth.cpp +135 -50
  4. package/cpp/HybridAuth.hpp +1 -0
  5. package/lib/commonjs/Auth.web.js +141 -73
  6. package/lib/commonjs/Auth.web.js.map +1 -1
  7. package/lib/commonjs/create-auth-service.js +71 -0
  8. package/lib/commonjs/create-auth-service.js.map +1 -0
  9. package/lib/commonjs/service.js +2 -79
  10. package/lib/commonjs/service.js.map +1 -1
  11. package/lib/commonjs/service.web.js +2 -79
  12. package/lib/commonjs/service.web.js.map +1 -1
  13. package/lib/commonjs/use-auth.js +6 -3
  14. package/lib/commonjs/use-auth.js.map +1 -1
  15. package/lib/module/Auth.web.js +141 -73
  16. package/lib/module/Auth.web.js.map +1 -1
  17. package/lib/module/create-auth-service.js +67 -0
  18. package/lib/module/create-auth-service.js.map +1 -0
  19. package/lib/module/service.js +2 -79
  20. package/lib/module/service.js.map +1 -1
  21. package/lib/module/service.web.js +2 -79
  22. package/lib/module/service.web.js.map +1 -1
  23. package/lib/module/use-auth.js +6 -3
  24. package/lib/module/use-auth.js.map +1 -1
  25. package/lib/typescript/commonjs/Auth.web.d.ts +4 -2
  26. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/create-auth-service.d.ts +5 -0
  28. package/lib/typescript/commonjs/create-auth-service.d.ts.map +1 -0
  29. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  32. package/lib/typescript/module/Auth.web.d.ts +4 -2
  33. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  34. package/lib/typescript/module/create-auth-service.d.ts +5 -0
  35. package/lib/typescript/module/create-auth-service.d.ts.map +1 -0
  36. package/lib/typescript/module/service.d.ts.map +1 -1
  37. package/lib/typescript/module/service.web.d.ts.map +1 -1
  38. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  39. package/package.json +7 -5
  40. package/react-native-nitro-auth.podspec +1 -0
  41. package/src/Auth.web.ts +261 -102
  42. package/src/create-auth-service.ts +97 -0
  43. package/src/service.ts +3 -101
  44. package/src/service.web.ts +3 -101
  45. package/src/use-auth.ts +7 -3
@@ -1 +1 @@
1
- {"version":3,"file":"Auth.web.d.ts","sourceRoot":"","sources":["../../../src/Auth.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,UAAU,EACX,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAkK7D,cAAM,OAAQ,YAAW,IAAI;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,UAAU,CAAgD;IAClE,OAAO,CAAC,eAAe,CAAwC;IAC/D,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,oBAAoB,CAAsB;IAClD,OAAO,CAAC,eAAe,CAAkC;IACzD,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,cAAc,CAAkB;;IAOxC,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,sBAAsB;IA0B9B,OAAO,CAAC,4BAA4B;IAOpC,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,iBAAiB;IAsCzB,OAAO,CAAC,SAAS;IAcjB,OAAO,CAAC,SAAS;IAYjB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,2BAA2B;IAgBnC,OAAO,CAAC,0BAA0B;IAYlC,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,aAAa;IAgDrB,OAAO,CAAC,eAAe;IAIvB,IAAI,WAAW,IAAI,QAAQ,GAAG,SAAS,CAEtC;IAED,IAAI,aAAa,IAAI,MAAM,EAAE,CAE5B;IAED,IAAI,eAAe,IAAI,OAAO,CAE7B;IAED,kBAAkB,CAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,KAAK,IAAI,GAC7C,MAAM,IAAI;IAQb,iBAAiB,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI;IAOrE,OAAO,CAAC,MAAM;IAMR,KAAK,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BpE,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB9C,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAY7C,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAW7C,YAAY,IAAI,OAAO,CAAC,UAAU,CAAC;YAgB3B,mBAAmB;IA6GjC,OAAO,CAAC,QAAQ;YAkCF,mBAAmB;IAQjC,OAAO,CAAC,eAAe;IAevB,OAAO,CAAC,oBAAoB;YA8Dd,WAAW;YAkBX,iBAAiB;IAyF/B,OAAO,CAAC,eAAe;YAcT,cAAc;YAoBd,oBAAoB;IA2GlC,OAAO,CAAC,oBAAoB;YAMd,qBAAqB;IAOnC,OAAO,CAAC,eAAe;YAKT,8BAA8B;IA8E5C,OAAO,CAAC,uBAAuB;IAY/B,OAAO,CAAC,kBAAkB;YAiBZ,oBAAoB;YA2DpB,UAAU;IAqClB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAcpC,MAAM,IAAI,IAAI;IASd,OAAO,CAAC,UAAU;IAOlB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIzC,qEAAqE;IACrE,oBAAoB,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,GAAG,IAAI;IAQjE,IAAI,SAAU;IACd,OAAO;IACP,MAAM,CAAC,KAAK,EAAE,OAAO;CAGtB;AAED,eAAO,MAAM,UAAU,SAAgB,CAAC"}
1
+ {"version":3,"file":"Auth.web.d.ts","sourceRoot":"","sources":["../../../src/Auth.web.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { Auth } from "./Auth.nitro";
2
+ type AuthSource = () => Auth;
3
+ export declare function createAuthService(getAuth: AuthSource): Auth;
4
+ export {};
5
+ //# sourceMappingURL=create-auth-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-auth-service.d.ts","sourceRoot":"","sources":["../../../src/create-auth-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EAKL,MAAM,cAAc,CAAC;AAGtB,KAAK,UAAU,GAAG,MAAM,IAAI,CAAC;AAiB7B,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAsE3D"}
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,IAAI,EAKL,MAAM,cAAc,CAAC;AAUtB,eAAO,MAAM,WAAW,EAAE,IA4FzB,CAAC"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAUzC,eAAO,MAAM,WAAW,EAAE,IAAsC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"service.web.d.ts","sourceRoot":"","sources":["../../../src/service.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EAKL,MAAM,cAAc,CAAC;AAItB,eAAO,MAAM,WAAW,EAAE,IA4FzB,CAAC"}
1
+ {"version":3,"file":"service.web.d.ts","sourceRoot":"","sources":["../../../src/service.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAIzC,eAAO,MAAM,WAAW,EAAE,IAA0C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-auth.d.ts","sourceRoot":"","sources":["../../../src/use-auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,UAAU,EACX,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAI/C,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;CAC9B,CAAC;AAwBF,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG;IACtC,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAClD,YAAY,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IACxC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC,CAAC;AAEF,wBAAgB,OAAO,IAAI,aAAa,CA+KvC"}
1
+ {"version":3,"file":"use-auth.d.ts","sourceRoot":"","sources":["../../../src/use-auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,UAAU,EACX,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAQ/C,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;CAC9B,CAAC;AAwBF,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG;IACtC,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAClD,YAAY,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IACxC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC,CAAC;AAEF,wBAAgB,OAAO,IAAI,aAAa,CA+KvC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-auth",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
4
4
  "description": "High-performance authentication library for React Native with Google Sign-In, Apple Sign-In, and Microsoft Sign-In support, powered by Nitro Modules (JSI)",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -17,6 +17,7 @@
17
17
  "ios",
18
18
  "nitrogen",
19
19
  "nitro.json",
20
+ "CHANGELOG.md",
20
21
  "*.podspec",
21
22
  "app.plugin.js",
22
23
  "!**/__tests__",
@@ -39,6 +40,7 @@
39
40
  "test": "jest",
40
41
  "test:coverage": "jest --coverage",
41
42
  "test:cpp": "node scripts/test-cpp.js",
43
+ "test:cpp:coverage": "node scripts/test-cpp.js --coverage",
42
44
  "prepublishOnly": "bun run clean && bun run codegen && bun run build && bun run typecheck && bun run lint && bun run test && bun run test:cpp",
43
45
  "prepack": "bun ../../scripts/sync-package-docs.ts",
44
46
  "pack:dry-run": "bun pm pack --dry-run",
@@ -82,13 +84,13 @@
82
84
  },
83
85
  "devDependencies": {
84
86
  "@expo/config-plugins": "^55.0.8",
85
- "@react-native/babel-preset": "^0.83.0",
87
+ "@react-native/babel-preset": "^0.83.6",
86
88
  "@testing-library/react": "^16.3.2",
87
- "@types/node": "^22.19.11",
89
+ "@types/node": "^22.19.17",
88
90
  "jest-environment-jsdom": "^29.7.0",
89
91
  "react": "19.2.0",
90
- "react-native": "0.83.4",
91
- "react-native-nitro-modules": "^0.35.4",
92
+ "react-native": "0.83.6",
93
+ "react-native-nitro-modules": "^0.35.5",
92
94
  "react-native-web": "^0.21.2",
93
95
  "typescript": "^5.9.3"
94
96
  },
@@ -19,6 +19,7 @@ Pod::Spec.new do |s|
19
19
  "ios/**/*.{h,m,mm,swift}",
20
20
  "cpp/**/*.{h,hpp,c,cpp}"
21
21
  ]
22
+ s.exclude_files = "cpp/__tests__/**/*"
22
23
 
23
24
  s.pod_target_xcconfig = {
24
25
  "CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
package/src/Auth.web.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  AuthProvider,
5
5
  LoginOptions,
6
6
  AuthTokens,
7
+ AuthErrorCode,
7
8
  } from "./Auth.nitro";
8
9
  import type { JSStorageAdapter } from "./js-storage-adapter";
9
10
  import { logger } from "./utils/logger";
@@ -23,6 +24,24 @@ const WEB_STORAGE_MODES = new Set([
23
24
  STORAGE_MODE_LOCAL,
24
25
  STORAGE_MODE_MEMORY,
25
26
  ] as const);
27
+ const WEB_AUTH_ERROR_CODES: ReadonlySet<string> = new Set<AuthErrorCode>([
28
+ "cancelled",
29
+ "timeout",
30
+ "popup_blocked",
31
+ "network_error",
32
+ "configuration_error",
33
+ "not_signed_in",
34
+ "operation_in_progress",
35
+ "unsupported_provider",
36
+ "invalid_state",
37
+ "invalid_nonce",
38
+ "token_error",
39
+ "no_id_token",
40
+ "parse_error",
41
+ "refresh_failed",
42
+ "unknown",
43
+ ]);
44
+ const JWT_BASE64_URL_RE = /^[A-Za-z0-9_-]+$/;
26
45
  const inMemoryWebStorage = new Map<string, string>();
27
46
  let _appleSdkLoadPromise: Promise<void> | undefined;
28
47
 
@@ -60,7 +79,7 @@ type JsonObject = Record<string, unknown>;
60
79
  class AuthWebError extends Error {
61
80
  public readonly underlyingError?: string;
62
81
 
63
- constructor(message: string, underlyingError?: string) {
82
+ constructor(message: AuthErrorCode, underlyingError?: string) {
64
83
  super(message);
65
84
  this.name = "AuthWebError";
66
85
  this.underlyingError = underlyingError;
@@ -86,7 +105,28 @@ const getOptionalNumber = (
86
105
  key: string,
87
106
  ): number | undefined => {
88
107
  const candidate = source[key];
89
- return typeof candidate === "number" ? candidate : undefined;
108
+ return typeof candidate === "number" && Number.isFinite(candidate)
109
+ ? candidate
110
+ : undefined;
111
+ };
112
+
113
+ const parseExpiresInSeconds = (value: unknown): number | undefined => {
114
+ const candidate =
115
+ typeof value === "number"
116
+ ? value
117
+ : typeof value === "string" && value.trim().length > 0
118
+ ? Number(value)
119
+ : undefined;
120
+
121
+ if (
122
+ typeof candidate !== "number" ||
123
+ !Number.isFinite(candidate) ||
124
+ candidate < 0
125
+ ) {
126
+ return undefined;
127
+ }
128
+
129
+ return candidate;
90
130
  };
91
131
 
92
132
  const parseAuthUser = (value: unknown): AuthUser | undefined => {
@@ -124,6 +164,9 @@ const parseScopes = (value: unknown): string[] | undefined => {
124
164
  return value.filter((scope): scope is string => typeof scope === "string");
125
165
  };
126
166
 
167
+ const isAuthErrorCode = (value: string): value is AuthErrorCode =>
168
+ WEB_AUTH_ERROR_CODES.has(value);
169
+
127
170
  const parseAuthWebExtraConfig = (value: unknown): AuthWebExtraConfig => {
128
171
  if (!isJsonObject(value)) {
129
172
  return {};
@@ -438,29 +481,67 @@ class AuthWeb implements Auth {
438
481
  }
439
482
 
440
483
  private notify() {
441
- this._listeners.forEach((l) => {
442
- l(this._currentUser);
443
- });
484
+ for (const listener of [...this._listeners]) {
485
+ listener(this._currentUser);
486
+ }
487
+ }
488
+
489
+ private notifyTokenListeners(tokens: AuthTokens): void {
490
+ for (const listener of [...this._tokenListeners]) {
491
+ listener(tokens);
492
+ }
493
+ }
494
+
495
+ private async runLoginOperation(
496
+ operation: () => Promise<void>,
497
+ ): Promise<void> {
498
+ if (this._loginInFlight) {
499
+ throw new AuthWebError(
500
+ "operation_in_progress",
501
+ "Another login is already in progress",
502
+ );
503
+ }
504
+
505
+ this._loginInFlight = true;
506
+ try {
507
+ await operation();
508
+ } finally {
509
+ this._loginInFlight = false;
510
+ }
444
511
  }
445
512
 
446
513
  async login(provider: AuthProvider, options?: LoginOptions): Promise<void> {
447
514
  const loginHint = options?.loginHint;
448
515
  logger.log(`Starting login with ${provider}`, { scopes: options?.scopes });
449
516
  try {
450
- if (provider === "google") {
451
- const scopes = options?.scopes ?? DEFAULT_SCOPES;
452
- await this.loginGoogle(scopes, loginHint);
453
- } else if (provider === "microsoft") {
454
- const scopes = options?.scopes ?? MS_DEFAULT_SCOPES;
455
- await this.loginMicrosoft(
456
- scopes,
457
- loginHint,
458
- options?.tenant,
459
- options?.prompt,
517
+ await this.runLoginOperation(async () => {
518
+ if (provider === "google") {
519
+ const scopes = options?.scopes ?? DEFAULT_SCOPES;
520
+ await this.loginGoogle(scopes, loginHint);
521
+ return;
522
+ }
523
+
524
+ if (provider === "microsoft") {
525
+ const scopes = options?.scopes ?? MS_DEFAULT_SCOPES;
526
+ await this.loginMicrosoft(
527
+ scopes,
528
+ loginHint,
529
+ options?.tenant,
530
+ options?.prompt,
531
+ );
532
+ return;
533
+ }
534
+
535
+ if (provider === "apple") {
536
+ await this.loginApple();
537
+ return;
538
+ }
539
+
540
+ throw new AuthWebError(
541
+ "unsupported_provider",
542
+ `Unsupported auth provider: ${provider}`,
460
543
  );
461
- } else {
462
- await this.loginApple();
463
- }
544
+ });
464
545
  logger.log(`Login successful with ${provider}`);
465
546
  } catch (e: unknown) {
466
547
  const error = this.mapError(e);
@@ -482,11 +563,14 @@ class AuthWeb implements Auth {
482
563
  logger.log("Requesting additional scopes:", scopes);
483
564
  const newScopes = [...new Set([...this._grantedScopes, ...scopes])];
484
565
  try {
485
- if (provider === "google") {
486
- await this.loginGoogle(newScopes);
487
- } else {
566
+ await this.runLoginOperation(async () => {
567
+ if (provider === "google") {
568
+ await this.loginGoogle(newScopes);
569
+ return;
570
+ }
571
+
488
572
  await this.loginMicrosoft(newScopes);
489
- }
573
+ });
490
574
  } catch (e) {
491
575
  const error = this.mapError(e);
492
576
  logger.error("Requesting scopes failed:", error.message);
@@ -573,7 +657,8 @@ class AuthWeb implements Auth {
573
657
 
574
658
  const json = await this.parseResponseObject(response);
575
659
  if (!response.ok) {
576
- throw new Error(
660
+ throw new AuthWebError(
661
+ "refresh_failed",
577
662
  getOptionalString(json, "error_description") ??
578
663
  getOptionalString(json, "error") ??
579
664
  "Token refresh failed",
@@ -583,16 +668,12 @@ class AuthWeb implements Auth {
583
668
  const idToken = getOptionalString(json, "id_token");
584
669
  const accessToken = getOptionalString(json, "access_token");
585
670
  const newRefreshToken = getOptionalString(json, "refresh_token");
586
- const expiresInSeconds = getOptionalNumber(json, "expires_in");
587
671
 
588
672
  if (newRefreshToken) {
589
673
  this.saveRefreshToken(newRefreshToken);
590
674
  }
591
675
 
592
- const expirationTime =
593
- typeof expiresInSeconds === "number"
594
- ? Date.now() + expiresInSeconds * 1000
595
- : undefined;
676
+ const expirationTime = this.getExpirationTime(json["expires_in"]);
596
677
 
597
678
  const effectiveIdToken = idToken ?? this._currentUser.idToken;
598
679
  const claims = effectiveIdToken
@@ -614,9 +695,7 @@ class AuthWeb implements Auth {
614
695
  refreshToken: newRefreshToken ?? undefined,
615
696
  expirationTime,
616
697
  };
617
- this._tokenListeners.forEach((l) => {
618
- l(tokens);
619
- });
698
+ this.notifyTokenListeners(tokens);
620
699
  return tokens;
621
700
  }
622
701
 
@@ -636,18 +715,22 @@ class AuthWeb implements Auth {
636
715
  refreshToken: this._currentUser.refreshToken,
637
716
  expirationTime: this._currentUser.expirationTime,
638
717
  };
639
- this._tokenListeners.forEach((l) => {
640
- l(tokens);
641
- });
718
+ this.notifyTokenListeners(tokens);
642
719
  return tokens;
643
720
  }
644
721
 
645
722
  private mapError(error: unknown): Error {
723
+ if (error instanceof AuthWebError) {
724
+ return error;
725
+ }
726
+
646
727
  const rawMessage = error instanceof Error ? error.message : String(error);
647
728
  const msg = rawMessage.toLowerCase();
648
- let mappedMsg = rawMessage;
729
+ let mappedMsg: AuthErrorCode = "unknown";
649
730
 
650
- if (msg.includes("cancel") || msg.includes("popup_closed")) {
731
+ if (isAuthErrorCode(rawMessage)) {
732
+ mappedMsg = rawMessage;
733
+ } else if (msg.includes("cancel") || msg.includes("popup_closed")) {
651
734
  mappedMsg = "cancelled";
652
735
  } else if (msg.includes("access_denied")) {
653
736
  mappedMsg = "cancelled";
@@ -655,6 +738,21 @@ class AuthWeb implements Auth {
655
738
  mappedMsg = "timeout";
656
739
  } else if (msg.includes("popup blocked")) {
657
740
  mappedMsg = "popup_blocked";
741
+ } else if (msg.includes("login is already in progress")) {
742
+ mappedMsg = "operation_in_progress";
743
+ } else if (msg.includes("state mismatch")) {
744
+ mappedMsg = "invalid_state";
745
+ } else if (msg.includes("nonce mismatch")) {
746
+ mappedMsg = "invalid_nonce";
747
+ } else if (msg.includes("no id_token") || msg.includes("no_id_token")) {
748
+ mappedMsg = "no_id_token";
749
+ } else if (msg.includes("invalid jwt") || msg.includes("json")) {
750
+ mappedMsg = "parse_error";
751
+ } else if (
752
+ msg.includes("no user logged in") ||
753
+ msg.includes("not signed in")
754
+ ) {
755
+ mappedMsg = "not_signed_in";
658
756
  } else if (
659
757
  msg.includes("network") ||
660
758
  msg.includes("server_error") ||
@@ -677,26 +775,105 @@ class AuthWeb implements Auth {
677
775
  }
678
776
 
679
777
  private async parseResponseObject(response: Response): Promise<JsonObject> {
680
- const parsed: unknown = await response.json();
778
+ let parsed: unknown;
779
+ try {
780
+ parsed = await response.json();
781
+ } catch (error) {
782
+ throw new AuthWebError("parse_error", String(error));
783
+ }
784
+
681
785
  if (!isJsonObject(parsed)) {
682
- throw new Error("Expected JSON object response from auth provider");
786
+ throw new AuthWebError(
787
+ "parse_error",
788
+ "Expected JSON object response from auth provider",
789
+ );
683
790
  }
684
791
  return parsed;
685
792
  }
686
793
 
687
794
  private parseJwtPayload(token: string): JsonObject {
688
- const payload = token.split(".")[1];
689
- if (!payload) {
690
- throw new Error("Invalid JWT payload");
795
+ const parts = token.split(".");
796
+ const payload = parts[1];
797
+ if (
798
+ parts.length < 2 ||
799
+ !payload ||
800
+ payload.length % 4 === 1 ||
801
+ !JWT_BASE64_URL_RE.test(payload)
802
+ ) {
803
+ throw new AuthWebError("parse_error", "Invalid JWT payload");
804
+ }
805
+
806
+ try {
807
+ const normalizedPayload = payload.replace(/-/g, "+").replace(/_/g, "/");
808
+ const padding = "=".repeat((4 - (normalizedPayload.length % 4)) % 4);
809
+ const binary = atob(`${normalizedPayload}${padding}`);
810
+ const decodedText =
811
+ typeof TextDecoder === "function"
812
+ ? new TextDecoder().decode(
813
+ Uint8Array.from(binary, (char) => char.charCodeAt(0)),
814
+ )
815
+ : decodeURIComponent(
816
+ Array.from(
817
+ binary,
818
+ (char) =>
819
+ `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`,
820
+ ).join(""),
821
+ );
822
+ const decoded: unknown = JSON.parse(decodedText);
823
+ if (!isJsonObject(decoded)) {
824
+ throw new Error("Expected JWT payload to be an object");
825
+ }
826
+ return decoded;
827
+ } catch (error) {
828
+ if (error instanceof AuthWebError) {
829
+ throw error;
830
+ }
831
+ throw new AuthWebError("parse_error", String(error));
832
+ }
833
+ }
834
+
835
+ private mapOAuthErrorCode(error: string): AuthErrorCode {
836
+ const normalizedError = error.trim().toLowerCase();
837
+ if (
838
+ normalizedError === "access_denied" ||
839
+ normalizedError === "popup_closed_by_user" ||
840
+ normalizedError === "user_cancelled"
841
+ ) {
842
+ return "cancelled";
843
+ }
844
+
845
+ if (
846
+ normalizedError === "server_error" ||
847
+ normalizedError === "temporarily_unavailable"
848
+ ) {
849
+ return "network_error";
850
+ }
851
+
852
+ if (
853
+ normalizedError === "invalid_client" ||
854
+ normalizedError === "invalid_scope" ||
855
+ normalizedError === "unauthorized_client"
856
+ ) {
857
+ return "configuration_error";
691
858
  }
692
859
 
693
- const normalizedPayload = payload.replace(/-/g, "+").replace(/_/g, "/");
694
- const padding = "=".repeat((4 - (normalizedPayload.length % 4)) % 4);
695
- const decoded: unknown = JSON.parse(atob(`${normalizedPayload}${padding}`));
696
- if (!isJsonObject(decoded)) {
697
- throw new Error("Expected JWT payload to be an object");
860
+ if (
861
+ normalizedError === "invalid_grant" ||
862
+ normalizedError === "invalid_token"
863
+ ) {
864
+ return "token_error";
698
865
  }
699
- return decoded;
866
+
867
+ return "unknown";
868
+ }
869
+
870
+ private getExpirationTime(expiresIn: unknown): number | undefined {
871
+ const expiresInSeconds = parseExpiresInSeconds(expiresIn);
872
+ if (expiresInSeconds === undefined) {
873
+ return undefined;
874
+ }
875
+
876
+ return Date.now() + expiresInSeconds * 1000;
700
877
  }
701
878
 
702
879
  private waitForPopupRedirect(
@@ -750,7 +927,8 @@ class AuthWeb implements Auth {
750
927
  }
751
928
 
752
929
  cleanup(intervalId, timeoutId, true);
753
- void Promise.resolve(onRedirect(url))
930
+ void Promise.resolve()
931
+ .then(() => onRedirect(url))
754
932
  .then(() => {
755
933
  resolve();
756
934
  })
@@ -764,24 +942,6 @@ class AuthWeb implements Auth {
764
942
  private async loginGoogle(
765
943
  scopes: string[],
766
944
  loginHint?: string,
767
- ): Promise<void> {
768
- if (this._loginInFlight) {
769
- throw new AuthWebError(
770
- "cancelled",
771
- "Another login is already in progress",
772
- );
773
- }
774
- this._loginInFlight = true;
775
- try {
776
- await this._loginGoogleInner(scopes, loginHint);
777
- } finally {
778
- this._loginInFlight = false;
779
- }
780
- }
781
-
782
- private async _loginGoogleInner(
783
- scopes: string[],
784
- loginHint?: string,
785
945
  ): Promise<void> {
786
946
  const clientId = this._config.googleWebClientId;
787
947
 
@@ -831,15 +991,27 @@ class AuthWeb implements Auth {
831
991
  const accessToken = params.get("access_token");
832
992
  const expiresIn = params.get("expires_in");
833
993
  const code = params.get("code");
994
+ const error = params.get("error");
995
+ const errorDescription = params.get("error_description");
996
+
997
+ if (error) {
998
+ throw new AuthWebError(
999
+ this.mapOAuthErrorCode(error),
1000
+ errorDescription ?? error,
1001
+ );
1002
+ }
834
1003
 
835
1004
  if (!idToken) {
836
- throw new Error("No id_token in response");
1005
+ throw new AuthWebError("no_id_token", "No id_token in response");
837
1006
  }
838
1007
 
839
1008
  const decoded = this.parseJwtPayload(idToken);
840
1009
  if (decoded["nonce"] !== this._pendingGoogleNonce) {
841
1010
  this._pendingGoogleNonce = undefined;
842
- throw new Error("Nonce mismatch - possible replay attack");
1011
+ throw new AuthWebError(
1012
+ "invalid_nonce",
1013
+ "Nonce mismatch - possible replay attack",
1014
+ );
843
1015
  }
844
1016
  this._pendingGoogleNonce = undefined;
845
1017
 
@@ -852,9 +1024,7 @@ class AuthWeb implements Auth {
852
1024
  accessToken: accessToken ?? undefined,
853
1025
  serverAuthCode: code ?? undefined,
854
1026
  scopes,
855
- expirationTime: expiresIn
856
- ? Date.now() + parseInt(expiresIn, 10) * 1000
857
- : undefined,
1027
+ expirationTime: this.getExpirationTime(expiresIn),
858
1028
  ...this.decodeGoogleJwt(idToken),
859
1029
  };
860
1030
  this.updateUser(user);
@@ -887,26 +1057,6 @@ class AuthWeb implements Auth {
887
1057
  loginHint?: string,
888
1058
  tenant?: string,
889
1059
  prompt?: string,
890
- ): Promise<void> {
891
- if (this._loginInFlight) {
892
- throw new AuthWebError(
893
- "cancelled",
894
- "Another login is already in progress",
895
- );
896
- }
897
- this._loginInFlight = true;
898
- try {
899
- await this._loginMicrosoftInner(scopes, loginHint, tenant, prompt);
900
- } finally {
901
- this._loginInFlight = false;
902
- }
903
- }
904
-
905
- private async _loginMicrosoftInner(
906
- scopes: string[],
907
- loginHint?: string,
908
- tenant?: string,
909
- prompt?: string,
910
1060
  ): Promise<void> {
911
1061
  const clientId = this._config.microsoftClientId;
912
1062
 
@@ -978,15 +1128,24 @@ class AuthWeb implements Auth {
978
1128
  const errorDescription = urlObj.searchParams.get("error_description");
979
1129
 
980
1130
  if (error) {
981
- throw new Error(errorDescription ?? error);
1131
+ throw new AuthWebError(
1132
+ this.mapOAuthErrorCode(error),
1133
+ errorDescription ?? error,
1134
+ );
982
1135
  }
983
1136
 
984
1137
  if (returnedState !== state) {
985
- throw new Error("State mismatch - possible CSRF attack");
1138
+ throw new AuthWebError(
1139
+ "invalid_state",
1140
+ "State mismatch - possible CSRF attack",
1141
+ );
986
1142
  }
987
1143
 
988
1144
  if (!code) {
989
- throw new Error("No authorization code in response");
1145
+ throw new AuthWebError(
1146
+ "token_error",
1147
+ "No authorization code in response",
1148
+ );
990
1149
  }
991
1150
 
992
1151
  await this.exchangeMicrosoftCodeForTokens(
@@ -1061,7 +1220,8 @@ class AuthWeb implements Auth {
1061
1220
  const json = await this.parseResponseObject(response);
1062
1221
 
1063
1222
  if (!response.ok) {
1064
- throw new Error(
1223
+ throw new AuthWebError(
1224
+ "token_error",
1065
1225
  getOptionalString(json, "error_description") ??
1066
1226
  getOptionalString(json, "error") ??
1067
1227
  "Token exchange failed",
@@ -1070,18 +1230,20 @@ class AuthWeb implements Auth {
1070
1230
 
1071
1231
  const idToken = getOptionalString(json, "id_token");
1072
1232
  if (!idToken) {
1073
- throw new Error("No id_token in token response");
1233
+ throw new AuthWebError("no_id_token", "No id_token in token response");
1074
1234
  }
1075
1235
 
1076
1236
  const claims = this.decodeMicrosoftJwt(idToken);
1077
1237
  const payload = this.parseJwtPayload(idToken);
1078
1238
  if (getOptionalString(payload, "nonce") !== expectedNonce) {
1079
- throw new Error("Nonce mismatch - token may be replayed");
1239
+ throw new AuthWebError(
1240
+ "invalid_nonce",
1241
+ "Nonce mismatch - token may be replayed",
1242
+ );
1080
1243
  }
1081
1244
 
1082
1245
  const accessToken = getOptionalString(json, "access_token");
1083
1246
  const refreshToken = getOptionalString(json, "refresh_token");
1084
- const expiresInSeconds = getOptionalNumber(json, "expires_in");
1085
1247
 
1086
1248
  if (refreshToken) {
1087
1249
  this.saveRefreshToken(refreshToken);
@@ -1096,10 +1258,7 @@ class AuthWeb implements Auth {
1096
1258
  accessToken: accessToken ?? undefined,
1097
1259
  refreshToken: refreshToken ?? undefined,
1098
1260
  scopes,
1099
- expirationTime:
1100
- typeof expiresInSeconds === "number"
1101
- ? Date.now() + expiresInSeconds * 1000
1102
- : undefined,
1261
+ expirationTime: this.getExpirationTime(json["expires_in"]),
1103
1262
  ...claims,
1104
1263
  };
1105
1264
  this.updateUser(user);