@oxyhq/core 11.0.1 → 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.
package/dist/esm/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * ```ts
11
11
  * import { OxyServices, oxyClient } from '@oxyhq/core';
12
12
  *
13
- * const user = await oxyClient.signIn(publicKey);
13
+ * const user = await oxyClient.getCurrentUser();
14
14
  * ```
15
15
  *
16
16
  * Every export below is NOMINAL — no `export *`, no barrels, no compat shims.
@@ -140,7 +140,7 @@ export { runColdBoot } from './utils/coldBoot.js';
140
140
  // Standard OAuth against auth.oxy.so/authorize — no FedCM/cookies/SSO bounce.
141
141
  // ---------------------------------------------------------------------------
142
142
  export { buildOAuthAuthorizeUrl, computeCodeChallenge, generateOAuthState, generatePkcePair, DEFAULT_OAUTH_SCOPE, OXY_AUTHORIZE_URL, OXY_OAUTH_STATE_STORAGE_KEY, OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY, OXY_SILENT_OAUTH_ATTEMPTED_KEY, OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY, normalizeOAuthRedirectUri, persistOAuthHandshake, readOAuthHandshake, clearOAuthHandshake, } from './utils/oauthPkce.js';
143
- export { buildIdpHubOrigin, buildHubSyncUrl, isIdpHubOrigin, isOfficialWebOrigin, isAllowedDeviceJoinOrigin, normalizeOfficialReturnOrigin, parseHubSyncReturnUrl, } from './utils/officialOrigins.js';
143
+ export { buildIdpHubOrigin, buildHubSyncUrl, isIdpHubOrigin, isLoopbackOrigin, isOfficialWebOrigin, isAllowedDeviceJoinOrigin, normalizeOfficialReturnOrigin, parseHubSyncReturnUrl, } from './utils/officialOrigins.js';
144
144
  export { syncHubAfterSignIn, redeemHubTicketOnHub, } from './session/hubSync.js';
145
145
  // ---------------------------------------------------------------------------
146
146
  // Session sync (device-scoped multi-account session client)
@@ -161,9 +161,10 @@ export { deviceStateToClientSessions, activeSessionIdOf, activeUserOf, accountId
161
161
  export { projectSwitchableAccounts, switchableAccountIds, } from './session/accountProjection.js';
162
162
  // Headless controller for the unified account dialog. Framework-agnostic
163
163
  // state machine + subscribe/getSnapshot store (bind via `useSyncExternalStore`)
164
- // — no password/2FA logic (that lives at the IdP; `openPasswordAtOxyAuth` only
165
- // hands off). Reuses `SessionClient.switchAccount` / `oxyServices.switchToAccount`
166
- // for the uniform switch and the existing device-flow methods for sign-in.
164
+ // — sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff;
165
+ // password, social login, and 2FA were removed ecosystem-wide. Reuses
166
+ // `SessionClient.switchAccount` / `oxyServices.switchToAccount` for the uniform
167
+ // switch and the existing device-flow methods for sign-in.
167
168
  export { AccountDialogController, createAccountDialogController, } from './session/accountDialogController.js';
168
169
  // ---------------------------------------------------------------------------
169
170
  // Device-first session machinery (zero-cookie transport).
@@ -736,117 +736,6 @@ export function OxyServicesAuthMixin(Base) {
736
736
  throw this.handleError(error);
737
737
  }
738
738
  }
739
- /**
740
- * Register a new user with email/username and password
741
- */
742
- async signUp(username, email, password, deviceName, deviceFingerprint) {
743
- try {
744
- const session = await this.makeRequest('POST', '/auth/signup', {
745
- username,
746
- email,
747
- password,
748
- deviceName,
749
- deviceFingerprint,
750
- }, { cache: false });
751
- return {
752
- ...session,
753
- user: normalizeUserIdentity(session.user),
754
- };
755
- }
756
- catch (error) {
757
- throw this.handleError(error);
758
- }
759
- }
760
- /**
761
- * Sign in with email or username and password
762
- */
763
- async signIn(identifier, password, deviceName, deviceFingerprint) {
764
- try {
765
- const session = await this.makeRequest('POST', '/auth/login', {
766
- identifier,
767
- password,
768
- deviceName,
769
- deviceFingerprint,
770
- }, { cache: false });
771
- return {
772
- ...session,
773
- user: normalizeUserIdentity(session.user),
774
- };
775
- }
776
- catch (error) {
777
- throw this.handleError(error);
778
- }
779
- }
780
- /**
781
- * Convenience helper for email sign-in
782
- */
783
- async signInWithEmail(email, password, deviceName, deviceFingerprint) {
784
- return this.signIn(email, password, deviceName, deviceFingerprint);
785
- }
786
- /**
787
- * Device-first password sign-in. Unlike the legacy {@link signIn} (which
788
- * assumes a one-step session and is kept intact for existing callers until
789
- * the F4 cutover), this returns the FULL `POST /auth/login` contract — the
790
- * discriminated {@link LoginResult}: either a 2FA challenge
791
- * (`{ twoFactorRequired, loginToken }`) to complete via
792
- * {@link completeTwoFactorSignIn}, or a session arm.
793
- *
794
- * On the session arm, a returned access token is planted immediately
795
- * (mirroring {@link verifyChallenge}), so the caller has an authenticated
796
- * client without a second round-trip. The response's `deviceId` +
797
- * `deviceSecret` are the zero-cookie restore credential the caller persists.
798
- */
799
- async passwordSignIn(identifier, password, options = {}) {
800
- try {
801
- const res = await this.makeRequest('POST', '/auth/login', {
802
- identifier,
803
- password,
804
- deviceName: options.deviceName,
805
- deviceFingerprint: options.deviceFingerprint,
806
- ...(options.deviceId ? { deviceId: options.deviceId } : {}),
807
- }, { cache: false });
808
- const parsed = safeParseContract(loginResultSchema, res);
809
- if (!parsed) {
810
- throw new Error('auth/login returned an unexpected response shape');
811
- }
812
- if (!('twoFactorRequired' in parsed) && parsed.accessToken) {
813
- this.setTokens(parsed.accessToken);
814
- }
815
- return parsed;
816
- }
817
- catch (error) {
818
- throw this.handleError(error);
819
- }
820
- }
821
- /**
822
- * Complete a 2FA-gated sign-in started by {@link passwordSignIn}. Presents
823
- * the short-lived `loginToken` with either a TOTP `token` or a `backupCode`
824
- * to `POST /security/2fa/verify-login`, which must resolve to the session
825
- * arm of {@link LoginResult} (a second 2FA challenge here is a protocol
826
- * error). A returned access token is planted immediately.
827
- */
828
- async completeTwoFactorSignIn(params) {
829
- try {
830
- const res = await this.makeRequest('POST', '/security/2fa/verify-login', {
831
- loginToken: params.loginToken,
832
- token: params.token,
833
- backupCode: params.backupCode,
834
- deviceName: params.deviceName,
835
- ...(params.deviceId ? { deviceId: params.deviceId } : {}),
836
- }, { cache: false });
837
- const parsed = safeParseContract(loginResultSchema, res);
838
- if (!parsed || 'twoFactorRequired' in parsed) {
839
- throw new Error('security/2fa/verify-login returned an unexpected response shape');
840
- }
841
- if (parsed.accessToken) {
842
- this.setTokens(parsed.accessToken);
843
- }
844
- return parsed;
845
- }
846
- catch (error) {
847
- throw this.handleError(error);
848
- }
849
- }
850
739
  /**
851
740
  * Begin a WebAuthn / passkey REGISTRATION ceremony. Requests the
852
741
  * `PublicKeyCredentialCreationOptions` the browser's `navigator.credentials
@@ -891,7 +780,7 @@ export function OxyServicesAuthMixin(Base) {
891
780
  if (!parsed) {
892
781
  throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
893
782
  }
894
- if (!('twoFactorRequired' in parsed) && parsed.accessToken) {
783
+ if (parsed.accessToken) {
895
784
  this.setTokens(parsed.accessToken);
896
785
  }
897
786
  return parsed;
@@ -929,9 +818,9 @@ export function OxyServicesAuthMixin(Base) {
929
818
  * Finish a WebAuthn / passkey AUTHENTICATION ceremony. Forwards the opaque
930
819
  * browser `AuthenticationResponseJSON` (`response`) alongside the
931
820
  * device-session envelope. Resolves to the SAME {@link LoginResult} contract
932
- * as `POST /auth/verify`; on the session arm the access token is planted
933
- * immediately (mirroring {@link passwordSignIn}), and the response's
934
- * `deviceId` + `deviceSecret` are the zero-cookie restore credential.
821
+ * as `POST /auth/verify`; the access token is planted immediately, and the
822
+ * response's `deviceId` + `deviceSecret` are the zero-cookie restore
823
+ * credential.
935
824
  */
936
825
  async webauthnLoginVerify(response, envelope = {}) {
937
826
  try {
@@ -940,7 +829,7 @@ export function OxyServicesAuthMixin(Base) {
940
829
  if (!parsed) {
941
830
  throw new Error('auth/webauthn/login/verify returned an unexpected response shape');
942
831
  }
943
- if (!('twoFactorRequired' in parsed) && parsed.accessToken) {
832
+ if (parsed.accessToken) {
944
833
  this.setTokens(parsed.accessToken);
945
834
  }
946
835
  return parsed;
@@ -951,7 +840,7 @@ export function OxyServicesAuthMixin(Base) {
951
840
  }
952
841
  /**
953
842
  * Exchange an OAuth authorization code (returned to the RP redirect URI
954
- * after password sign-in at auth.oxy.so) for a device-first session.
843
+ * after sign-in at auth.oxy.so) for a device-first session.
955
844
  * Public first-party clients use PKCE (`codeVerifier`); the access token is
956
845
  * planted immediately on success.
957
846
  */
@@ -100,15 +100,15 @@ export function OxyServicesIdentityMixin(Base) {
100
100
  }
101
101
  }
102
102
  /**
103
- * Link password authentication to the current account. Adds a `password`
104
- * auth method (does not remove existing methods).
103
+ * Unlink an authentication method from the current account. The server
104
+ * refuses to remove the last remaining method (the account would become
105
+ * inaccessible). Unlinking `identity` downgrades the account to custodial.
105
106
  *
106
- * @param email - The email to associate with password auth.
107
- * @param password - The new password (server enforces strength rules).
107
+ * @param type - The auth-method type to remove.
108
108
  */
109
- async linkPassword(email, password) {
109
+ async unlinkAuthMethod(type) {
110
110
  try {
111
- const result = await this.makeRequest('POST', '/auth/link', { type: 'password', email, password }, { cache: false });
111
+ const result = await this.makeRequest('DELETE', `/auth/link/${encodeURIComponent(type)}`, undefined, { cache: false });
112
112
  this._invalidateIdentityCaches(this.getCurrentUserId());
113
113
  return result;
114
114
  }
@@ -117,15 +117,19 @@ export function OxyServicesIdentityMixin(Base) {
117
117
  }
118
118
  }
119
119
  /**
120
- * Unlink an authentication method from the current account. The server
121
- * refuses to remove the last remaining method (the account would become
122
- * inaccessible). Unlinking `identity` downgrades the account to custodial.
120
+ * Remove ONE passkey (WebAuthn credential) from the current account.
123
121
  *
124
- * @param type - The auth-method type to remove.
122
+ * Passkeys are per-credential, so unlike {@link unlinkAuthMethod} (which
123
+ * removes an auth method by type) this targets a specific credential id.
124
+ * The server refuses to remove the last remaining auth method (the account
125
+ * would become inaccessible) and deletes the stored `WebauthnCredential`.
126
+ *
127
+ * @param credentialId - The passkey's public credential id
128
+ * (`AuthMethodEntry.credentialId`).
125
129
  */
126
- async unlinkAuthMethod(type) {
130
+ async removePasskey(credentialId) {
127
131
  try {
128
- const result = await this.makeRequest('DELETE', `/auth/link/${encodeURIComponent(type)}`, undefined, { cache: false });
132
+ const result = await this.makeRequest('DELETE', `/auth/link/webauthn/${encodeURIComponent(credentialId)}`, undefined, { cache: false });
129
133
  this._invalidateIdentityCaches(this.getCurrentUserId());
130
134
  return result;
131
135
  }
@@ -21,14 +21,11 @@
21
21
  * `oxyServices.signInWithSharedIdentity`, else the cross-device QR handoff
22
22
  * via `startCommonsSignIn` → poll → `claimSessionByToken`).
23
23
  *
24
- * It deliberately owns NO password/2FA logic those live at the IdP
25
- * (auth.oxy.so). {@link AccountDialogController.openPasswordAtOxyAuth} only
26
- * builds the hand-off URL; device-first convergence syncs the session back.
24
+ * Sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff
25
+ * password, social login, and 2FA were removed ecosystem-wide.
27
26
  */
28
27
  import { logger } from '../logger/index.js';
29
28
  import { extractErrorStatus } from '../utils/errorUtils.js';
30
- import { CENTRAL_IDP_APEX } from '../utils/authWebUrl.js';
31
- import { generateOAuthState, generatePkcePair, normalizeOAuthRedirectUri, persistOAuthHandshake, } from '../utils/oauthPkce.js';
32
29
  import { projectSwitchableAccounts, switchableAccountIds, } from './accountProjection.js';
33
30
  /**
34
31
  * Slow FALLBACK poll cadence for the QR flow. The `/auth-session` socket delivers
@@ -95,8 +92,6 @@ export class AccountDialogController {
95
92
  this.locale = options.locale;
96
93
  this.commitSession = options.commitSession;
97
94
  this.onSignedIn = options.onSignedIn;
98
- this.idpApex = options.idpApex ?? CENTRAL_IDP_APEX;
99
- this.authRedirectUri = options.authRedirectUri ?? null;
100
95
  this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
101
96
  this.openUrl = options.openUrl;
102
97
  this.canOpenApp = options.canOpenApp;
@@ -485,46 +480,6 @@ export class AccountDialogController {
485
480
  this.setSignIn(IDLE_SIGN_IN);
486
481
  }
487
482
  }
488
- /**
489
- * Build (and, when an `openUrl` handler was supplied, open) the auth.oxy.so
490
- * password sign-in URL. Password + 2FA are NOT in the SDK — they live at the
491
- * IdP; this only hands off. Device-first: after login at the IdP the device
492
- * session converges and the caller is woken via the device socket /
493
- * `BroadcastChannel`, so the URL only needs to point at the IdP sign-in with
494
- * the right return.
495
- *
496
- * @param params.returnUrl - Where the IdP returns after login. Defaults to the
497
- * current document URL on web (`globalThis.location.href`); pass explicitly
498
- * on native (no `location`).
499
- * @param params.state - Optional opaque state echoed back on return.
500
- * @returns The absolute auth.oxy.so sign-in URL.
501
- */
502
- async openPasswordAtOxyAuth(params = {}) {
503
- const base = `https://auth.${this.idpApex}`;
504
- const url = new URL('/login', base);
505
- const rawRedirect = params.redirectUri ??
506
- this.authRedirectUri ??
507
- params.returnUrl ??
508
- currentLocationOrigin();
509
- const redirectUri = rawRedirect ? normalizeOAuthRedirectUri(rawRedirect) : '';
510
- if (redirectUri) {
511
- url.searchParams.set('redirect_uri', redirectUri);
512
- }
513
- if (this.clientId) {
514
- url.searchParams.set('client_id', this.clientId);
515
- }
516
- const state = params.state ?? (await generateOAuthState());
517
- url.searchParams.set('state', state);
518
- const { codeChallenge, codeVerifier } = await generatePkcePair();
519
- url.searchParams.set('code_challenge', codeChallenge);
520
- url.searchParams.set('code_challenge_method', 'S256');
521
- if (!persistOAuthHandshake(state, codeVerifier)) {
522
- throw new Error('Could not persist OAuth handshake for password sign-in');
523
- }
524
- const href = url.toString();
525
- this.openUrl?.(href);
526
- return href;
527
- }
528
483
  // =========================================================================
529
484
  // Internal sign-in helpers
530
485
  // =========================================================================
@@ -772,11 +727,3 @@ export class AccountDialogController {
772
727
  export function createAccountDialogController(options) {
773
728
  return new AccountDialogController(options);
774
729
  }
775
- // ---------------------------------------------------------------------------
776
- // Local helpers
777
- // ---------------------------------------------------------------------------
778
- /** Current document origin on web; empty string where `location` is absent (native/SSR). */
779
- function currentLocationOrigin() {
780
- const location = globalThis.location;
781
- return typeof location?.origin === 'string' ? location.origin : '';
782
- }
@@ -37,7 +37,12 @@ export function isIdpHubOrigin() {
37
37
  return false;
38
38
  }
39
39
  }
40
- function isLoopbackOrigin(origin) {
40
+ /**
41
+ * Whether an origin is a loopback / local-dev origin (`localhost`, `127.0.0.1`,
42
+ * or `[::1]` on any port, http or https). Local dev must never be bounced to a
43
+ * hosted IdP for cross-origin session restore.
44
+ */
45
+ export function isLoopbackOrigin(origin) {
41
46
  try {
42
47
  const parsed = new URL(origin);
43
48
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {