@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +13 -6
- package/dist/cjs/mixins/OxyServices.auth.js +65 -74
- 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/cjs/utils/webauthnOrigin.js +51 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +10 -5
- package/dist/esm/mixins/OxyServices.auth.js +65 -74
- 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/esm/utils/webauthnOrigin.js +48 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/mixins/OxyServices.auth.d.ts +50 -36
- 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/dist/types/utils/webauthnOrigin.d.ts +32 -0
- package/package.json +2 -2
- package/src/index.ts +11 -4
- package/src/mixins/OxyServices.auth.ts +95 -100
- 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 +206 -0
- 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/__tests__/webauthnOrigin.test.ts +83 -0
- package/src/utils/officialOrigins.ts +6 -1
- package/src/utils/webauthnOrigin.ts +52 -0
- 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.
|
|
@@ -82,11 +82,12 @@ export { buildAccountsArray, createQuickAccount, getAccountDisplayName, getAccou
|
|
|
82
82
|
export type { QuickAccount, DisplayNameUserShape } from './utils/accountUtils';
|
|
83
83
|
export { registrableApex } from './utils/registrableApex';
|
|
84
84
|
export { CENTRAL_IDP_APEX } from './utils/authWebUrl';
|
|
85
|
+
export { isOxyRpOrigin } from './utils/webauthnOrigin';
|
|
85
86
|
export { runColdBoot } from './utils/coldBoot';
|
|
86
87
|
export type { ColdBootStep, ColdBootStepResult, ColdBootSession, ColdBootSkip, ColdBootOutcome, RunColdBootOptions, } from './utils/coldBoot';
|
|
87
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';
|
|
88
89
|
export type { PkcePair, BuildOAuthAuthorizeUrlParams } from './utils/oauthPkce';
|
|
89
|
-
export { buildIdpHubOrigin, buildHubSyncUrl, isIdpHubOrigin, isOfficialWebOrigin, isAllowedDeviceJoinOrigin, normalizeOfficialReturnOrigin, parseHubSyncReturnUrl, } from './utils/officialOrigins';
|
|
90
|
+
export { buildIdpHubOrigin, buildHubSyncUrl, isIdpHubOrigin, isLoopbackOrigin, isOfficialWebOrigin, isAllowedDeviceJoinOrigin, normalizeOfficialReturnOrigin, parseHubSyncReturnUrl, } from './utils/officialOrigins';
|
|
90
91
|
export { syncHubAfterSignIn, redeemHubTicketOnHub, } from './session/hubSync';
|
|
91
92
|
export type { SyncHubAfterSignInOptions } from './session/hubSync';
|
|
92
93
|
export { SessionClient } from './session/SessionClient';
|
|
@@ -451,52 +451,66 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
451
451
|
message: string;
|
|
452
452
|
}>;
|
|
453
453
|
/**
|
|
454
|
-
*
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
*
|
|
459
|
-
|
|
460
|
-
|
|
454
|
+
* Begin a WebAuthn / passkey REGISTRATION ceremony. Requests the
|
|
455
|
+
* `PublicKeyCredentialCreationOptions` the browser's `navigator.credentials
|
|
456
|
+
* .create()` (or `@simplewebauthn/browser`'s `startRegistration`) needs.
|
|
457
|
+
*
|
|
458
|
+
* With a bearer token planted this links a passkey to the signed-in account
|
|
459
|
+
* (`username` ignored); without one it is a prospective signup and `username`
|
|
460
|
+
* is the desired handle. The returned options are OPAQUE — Oxy does not own
|
|
461
|
+
* their shape (the browser / `@simplewebauthn` does), so they pass through
|
|
462
|
+
* as `unknown` for the caller to hand straight to the ceremony.
|
|
463
|
+
*/
|
|
464
|
+
webauthnRegisterOptions(username?: string): Promise<unknown>;
|
|
465
|
+
/**
|
|
466
|
+
* Finish a WebAuthn / passkey REGISTRATION ceremony. Forwards the opaque
|
|
467
|
+
* browser `RegistrationResponseJSON` (`response`) alongside the Oxy envelope
|
|
468
|
+
* (desired `username` for signup + the device-session naming fields).
|
|
469
|
+
*
|
|
470
|
+
* Two server branches, disambiguated by the response shape:
|
|
471
|
+
* - **Signup** (no bearer): the account is created and a session minted —
|
|
472
|
+
* the response carries `sessionId`, is the SAME {@link LoginResult}
|
|
473
|
+
* contract as `POST /auth/verify`, and its access token is planted here.
|
|
474
|
+
* - **Link** (bearer present): the passkey is attached to the signed-in
|
|
475
|
+
* account and the server returns `{ success, message }` with no session,
|
|
476
|
+
* which is returned verbatim (no token planting).
|
|
477
|
+
*/
|
|
478
|
+
webauthnRegisterVerify(response: unknown, envelope?: {
|
|
479
|
+
username?: string;
|
|
480
|
+
deviceName?: string;
|
|
481
|
+
deviceFingerprint?: string;
|
|
482
|
+
deviceId?: string;
|
|
483
|
+
}): Promise<{
|
|
484
|
+
success: true;
|
|
485
|
+
message: string;
|
|
486
|
+
} | LoginResult>;
|
|
461
487
|
/**
|
|
462
|
-
*
|
|
488
|
+
* Begin a WebAuthn / passkey AUTHENTICATION ceremony. Requests the
|
|
489
|
+
* `PublicKeyCredentialRequestOptions` the browser's `navigator.credentials
|
|
490
|
+
* .get()` (or `@simplewebauthn/browser`'s `startAuthentication`) needs.
|
|
491
|
+
*
|
|
492
|
+
* When `username` is present the server scopes `allowCredentials` to that
|
|
493
|
+
* user's passkeys (username-first); when omitted it returns an empty
|
|
494
|
+
* allow-list for the usernameless / discoverable-credential flow. The
|
|
495
|
+
* returned options are OPAQUE and pass through as `unknown`.
|
|
463
496
|
*/
|
|
464
|
-
|
|
497
|
+
webauthnLoginOptions(username?: string): Promise<unknown>;
|
|
465
498
|
/**
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
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.
|
|
499
|
+
* Finish a WebAuthn / passkey AUTHENTICATION ceremony. Forwards the opaque
|
|
500
|
+
* browser `AuthenticationResponseJSON` (`response`) alongside the
|
|
501
|
+
* device-session envelope. Resolves to the SAME {@link LoginResult} contract
|
|
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.
|
|
477
505
|
*/
|
|
478
|
-
|
|
506
|
+
webauthnLoginVerify(response: unknown, envelope?: {
|
|
479
507
|
deviceName?: string;
|
|
480
508
|
deviceFingerprint?: string;
|
|
481
509
|
deviceId?: string;
|
|
482
510
|
}): 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
511
|
/**
|
|
498
512
|
* Exchange an OAuth authorization code (returned to the RP redirect URI
|
|
499
|
-
* after
|
|
513
|
+
* after sign-in at auth.oxy.so) for a device-first session.
|
|
500
514
|
* Public first-party clients use PKCE (`codeVerifier`); the access token is
|
|
501
515
|
* planted immediately on success.
|
|
502
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
|
|
@@ -21,9 +21,8 @@
|
|
|
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 type { OxyServices } from '../OxyServices';
|
|
29
28
|
import type { SessionLoginResponse, MinimalUserData } from '../models/session';
|
|
@@ -96,14 +95,6 @@ export interface AccountDialogControllerOptions {
|
|
|
96
95
|
commitSession?: (session: SessionLoginResponse) => Promise<void>;
|
|
97
96
|
/** Notified after a completed sign-in (bearer planted + session committed). */
|
|
98
97
|
onSignedIn?: (user: MinimalUserData) => void;
|
|
99
|
-
/** Central IdP apex for `openPasswordAtOxyAuth` (defaults to `CENTRAL_IDP_APEX`). */
|
|
100
|
-
idpApex?: string;
|
|
101
|
-
/**
|
|
102
|
-
* Registered OAuth redirect URI for this RP (exact match against
|
|
103
|
-
* `Application.redirectUris`). When set, wins over `returnUrl` /
|
|
104
|
-
* `location.origin` normalization in {@link openPasswordAtOxyAuth}.
|
|
105
|
-
*/
|
|
106
|
-
authRedirectUri?: string | null;
|
|
107
98
|
/**
|
|
108
99
|
* QR device-flow FALLBACK poll interval in ms (default 12000). The primary
|
|
109
100
|
* approval signal is the `/auth-session` socket's `auth_update` event (instant);
|
|
@@ -119,8 +110,8 @@ export interface AccountDialogControllerOptions {
|
|
|
119
110
|
*/
|
|
120
111
|
socketFactory?: SocketIOFactory;
|
|
121
112
|
/**
|
|
122
|
-
* Optional URL opener. When provided,
|
|
123
|
-
* the
|
|
113
|
+
* Optional URL opener. When provided, the controller invokes it to deep-link
|
|
114
|
+
* the Commons app for the QR handoff (web: `location.assign`; native:
|
|
124
115
|
* `Linking.openURL`). Headless core never touches `window`/`Linking` itself.
|
|
125
116
|
*/
|
|
126
117
|
openUrl?: (url: string) => void;
|
|
@@ -143,8 +134,6 @@ export declare class AccountDialogController {
|
|
|
143
134
|
private readonly locale?;
|
|
144
135
|
private readonly commitSession?;
|
|
145
136
|
private readonly onSignedIn?;
|
|
146
|
-
private readonly idpApex;
|
|
147
|
-
private readonly authRedirectUri;
|
|
148
137
|
private readonly pollIntervalMs;
|
|
149
138
|
private readonly openUrl?;
|
|
150
139
|
private readonly canOpenApp?;
|
|
@@ -277,25 +266,6 @@ export declare class AccountDialogController {
|
|
|
277
266
|
private maybeOpenCommons;
|
|
278
267
|
/** Tear down the active sign-in device flow (timers + socket + token) and reset to idle. */
|
|
279
268
|
cancelSignIn(): void;
|
|
280
|
-
/**
|
|
281
|
-
* Build (and, when an `openUrl` handler was supplied, open) the auth.oxy.so
|
|
282
|
-
* password sign-in URL. Password + 2FA are NOT in the SDK — they live at the
|
|
283
|
-
* IdP; this only hands off. Device-first: after login at the IdP the device
|
|
284
|
-
* session converges and the caller is woken via the device socket /
|
|
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.
|
|
293
|
-
*/
|
|
294
|
-
openPasswordAtOxyAuth(params?: {
|
|
295
|
-
returnUrl?: string;
|
|
296
|
-
state?: string;
|
|
297
|
-
redirectUri?: string;
|
|
298
|
-
}): Promise<string>;
|
|
299
269
|
private scheduleNextPoll;
|
|
300
270
|
/**
|
|
301
271
|
* Run one status check + (on approval) claim. Triggered by the fallback timer
|
|
@@ -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. */
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebAuthn relying-party origin guard (client side).
|
|
3
|
+
*
|
|
4
|
+
* The passkey ceremonies (`OxyServices.webauthn*`) are only meaningful when the
|
|
5
|
+
* page is served from a first-party Oxy web origin: a credential minted with
|
|
6
|
+
* `WEBAUTHN_RP_ID=oxy.so` can only be created/asserted from `oxy.so`, one of its
|
|
7
|
+
* subdomains, or a loopback dev server. This is the browser-side mirror of the
|
|
8
|
+
* server's `isOxyApexOrigin` (`packages/api/src/utils/origin.ts`), which forms
|
|
9
|
+
* the server's `expectedOrigin` allow-set — consumers use it to decide whether to
|
|
10
|
+
* even offer the passkey UI on the current page.
|
|
11
|
+
*
|
|
12
|
+
* It reads `globalThis.location` directly (no argument) because that is the only
|
|
13
|
+
* origin the browser will let a WebAuthn ceremony run against. On native / SSR /
|
|
14
|
+
* any environment without a DOM `location`, it returns `false` (there is no
|
|
15
|
+
* relying-party origin, so passkeys are not applicable).
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* True iff the current page's host is a first-party Oxy relying-party origin:
|
|
19
|
+
* `oxy.so`, any `*.oxy.so` subdomain, or a loopback dev host
|
|
20
|
+
* (`localhost` / `127.0.0.1` / `[::1]`).
|
|
21
|
+
*
|
|
22
|
+
* Fails closed: no `location` (native/SSR), a non-string/empty hostname, or a
|
|
23
|
+
* host that merely ends in the literal `oxy.so` without the dot boundary
|
|
24
|
+
* (`evil-oxy.so`, `oxy.so.evil.com`) all return `false`.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* isOxyRpOrigin() // true on https://accounts.oxy.so
|
|
28
|
+
* isOxyRpOrigin() // true on http://localhost:8081
|
|
29
|
+
* isOxyRpOrigin() // false on https://evil.com
|
|
30
|
+
* isOxyRpOrigin() // false in a React Native / SSR context (no location)
|
|
31
|
+
*/
|
|
32
|
+
export declare function isOxyRpOrigin(): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.0.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.
|
|
@@ -517,6 +517,11 @@ export type { QuickAccount, DisplayNameUserShape } from './utils/accountUtils';
|
|
|
517
517
|
export { registrableApex } from './utils/registrableApex';
|
|
518
518
|
export { CENTRAL_IDP_APEX } from './utils/authWebUrl';
|
|
519
519
|
|
|
520
|
+
// WebAuthn relying-party origin guard (client side). Mirrors the server's
|
|
521
|
+
// `isOxyApexOrigin` so consumers can decide whether to offer passkey UI on the
|
|
522
|
+
// current page (first-party Oxy origin / loopback only).
|
|
523
|
+
export { isOxyRpOrigin } from './utils/webauthnOrigin';
|
|
524
|
+
|
|
520
525
|
export { runColdBoot } from './utils/coldBoot';
|
|
521
526
|
export type {
|
|
522
527
|
ColdBootStep,
|
|
@@ -553,6 +558,7 @@ export {
|
|
|
553
558
|
buildIdpHubOrigin,
|
|
554
559
|
buildHubSyncUrl,
|
|
555
560
|
isIdpHubOrigin,
|
|
561
|
+
isLoopbackOrigin,
|
|
556
562
|
isOfficialWebOrigin,
|
|
557
563
|
isAllowedDeviceJoinOrigin,
|
|
558
564
|
normalizeOfficialReturnOrigin,
|
|
@@ -605,9 +611,10 @@ export type {
|
|
|
605
611
|
|
|
606
612
|
// Headless controller for the unified account dialog. Framework-agnostic
|
|
607
613
|
// state machine + subscribe/getSnapshot store (bind via `useSyncExternalStore`)
|
|
608
|
-
// —
|
|
609
|
-
//
|
|
610
|
-
//
|
|
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.
|
|
611
618
|
export {
|
|
612
619
|
AccountDialogController,
|
|
613
620
|
createAccountDialogController,
|
|
@@ -1059,133 +1059,128 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1059
1059
|
}
|
|
1060
1060
|
|
|
1061
1061
|
/**
|
|
1062
|
-
*
|
|
1062
|
+
* Begin a WebAuthn / passkey REGISTRATION ceremony. Requests the
|
|
1063
|
+
* `PublicKeyCredentialCreationOptions` the browser's `navigator.credentials
|
|
1064
|
+
* .create()` (or `@simplewebauthn/browser`'s `startRegistration`) needs.
|
|
1065
|
+
*
|
|
1066
|
+
* With a bearer token planted this links a passkey to the signed-in account
|
|
1067
|
+
* (`username` ignored); without one it is a prospective signup and `username`
|
|
1068
|
+
* is the desired handle. The returned options are OPAQUE — Oxy does not own
|
|
1069
|
+
* their shape (the browser / `@simplewebauthn` does), so they pass through
|
|
1070
|
+
* as `unknown` for the caller to hand straight to the ceremony.
|
|
1063
1071
|
*/
|
|
1064
|
-
async
|
|
1065
|
-
username: string,
|
|
1066
|
-
email: string,
|
|
1067
|
-
password: string,
|
|
1068
|
-
deviceName?: string,
|
|
1069
|
-
deviceFingerprint?: any
|
|
1070
|
-
): Promise<SessionLoginResponse> {
|
|
1072
|
+
async webauthnRegisterOptions(username?: string): Promise<unknown> {
|
|
1071
1073
|
try {
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}, { cache: false });
|
|
1079
|
-
return {
|
|
1080
|
-
...session,
|
|
1081
|
-
user: normalizeUserIdentity(session.user),
|
|
1082
|
-
};
|
|
1074
|
+
return await this.makeRequest<unknown>(
|
|
1075
|
+
'POST',
|
|
1076
|
+
'/auth/webauthn/register/options',
|
|
1077
|
+
{ ...(username !== undefined ? { username } : {}) },
|
|
1078
|
+
{ cache: false },
|
|
1079
|
+
);
|
|
1083
1080
|
} catch (error) {
|
|
1084
1081
|
throw this.handleError(error);
|
|
1085
1082
|
}
|
|
1086
1083
|
}
|
|
1087
1084
|
|
|
1088
1085
|
/**
|
|
1089
|
-
*
|
|
1086
|
+
* Finish a WebAuthn / passkey REGISTRATION ceremony. Forwards the opaque
|
|
1087
|
+
* browser `RegistrationResponseJSON` (`response`) alongside the Oxy envelope
|
|
1088
|
+
* (desired `username` for signup + the device-session naming fields).
|
|
1089
|
+
*
|
|
1090
|
+
* Two server branches, disambiguated by the response shape:
|
|
1091
|
+
* - **Signup** (no bearer): the account is created and a session minted —
|
|
1092
|
+
* the response carries `sessionId`, is the SAME {@link LoginResult}
|
|
1093
|
+
* contract as `POST /auth/verify`, and its access token is planted here.
|
|
1094
|
+
* - **Link** (bearer present): the passkey is attached to the signed-in
|
|
1095
|
+
* account and the server returns `{ success, message }` with no session,
|
|
1096
|
+
* which is returned verbatim (no token planting).
|
|
1090
1097
|
*/
|
|
1091
|
-
async
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1098
|
+
async webauthnRegisterVerify(
|
|
1099
|
+
response: unknown,
|
|
1100
|
+
envelope: {
|
|
1101
|
+
username?: string;
|
|
1102
|
+
deviceName?: string;
|
|
1103
|
+
deviceFingerprint?: string;
|
|
1104
|
+
deviceId?: string;
|
|
1105
|
+
} = {},
|
|
1106
|
+
): Promise<{ success: true; message: string } | LoginResult> {
|
|
1097
1107
|
try {
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
+
const res = await this.makeRequest<unknown>(
|
|
1109
|
+
'POST',
|
|
1110
|
+
'/auth/webauthn/register/verify',
|
|
1111
|
+
{ response, ...envelope },
|
|
1112
|
+
{ cache: false },
|
|
1113
|
+
);
|
|
1114
|
+
if (res && typeof res === 'object') {
|
|
1115
|
+
const record = res as Record<string, unknown>;
|
|
1116
|
+
// Signup branch: mints a session (LoginSessionResult, carries
|
|
1117
|
+
// `sessionId`). Parse against the login contract and plant the token.
|
|
1118
|
+
if ('sessionId' in record) {
|
|
1119
|
+
const parsed = safeParseContract(loginResultSchema, record);
|
|
1120
|
+
if (!parsed) {
|
|
1121
|
+
throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
|
|
1122
|
+
}
|
|
1123
|
+
if (parsed.accessToken) {
|
|
1124
|
+
this.setTokens(parsed.accessToken);
|
|
1125
|
+
}
|
|
1126
|
+
return parsed;
|
|
1127
|
+
}
|
|
1128
|
+
// Link branch: passkey attached to the signed-in account, no session.
|
|
1129
|
+
if (record.success === true && typeof record.message === 'string') {
|
|
1130
|
+
return { success: true, message: record.message };
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
|
|
1108
1134
|
} catch (error) {
|
|
1109
1135
|
throw this.handleError(error);
|
|
1110
1136
|
}
|
|
1111
1137
|
}
|
|
1112
1138
|
|
|
1113
1139
|
/**
|
|
1114
|
-
*
|
|
1115
|
-
|
|
1116
|
-
|
|
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.
|
|
1140
|
+
* Begin a WebAuthn / passkey AUTHENTICATION ceremony. Requests the
|
|
1141
|
+
* `PublicKeyCredentialRequestOptions` the browser's `navigator.credentials
|
|
1142
|
+
* .get()` (or `@simplewebauthn/browser`'s `startAuthentication`) needs.
|
|
1132
1143
|
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1144
|
+
* When `username` is present the server scopes `allowCredentials` to that
|
|
1145
|
+
* user's passkeys (username-first); when omitted it returns an empty
|
|
1146
|
+
* allow-list for the usernameless / discoverable-credential flow. The
|
|
1147
|
+
* returned options are OPAQUE and pass through as `unknown`.
|
|
1137
1148
|
*/
|
|
1138
|
-
async
|
|
1139
|
-
identifier: string,
|
|
1140
|
-
password: string,
|
|
1141
|
-
options: { deviceName?: string; deviceFingerprint?: string; deviceId?: string } = {},
|
|
1142
|
-
): Promise<LoginResult> {
|
|
1149
|
+
async webauthnLoginOptions(username?: string): Promise<unknown> {
|
|
1143
1150
|
try {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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;
|
|
1151
|
+
return await this.makeRequest<unknown>(
|
|
1152
|
+
'POST',
|
|
1153
|
+
'/auth/webauthn/login/options',
|
|
1154
|
+
{ ...(username !== undefined ? { username } : {}) },
|
|
1155
|
+
{ cache: false },
|
|
1156
|
+
);
|
|
1159
1157
|
} catch (error) {
|
|
1160
1158
|
throw this.handleError(error);
|
|
1161
1159
|
}
|
|
1162
1160
|
}
|
|
1163
1161
|
|
|
1164
1162
|
/**
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1163
|
+
* Finish a WebAuthn / passkey AUTHENTICATION ceremony. Forwards the opaque
|
|
1164
|
+
* browser `AuthenticationResponseJSON` (`response`) alongside the
|
|
1165
|
+
* device-session envelope. Resolves to the SAME {@link LoginResult} contract
|
|
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.
|
|
1170
1169
|
*/
|
|
1171
|
-
async
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
deviceName?: string;
|
|
1176
|
-
deviceId?: string;
|
|
1177
|
-
}): Promise<LoginSessionResult> {
|
|
1170
|
+
async webauthnLoginVerify(
|
|
1171
|
+
response: unknown,
|
|
1172
|
+
envelope: { deviceName?: string; deviceFingerprint?: string; deviceId?: string } = {},
|
|
1173
|
+
): Promise<LoginResult> {
|
|
1178
1174
|
try {
|
|
1179
|
-
const res = await this.makeRequest<unknown>(
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}, { cache: false });
|
|
1175
|
+
const res = await this.makeRequest<unknown>(
|
|
1176
|
+
'POST',
|
|
1177
|
+
'/auth/webauthn/login/verify',
|
|
1178
|
+
{ response, ...envelope },
|
|
1179
|
+
{ cache: false },
|
|
1180
|
+
);
|
|
1186
1181
|
const parsed = safeParseContract(loginResultSchema, res);
|
|
1187
|
-
if (!parsed
|
|
1188
|
-
throw new Error('
|
|
1182
|
+
if (!parsed) {
|
|
1183
|
+
throw new Error('auth/webauthn/login/verify returned an unexpected response shape');
|
|
1189
1184
|
}
|
|
1190
1185
|
if (parsed.accessToken) {
|
|
1191
1186
|
this.setTokens(parsed.accessToken);
|
|
@@ -1198,7 +1193,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1198
1193
|
|
|
1199
1194
|
/**
|
|
1200
1195
|
* Exchange an OAuth authorization code (returned to the RP redirect URI
|
|
1201
|
-
* after
|
|
1196
|
+
* after sign-in at auth.oxy.so) for a device-first session.
|
|
1202
1197
|
* Public first-party clients use PKCE (`codeVerifier`); the access token is
|
|
1203
1198
|
* planted immediately on success.
|
|
1204
1199
|
*/
|