@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/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +7 -5
- package/dist/cjs/mixins/OxyServices.auth.js +6 -117
- package/dist/cjs/mixins/OxyServices.identity.js +16 -12
- package/dist/cjs/session/accountDialogController.js +2 -55
- package/dist/cjs/utils/officialOrigins.js +6 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +6 -5
- package/dist/esm/mixins/OxyServices.auth.js +6 -117
- package/dist/esm/mixins/OxyServices.identity.js +16 -12
- package/dist/esm/session/accountDialogController.js +2 -55
- package/dist/esm/utils/officialOrigins.js +6 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/mixins/OxyServices.auth.d.ts +4 -48
- package/dist/types/mixins/OxyServices.identity.d.ts +13 -9
- package/dist/types/session/accountDialogController.d.ts +4 -34
- package/dist/types/utils/officialOrigins.d.ts +6 -0
- package/package.json +2 -2
- package/src/index.ts +6 -4
- package/src/mixins/OxyServices.auth.ts +6 -144
- package/src/mixins/OxyServices.identity.ts +19 -15
- package/src/mixins/__tests__/OxyServices.identity.test.ts +26 -14
- package/src/mixins/__tests__/webauthnAuth.test.ts +1 -1
- package/src/session/__tests__/accountDialogController.test.ts +0 -66
- package/src/session/accountDialogController.ts +4 -78
- package/src/utils/__tests__/officialOrigins.test.ts +14 -0
- package/src/utils/officialOrigins.ts +6 -1
- package/src/mixins/__tests__/passwordSignIn.test.ts +0 -115
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.
|
|
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
|
-
// —
|
|
165
|
-
//
|
|
166
|
-
//
|
|
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 (
|
|
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`;
|
|
933
|
-
*
|
|
934
|
-
*
|
|
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 (
|
|
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
|
|
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
|
-
*
|
|
104
|
-
*
|
|
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
|
|
107
|
-
* @param password - The new password (server enforces strength rules).
|
|
107
|
+
* @param type - The auth-method type to remove.
|
|
108
108
|
*/
|
|
109
|
-
async
|
|
109
|
+
async unlinkAuthMethod(type) {
|
|
110
110
|
try {
|
|
111
|
-
const result = await this.makeRequest('
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
130
|
+
async removePasskey(credentialId) {
|
|
127
131
|
try {
|
|
128
|
-
const result = await this.makeRequest('DELETE', `/auth/link/${encodeURIComponent(
|
|
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
|
-
*
|
|
25
|
-
*
|
|
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
|
-
|
|
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:') {
|