@oxyhq/core 3.10.0 → 3.11.0

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 (105) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/AuthManager.js +9 -2
  3. package/dist/cjs/HttpService.js +27 -9
  4. package/dist/cjs/OxyServices.base.js +3 -2
  5. package/dist/cjs/crypto/canonicalJson.js +107 -0
  6. package/dist/cjs/crypto/keyManager.js +67 -8
  7. package/dist/cjs/crypto/signatureService.js +103 -0
  8. package/dist/cjs/i18n/locales/en-US.json +9 -0
  9. package/dist/cjs/i18n/locales/es-ES.json +9 -0
  10. package/dist/cjs/i18n/locales/locales/en-US.json +9 -0
  11. package/dist/cjs/i18n/locales/locales/es-ES.json +9 -0
  12. package/dist/cjs/index.js +15 -5
  13. package/dist/cjs/mixins/OxyServices.assets.js +45 -7
  14. package/dist/cjs/mixins/OxyServices.auth.js +190 -1
  15. package/dist/cjs/mixins/OxyServices.identity.js +291 -0
  16. package/dist/cjs/mixins/OxyServices.sso.js +28 -1
  17. package/dist/cjs/mixins/OxyServices.user.js +1 -0
  18. package/dist/cjs/mixins/OxyServices.utility.js +52 -23
  19. package/dist/cjs/mixins/index.js +3 -0
  20. package/dist/cjs/server/cors.js +20 -21
  21. package/dist/cjs/server/rateLimit.js +32 -8
  22. package/dist/cjs/utils/fapiAutoDetect.js +12 -42
  23. package/dist/cjs/utils/ssoReturn.js +1 -1
  24. package/dist/esm/.tsbuildinfo +1 -1
  25. package/dist/esm/AuthManager.js +9 -2
  26. package/dist/esm/HttpService.js +27 -9
  27. package/dist/esm/OxyServices.base.js +3 -2
  28. package/dist/esm/crypto/canonicalJson.js +104 -0
  29. package/dist/esm/crypto/keyManager.js +67 -8
  30. package/dist/esm/crypto/signatureService.js +102 -0
  31. package/dist/esm/i18n/locales/en-US.json +9 -0
  32. package/dist/esm/i18n/locales/es-ES.json +9 -0
  33. package/dist/esm/i18n/locales/locales/en-US.json +9 -0
  34. package/dist/esm/i18n/locales/locales/es-ES.json +9 -0
  35. package/dist/esm/index.js +10 -2
  36. package/dist/esm/mixins/OxyServices.assets.js +45 -7
  37. package/dist/esm/mixins/OxyServices.auth.js +190 -1
  38. package/dist/esm/mixins/OxyServices.identity.js +287 -0
  39. package/dist/esm/mixins/OxyServices.sso.js +28 -1
  40. package/dist/esm/mixins/OxyServices.user.js +1 -0
  41. package/dist/esm/mixins/OxyServices.utility.js +52 -23
  42. package/dist/esm/mixins/index.js +3 -0
  43. package/dist/esm/server/cors.js +20 -21
  44. package/dist/esm/server/rateLimit.js +32 -8
  45. package/dist/esm/utils/fapiAutoDetect.js +12 -41
  46. package/dist/esm/utils/ssoReturn.js +1 -1
  47. package/dist/types/.tsbuildinfo +1 -1
  48. package/dist/types/HttpService.d.ts +3 -0
  49. package/dist/types/OxyServices.d.ts +2 -2
  50. package/dist/types/crypto/canonicalJson.d.ts +44 -0
  51. package/dist/types/crypto/keyManager.d.ts +7 -0
  52. package/dist/types/crypto/signatureService.d.ts +61 -0
  53. package/dist/types/index.d.ts +7 -3
  54. package/dist/types/mixins/OxyServices.assets.d.ts +6 -1
  55. package/dist/types/mixins/OxyServices.auth.d.ts +136 -0
  56. package/dist/types/mixins/OxyServices.identity.d.ts +249 -0
  57. package/dist/types/mixins/OxyServices.sso.d.ts +4 -1
  58. package/dist/types/mixins/OxyServices.utility.d.ts +3 -3
  59. package/dist/types/mixins/index.d.ts +2 -1
  60. package/dist/types/models/interfaces.d.ts +3 -0
  61. package/dist/types/server/cors.d.ts +5 -5
  62. package/dist/types/utils/fapiAutoDetect.d.ts +6 -23
  63. package/dist/types/utils/ssoReturn.d.ts +1 -1
  64. package/package.json +3 -2
  65. package/src/AuthManager.ts +8 -2
  66. package/src/HttpService.ts +36 -8
  67. package/src/OxyServices.base.ts +3 -2
  68. package/src/OxyServices.ts +1 -1
  69. package/src/__tests__/authManager.security.test.ts +31 -0
  70. package/src/__tests__/authSocket.test.ts +96 -0
  71. package/src/__tests__/httpServiceCsrf.test.ts +75 -0
  72. package/src/crypto/__tests__/canonicalJson.test.ts +116 -0
  73. package/src/crypto/__tests__/keyManager.atomicity.test.ts +41 -2
  74. package/src/crypto/__tests__/signChallengeShared.test.ts +64 -0
  75. package/src/crypto/__tests__/signedRecord.test.ts +125 -0
  76. package/src/crypto/canonicalJson.ts +120 -0
  77. package/src/crypto/keyManager.ts +62 -12
  78. package/src/crypto/signatureService.ts +126 -0
  79. package/src/i18n/locales/en-US.json +9 -0
  80. package/src/i18n/locales/es-ES.json +9 -0
  81. package/src/index.ts +28 -3
  82. package/src/mixins/OxyServices.assets.ts +56 -7
  83. package/src/mixins/OxyServices.auth.ts +309 -1
  84. package/src/mixins/OxyServices.identity.ts +445 -0
  85. package/src/mixins/OxyServices.sso.ts +30 -1
  86. package/src/mixins/OxyServices.user.ts +1 -0
  87. package/src/mixins/OxyServices.utility.ts +57 -23
  88. package/src/mixins/__tests__/OxyServices.identity.test.ts +364 -0
  89. package/src/mixins/__tests__/assetCredentials.test.ts +47 -0
  90. package/src/mixins/__tests__/assetUpload.test.ts +191 -0
  91. package/src/mixins/__tests__/commonsSignIn.test.ts +277 -0
  92. package/src/mixins/__tests__/getFileDownloadUrl.test.ts +13 -0
  93. package/src/mixins/__tests__/serviceAuth.test.ts +49 -2
  94. package/src/mixins/__tests__/sso.test.ts +31 -0
  95. package/src/mixins/index.ts +4 -0
  96. package/src/models/interfaces.ts +3 -0
  97. package/src/server/__tests__/cors.test.ts +5 -1
  98. package/src/server/__tests__/rateLimit.test.ts +116 -0
  99. package/src/server/cors.ts +25 -20
  100. package/src/server/rateLimit.ts +39 -8
  101. package/src/utils/__tests__/consumeSsoReturn.test.ts +1 -1
  102. package/src/utils/__tests__/fapiAutoDetect.test.ts +40 -11
  103. package/src/utils/__tests__/ssoReturn.test.ts +1 -1
  104. package/src/utils/fapiAutoDetect.ts +12 -39
  105. package/src/utils/ssoReturn.ts +2 -2
@@ -691,33 +691,45 @@ function OxyServicesUtilityMixin(Base) {
691
691
  }
692
692
  return next(new Error('Invalid token'));
693
693
  }
694
- const userId = decoded.userId || decoded.id;
695
- if (!userId) {
694
+ const claimedUserId = decoded.userId || decoded.id;
695
+ if (!claimedUserId) {
696
696
  return next(new Error('Invalid token payload'));
697
697
  }
698
698
  // Check expiration — reject tokens at exact expiry second (use <=)
699
699
  if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
700
700
  return next(new Error('Token expired'));
701
701
  }
702
- // Validate session if available
703
- if (decoded.sessionId) {
704
- try {
705
- const result = await oxyInstance.validateSession(decoded.sessionId, {
706
- useHeaderValidation: true,
707
- });
708
- if (!result || !result.valid) {
709
- return next(new Error('Session invalid'));
710
- }
702
+ // A server-validated session is mandatory. A bare decoded JWT proves
703
+ // nothing — the signature is not verified here, so without a session
704
+ // round-trip a forged token could claim any user id.
705
+ if (!decoded.sessionId) {
706
+ return next(new Error('Session required'));
707
+ }
708
+ let userId = claimedUserId;
709
+ try {
710
+ const result = await oxyInstance.validateSession(decoded.sessionId, {
711
+ useHeaderValidation: true,
712
+ });
713
+ if (!result || !result.valid || !result.user) {
714
+ return next(new Error('Session invalid'));
711
715
  }
712
- catch (validateErr) {
713
- if (debug) {
714
- loggerUtils_1.logger.debug('[oxy.authSocket] Session validation failed', {
715
- component: 'auth',
716
- method: 'authSocket',
717
- }, validateErr);
718
- }
719
- return next(new Error('Session validation failed'));
716
+ // The session is the source of truth. The client-claimed user id
717
+ // must match the server-validated identity, otherwise a valid
718
+ // session could be paired with a forged user id.
719
+ const validatedUserId = getUserIdentityId(result.user);
720
+ if (!validatedUserId || validatedUserId !== claimedUserId) {
721
+ return next(new Error('Session user mismatch'));
720
722
  }
723
+ userId = validatedUserId;
724
+ }
725
+ catch (validateErr) {
726
+ if (debug) {
727
+ loggerUtils_1.logger.debug('[oxy.authSocket] Session validation failed', {
728
+ component: 'auth',
729
+ method: 'authSocket',
730
+ }, validateErr);
731
+ }
732
+ return next(new Error('Session validation failed'));
721
733
  }
722
734
  // Attach user data to socket. We expose BOTH `socket.data.userId`
723
735
  // (the official Socket.IO data slot) and `socket.user` because
@@ -782,9 +794,9 @@ function OxyServicesUtilityMixin(Base) {
782
794
  * Express.js middleware that enforces a specific service-token scope.
783
795
  *
784
796
  * Mount AFTER `auth()` / `serviceAuth()` — relies on `req.serviceApp` and
785
- * (when delegation is in effect) `req.serviceActingAs.scopes`. The scope
786
- * is granted if EITHER list contains it, mirroring the OAuth2 model where
787
- * the app's app-level scopes and the per-user delegated scopes both count.
797
+ * (when delegation is in effect) `req.serviceActingAs.scopes`. App-only
798
+ * service requests require the app scope. Delegated user requests require
799
+ * BOTH the app scope and the per-user delegation scope.
788
800
  *
789
801
  * Requests authenticated as a regular user (no service token) are rejected
790
802
  * with 403 — scope-protected endpoints are service-to-service by design.
@@ -814,7 +826,12 @@ function OxyServicesUtilityMixin(Base) {
814
826
  });
815
827
  return;
816
828
  }
817
- if (appScopes.includes(scope) || delegatedScopes.includes(scope)) {
829
+ const appHasScope = appScopes.includes(scope);
830
+ const delegationHasScope = delegatedScopes.includes(scope);
831
+ const hasRequiredScope = req.serviceActingAs
832
+ ? appHasScope && delegationHasScope
833
+ : appHasScope;
834
+ if (hasRequiredScope) {
818
835
  next();
819
836
  return;
820
837
  }
@@ -872,6 +889,18 @@ async function verifyServiceTokenSignature(token, secret) {
872
889
  * access token signed by the same shared secret could be replayed as a
873
890
  * service token because no claim binding existed.
874
891
  */
892
+ /**
893
+ * Resolve the canonical user id from a validated session's user object.
894
+ *
895
+ * The API serializer emits `id`, but some upstream shapes carry the raw Mongo
896
+ * `_id` instead. We accept either, but only a non-empty string — anything else
897
+ * means the validated identity is unusable and the caller must reject.
898
+ */
899
+ function getUserIdentityId(user) {
900
+ const candidate = user.id
901
+ ?? user._id;
902
+ return typeof candidate === 'string' && candidate.length > 0 ? candidate : null;
903
+ }
875
904
  function verifyServiceTokenClaims(decoded, expected) {
876
905
  if (decoded.type !== 'service') {
877
906
  throw new ServiceTokenClaimError(`Service token has unexpected type '${String(decoded.type)}'`);
@@ -15,6 +15,7 @@ const OxyServices_silent_1 = require("./OxyServices.silent");
15
15
  const OxyServices_redirect_1 = require("./OxyServices.redirect");
16
16
  const OxyServices_sso_1 = require("./OxyServices.sso");
17
17
  const OxyServices_user_1 = require("./OxyServices.user");
18
+ const OxyServices_identity_1 = require("./OxyServices.identity");
18
19
  const OxyServices_privacy_1 = require("./OxyServices.privacy");
19
20
  const OxyServices_language_1 = require("./OxyServices.language");
20
21
  const OxyServices_payment_1 = require("./OxyServices.payment");
@@ -58,6 +59,8 @@ const MIXIN_PIPELINE = [
58
59
  OxyServices_sso_1.OxyServicesSsoMixin,
59
60
  // User management (requires auth)
60
61
  OxyServices_user_1.OxyServicesUserMixin,
62
+ // Self-sovereign identity (DID, signed records, auth-method ↔ VM mapping)
63
+ OxyServices_identity_1.OxyServicesIdentityMixin,
61
64
  OxyServices_privacy_1.OxyServicesPrivacyMixin,
62
65
  // Feature mixins
63
66
  OxyServices_language_1.OxyServicesLanguageMixin,
@@ -12,10 +12,9 @@
12
12
  *
13
13
  * `createOxyCors` returns a self-contained Express middleware (no `cors`
14
14
  * package dependency) that:
15
- * - allows the Oxy apex origin family (anything under `*.${CENTRAL_IDP_APEX}`,
16
- * i.e. `oxy.so` covering `auth.oxy.so`, `api.oxy.so`, `accounts.oxy.so`,
17
- * `console.oxy.so`, `inbox.oxy.so`, the marketing site, …) reusing the
18
- * central-origin constants already in core, NOT a fresh hardcoded list,
15
+ * - allows the Oxy apex origin family over HTTPS only: the apex plus
16
+ * one-label subdomains such as `auth.oxy.so`, `api.oxy.so`,
17
+ * `accounts.oxy.so`, `console.oxy.so`, and `inbox.oxy.so`,
19
18
  * - allows the caller's explicit `appOrigins`,
20
19
  * - DENIES everything else (no reflection, never a wildcard with credentials),
21
20
  * - echoes back the EXACT matched origin (so credentialed requests work) and
@@ -27,7 +26,6 @@
27
26
  Object.defineProperty(exports, "__esModule", { value: true });
28
27
  exports.createOxyCors = createOxyCors;
29
28
  const authWebUrl_1 = require("../utils/authWebUrl");
30
- const fapiAutoDetect_1 = require("../utils/fapiAutoDetect");
31
29
  /** Default HTTP methods allowed across origins. */
32
30
  const DEFAULT_ALLOWED_METHODS = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'];
33
31
  /** Default request headers a browser may send on a credentialed cross-origin call. */
@@ -41,29 +39,30 @@ const DEFAULT_ALLOWED_HEADERS = [
41
39
  ];
42
40
  /** How long (seconds) a browser may cache a successful preflight. */
43
41
  const DEFAULT_MAX_AGE_SECONDS = 86400;
42
+ const OXY_ONE_LABEL_SUBDOMAIN_PATTERN = new RegExp(`^[a-z0-9-]+\\.${authWebUrl_1.CENTRAL_IDP_APEX.replace('.', '\\.')}$`);
44
43
  /**
45
- * Whether `candidate` belongs to the Oxy apex origin family — i.e. its
46
- * registrable apex equals {@link CENTRAL_IDP_APEX} (`oxy.so`). This matches the
47
- * apex itself (`https://oxy.so`) and any subdomain (`https://auth.oxy.so`,
48
- * `https://api.oxy.so`, …) over http or https, ports allowed. Returns false on
49
- * any parse failure (fail closed).
44
+ * Whether `candidate` belongs to the built-in Oxy apex origin family. This
45
+ * intentionally mirrors the API allowlist shape: HTTPS only, no custom port,
46
+ * the apex itself (`https://oxy.so`), or exactly one lowercase subdomain label
47
+ * (`https://auth.oxy.so`, `https://api.oxy.so`, …).
48
+ *
49
+ * Arbitrary/multi-level subdomains and `http://*.oxy.so` are not implicitly
50
+ * trusted for credentialed CORS. If a service needs a non-standard development
51
+ * or tenant origin, it must opt in explicitly via `appOrigins`.
50
52
  */
51
53
  function isOxyFamilyOrigin(candidate) {
52
- let hostname;
53
- let protocol;
54
54
  try {
55
55
  const url = new URL(candidate);
56
- hostname = url.hostname.toLowerCase();
57
- protocol = url.protocol;
56
+ if (url.protocol !== 'https:' || url.port !== '')
57
+ return false;
58
+ const hostname = url.hostname;
59
+ if (hostname === authWebUrl_1.CENTRAL_IDP_APEX)
60
+ return true;
61
+ return OXY_ONE_LABEL_SUBDOMAIN_PATTERN.test(hostname);
58
62
  }
59
63
  catch {
60
64
  return false;
61
65
  }
62
- if (protocol !== 'https:' && protocol !== 'http:')
63
- return false;
64
- if (hostname === authWebUrl_1.CENTRAL_IDP_APEX)
65
- return true;
66
- return (0, fapiAutoDetect_1.registrableApex)(hostname) === authWebUrl_1.CENTRAL_IDP_APEX;
67
66
  }
68
67
  /** Normalize a raw origin string to its canonical `scheme://host[:port]` form. */
69
68
  function normalizeOrigin(raw) {
@@ -75,8 +74,8 @@ function normalizeOrigin(raw) {
75
74
  }
76
75
  }
77
76
  /**
78
- * Build the origin-matching predicate: true iff `origin` is in the Oxy apex
79
- * family OR exactly matches one of the configured app origins.
77
+ * Build the origin-matching predicate: true iff `origin` is in the built-in
78
+ * HTTPS Oxy apex family OR exactly matches one of the configured app origins.
80
79
  */
81
80
  function buildOriginAllowed(appOrigins) {
82
81
  const explicit = new Set();
@@ -25,12 +25,36 @@ function isBuiltInExempt(req) {
25
25
  function ipKeyGenerator(ip) {
26
26
  return ip.replace(/:/g, '_');
27
27
  }
28
- /** Resolve the rate-limit key: per authenticated user, else per (IPv6-safe) IP. */
29
- function resolveKey(req) {
28
+ /**
29
+ * Resolve the trusted authenticated rate-limit key.
30
+ *
31
+ * `oxy.auth({ optional: true })` preserves legacy non-session user tokens by
32
+ * decoding their JWT claims locally. Those claims are not cryptographically
33
+ * verified and therefore MUST NOT influence abuse-control buckets. Only use
34
+ * identities that came from a server-validated session or a verified service
35
+ * token/delegation.
36
+ */
37
+ function resolveTrustedAuthenticatedKey(req) {
30
38
  const userId = req.userId ?? req.user?.id ?? req.user?._id;
31
- if (userId) {
39
+ if (userId && req.sessionId) {
32
40
  return `user:${userId}`;
33
41
  }
42
+ const delegatedUserId = req.serviceActingAs?.userId;
43
+ if (delegatedUserId && req.serviceApp?.appId) {
44
+ return `user:${delegatedUserId}`;
45
+ }
46
+ const serviceAppId = req.serviceApp?.appId;
47
+ if (serviceAppId) {
48
+ return `service:${serviceAppId}`;
49
+ }
50
+ return null;
51
+ }
52
+ /** Resolve the rate-limit key: per trusted authenticated identity, else per (IPv6-safe) IP. */
53
+ function resolveKey(req) {
54
+ const authenticatedKey = resolveTrustedAuthenticatedKey(req);
55
+ if (authenticatedKey) {
56
+ return authenticatedKey;
57
+ }
34
58
  const ip = req.ip || req.socket.remoteAddress || 'unknown';
35
59
  return ipKeyGenerator(ip);
36
60
  }
@@ -47,9 +71,9 @@ function createOxyRateLimit(oxy, options = {}) {
47
71
  windowMs,
48
72
  ...(store ? { store } : {}),
49
73
  max: (req) => {
50
- const authed = req;
51
- const userId = authed.userId ?? authed.user?.id ?? authed.user?._id;
52
- return userId ? authenticatedMax : anonymousMax;
74
+ return resolveTrustedAuthenticatedKey(req)
75
+ ? authenticatedMax
76
+ : anonymousMax;
53
77
  },
54
78
  keyGenerator: (req) => resolveKey(req),
55
79
  message,
@@ -67,8 +91,8 @@ function createOxyRateLimit(oxy, options = {}) {
67
91
  resolveSession(req, res, (err) => {
68
92
  if (err) {
69
93
  // Optional auth never rejects; a token error just means "anonymous".
70
- // Swallow the error and continue to limit as anonymous.
71
- next();
94
+ // Swallow the error and continue through the anonymous limiter.
95
+ limiter(req, res, next);
72
96
  return;
73
97
  }
74
98
  limiter(req, res, next);
@@ -21,10 +21,9 @@
21
21
  * - SSR / non-browser (no `window`).
22
22
  * - `localhost`, `127.0.0.1`, IPv4/IPv6 literals.
23
23
  * - Hostnames with fewer than two labels.
24
- * - Hostnames whose trailing two labels form a known multi-part public
25
- * suffix (e.g. `co.uk`), where the naive `labels.slice(-2)` apex would be
26
- * an attacker-registrable suffix like `auth.co.uk` rather than the real
27
- * registrable domain.
24
+ * - Hostnames where a registrable domain cannot be determined from the
25
+ * Public Suffix List, including private hosted suffixes such as
26
+ * `github.io`, `pages.dev`, and `netlify.app`.
28
27
  *
29
28
  * When the page is already loaded ON the IdP itself (`auth.<anything>`),
30
29
  * the helper returns the current origin so the SDK keeps everything
@@ -36,36 +35,12 @@
36
35
  * is required for end-to-end FedCM correctness — no per-RP config.
37
36
  */
38
37
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.MULTIPART_TLDS = void 0;
40
38
  exports.registrableApex = registrableApex;
41
39
  exports.autoDetectAuthWebUrl = autoDetectAuthWebUrl;
40
+ const tldts_1 = require("tldts");
42
41
  /**
43
- * Known multi-part public suffixes where the registrable domain is the LAST
44
- * THREE labels, not two. Deriving an apex from `labels.slice(-2)` against any
45
- * of these would yield an attacker-registrable suffix (e.g. `auth.co.uk`),
46
- * so we bail out instead.
47
- *
48
- * This is intentionally a small, explicit allow-list rather than the full
49
- * Public Suffix List — it covers the suffixes the Oxy ecosystem's RPs use.
50
- * Any multi-part-TLD RP MUST extend this set (or wire in a proper PSL check)
51
- * before relying on this helper, otherwise auto-detection silently bails to
52
- * `undefined` and the consumer must pass `authWebUrl` explicitly.
53
- */
54
- exports.MULTIPART_TLDS = new Set([
55
- 'co.uk',
56
- 'com.au',
57
- 'co.jp',
58
- 'co.nz',
59
- 'com.br',
60
- 'co.za',
61
- 'com.mx',
62
- 'co.in',
63
- 'co.kr',
64
- 'com.sg',
65
- ]);
66
- /**
67
- * Compute the bare registrable apex (eTLD+1) of a hostname, guarding against
68
- * multi-part public suffixes.
42
+ * Compute the bare registrable apex (eTLD+1) of a hostname using the Public
43
+ * Suffix List, including private hosted suffixes.
69
44
  *
70
45
  * This is the pure host-handling kernel shared by {@link autoDetectAuthWebUrl}
71
46
  * and the IdP worker — it performs NO protocol handling, NO `auth.` prefixing,
@@ -77,10 +52,7 @@ exports.MULTIPART_TLDS = new Set([
77
52
  * - IPv4 literals (`192.168.1.10`);
78
53
  * - IPv6 literals or any host carrying a port (`[::1]`, anything with `:`);
79
54
  * - single-label hosts (`intranet`, `localhost`);
80
- * - hosts whose trailing two labels form a known multi-part public suffix
81
- * (e.g. `foo.co.uk`), where `labels.slice(-2)` would yield an
82
- * attacker-registrable suffix (`co.uk`) rather than a real registrable
83
- * domain. Such hosts MUST configure `authWebUrl` explicitly.
55
+ * - public suffixes without a registrable label (e.g. `co.uk`, `github.io`).
84
56
  *
85
57
  * @param hostname - A bare hostname (no scheme), e.g. `www.mention.earth`.
86
58
  * @returns The eTLD+1 (`mention.earth`), or `null` when undefinable.
@@ -95,13 +67,11 @@ function registrableApex(hostname) {
95
67
  // yields a registrable apex.
96
68
  if (host.startsWith('[') || host.includes(':'))
97
69
  return null;
98
- const labels = host.split('.');
99
- if (labels.length < 2)
100
- return null;
101
- const lastTwo = labels.slice(-2).join('.');
102
- if (exports.MULTIPART_TLDS.has(lastTwo))
103
- return null;
104
- return lastTwo;
70
+ // The Public Suffix List (private suffixes included) is the source of truth
71
+ // for what an attacker can register. A multi-part public suffix like `co.uk`
72
+ // or a hosted suffix like `github.io` returns no registrable domain, so
73
+ // deriving `auth.<apex>` against it is impossible — `getDomain` returns null.
74
+ return (0, tldts_1.getDomain)(host, { allowPrivateDomains: true });
105
75
  }
106
76
  function autoDetectAuthWebUrl(location = typeof window !== 'undefined' ? window.location : undefined) {
107
77
  if (!location)
@@ -241,7 +241,7 @@ async function consumeSsoReturn(oxy, deps = {}) {
241
241
  }
242
242
  let session;
243
243
  try {
244
- session = await oxy.exchangeSsoCode(ret.code);
244
+ session = await oxy.exchangeSsoCode(ret.code, ret.state);
245
245
  }
246
246
  catch (error) {
247
247
  onExchangeError?.(error);