@oxyhq/core 11.0.0 → 12.0.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 (34) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/index.js +13 -6
  3. package/dist/cjs/mixins/OxyServices.auth.js +65 -74
  4. package/dist/cjs/mixins/OxyServices.identity.js +16 -12
  5. package/dist/cjs/session/accountDialogController.js +2 -55
  6. package/dist/cjs/utils/officialOrigins.js +6 -0
  7. package/dist/cjs/utils/webauthnOrigin.js +51 -0
  8. package/dist/esm/.tsbuildinfo +1 -1
  9. package/dist/esm/index.js +10 -5
  10. package/dist/esm/mixins/OxyServices.auth.js +65 -74
  11. package/dist/esm/mixins/OxyServices.identity.js +16 -12
  12. package/dist/esm/session/accountDialogController.js +2 -55
  13. package/dist/esm/utils/officialOrigins.js +6 -1
  14. package/dist/esm/utils/webauthnOrigin.js +48 -0
  15. package/dist/types/.tsbuildinfo +1 -1
  16. package/dist/types/index.d.ts +3 -2
  17. package/dist/types/mixins/OxyServices.auth.d.ts +50 -36
  18. package/dist/types/mixins/OxyServices.identity.d.ts +13 -9
  19. package/dist/types/session/accountDialogController.d.ts +4 -34
  20. package/dist/types/utils/officialOrigins.d.ts +6 -0
  21. package/dist/types/utils/webauthnOrigin.d.ts +32 -0
  22. package/package.json +2 -2
  23. package/src/index.ts +11 -4
  24. package/src/mixins/OxyServices.auth.ts +95 -100
  25. package/src/mixins/OxyServices.identity.ts +19 -15
  26. package/src/mixins/__tests__/OxyServices.identity.test.ts +26 -14
  27. package/src/mixins/__tests__/webauthnAuth.test.ts +206 -0
  28. package/src/session/__tests__/accountDialogController.test.ts +0 -66
  29. package/src/session/accountDialogController.ts +4 -78
  30. package/src/utils/__tests__/officialOrigins.test.ts +14 -0
  31. package/src/utils/__tests__/webauthnOrigin.test.ts +83 -0
  32. package/src/utils/officialOrigins.ts +6 -1
  33. package/src/utils/webauthnOrigin.ts +52 -0
  34. package/src/mixins/__tests__/passwordSignIn.test.ts +0 -115
package/dist/cjs/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * ```ts
12
12
  * import { OxyServices, oxyClient } from '@oxyhq/core';
13
13
  *
14
- * const user = await oxyClient.signIn(publicKey);
14
+ * const user = await oxyClient.getCurrentUser();
15
15
  * ```
16
16
  *
17
17
  * Every export below is NOMINAL — no `export *`, no barrels, no compat shims.
@@ -20,8 +20,8 @@
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.getPrimaryLanguage = exports.getUserLanguages = exports.isRTLLocale = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.isSupportedLocale = exports.normalizeLocale = exports.getBaseLanguage = exports.FALLBACK_LOCALE = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.DeviceManager = exports.RecoveryPhraseService = exports.SignatureService = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.verifyPublicCardAttestation = exports.parseAttestPayload = exports.parseIdPayload = exports.buildUserDid = exports.ORGANIZATION_CATEGORIES = exports.normalizeProfileLinks = exports.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.OxyAppDataIdentifierError = exports.ServiceCredentialMismatchError = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyServices = void 0;
22
22
  exports.isValidUsername = exports.isValidEmail = exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.translate = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isWebBrowser = exports.isAndroid = exports.isIOS = exports.isNative = void 0;
23
- exports.buildIdpHubOrigin = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.normalizeOAuthRedirectUri = exports.OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY = exports.OXY_SILENT_OAUTH_ATTEMPTED_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.runColdBoot = exports.CENTRAL_IDP_APEX = exports.registrableApex = exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = exports.normalizeMultilineText = exports.normalizeInlineText = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.isValidDisplayName = exports.isValidPassword = void 0;
24
- exports.packageInfo = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshDeviceSecretArm = exports.refreshPersistedSession = exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = exports.AccountDialogController = exports.switchableAccountIds = exports.projectSwitchableAccounts = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.redeemHubTicketOnHub = exports.syncHubAfterSignIn = exports.parseHubSyncReturnUrl = exports.normalizeOfficialReturnOrigin = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isIdpHubOrigin = exports.buildHubSyncUrl = void 0;
23
+ exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.normalizeOAuthRedirectUri = exports.OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY = exports.OXY_SILENT_OAUTH_ATTEMPTED_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.runColdBoot = exports.isOxyRpOrigin = exports.CENTRAL_IDP_APEX = exports.registrableApex = exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = exports.normalizeMultilineText = exports.normalizeInlineText = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.isValidDisplayName = exports.isValidPassword = void 0;
24
+ exports.packageInfo = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshDeviceSecretArm = exports.refreshPersistedSession = exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = exports.AccountDialogController = exports.switchableAccountIds = exports.projectSwitchableAccounts = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.redeemHubTicketOnHub = exports.syncHubAfterSignIn = exports.parseHubSyncReturnUrl = exports.normalizeOfficialReturnOrigin = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.isIdpHubOrigin = exports.buildHubSyncUrl = exports.buildIdpHubOrigin = void 0;
25
25
  // Ensure crypto polyfills are loaded before anything else
26
26
  require("./crypto/polyfill");
27
27
  // ---------------------------------------------------------------------------
@@ -270,6 +270,11 @@ var registrableApex_1 = require("./utils/registrableApex");
270
270
  Object.defineProperty(exports, "registrableApex", { enumerable: true, get: function () { return registrableApex_1.registrableApex; } });
271
271
  var authWebUrl_1 = require("./utils/authWebUrl");
272
272
  Object.defineProperty(exports, "CENTRAL_IDP_APEX", { enumerable: true, get: function () { return authWebUrl_1.CENTRAL_IDP_APEX; } });
273
+ // WebAuthn relying-party origin guard (client side). Mirrors the server's
274
+ // `isOxyApexOrigin` so consumers can decide whether to offer passkey UI on the
275
+ // current page (first-party Oxy origin / loopback only).
276
+ var webauthnOrigin_1 = require("./utils/webauthnOrigin");
277
+ Object.defineProperty(exports, "isOxyRpOrigin", { enumerable: true, get: function () { return webauthnOrigin_1.isOxyRpOrigin; } });
273
278
  var coldBoot_1 = require("./utils/coldBoot");
274
279
  Object.defineProperty(exports, "runColdBoot", { enumerable: true, get: function () { return coldBoot_1.runColdBoot; } });
275
280
  // ---------------------------------------------------------------------------
@@ -295,6 +300,7 @@ var officialOrigins_1 = require("./utils/officialOrigins");
295
300
  Object.defineProperty(exports, "buildIdpHubOrigin", { enumerable: true, get: function () { return officialOrigins_1.buildIdpHubOrigin; } });
296
301
  Object.defineProperty(exports, "buildHubSyncUrl", { enumerable: true, get: function () { return officialOrigins_1.buildHubSyncUrl; } });
297
302
  Object.defineProperty(exports, "isIdpHubOrigin", { enumerable: true, get: function () { return officialOrigins_1.isIdpHubOrigin; } });
303
+ Object.defineProperty(exports, "isLoopbackOrigin", { enumerable: true, get: function () { return officialOrigins_1.isLoopbackOrigin; } });
298
304
  Object.defineProperty(exports, "isOfficialWebOrigin", { enumerable: true, get: function () { return officialOrigins_1.isOfficialWebOrigin; } });
299
305
  Object.defineProperty(exports, "isAllowedDeviceJoinOrigin", { enumerable: true, get: function () { return officialOrigins_1.isAllowedDeviceJoinOrigin; } });
300
306
  Object.defineProperty(exports, "normalizeOfficialReturnOrigin", { enumerable: true, get: function () { return officialOrigins_1.normalizeOfficialReturnOrigin; } });
@@ -330,9 +336,10 @@ Object.defineProperty(exports, "projectSwitchableAccounts", { enumerable: true,
330
336
  Object.defineProperty(exports, "switchableAccountIds", { enumerable: true, get: function () { return accountProjection_1.switchableAccountIds; } });
331
337
  // Headless controller for the unified account dialog. Framework-agnostic
332
338
  // state machine + subscribe/getSnapshot store (bind via `useSyncExternalStore`)
333
- // — no password/2FA logic (that lives at the IdP; `openPasswordAtOxyAuth` only
334
- // hands off). Reuses `SessionClient.switchAccount` / `oxyServices.switchToAccount`
335
- // for the uniform switch and the existing device-flow methods for sign-in.
339
+ // — sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff;
340
+ // password, social login, and 2FA were removed ecosystem-wide. Reuses
341
+ // `SessionClient.switchAccount` / `oxyServices.switchToAccount` for the uniform
342
+ // switch and the existing device-flow methods for sign-in.
336
343
  var accountDialogController_1 = require("./session/accountDialogController");
337
344
  Object.defineProperty(exports, "AccountDialogController", { enumerable: true, get: function () { return accountDialogController_1.AccountDialogController; } });
338
345
  Object.defineProperty(exports, "createAccountDialogController", { enumerable: true, get: function () { return accountDialogController_1.createAccountDialogController; } });
@@ -742,106 +742,97 @@ function OxyServicesAuthMixin(Base) {
742
742
  }
743
743
  }
744
744
  /**
745
- * Register a new user with email/username and password
745
+ * Begin a WebAuthn / passkey REGISTRATION ceremony. Requests the
746
+ * `PublicKeyCredentialCreationOptions` the browser's `navigator.credentials
747
+ * .create()` (or `@simplewebauthn/browser`'s `startRegistration`) needs.
748
+ *
749
+ * With a bearer token planted this links a passkey to the signed-in account
750
+ * (`username` ignored); without one it is a prospective signup and `username`
751
+ * is the desired handle. The returned options are OPAQUE — Oxy does not own
752
+ * their shape (the browser / `@simplewebauthn` does), so they pass through
753
+ * as `unknown` for the caller to hand straight to the ceremony.
746
754
  */
747
- async signUp(username, email, password, deviceName, deviceFingerprint) {
755
+ async webauthnRegisterOptions(username) {
748
756
  try {
749
- const session = await this.makeRequest('POST', '/auth/signup', {
750
- username,
751
- email,
752
- password,
753
- deviceName,
754
- deviceFingerprint,
755
- }, { cache: false });
756
- return {
757
- ...session,
758
- user: (0, userIdentity_1.normalizeUserIdentity)(session.user),
759
- };
757
+ return await this.makeRequest('POST', '/auth/webauthn/register/options', { ...(username !== undefined ? { username } : {}) }, { cache: false });
760
758
  }
761
759
  catch (error) {
762
760
  throw this.handleError(error);
763
761
  }
764
762
  }
765
763
  /**
766
- * Sign in with email or username and password
767
- */
768
- async signIn(identifier, password, deviceName, deviceFingerprint) {
764
+ * Finish a WebAuthn / passkey REGISTRATION ceremony. Forwards the opaque
765
+ * browser `RegistrationResponseJSON` (`response`) alongside the Oxy envelope
766
+ * (desired `username` for signup + the device-session naming fields).
767
+ *
768
+ * Two server branches, disambiguated by the response shape:
769
+ * - **Signup** (no bearer): the account is created and a session minted —
770
+ * the response carries `sessionId`, is the SAME {@link LoginResult}
771
+ * contract as `POST /auth/verify`, and its access token is planted here.
772
+ * - **Link** (bearer present): the passkey is attached to the signed-in
773
+ * account and the server returns `{ success, message }` with no session,
774
+ * which is returned verbatim (no token planting).
775
+ */
776
+ async webauthnRegisterVerify(response, envelope = {}) {
769
777
  try {
770
- const session = await this.makeRequest('POST', '/auth/login', {
771
- identifier,
772
- password,
773
- deviceName,
774
- deviceFingerprint,
775
- }, { cache: false });
776
- return {
777
- ...session,
778
- user: (0, userIdentity_1.normalizeUserIdentity)(session.user),
779
- };
778
+ const res = await this.makeRequest('POST', '/auth/webauthn/register/verify', { response, ...envelope }, { cache: false });
779
+ if (res && typeof res === 'object') {
780
+ const record = res;
781
+ // Signup branch: mints a session (LoginSessionResult, carries
782
+ // `sessionId`). Parse against the login contract and plant the token.
783
+ if ('sessionId' in record) {
784
+ const parsed = (0, contracts_1.safeParseContract)(contracts_1.loginResultSchema, record);
785
+ if (!parsed) {
786
+ throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
787
+ }
788
+ if (parsed.accessToken) {
789
+ this.setTokens(parsed.accessToken);
790
+ }
791
+ return parsed;
792
+ }
793
+ // Link branch: passkey attached to the signed-in account, no session.
794
+ if (record.success === true && typeof record.message === 'string') {
795
+ return { success: true, message: record.message };
796
+ }
797
+ }
798
+ throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
780
799
  }
781
800
  catch (error) {
782
801
  throw this.handleError(error);
783
802
  }
784
803
  }
785
804
  /**
786
- * Convenience helper for email sign-in
787
- */
788
- async signInWithEmail(email, password, deviceName, deviceFingerprint) {
789
- return this.signIn(email, password, deviceName, deviceFingerprint);
790
- }
791
- /**
792
- * Device-first password sign-in. Unlike the legacy {@link signIn} (which
793
- * assumes a one-step session and is kept intact for existing callers until
794
- * the F4 cutover), this returns the FULL `POST /auth/login` contract — the
795
- * discriminated {@link LoginResult}: either a 2FA challenge
796
- * (`{ twoFactorRequired, loginToken }`) to complete via
797
- * {@link completeTwoFactorSignIn}, or a session arm.
805
+ * Begin a WebAuthn / passkey AUTHENTICATION ceremony. Requests the
806
+ * `PublicKeyCredentialRequestOptions` the browser's `navigator.credentials
807
+ * .get()` (or `@simplewebauthn/browser`'s `startAuthentication`) needs.
798
808
  *
799
- * On the session arm, a returned access token is planted immediately
800
- * (mirroring {@link verifyChallenge}), so the caller has an authenticated
801
- * client without a second round-trip. The response's `deviceId` +
802
- * `deviceSecret` are the zero-cookie restore credential the caller persists.
809
+ * When `username` is present the server scopes `allowCredentials` to that
810
+ * user's passkeys (username-first); when omitted it returns an empty
811
+ * allow-list for the usernameless / discoverable-credential flow. The
812
+ * returned options are OPAQUE and pass through as `unknown`.
803
813
  */
804
- async passwordSignIn(identifier, password, options = {}) {
814
+ async webauthnLoginOptions(username) {
805
815
  try {
806
- const res = await this.makeRequest('POST', '/auth/login', {
807
- identifier,
808
- password,
809
- deviceName: options.deviceName,
810
- deviceFingerprint: options.deviceFingerprint,
811
- ...(options.deviceId ? { deviceId: options.deviceId } : {}),
812
- }, { cache: false });
813
- const parsed = (0, contracts_1.safeParseContract)(contracts_1.loginResultSchema, res);
814
- if (!parsed) {
815
- throw new Error('auth/login returned an unexpected response shape');
816
- }
817
- if (!('twoFactorRequired' in parsed) && parsed.accessToken) {
818
- this.setTokens(parsed.accessToken);
819
- }
820
- return parsed;
816
+ return await this.makeRequest('POST', '/auth/webauthn/login/options', { ...(username !== undefined ? { username } : {}) }, { cache: false });
821
817
  }
822
818
  catch (error) {
823
819
  throw this.handleError(error);
824
820
  }
825
821
  }
826
822
  /**
827
- * Complete a 2FA-gated sign-in started by {@link passwordSignIn}. Presents
828
- * the short-lived `loginToken` with either a TOTP `token` or a `backupCode`
829
- * to `POST /security/2fa/verify-login`, which must resolve to the session
830
- * arm of {@link LoginResult} (a second 2FA challenge here is a protocol
831
- * error). A returned access token is planted immediately.
823
+ * Finish a WebAuthn / passkey AUTHENTICATION ceremony. Forwards the opaque
824
+ * browser `AuthenticationResponseJSON` (`response`) alongside the
825
+ * device-session envelope. Resolves to the SAME {@link LoginResult} contract
826
+ * as `POST /auth/verify`; the access token is planted immediately, and the
827
+ * response's `deviceId` + `deviceSecret` are the zero-cookie restore
828
+ * credential.
832
829
  */
833
- async completeTwoFactorSignIn(params) {
830
+ async webauthnLoginVerify(response, envelope = {}) {
834
831
  try {
835
- const res = await this.makeRequest('POST', '/security/2fa/verify-login', {
836
- loginToken: params.loginToken,
837
- token: params.token,
838
- backupCode: params.backupCode,
839
- deviceName: params.deviceName,
840
- ...(params.deviceId ? { deviceId: params.deviceId } : {}),
841
- }, { cache: false });
832
+ const res = await this.makeRequest('POST', '/auth/webauthn/login/verify', { response, ...envelope }, { cache: false });
842
833
  const parsed = (0, contracts_1.safeParseContract)(contracts_1.loginResultSchema, res);
843
- if (!parsed || 'twoFactorRequired' in parsed) {
844
- throw new Error('security/2fa/verify-login returned an unexpected response shape');
834
+ if (!parsed) {
835
+ throw new Error('auth/webauthn/login/verify returned an unexpected response shape');
845
836
  }
846
837
  if (parsed.accessToken) {
847
838
  this.setTokens(parsed.accessToken);
@@ -854,7 +845,7 @@ function OxyServicesAuthMixin(Base) {
854
845
  }
855
846
  /**
856
847
  * Exchange an OAuth authorization code (returned to the RP redirect URI
857
- * after password sign-in at auth.oxy.so) for a device-first session.
848
+ * after sign-in at auth.oxy.so) for a device-first session.
858
849
  * Public first-party clients use PKCE (`codeVerifier`); the access token is
859
850
  * planted immediately on success.
860
851
  */
@@ -104,15 +104,15 @@ function OxyServicesIdentityMixin(Base) {
104
104
  }
105
105
  }
106
106
  /**
107
- * Link password authentication to the current account. Adds a `password`
108
- * auth method (does not remove existing methods).
107
+ * Unlink an authentication method from the current account. The server
108
+ * refuses to remove the last remaining method (the account would become
109
+ * inaccessible). Unlinking `identity` downgrades the account to custodial.
109
110
  *
110
- * @param email - The email to associate with password auth.
111
- * @param password - The new password (server enforces strength rules).
111
+ * @param type - The auth-method type to remove.
112
112
  */
113
- async linkPassword(email, password) {
113
+ async unlinkAuthMethod(type) {
114
114
  try {
115
- const result = await this.makeRequest('POST', '/auth/link', { type: 'password', email, password }, { cache: false });
115
+ const result = await this.makeRequest('DELETE', `/auth/link/${encodeURIComponent(type)}`, undefined, { cache: false });
116
116
  this._invalidateIdentityCaches(this.getCurrentUserId());
117
117
  return result;
118
118
  }
@@ -121,15 +121,19 @@ function OxyServicesIdentityMixin(Base) {
121
121
  }
122
122
  }
123
123
  /**
124
- * Unlink an authentication method from the current account. The server
125
- * refuses to remove the last remaining method (the account would become
126
- * inaccessible). Unlinking `identity` downgrades the account to custodial.
124
+ * Remove ONE passkey (WebAuthn credential) from the current account.
127
125
  *
128
- * @param type - The auth-method type to remove.
126
+ * Passkeys are per-credential, so unlike {@link unlinkAuthMethod} (which
127
+ * removes an auth method by type) this targets a specific credential id.
128
+ * The server refuses to remove the last remaining auth method (the account
129
+ * would become inaccessible) and deletes the stored `WebauthnCredential`.
130
+ *
131
+ * @param credentialId - The passkey's public credential id
132
+ * (`AuthMethodEntry.credentialId`).
129
133
  */
130
- async unlinkAuthMethod(type) {
134
+ async removePasskey(credentialId) {
131
135
  try {
132
- const result = await this.makeRequest('DELETE', `/auth/link/${encodeURIComponent(type)}`, undefined, { cache: false });
136
+ const result = await this.makeRequest('DELETE', `/auth/link/webauthn/${encodeURIComponent(credentialId)}`, undefined, { cache: false });
133
137
  this._invalidateIdentityCaches(this.getCurrentUserId());
134
138
  return result;
135
139
  }
@@ -22,17 +22,14 @@
22
22
  * `oxyServices.signInWithSharedIdentity`, else the cross-device QR handoff
23
23
  * via `startCommonsSignIn` → poll → `claimSessionByToken`).
24
24
  *
25
- * It deliberately owns NO password/2FA logic those live at the IdP
26
- * (auth.oxy.so). {@link AccountDialogController.openPasswordAtOxyAuth} only
27
- * builds the hand-off URL; device-first convergence syncs the session back.
25
+ * Sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff
26
+ * password, social login, and 2FA were removed ecosystem-wide.
28
27
  */
29
28
  Object.defineProperty(exports, "__esModule", { value: true });
30
29
  exports.AccountDialogController = void 0;
31
30
  exports.createAccountDialogController = createAccountDialogController;
32
31
  const logger_1 = require("../logger");
33
32
  const errorUtils_1 = require("../utils/errorUtils");
34
- const authWebUrl_1 = require("../utils/authWebUrl");
35
- const oauthPkce_1 = require("../utils/oauthPkce");
36
33
  const accountProjection_1 = require("./accountProjection");
37
34
  /**
38
35
  * Slow FALLBACK poll cadence for the QR flow. The `/auth-session` socket delivers
@@ -99,8 +96,6 @@ class AccountDialogController {
99
96
  this.locale = options.locale;
100
97
  this.commitSession = options.commitSession;
101
98
  this.onSignedIn = options.onSignedIn;
102
- this.idpApex = options.idpApex ?? authWebUrl_1.CENTRAL_IDP_APEX;
103
- this.authRedirectUri = options.authRedirectUri ?? null;
104
99
  this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
105
100
  this.openUrl = options.openUrl;
106
101
  this.canOpenApp = options.canOpenApp;
@@ -489,46 +484,6 @@ class AccountDialogController {
489
484
  this.setSignIn(IDLE_SIGN_IN);
490
485
  }
491
486
  }
492
- /**
493
- * Build (and, when an `openUrl` handler was supplied, open) the auth.oxy.so
494
- * password sign-in URL. Password + 2FA are NOT in the SDK — they live at the
495
- * IdP; this only hands off. Device-first: after login at the IdP the device
496
- * session converges and the caller is woken via the device socket /
497
- * `BroadcastChannel`, so the URL only needs to point at the IdP sign-in with
498
- * the right return.
499
- *
500
- * @param params.returnUrl - Where the IdP returns after login. Defaults to the
501
- * current document URL on web (`globalThis.location.href`); pass explicitly
502
- * on native (no `location`).
503
- * @param params.state - Optional opaque state echoed back on return.
504
- * @returns The absolute auth.oxy.so sign-in URL.
505
- */
506
- async openPasswordAtOxyAuth(params = {}) {
507
- const base = `https://auth.${this.idpApex}`;
508
- const url = new URL('/login', base);
509
- const rawRedirect = params.redirectUri ??
510
- this.authRedirectUri ??
511
- params.returnUrl ??
512
- currentLocationOrigin();
513
- const redirectUri = rawRedirect ? (0, oauthPkce_1.normalizeOAuthRedirectUri)(rawRedirect) : '';
514
- if (redirectUri) {
515
- url.searchParams.set('redirect_uri', redirectUri);
516
- }
517
- if (this.clientId) {
518
- url.searchParams.set('client_id', this.clientId);
519
- }
520
- const state = params.state ?? (await (0, oauthPkce_1.generateOAuthState)());
521
- url.searchParams.set('state', state);
522
- const { codeChallenge, codeVerifier } = await (0, oauthPkce_1.generatePkcePair)();
523
- url.searchParams.set('code_challenge', codeChallenge);
524
- url.searchParams.set('code_challenge_method', 'S256');
525
- if (!(0, oauthPkce_1.persistOAuthHandshake)(state, codeVerifier)) {
526
- throw new Error('Could not persist OAuth handshake for password sign-in');
527
- }
528
- const href = url.toString();
529
- this.openUrl?.(href);
530
- return href;
531
- }
532
487
  // =========================================================================
533
488
  // Internal sign-in helpers
534
489
  // =========================================================================
@@ -777,11 +732,3 @@ exports.AccountDialogController = AccountDialogController;
777
732
  function createAccountDialogController(options) {
778
733
  return new AccountDialogController(options);
779
734
  }
780
- // ---------------------------------------------------------------------------
781
- // Local helpers
782
- // ---------------------------------------------------------------------------
783
- /** Current document origin on web; empty string where `location` is absent (native/SSR). */
784
- function currentLocationOrigin() {
785
- const location = globalThis.location;
786
- return typeof location?.origin === 'string' ? location.origin : '';
787
- }
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.isAllowedDeviceJoinOrigin = void 0;
8
8
  exports.buildIdpHubOrigin = buildIdpHubOrigin;
9
9
  exports.isIdpHubOrigin = isIdpHubOrigin;
10
+ exports.isLoopbackOrigin = isLoopbackOrigin;
10
11
  exports.isOfficialWebOrigin = isOfficialWebOrigin;
11
12
  exports.normalizeOfficialReturnOrigin = normalizeOfficialReturnOrigin;
12
13
  exports.parseHubSyncReturnUrl = parseHubSyncReturnUrl;
@@ -46,6 +47,11 @@ function isIdpHubOrigin() {
46
47
  return false;
47
48
  }
48
49
  }
50
+ /**
51
+ * Whether an origin is a loopback / local-dev origin (`localhost`, `127.0.0.1`,
52
+ * or `[::1]` on any port, http or https). Local dev must never be bounced to a
53
+ * hosted IdP for cross-origin session restore.
54
+ */
49
55
  function isLoopbackOrigin(origin) {
50
56
  try {
51
57
  const parsed = new URL(origin);
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * WebAuthn relying-party origin guard (client side).
4
+ *
5
+ * The passkey ceremonies (`OxyServices.webauthn*`) are only meaningful when the
6
+ * page is served from a first-party Oxy web origin: a credential minted with
7
+ * `WEBAUTHN_RP_ID=oxy.so` can only be created/asserted from `oxy.so`, one of its
8
+ * subdomains, or a loopback dev server. This is the browser-side mirror of the
9
+ * server's `isOxyApexOrigin` (`packages/api/src/utils/origin.ts`), which forms
10
+ * the server's `expectedOrigin` allow-set — consumers use it to decide whether to
11
+ * even offer the passkey UI on the current page.
12
+ *
13
+ * It reads `globalThis.location` directly (no argument) because that is the only
14
+ * origin the browser will let a WebAuthn ceremony run against. On native / SSR /
15
+ * any environment without a DOM `location`, it returns `false` (there is no
16
+ * relying-party origin, so passkeys are not applicable).
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.isOxyRpOrigin = isOxyRpOrigin;
20
+ /**
21
+ * True iff the current page's host is a first-party Oxy relying-party origin:
22
+ * `oxy.so`, any `*.oxy.so` subdomain, or a loopback dev host
23
+ * (`localhost` / `127.0.0.1` / `[::1]`).
24
+ *
25
+ * Fails closed: no `location` (native/SSR), a non-string/empty hostname, or a
26
+ * host that merely ends in the literal `oxy.so` without the dot boundary
27
+ * (`evil-oxy.so`, `oxy.so.evil.com`) all return `false`.
28
+ *
29
+ * @example
30
+ * isOxyRpOrigin() // true on https://accounts.oxy.so
31
+ * isOxyRpOrigin() // true on http://localhost:8081
32
+ * isOxyRpOrigin() // false on https://evil.com
33
+ * isOxyRpOrigin() // false in a React Native / SSR context (no location)
34
+ */
35
+ function isOxyRpOrigin() {
36
+ // `globalThis.location` is typed `Location` by the DOM lib, but is genuinely
37
+ // `undefined` on native/SSR — widen to a nullable local so the guard is a real
38
+ // runtime check, not a type-level no-op.
39
+ const location = globalThis.location;
40
+ if (!location || typeof location.hostname !== 'string') {
41
+ return false;
42
+ }
43
+ const hostname = location.hostname.toLowerCase();
44
+ if (hostname.length === 0) {
45
+ return false;
46
+ }
47
+ if (hostname === 'oxy.so' || hostname.endsWith('.oxy.so')) {
48
+ return true;
49
+ }
50
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
51
+ }