@oxyhq/core 11.0.1 → 12.1.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 +84 -75
- 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 +84 -75
- package/dist/esm/utils/officialOrigins.js +6 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +3 -3
- 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 +84 -49
- package/dist/types/utils/officialOrigins.d.ts +6 -0
- package/package.json +2 -2
- package/src/index.ts +7 -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 +75 -64
- package/src/session/__tests__/accountProjection.test.ts +30 -0
- package/src/session/accountDialogController.ts +131 -104
- 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/types/index.d.ts
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.
|
|
@@ -87,7 +87,7 @@ export { runColdBoot } from './utils/coldBoot';
|
|
|
87
87
|
export type { ColdBootStep, ColdBootStepResult, ColdBootSession, ColdBootSkip, ColdBootOutcome, RunColdBootOptions, } from './utils/coldBoot';
|
|
88
88
|
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';
|
|
89
89
|
export type { PkcePair, BuildOAuthAuthorizeUrlParams } from './utils/oauthPkce';
|
|
90
|
-
export { buildIdpHubOrigin, buildHubSyncUrl, isIdpHubOrigin, isOfficialWebOrigin, isAllowedDeviceJoinOrigin, normalizeOfficialReturnOrigin, parseHubSyncReturnUrl, } from './utils/officialOrigins';
|
|
90
|
+
export { buildIdpHubOrigin, buildHubSyncUrl, isIdpHubOrigin, isLoopbackOrigin, isOfficialWebOrigin, isAllowedDeviceJoinOrigin, normalizeOfficialReturnOrigin, parseHubSyncReturnUrl, } from './utils/officialOrigins';
|
|
91
91
|
export { syncHubAfterSignIn, redeemHubTicketOnHub, } from './session/hubSync';
|
|
92
92
|
export type { SyncHubAfterSignInOptions } from './session/hubSync';
|
|
93
93
|
export { SessionClient } from './session/SessionClient';
|
|
@@ -99,7 +99,7 @@ export { deviceStateToClientSessions, activeSessionIdOf, activeUserOf, accountId
|
|
|
99
99
|
export { projectSwitchableAccounts, switchableAccountIds, } from './session/accountProjection';
|
|
100
100
|
export type { SwitchableAccount, SwitchableAccountUser, ProjectSwitchableAccountsInput, } from './session/accountProjection';
|
|
101
101
|
export { AccountDialogController, createAccountDialogController, } from './session/accountDialogController';
|
|
102
|
-
export type { AccountDialogControllerOptions, AccountDialogSnapshot, AccountDialogView, SignInFlowPhase, SignInFlowState, } from './session/accountDialogController';
|
|
102
|
+
export type { AccountDialogControllerOptions, AccountDialogSnapshot, AccountDialogView, CommonsAvailability, SignInFlowPhase, SignInFlowState, } from './session/accountDialogController';
|
|
103
103
|
export { createWebAuthStateStore, createNativeAuthStateStore, createMemoryAuthStateStore, AUTH_STATE_STORAGE_KEY, } from './session/authStateStore';
|
|
104
104
|
export type { PersistedAuthState, AuthStateStore, NativeKeyValueStorage, } from './session/authStateStore';
|
|
105
105
|
export { refreshPersistedSession, refreshDeviceSecretArm, createAuthRefreshHandler, installAuthRefreshHandler, startTokenRefreshScheduler, TOKEN_REFRESH_LEAD_MS, } from './session/refresh';
|
|
@@ -450,50 +450,6 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
450
450
|
available: boolean;
|
|
451
451
|
message: string;
|
|
452
452
|
}>;
|
|
453
|
-
/**
|
|
454
|
-
* Register a new user with email/username and password
|
|
455
|
-
*/
|
|
456
|
-
signUp(username: string, email: string, password: string, deviceName?: string, deviceFingerprint?: any): Promise<SessionLoginResponse>;
|
|
457
|
-
/**
|
|
458
|
-
* Sign in with email or username and password
|
|
459
|
-
*/
|
|
460
|
-
signIn(identifier: string, password: string, deviceName?: string, deviceFingerprint?: any): Promise<SessionLoginResponse>;
|
|
461
|
-
/**
|
|
462
|
-
* Convenience helper for email sign-in
|
|
463
|
-
*/
|
|
464
|
-
signInWithEmail(email: string, password: string, deviceName?: string, deviceFingerprint?: any): Promise<SessionLoginResponse>;
|
|
465
|
-
/**
|
|
466
|
-
* Device-first password sign-in. Unlike the legacy {@link signIn} (which
|
|
467
|
-
* assumes a one-step session and is kept intact for existing callers until
|
|
468
|
-
* the F4 cutover), this returns the FULL `POST /auth/login` contract — the
|
|
469
|
-
* discriminated {@link LoginResult}: either a 2FA challenge
|
|
470
|
-
* (`{ twoFactorRequired, loginToken }`) to complete via
|
|
471
|
-
* {@link completeTwoFactorSignIn}, or a session arm.
|
|
472
|
-
*
|
|
473
|
-
* On the session arm, a returned access token is planted immediately
|
|
474
|
-
* (mirroring {@link verifyChallenge}), so the caller has an authenticated
|
|
475
|
-
* client without a second round-trip. The response's `deviceId` +
|
|
476
|
-
* `deviceSecret` are the zero-cookie restore credential the caller persists.
|
|
477
|
-
*/
|
|
478
|
-
passwordSignIn(identifier: string, password: string, options?: {
|
|
479
|
-
deviceName?: string;
|
|
480
|
-
deviceFingerprint?: string;
|
|
481
|
-
deviceId?: string;
|
|
482
|
-
}): Promise<LoginResult>;
|
|
483
|
-
/**
|
|
484
|
-
* Complete a 2FA-gated sign-in started by {@link passwordSignIn}. Presents
|
|
485
|
-
* the short-lived `loginToken` with either a TOTP `token` or a `backupCode`
|
|
486
|
-
* to `POST /security/2fa/verify-login`, which must resolve to the session
|
|
487
|
-
* arm of {@link LoginResult} (a second 2FA challenge here is a protocol
|
|
488
|
-
* error). A returned access token is planted immediately.
|
|
489
|
-
*/
|
|
490
|
-
completeTwoFactorSignIn(params: {
|
|
491
|
-
loginToken: string;
|
|
492
|
-
token?: string;
|
|
493
|
-
backupCode?: string;
|
|
494
|
-
deviceName?: string;
|
|
495
|
-
deviceId?: string;
|
|
496
|
-
}): Promise<LoginSessionResult>;
|
|
497
453
|
/**
|
|
498
454
|
* Begin a WebAuthn / passkey REGISTRATION ceremony. Requests the
|
|
499
455
|
* `PublicKeyCredentialCreationOptions` the browser's `navigator.credentials
|
|
@@ -543,9 +499,9 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
543
499
|
* Finish a WebAuthn / passkey AUTHENTICATION ceremony. Forwards the opaque
|
|
544
500
|
* browser `AuthenticationResponseJSON` (`response`) alongside the
|
|
545
501
|
* device-session envelope. Resolves to the SAME {@link LoginResult} contract
|
|
546
|
-
* as `POST /auth/verify`;
|
|
547
|
-
*
|
|
548
|
-
*
|
|
502
|
+
* as `POST /auth/verify`; the access token is planted immediately, and the
|
|
503
|
+
* response's `deviceId` + `deviceSecret` are the zero-cookie restore
|
|
504
|
+
* credential.
|
|
549
505
|
*/
|
|
550
506
|
webauthnLoginVerify(response: unknown, envelope?: {
|
|
551
507
|
deviceName?: string;
|
|
@@ -554,7 +510,7 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
554
510
|
}): Promise<LoginResult>;
|
|
555
511
|
/**
|
|
556
512
|
* Exchange an OAuth authorization code (returned to the RP redirect URI
|
|
557
|
-
* after
|
|
513
|
+
* after sign-in at auth.oxy.so) for a device-first session.
|
|
558
514
|
* Public first-party clients use PKCE (`codeVerifier`); the access token is
|
|
559
515
|
* planted immediately on success.
|
|
560
516
|
*/
|
|
@@ -33,7 +33,7 @@ import type { OxyServicesBase } from '../OxyServices.base';
|
|
|
33
33
|
*/
|
|
34
34
|
export type IdentityRecordType = OxySignedRecordType;
|
|
35
35
|
/** Auth-method types that can be unlinked via {@link OxyServicesIdentityMixin}. */
|
|
36
|
-
export type UnlinkableAuthMethodType = 'identity' | '
|
|
36
|
+
export type UnlinkableAuthMethodType = 'identity' | 'webauthn';
|
|
37
37
|
/**
|
|
38
38
|
* Result of a link/unlink auth-method mutation (`POST /auth/link`,
|
|
39
39
|
* `DELETE /auth/link/:type`).
|
|
@@ -106,14 +106,6 @@ export declare function OxyServicesIdentityMixin<T extends typeof OxyServicesBas
|
|
|
106
106
|
* exact bytes the server reconstructs and verifies.
|
|
107
107
|
*/
|
|
108
108
|
linkIdentityKey(): Promise<LinkAuthMethodResult>;
|
|
109
|
-
/**
|
|
110
|
-
* Link password authentication to the current account. Adds a `password`
|
|
111
|
-
* auth method (does not remove existing methods).
|
|
112
|
-
*
|
|
113
|
-
* @param email - The email to associate with password auth.
|
|
114
|
-
* @param password - The new password (server enforces strength rules).
|
|
115
|
-
*/
|
|
116
|
-
linkPassword(email: string, password: string): Promise<LinkAuthMethodResult>;
|
|
117
109
|
/**
|
|
118
110
|
* Unlink an authentication method from the current account. The server
|
|
119
111
|
* refuses to remove the last remaining method (the account would become
|
|
@@ -122,6 +114,18 @@ export declare function OxyServicesIdentityMixin<T extends typeof OxyServicesBas
|
|
|
122
114
|
* @param type - The auth-method type to remove.
|
|
123
115
|
*/
|
|
124
116
|
unlinkAuthMethod(type: UnlinkableAuthMethodType): Promise<LinkAuthMethodResult>;
|
|
117
|
+
/**
|
|
118
|
+
* Remove ONE passkey (WebAuthn credential) from the current account.
|
|
119
|
+
*
|
|
120
|
+
* Passkeys are per-credential, so unlike {@link unlinkAuthMethod} (which
|
|
121
|
+
* removes an auth method by type) this targets a specific credential id.
|
|
122
|
+
* The server refuses to remove the last remaining auth method (the account
|
|
123
|
+
* would become inaccessible) and deletes the stored `WebauthnCredential`.
|
|
124
|
+
*
|
|
125
|
+
* @param credentialId - The passkey's public credential id
|
|
126
|
+
* (`AuthMethodEntry.credentialId`).
|
|
127
|
+
*/
|
|
128
|
+
removePasskey(credentialId: string): Promise<LinkAuthMethodResult>;
|
|
125
129
|
/**
|
|
126
130
|
* Sign a record with the on-device identity key, WITHOUT publishing it.
|
|
127
131
|
* The subject is the current user's DID. NATIVE-ONLY (requires a stored
|
|
@@ -12,18 +12,23 @@
|
|
|
12
12
|
* - the unified account list (via {@link projectSwitchableAccounts}), fetched
|
|
13
13
|
* from `SessionClient` state ∪ `oxyServices.listAccounts()` and hydrated
|
|
14
14
|
* with `oxyServices.getUsersByIds()`;
|
|
15
|
-
* - the dialog `view` state machine (`accounts` | `signin` | `qr` | `add`
|
|
15
|
+
* - the dialog `view` state machine (`accounts` | `signin` | `qr` | `add` |
|
|
16
|
+
* `signup`);
|
|
16
17
|
* - `switchTo` (the uniform switch: `SessionClient.switchAccount` for an
|
|
17
18
|
* account already on the device, `oxyServices.switchToAccount` to mint on
|
|
18
19
|
* first entry into a graph account — reusing the existing SDK primitives, no
|
|
19
20
|
* new switch path);
|
|
20
21
|
* - the "Sign in with Oxy" device flow (same-device shared-keychain via
|
|
21
22
|
* `oxyServices.signInWithSharedIdentity`, else the cross-device QR handoff
|
|
22
|
-
* via `startCommonsSignIn` → poll → `claimSessionByToken`)
|
|
23
|
+
* via `startCommonsSignIn` → poll → `claimSessionByToken`);
|
|
24
|
+
* - `commonsAvailability` — whether Commons is installed on this device
|
|
25
|
+
* (native only, via the injected `canOpenApp` probe), so the QR view can
|
|
26
|
+
* offer a "Get Commons" fallback instead of a same-device dead end.
|
|
23
27
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
28
|
+
* Sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff —
|
|
29
|
+
* password, social login, and 2FA were removed ecosystem-wide. Account
|
|
30
|
+
* creation (`signup` view) is the same two identity backends: a passkey
|
|
31
|
+
* ceremony on web, or a Commons-created identity.
|
|
27
32
|
*/
|
|
28
33
|
import type { OxyServices } from '../OxyServices';
|
|
29
34
|
import type { SessionLoginResponse, MinimalUserData } from '../models/session';
|
|
@@ -31,7 +36,19 @@ import type { SessionClient } from './SessionClient';
|
|
|
31
36
|
import type { SocketIOFactory } from './socketLoader';
|
|
32
37
|
import { type SwitchableAccount } from './accountProjection';
|
|
33
38
|
/** The dialog's top-level view. */
|
|
34
|
-
export type AccountDialogView = 'accounts' | 'signin' | 'qr' | 'add';
|
|
39
|
+
export type AccountDialogView = 'accounts' | 'signin' | 'qr' | 'add' | 'signup';
|
|
40
|
+
/**
|
|
41
|
+
* Whether Commons is installed on this device, as resolved by the injected
|
|
42
|
+
* `canOpenApp` probe:
|
|
43
|
+
* - `'unknown'` — not yet probed, OR no probe was injected (web — there is
|
|
44
|
+
* no API to ask a browser whether a custom URL scheme is registered, so
|
|
45
|
+
* this stays `'unknown'` forever there and the QR view renders
|
|
46
|
+
* unconditionally, no gating).
|
|
47
|
+
* - `'checking'` — the probe is in flight.
|
|
48
|
+
* - `'available'` / `'unavailable'` — the probe's resolved terminal answer
|
|
49
|
+
* (native only). A probe error is treated as `'unavailable'` (fail-closed).
|
|
50
|
+
*/
|
|
51
|
+
export type CommonsAvailability = 'unknown' | 'checking' | 'available' | 'unavailable';
|
|
35
52
|
/** Lifecycle phase of the "Sign in with Oxy" device flow. */
|
|
36
53
|
export type SignInFlowPhase = 'idle' | 'starting' | 'waiting' | 'authorized' | 'error';
|
|
37
54
|
/** State of the "Sign in with Oxy" (shared-key / QR) device flow. */
|
|
@@ -68,6 +85,8 @@ export interface AccountDialogSnapshot {
|
|
|
68
85
|
switchingAccountId: string | null;
|
|
69
86
|
/** The "Sign in with Oxy" device-flow state. */
|
|
70
87
|
signIn: SignInFlowState;
|
|
88
|
+
/** Whether Commons is installed on this device. See {@link CommonsAvailability}. */
|
|
89
|
+
commonsAvailability: CommonsAvailability;
|
|
71
90
|
}
|
|
72
91
|
/** Construction options for {@link AccountDialogController}. */
|
|
73
92
|
export interface AccountDialogControllerOptions {
|
|
@@ -85,25 +104,38 @@ export interface AccountDialogControllerOptions {
|
|
|
85
104
|
/** Locale for display-name resolution. */
|
|
86
105
|
locale?: string;
|
|
87
106
|
/**
|
|
88
|
-
* Commit a freshly-authorized session (device flow / shared identity
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
107
|
+
* Commit a freshly-authorized SIGN-IN session (device flow / shared identity)
|
|
108
|
+
* into the host's session set — device-first registration + durable persist +
|
|
109
|
+
* profile hydration. The consumer supplies its provider's commit path
|
|
110
|
+
* (`useOxy().handleWebSession` / the auth-sdk equivalent). Called AFTER the SDK
|
|
111
|
+
* has planted the access token. When omitted the controller falls back to
|
|
112
|
+
* `SessionClient.registerAndActivate` (registration + activation only — no
|
|
113
|
+
* provider-side durable persist/hydration).
|
|
114
|
+
*
|
|
115
|
+
* This is the SIGN-IN commit: on an official web origin it may run the
|
|
116
|
+
* cross-origin hub-sync (a full-page redirect to `auth.oxy.so/sync`) that
|
|
117
|
+
* bootstraps silent OAuth restore on OTHER origins. A first sign-in on a web
|
|
118
|
+
* origin legitimately needs that. An account SWITCH does NOT — see
|
|
119
|
+
* {@link commitSwitchedSession}.
|
|
95
120
|
*/
|
|
96
121
|
commitSession?: (session: SessionLoginResponse) => Promise<void>;
|
|
97
|
-
/** Notified after a completed sign-in (bearer planted + session committed). */
|
|
98
|
-
onSignedIn?: (user: MinimalUserData) => void;
|
|
99
|
-
/** Central IdP apex for `openPasswordAtOxyAuth` (defaults to `CENTRAL_IDP_APEX`). */
|
|
100
|
-
idpApex?: string;
|
|
101
122
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
123
|
+
* Commit a minted graph SWITCH session into the host's session set — same
|
|
124
|
+
* device-first registration + durable persist + profile hydration as
|
|
125
|
+
* {@link commitSession}, but IN-PLACE: it must NOT trigger the cross-origin
|
|
126
|
+
* hub-sync redirect. Switching into an account you already operate reuses the
|
|
127
|
+
* device credential that was already hub-synced at the original sign-in, so
|
|
128
|
+
* re-syncing is redundant and a full-page redirect on switch is the exact
|
|
129
|
+
* regression this separation prevents. Cross-tab/app propagation of the switch
|
|
130
|
+
* still happens instantly via the server's device-scoped `session_state` /
|
|
131
|
+
* `session_accounts_changed` socket broadcast — no navigation required.
|
|
132
|
+
*
|
|
133
|
+
* When omitted the controller falls back to {@link commitSession} (if wired)
|
|
134
|
+
* and then to `SessionClient.registerAndActivate`.
|
|
105
135
|
*/
|
|
106
|
-
|
|
136
|
+
commitSwitchedSession?: (session: SessionLoginResponse) => Promise<void>;
|
|
137
|
+
/** Notified after a completed sign-in (bearer planted + session committed). */
|
|
138
|
+
onSignedIn?: (user: MinimalUserData) => void;
|
|
107
139
|
/**
|
|
108
140
|
* QR device-flow FALLBACK poll interval in ms (default 12000). The primary
|
|
109
141
|
* approval signal is the `/auth-session` socket's `auth_update` event (instant);
|
|
@@ -119,8 +151,8 @@ export interface AccountDialogControllerOptions {
|
|
|
119
151
|
*/
|
|
120
152
|
socketFactory?: SocketIOFactory;
|
|
121
153
|
/**
|
|
122
|
-
* Optional URL opener. When provided,
|
|
123
|
-
* the
|
|
154
|
+
* Optional URL opener. When provided, the controller invokes it to deep-link
|
|
155
|
+
* the Commons app for the QR handoff (web: `location.assign`; native:
|
|
124
156
|
* `Linking.openURL`). Headless core never touches `window`/`Linking` itself.
|
|
125
157
|
*/
|
|
126
158
|
openUrl?: (url: string) => void;
|
|
@@ -142,9 +174,8 @@ export declare class AccountDialogController {
|
|
|
142
174
|
private readonly clientId;
|
|
143
175
|
private readonly locale?;
|
|
144
176
|
private readonly commitSession?;
|
|
177
|
+
private readonly commitSwitchedSession?;
|
|
145
178
|
private readonly onSignedIn?;
|
|
146
|
-
private readonly idpApex;
|
|
147
|
-
private readonly authRedirectUri;
|
|
148
179
|
private readonly pollIntervalMs;
|
|
149
180
|
private readonly openUrl?;
|
|
150
181
|
private readonly canOpenApp?;
|
|
@@ -157,6 +188,7 @@ export declare class AccountDialogController {
|
|
|
157
188
|
private error;
|
|
158
189
|
private switchingAccountId;
|
|
159
190
|
private signIn;
|
|
191
|
+
private commonsAvailability;
|
|
160
192
|
/** The secret device-flow token of the active QR flow (never surfaced). */
|
|
161
193
|
private signInToken;
|
|
162
194
|
private pollTimer;
|
|
@@ -225,6 +257,8 @@ export declare class AccountDialogController {
|
|
|
225
257
|
close(): void;
|
|
226
258
|
/** Switch to the "add account" view (the sign-in entry chooser). */
|
|
227
259
|
add(): void;
|
|
260
|
+
/** Switch to the "create account" view (passkey / Commons signup entry). */
|
|
261
|
+
startSignup(): void;
|
|
228
262
|
/**
|
|
229
263
|
* Reload the account graph and per-account profiles, then re-project. Safe to
|
|
230
264
|
* call repeatedly; concurrent calls are reconciled by a sequence guard so a
|
|
@@ -270,32 +304,27 @@ export declare class AccountDialogController {
|
|
|
270
304
|
*/
|
|
271
305
|
showQr(): Promise<void>;
|
|
272
306
|
/**
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
307
|
+
* Resolve whether Commons is installed on this device via the injected
|
|
308
|
+
* `canOpenApp` probe, updating {@link commonsAvailability} as durable,
|
|
309
|
+
* observable snapshot state. Native only — a no-op when `canOpenApp` was
|
|
310
|
+
* not injected (web), where `commonsAvailability` stays `'unknown'` forever
|
|
311
|
+
* and the QR view renders unconditionally (no gating).
|
|
312
|
+
*
|
|
313
|
+
* Replaces the old `maybeOpenCommons` fire-and-forget probe, whose outcome
|
|
314
|
+
* was only ever reflected by whether Commons silently opened — a probe
|
|
315
|
+
* failure or "not installed" answer was swallowed into a debug log with no
|
|
316
|
+
* way for the UI to react. `commonsAvailability` fixes that.
|
|
276
317
|
*/
|
|
277
|
-
private
|
|
278
|
-
/** Tear down the active sign-in device flow (timers + socket + token) and reset to idle. */
|
|
279
|
-
cancelSignIn(): void;
|
|
318
|
+
private resolveCommonsAvailability;
|
|
280
319
|
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* `BroadcastChannel`, so the URL only needs to point at the IdP sign-in with
|
|
286
|
-
* the right return.
|
|
287
|
-
*
|
|
288
|
-
* @param params.returnUrl - Where the IdP returns after login. Defaults to the
|
|
289
|
-
* current document URL on web (`globalThis.location.href`); pass explicitly
|
|
290
|
-
* on native (no `location`).
|
|
291
|
-
* @param params.state - Optional opaque state echoed back on return.
|
|
292
|
-
* @returns The absolute auth.oxy.so sign-in URL.
|
|
320
|
+
* When Commons is confirmed installed, deep-link straight into its approve
|
|
321
|
+
* screen via the injected `openUrl` with the same `oxycommons://approve?...`
|
|
322
|
+
* payload the QR encodes. Best-effort and non-blocking — the QR/polling
|
|
323
|
+
* fallback stays live regardless of the outcome here.
|
|
293
324
|
*/
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
redirectUri?: string;
|
|
298
|
-
}): Promise<string>;
|
|
325
|
+
private deepLinkIntoCommonsIfAvailable;
|
|
326
|
+
/** Tear down the active sign-in device flow (timers + socket + token) and reset to idle. */
|
|
327
|
+
cancelSignIn(): void;
|
|
299
328
|
private scheduleNextPoll;
|
|
300
329
|
/**
|
|
301
330
|
* Run one status check + (on approval) claim. Triggered by the fallback timer
|
|
@@ -313,8 +342,14 @@ export declare class AccountDialogController {
|
|
|
313
342
|
private completeSignIn;
|
|
314
343
|
/**
|
|
315
344
|
* Register a token-planted session into the device set. Prefers the
|
|
316
|
-
* consumer's
|
|
345
|
+
* consumer's commit funnel (durable persist + hydration); falls back to
|
|
317
346
|
* `SessionClient.registerAndActivate` (registration + activation only).
|
|
347
|
+
*
|
|
348
|
+
* A SWITCH (`opts.fromSwitch`) uses the IN-PLACE `commitSwitchedSession` funnel
|
|
349
|
+
* so it never runs the cross-origin hub-sync redirect; a SIGN-IN uses
|
|
350
|
+
* `commitSession` (which may hub-sync on an official web origin). When the
|
|
351
|
+
* switch funnel is not wired it falls back to the sign-in funnel, then to
|
|
352
|
+
* `registerAndActivate`.
|
|
318
353
|
*/
|
|
319
354
|
private commitAuthorizedSession;
|
|
320
355
|
private failSignIn;
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
export declare function buildIdpHubOrigin(): string;
|
|
6
6
|
/** Whether the current web origin is the central IdP hub (`auth.oxy.so`). */
|
|
7
7
|
export declare function isIdpHubOrigin(): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Whether an origin is a loopback / local-dev origin (`localhost`, `127.0.0.1`,
|
|
10
|
+
* or `[::1]` on any port, http or https). Local dev must never be bounced to a
|
|
11
|
+
* hosted IdP for cross-origin session restore.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isLoopbackOrigin(origin: string): boolean;
|
|
8
14
|
/** Whether an origin belongs to the official Oxy web ecosystem. */
|
|
9
15
|
export declare function isOfficialWebOrigin(origin: string): boolean;
|
|
10
16
|
/** Normalize and validate a return URL against official origins. Returns origin only. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.1.0",
|
|
4
4
|
"description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
}
|
|
110
110
|
},
|
|
111
111
|
"dependencies": {
|
|
112
|
-
"@oxyhq/contracts": "^0.
|
|
112
|
+
"@oxyhq/contracts": "^0.15.0",
|
|
113
113
|
"@oxyhq/protocol": "^0.1.5",
|
|
114
114
|
"bip39": "^3.1.0",
|
|
115
115
|
"buffer": "^6.0.3",
|
package/src/index.ts
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.
|
|
@@ -558,6 +558,7 @@ export {
|
|
|
558
558
|
buildIdpHubOrigin,
|
|
559
559
|
buildHubSyncUrl,
|
|
560
560
|
isIdpHubOrigin,
|
|
561
|
+
isLoopbackOrigin,
|
|
561
562
|
isOfficialWebOrigin,
|
|
562
563
|
isAllowedDeviceJoinOrigin,
|
|
563
564
|
normalizeOfficialReturnOrigin,
|
|
@@ -610,9 +611,10 @@ export type {
|
|
|
610
611
|
|
|
611
612
|
// Headless controller for the unified account dialog. Framework-agnostic
|
|
612
613
|
// state machine + subscribe/getSnapshot store (bind via `useSyncExternalStore`)
|
|
613
|
-
// —
|
|
614
|
-
//
|
|
615
|
-
//
|
|
614
|
+
// — sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff;
|
|
615
|
+
// password, social login, and 2FA were removed ecosystem-wide. Reuses
|
|
616
|
+
// `SessionClient.switchAccount` / `oxyServices.switchToAccount` for the uniform
|
|
617
|
+
// switch and the existing device-flow methods for sign-in.
|
|
616
618
|
export {
|
|
617
619
|
AccountDialogController,
|
|
618
620
|
createAccountDialogController,
|
|
@@ -621,6 +623,7 @@ export type {
|
|
|
621
623
|
AccountDialogControllerOptions,
|
|
622
624
|
AccountDialogSnapshot,
|
|
623
625
|
AccountDialogView,
|
|
626
|
+
CommonsAvailability,
|
|
624
627
|
SignInFlowPhase,
|
|
625
628
|
SignInFlowState,
|
|
626
629
|
} from './session/accountDialogController';
|
|
@@ -1058,144 +1058,6 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1058
1058
|
}
|
|
1059
1059
|
}
|
|
1060
1060
|
|
|
1061
|
-
/**
|
|
1062
|
-
* Register a new user with email/username and password
|
|
1063
|
-
*/
|
|
1064
|
-
async signUp(
|
|
1065
|
-
username: string,
|
|
1066
|
-
email: string,
|
|
1067
|
-
password: string,
|
|
1068
|
-
deviceName?: string,
|
|
1069
|
-
deviceFingerprint?: any
|
|
1070
|
-
): Promise<SessionLoginResponse> {
|
|
1071
|
-
try {
|
|
1072
|
-
const session = await this.makeRequest<SessionLoginResponse>('POST', '/auth/signup', {
|
|
1073
|
-
username,
|
|
1074
|
-
email,
|
|
1075
|
-
password,
|
|
1076
|
-
deviceName,
|
|
1077
|
-
deviceFingerprint,
|
|
1078
|
-
}, { cache: false });
|
|
1079
|
-
return {
|
|
1080
|
-
...session,
|
|
1081
|
-
user: normalizeUserIdentity(session.user),
|
|
1082
|
-
};
|
|
1083
|
-
} catch (error) {
|
|
1084
|
-
throw this.handleError(error);
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
/**
|
|
1089
|
-
* Sign in with email or username and password
|
|
1090
|
-
*/
|
|
1091
|
-
async signIn(
|
|
1092
|
-
identifier: string,
|
|
1093
|
-
password: string,
|
|
1094
|
-
deviceName?: string,
|
|
1095
|
-
deviceFingerprint?: any
|
|
1096
|
-
): Promise<SessionLoginResponse> {
|
|
1097
|
-
try {
|
|
1098
|
-
const session = await this.makeRequest<SessionLoginResponse>('POST', '/auth/login', {
|
|
1099
|
-
identifier,
|
|
1100
|
-
password,
|
|
1101
|
-
deviceName,
|
|
1102
|
-
deviceFingerprint,
|
|
1103
|
-
}, { cache: false });
|
|
1104
|
-
return {
|
|
1105
|
-
...session,
|
|
1106
|
-
user: normalizeUserIdentity(session.user),
|
|
1107
|
-
};
|
|
1108
|
-
} catch (error) {
|
|
1109
|
-
throw this.handleError(error);
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
/**
|
|
1114
|
-
* Convenience helper for email sign-in
|
|
1115
|
-
*/
|
|
1116
|
-
async signInWithEmail(
|
|
1117
|
-
email: string,
|
|
1118
|
-
password: string,
|
|
1119
|
-
deviceName?: string,
|
|
1120
|
-
deviceFingerprint?: any
|
|
1121
|
-
): Promise<SessionLoginResponse> {
|
|
1122
|
-
return this.signIn(email, password, deviceName, deviceFingerprint);
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
/**
|
|
1126
|
-
* Device-first password sign-in. Unlike the legacy {@link signIn} (which
|
|
1127
|
-
* assumes a one-step session and is kept intact for existing callers until
|
|
1128
|
-
* the F4 cutover), this returns the FULL `POST /auth/login` contract — the
|
|
1129
|
-
* discriminated {@link LoginResult}: either a 2FA challenge
|
|
1130
|
-
* (`{ twoFactorRequired, loginToken }`) to complete via
|
|
1131
|
-
* {@link completeTwoFactorSignIn}, or a session arm.
|
|
1132
|
-
*
|
|
1133
|
-
* On the session arm, a returned access token is planted immediately
|
|
1134
|
-
* (mirroring {@link verifyChallenge}), so the caller has an authenticated
|
|
1135
|
-
* client without a second round-trip. The response's `deviceId` +
|
|
1136
|
-
* `deviceSecret` are the zero-cookie restore credential the caller persists.
|
|
1137
|
-
*/
|
|
1138
|
-
async passwordSignIn(
|
|
1139
|
-
identifier: string,
|
|
1140
|
-
password: string,
|
|
1141
|
-
options: { deviceName?: string; deviceFingerprint?: string; deviceId?: string } = {},
|
|
1142
|
-
): Promise<LoginResult> {
|
|
1143
|
-
try {
|
|
1144
|
-
const res = await this.makeRequest<unknown>('POST', '/auth/login', {
|
|
1145
|
-
identifier,
|
|
1146
|
-
password,
|
|
1147
|
-
deviceName: options.deviceName,
|
|
1148
|
-
deviceFingerprint: options.deviceFingerprint,
|
|
1149
|
-
...(options.deviceId ? { deviceId: options.deviceId } : {}),
|
|
1150
|
-
}, { cache: false });
|
|
1151
|
-
const parsed = safeParseContract(loginResultSchema, res);
|
|
1152
|
-
if (!parsed) {
|
|
1153
|
-
throw new Error('auth/login returned an unexpected response shape');
|
|
1154
|
-
}
|
|
1155
|
-
if (!('twoFactorRequired' in parsed) && parsed.accessToken) {
|
|
1156
|
-
this.setTokens(parsed.accessToken);
|
|
1157
|
-
}
|
|
1158
|
-
return parsed;
|
|
1159
|
-
} catch (error) {
|
|
1160
|
-
throw this.handleError(error);
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
/**
|
|
1165
|
-
* Complete a 2FA-gated sign-in started by {@link passwordSignIn}. Presents
|
|
1166
|
-
* the short-lived `loginToken` with either a TOTP `token` or a `backupCode`
|
|
1167
|
-
* to `POST /security/2fa/verify-login`, which must resolve to the session
|
|
1168
|
-
* arm of {@link LoginResult} (a second 2FA challenge here is a protocol
|
|
1169
|
-
* error). A returned access token is planted immediately.
|
|
1170
|
-
*/
|
|
1171
|
-
async completeTwoFactorSignIn(params: {
|
|
1172
|
-
loginToken: string;
|
|
1173
|
-
token?: string;
|
|
1174
|
-
backupCode?: string;
|
|
1175
|
-
deviceName?: string;
|
|
1176
|
-
deviceId?: string;
|
|
1177
|
-
}): Promise<LoginSessionResult> {
|
|
1178
|
-
try {
|
|
1179
|
-
const res = await this.makeRequest<unknown>('POST', '/security/2fa/verify-login', {
|
|
1180
|
-
loginToken: params.loginToken,
|
|
1181
|
-
token: params.token,
|
|
1182
|
-
backupCode: params.backupCode,
|
|
1183
|
-
deviceName: params.deviceName,
|
|
1184
|
-
...(params.deviceId ? { deviceId: params.deviceId } : {}),
|
|
1185
|
-
}, { cache: false });
|
|
1186
|
-
const parsed = safeParseContract(loginResultSchema, res);
|
|
1187
|
-
if (!parsed || 'twoFactorRequired' in parsed) {
|
|
1188
|
-
throw new Error('security/2fa/verify-login returned an unexpected response shape');
|
|
1189
|
-
}
|
|
1190
|
-
if (parsed.accessToken) {
|
|
1191
|
-
this.setTokens(parsed.accessToken);
|
|
1192
|
-
}
|
|
1193
|
-
return parsed;
|
|
1194
|
-
} catch (error) {
|
|
1195
|
-
throw this.handleError(error);
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
1061
|
/**
|
|
1200
1062
|
* Begin a WebAuthn / passkey REGISTRATION ceremony. Requests the
|
|
1201
1063
|
* `PublicKeyCredentialCreationOptions` the browser's `navigator.credentials
|
|
@@ -1258,7 +1120,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1258
1120
|
if (!parsed) {
|
|
1259
1121
|
throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
|
|
1260
1122
|
}
|
|
1261
|
-
if (
|
|
1123
|
+
if (parsed.accessToken) {
|
|
1262
1124
|
this.setTokens(parsed.accessToken);
|
|
1263
1125
|
}
|
|
1264
1126
|
return parsed;
|
|
@@ -1301,9 +1163,9 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1301
1163
|
* Finish a WebAuthn / passkey AUTHENTICATION ceremony. Forwards the opaque
|
|
1302
1164
|
* browser `AuthenticationResponseJSON` (`response`) alongside the
|
|
1303
1165
|
* device-session envelope. Resolves to the SAME {@link LoginResult} contract
|
|
1304
|
-
* as `POST /auth/verify`;
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
1166
|
+
* as `POST /auth/verify`; the access token is planted immediately, and the
|
|
1167
|
+
* response's `deviceId` + `deviceSecret` are the zero-cookie restore
|
|
1168
|
+
* credential.
|
|
1307
1169
|
*/
|
|
1308
1170
|
async webauthnLoginVerify(
|
|
1309
1171
|
response: unknown,
|
|
@@ -1320,7 +1182,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1320
1182
|
if (!parsed) {
|
|
1321
1183
|
throw new Error('auth/webauthn/login/verify returned an unexpected response shape');
|
|
1322
1184
|
}
|
|
1323
|
-
if (
|
|
1185
|
+
if (parsed.accessToken) {
|
|
1324
1186
|
this.setTokens(parsed.accessToken);
|
|
1325
1187
|
}
|
|
1326
1188
|
return parsed;
|
|
@@ -1331,7 +1193,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1331
1193
|
|
|
1332
1194
|
/**
|
|
1333
1195
|
* Exchange an OAuth authorization code (returned to the RP redirect URI
|
|
1334
|
-
* after
|
|
1196
|
+
* after sign-in at auth.oxy.so) for a device-first session.
|
|
1335
1197
|
* Public first-party clients use PKCE (`codeVerifier`); the access token is
|
|
1336
1198
|
* planted immediately on success.
|
|
1337
1199
|
*/
|