@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.
Files changed (30) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/index.js +7 -5
  3. package/dist/cjs/mixins/OxyServices.auth.js +6 -117
  4. package/dist/cjs/mixins/OxyServices.identity.js +16 -12
  5. package/dist/cjs/session/accountDialogController.js +84 -75
  6. package/dist/cjs/utils/officialOrigins.js +6 -0
  7. package/dist/esm/.tsbuildinfo +1 -1
  8. package/dist/esm/index.js +6 -5
  9. package/dist/esm/mixins/OxyServices.auth.js +6 -117
  10. package/dist/esm/mixins/OxyServices.identity.js +16 -12
  11. package/dist/esm/session/accountDialogController.js +84 -75
  12. package/dist/esm/utils/officialOrigins.js +6 -1
  13. package/dist/types/.tsbuildinfo +1 -1
  14. package/dist/types/index.d.ts +3 -3
  15. package/dist/types/mixins/OxyServices.auth.d.ts +4 -48
  16. package/dist/types/mixins/OxyServices.identity.d.ts +13 -9
  17. package/dist/types/session/accountDialogController.d.ts +84 -49
  18. package/dist/types/utils/officialOrigins.d.ts +6 -0
  19. package/package.json +2 -2
  20. package/src/index.ts +7 -4
  21. package/src/mixins/OxyServices.auth.ts +6 -144
  22. package/src/mixins/OxyServices.identity.ts +19 -15
  23. package/src/mixins/__tests__/OxyServices.identity.test.ts +26 -14
  24. package/src/mixins/__tests__/webauthnAuth.test.ts +1 -1
  25. package/src/session/__tests__/accountDialogController.test.ts +75 -64
  26. package/src/session/__tests__/accountProjection.test.ts +30 -0
  27. package/src/session/accountDialogController.ts +131 -104
  28. package/src/utils/__tests__/officialOrigins.test.ts +14 -0
  29. package/src/utils/officialOrigins.ts +6 -1
  30. package/src/mixins/__tests__/passwordSignIn.test.ts +0 -115
@@ -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.
@@ -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`; on the session arm the access token is planted
547
- * immediately (mirroring {@link passwordSignIn}), and the response's
548
- * `deviceId` + `deviceSecret` are the zero-cookie restore credential.
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 password sign-in at auth.oxy.so) for a device-first session.
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' | 'password' | 'google' | 'apple' | 'github';
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
- * 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.
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 / minted
89
- * graph switch) into the host's session set — device-first registration +
90
- * durable persist + profile hydration. The consumer supplies its provider's
91
- * commit path (`useOxy().handleWebSession` / the auth-sdk equivalent). Called
92
- * AFTER the SDK has planted the access token. When omitted the controller
93
- * falls back to `SessionClient.registerAndActivate` (registration + activation
94
- * only — no provider-side durable persist/hydration).
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
- * 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}.
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
- authRedirectUri?: string | null;
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, `openPasswordAtOxyAuth` invokes it with
123
- * the built URL in addition to returning it (web: `location.assign`; native:
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
- * When a `canOpenApp` probe is injected and reports Commons installed, open the
274
- * approve deep link via the injected `openUrl`. Best-effort and non-blocking: a
275
- * probe/open failure is logged and swallowed the QR/polling fallback remains.
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 onlya 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 maybeOpenCommons;
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
- * 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.
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
- openPasswordAtOxyAuth(params?: {
295
- returnUrl?: string;
296
- state?: string;
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 `commitSession` (durable persist + hydration); falls back to
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": "11.0.1",
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.14.2",
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.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.
@@ -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
- // — no password/2FA logic (that lives at the IdP; `openPasswordAtOxyAuth` only
614
- // hands off). Reuses `SessionClient.switchAccount` / `oxyServices.switchToAccount`
615
- // for the uniform switch and the existing device-flow methods for sign-in.
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 (!('twoFactorRequired' in parsed) && parsed.accessToken) {
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`; on the session arm the access token is planted
1305
- * immediately (mirroring {@link passwordSignIn}), and the response's
1306
- * `deviceId` + `deviceSecret` are the zero-cookie restore credential.
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 (!('twoFactorRequired' in parsed) && parsed.accessToken) {
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 password sign-in at auth.oxy.so) for a device-first session.
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
  */