@oxyhq/core 9.1.0 → 9.2.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/README.md +1 -1
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/boot/{coldBootV2.js → sessionColdBoot.js} +2 -2
- package/dist/cjs/index.js +23 -4
- package/dist/cjs/mixins/OxyServices.auth.js +61 -0
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +29 -1
- package/dist/cjs/server/index.js +8 -1
- package/dist/cjs/session/SessionClient.js +57 -25
- package/dist/cjs/session/accountDialogController.js +20 -9
- package/dist/cjs/session/authStateStore.js +13 -7
- package/dist/cjs/session/hubSync.js +55 -0
- package/dist/cjs/session/sessionClientHost.js +5 -0
- package/dist/cjs/utils/coldBoot.js +1 -1
- package/dist/cjs/utils/oauthPkce.js +65 -1
- package/dist/cjs/utils/officialOrigins.js +128 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/boot/{coldBootV2.js → sessionColdBoot.js} +2 -2
- package/dist/esm/index.js +4 -2
- package/dist/esm/mixins/OxyServices.auth.js +61 -0
- package/dist/esm/mixins/OxyServices.deviceBoot.js +30 -2
- package/dist/esm/server/index.js +1 -0
- package/dist/esm/session/SessionClient.js +57 -25
- package/dist/esm/session/accountDialogController.js +20 -9
- package/dist/esm/session/authStateStore.js +13 -7
- package/dist/esm/session/hubSync.js +51 -0
- package/dist/esm/session/sessionClientHost.js +5 -0
- package/dist/esm/utils/coldBoot.js +1 -1
- package/dist/esm/utils/oauthPkce.js +60 -0
- package/dist/esm/utils/officialOrigins.js +119 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/boot/{coldBootV2.d.ts → sessionColdBoot.d.ts} +1 -1
- package/dist/types/index.d.ts +7 -4
- package/dist/types/mixins/OxyServices.auth.d.ts +14 -0
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +6 -2
- package/dist/types/server/index.d.ts +1 -0
- package/dist/types/session/SessionClient.d.ts +6 -0
- package/dist/types/session/accountDialogController.d.ts +9 -1
- package/dist/types/session/authStateStore.d.ts +1 -1
- package/dist/types/session/hubSync.d.ts +20 -0
- package/dist/types/session/sessionClientHost.d.ts +2 -1
- package/dist/types/utils/coldBoot.d.ts +2 -2
- package/dist/types/utils/oauthPkce.d.ts +27 -0
- package/dist/types/utils/officialOrigins.d.ts +17 -0
- package/package.json +2 -2
- package/src/boot/__tests__/{coldBootV2.test.ts → sessionColdBoot.test.ts} +1 -1
- package/src/boot/{coldBootV2.ts → sessionColdBoot.ts} +2 -2
- package/src/index.ts +27 -3
- package/src/mixins/OxyServices.auth.ts +70 -1
- package/src/mixins/OxyServices.deviceBoot.ts +46 -1
- package/src/server/index.ts +8 -0
- package/src/session/SessionClient.ts +68 -26
- package/src/session/__tests__/SessionClient.additive.test.ts +1 -0
- package/src/session/__tests__/SessionClient.broadcastChannel.test.ts +1 -0
- package/src/session/__tests__/SessionClient.diagnostics.test.ts +1 -0
- package/src/session/__tests__/SessionClient.rest.test.ts +1 -0
- package/src/session/__tests__/SessionClient.socket.test.ts +1 -0
- package/src/session/__tests__/SessionClient.socketFactory.test.ts +1 -0
- package/src/session/__tests__/SessionClient.state.test.ts +1 -0
- package/src/session/__tests__/accountDialogController.test.ts +38 -6
- package/src/session/__tests__/sessionIntegration.test.ts +7 -0
- package/src/session/accountDialogController.ts +36 -10
- package/src/session/authStateStore.ts +18 -9
- package/src/session/hubSync.ts +79 -0
- package/src/session/sessionClientHost.ts +10 -2
- package/src/utils/__tests__/officialOrigins.test.ts +74 -0
- package/src/utils/coldBoot.ts +2 -2
- package/src/utils/oauthPkce.ts +71 -0
- package/src/utils/officialOrigins.ts +124 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Device-first session cold boot for every consumer.
|
|
3
3
|
*
|
|
4
4
|
* On a fresh page load / app launch this resolves the device's session in a
|
|
5
5
|
* deterministic order, built on the pure `runColdBoot` primitive. It NEVER
|
package/dist/types/index.d.ts
CHANGED
|
@@ -83,10 +83,13 @@ export { registrableApex } from './utils/registrableApex';
|
|
|
83
83
|
export { CENTRAL_IDP_APEX } from './utils/authWebUrl';
|
|
84
84
|
export { runColdBoot } from './utils/coldBoot';
|
|
85
85
|
export type { ColdBootStep, ColdBootStepResult, ColdBootSession, ColdBootSkip, ColdBootOutcome, RunColdBootOptions, } from './utils/coldBoot';
|
|
86
|
-
export { buildOAuthAuthorizeUrl, computeCodeChallenge, generateOAuthState, generatePkcePair, DEFAULT_OAUTH_SCOPE, OXY_AUTHORIZE_URL, } from './utils/oauthPkce';
|
|
86
|
+
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';
|
|
87
87
|
export type { PkcePair, BuildOAuthAuthorizeUrlParams } from './utils/oauthPkce';
|
|
88
|
+
export { buildIdpHubOrigin, buildHubSyncUrl, isIdpHubOrigin, isOfficialWebOrigin, isAllowedDeviceJoinOrigin, normalizeOfficialReturnOrigin, parseHubSyncReturnUrl, } from './utils/officialOrigins';
|
|
89
|
+
export { syncHubAfterSignIn, redeemHubTicketOnHub, } from './session/hubSync';
|
|
90
|
+
export type { SyncHubAfterSignInOptions } from './session/hubSync';
|
|
88
91
|
export { SessionClient } from './session/SessionClient';
|
|
89
|
-
export type { TokenTransport, SessionClientHost, SessionClientOptions } from './session/SessionClient';
|
|
92
|
+
export type { TokenTransport, SessionClientHost, SessionClientOptions, DeviceCredential } from './session/SessionClient';
|
|
90
93
|
export type { SocketIOFactory, MinimalSocket } from './session/socketLoader';
|
|
91
94
|
export { createSessionClientHost } from './session/sessionClientHost';
|
|
92
95
|
export { createSessionClient } from './session/createSessionClient';
|
|
@@ -99,8 +102,8 @@ export { createWebAuthStateStore, createNativeAuthStateStore, createMemoryAuthSt
|
|
|
99
102
|
export type { PersistedAuthState, AuthStateStore, NativeKeyValueStorage, } from './session/authStateStore';
|
|
100
103
|
export { refreshPersistedSession, createAuthRefreshHandler, installAuthRefreshHandler, startTokenRefreshScheduler, TOKEN_REFRESH_LEAD_MS, } from './session/refresh';
|
|
101
104
|
export type { RefreshDeps, TokenRefreshSchedulerHandle } from './session/refresh';
|
|
102
|
-
export { runSessionColdBoot } from './boot/
|
|
103
|
-
export type { RunSessionColdBootOptions, SignedOutReason, DeviceBootSession, } from './boot/
|
|
105
|
+
export { runSessionColdBoot } from './boot/sessionColdBoot';
|
|
106
|
+
export type { RunSessionColdBootOptions, SignedOutReason, DeviceBootSession, } from './boot/sessionColdBoot';
|
|
104
107
|
export { packageInfo } from './constants/version';
|
|
105
108
|
import { OxyServices } from './OxyServices';
|
|
106
109
|
export default OxyServices;
|
|
@@ -477,6 +477,7 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
477
477
|
passwordSignIn(identifier: string, password: string, options?: {
|
|
478
478
|
deviceName?: string;
|
|
479
479
|
deviceFingerprint?: string;
|
|
480
|
+
deviceId?: string;
|
|
480
481
|
}): Promise<LoginResult>;
|
|
481
482
|
/**
|
|
482
483
|
* Complete a 2FA-gated sign-in started by {@link passwordSignIn}. Presents
|
|
@@ -490,6 +491,19 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
490
491
|
token?: string;
|
|
491
492
|
backupCode?: string;
|
|
492
493
|
deviceName?: string;
|
|
494
|
+
deviceId?: string;
|
|
495
|
+
}): Promise<LoginSessionResult>;
|
|
496
|
+
/**
|
|
497
|
+
* Exchange an OAuth authorization code (returned to the RP redirect URI
|
|
498
|
+
* after password sign-in at auth.oxy.so) for a device-first session.
|
|
499
|
+
* Public first-party clients use PKCE (`codeVerifier`); the access token is
|
|
500
|
+
* planted immediately on success.
|
|
501
|
+
*/
|
|
502
|
+
exchangeOAuthCode(params: {
|
|
503
|
+
code: string;
|
|
504
|
+
clientId: string;
|
|
505
|
+
redirectUri: string;
|
|
506
|
+
codeVerifier: string;
|
|
493
507
|
}): Promise<LoginSessionResult>;
|
|
494
508
|
httpService: import("../HttpService").HttpService;
|
|
495
509
|
cloudURL: string;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Device-first token mint mixin.
|
|
3
3
|
*
|
|
4
4
|
* The client half of the zero-cookie device transport: the single network call
|
|
5
|
-
* the cold boot (`
|
|
5
|
+
* the cold boot (`sessionColdBoot`) and the unified re-mint handler (`refresh.ts`)
|
|
6
6
|
* make to turn a first-party `deviceId` + `deviceSecret` into a fresh access
|
|
7
7
|
* token. The response is validated against the `@oxyhq/contracts`
|
|
8
8
|
* `deviceTokenMintResponseSchema`, so producer (oxy-api) and consumer cannot
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* the cold boot / re-mint handler own persistence and `setTokens`, so the same
|
|
14
14
|
* primitive can be reused from either without double-planting.
|
|
15
15
|
*/
|
|
16
|
-
import { type DeviceTokenMintResponse } from '@oxyhq/contracts';
|
|
16
|
+
import { type DeviceTokenMintResponse, type DeviceHubTicketIssueResponse, type DeviceHubTicketRedeemResponse } from '@oxyhq/contracts';
|
|
17
17
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
18
18
|
export declare function OxyServicesDeviceBootMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
19
19
|
new (...args: any[]): {
|
|
@@ -33,6 +33,10 @@ export declare function OxyServicesDeviceBootMixin<T extends typeof OxyServicesB
|
|
|
33
33
|
* @throws if the response does not match {@link deviceTokenMintResponseSchema}.
|
|
34
34
|
*/
|
|
35
35
|
mintFromDeviceSecret(deviceId: string, deviceSecret: string): Promise<DeviceTokenMintResponse>;
|
|
36
|
+
/** Mint a one-time hub sync ticket (bearer required). */
|
|
37
|
+
issueHubTicket(returnOrigin: string): Promise<DeviceHubTicketIssueResponse>;
|
|
38
|
+
/** Redeem a hub sync ticket for a fresh device secret (public). */
|
|
39
|
+
redeemHubTicket(ticket: string, returnOrigin: string): Promise<DeviceHubTicketRedeemResponse>;
|
|
36
40
|
httpService: import("../HttpService").HttpService;
|
|
37
41
|
cloudURL: string;
|
|
38
42
|
config: import("../OxyServices.base").OxyConfig;
|
|
@@ -24,3 +24,4 @@ export { createOxyCors } from './cors';
|
|
|
24
24
|
export type { OxyCorsOptions } from './cors';
|
|
25
25
|
export { verifySecret } from './verifySecret';
|
|
26
26
|
export { registrableApex } from '../utils/registrableApex';
|
|
27
|
+
export { buildIdpHubOrigin, buildHubSyncUrl, isIdpHubOrigin, isOfficialWebOrigin, normalizeOfficialReturnOrigin, parseHubSyncReturnUrl, } from '../utils/officialOrigins';
|
|
@@ -4,12 +4,18 @@ export interface TokenTransport {
|
|
|
4
4
|
/** Ensure this app holds a per-domain access token for state.activeAccountId (mint via the persisted refresh family / shared keychain). Best-effort. */
|
|
5
5
|
ensureActiveToken(state: DeviceSessionState): Promise<void>;
|
|
6
6
|
}
|
|
7
|
+
export interface DeviceCredential {
|
|
8
|
+
deviceId: string;
|
|
9
|
+
deviceSecret: string;
|
|
10
|
+
}
|
|
7
11
|
export interface SessionClientHost {
|
|
8
12
|
makeRequest<T>(method: 'GET' | 'POST', url: string, data?: unknown, options?: {
|
|
9
13
|
cache?: boolean;
|
|
10
14
|
}): Promise<T>;
|
|
11
15
|
getBaseURL(): string;
|
|
12
16
|
getAccessToken(): string | null;
|
|
17
|
+
/** Zero-cookie device credential for socket handshake when no bearer is planted yet. */
|
|
18
|
+
getDeviceCredential(): DeviceCredential | null;
|
|
13
19
|
onTokensChanged(listener: (token: string | null) => void): () => void;
|
|
14
20
|
setTokens(accessToken: string): void;
|
|
15
21
|
getCurrentAccountId(): string | null;
|
|
@@ -97,6 +97,12 @@ export interface AccountDialogControllerOptions {
|
|
|
97
97
|
onSignedIn?: (user: MinimalUserData) => void;
|
|
98
98
|
/** Central IdP apex for `openPasswordAtOxyAuth` (defaults to `CENTRAL_IDP_APEX`). */
|
|
99
99
|
idpApex?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Registered OAuth redirect URI for this RP (exact match against
|
|
102
|
+
* `Application.redirectUris`). When set, wins over `returnUrl` /
|
|
103
|
+
* `location.origin` normalization in {@link openPasswordAtOxyAuth}.
|
|
104
|
+
*/
|
|
105
|
+
authRedirectUri?: string | null;
|
|
100
106
|
/** QR device-flow poll interval in ms (default 3000). */
|
|
101
107
|
pollIntervalMs?: number;
|
|
102
108
|
/**
|
|
@@ -115,6 +121,7 @@ export declare class AccountDialogController {
|
|
|
115
121
|
private readonly commitSession?;
|
|
116
122
|
private readonly onSignedIn?;
|
|
117
123
|
private readonly idpApex;
|
|
124
|
+
private readonly authRedirectUri;
|
|
118
125
|
private readonly pollIntervalMs;
|
|
119
126
|
private readonly openUrl?;
|
|
120
127
|
private readonly listeners;
|
|
@@ -244,7 +251,8 @@ export declare class AccountDialogController {
|
|
|
244
251
|
openPasswordAtOxyAuth(params?: {
|
|
245
252
|
returnUrl?: string;
|
|
246
253
|
state?: string;
|
|
247
|
-
|
|
254
|
+
redirectUri?: string;
|
|
255
|
+
}): Promise<string>;
|
|
248
256
|
private scheduleNextPoll;
|
|
249
257
|
private pollOnce;
|
|
250
258
|
private claimAndComplete;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* a reload restores the session locally without a redirect: `deviceId` +
|
|
6
6
|
* `deviceSecret` mint a fresh access token via `POST /session/device/token`.
|
|
7
7
|
* This module is the storage seam: a tiny `load / save / clear` interface plus
|
|
8
|
-
* platform factories, so the cold boot (`
|
|
8
|
+
* platform factories, so the cold boot (`sessionColdBoot`) and the unified re-mint
|
|
9
9
|
* handler (`refresh.ts`) never touch a platform storage API directly.
|
|
10
10
|
*
|
|
11
11
|
* Platform-agnostic — the native factory takes an INJECTED key/value store
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-sign-in hub sync — plant device credentials on auth.oxy.so via a
|
|
3
|
+
* one-time server ticket (no secrets in URL fragments).
|
|
4
|
+
*/
|
|
5
|
+
import type { OxyServices } from '../OxyServices';
|
|
6
|
+
import type { AuthStateStore } from './authStateStore';
|
|
7
|
+
export interface SyncHubAfterSignInOptions {
|
|
8
|
+
/** Skip sync when false (OxyProvider hubSync prop). @default true */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* After a successful sign-in on an official web app, mint a hub ticket and
|
|
13
|
+
* redirect to auth.oxy.so/sync so the IdP hub can redeem it and persist the
|
|
14
|
+
* shared device credential for silent OAuth restore on other origins.
|
|
15
|
+
*
|
|
16
|
+
* No-op on native, non-official origins, and when already on the IdP hub.
|
|
17
|
+
*/
|
|
18
|
+
export declare function syncHubAfterSignIn(oxy: Pick<OxyServices, 'issueHubTicket'>, opts?: SyncHubAfterSignInOptions): Promise<boolean>;
|
|
19
|
+
/** Redeem a hub ticket on auth.oxy.so and persist credentials locally. */
|
|
20
|
+
export declare function redeemHubTicketOnHub(oxy: Pick<OxyServices, 'redeemHubTicket'>, store: AuthStateStore, ticket: string): Promise<boolean>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OxyServices } from '../OxyServices';
|
|
2
|
-
import type { SessionClientHost } from './SessionClient';
|
|
2
|
+
import type { DeviceCredential, SessionClientHost } from './SessionClient';
|
|
3
3
|
/**
|
|
4
4
|
* Thin `SessionClientHost` adapter over an `OxyServices` instance.
|
|
5
5
|
*
|
|
@@ -14,4 +14,5 @@ import type { SessionClientHost } from './SessionClient';
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function createSessionClientHost(oxyServices: OxyServices): SessionClientHost & {
|
|
16
16
|
setCurrentAccountId(id: string | null): void;
|
|
17
|
+
setDeviceCredential(credential: DeviceCredential | null): void;
|
|
17
18
|
};
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* web bundle — the reason any run-once guard for a step must live in the
|
|
17
17
|
* calling consumer, never in a core module-level singleton).
|
|
18
18
|
* - Architecture-agnostic: it knows nothing about HOW a step resolves a
|
|
19
|
-
* session; `runSessionColdBoot` (`boot/
|
|
19
|
+
* session; `runSessionColdBoot` (`boot/sessionColdBoot.ts`) is the current
|
|
20
20
|
* device-first consumer.
|
|
21
21
|
*
|
|
22
22
|
* A step is skipped (without running) when its `enabled` predicate returns
|
|
@@ -94,7 +94,7 @@ export interface RunColdBootOptions<S> {
|
|
|
94
94
|
* fails to settle before the deadline, the runner abandons the await for that
|
|
95
95
|
* step (reporting it via `onStepDeadline`) and CONTINUES to the next step,
|
|
96
96
|
* each now racing against an already-expired deadline. This is deliberate:
|
|
97
|
-
* the runner keeps iterating so the TERMINAL step (e.g. `
|
|
97
|
+
* the runner keeps iterating so the TERMINAL step (e.g. `sessionColdBoot`'s
|
|
98
98
|
* `bootstrap-hop`, whose `run()` performs its navigation side effect
|
|
99
99
|
* synchronously before its first `await`) still gets to fire. A step that
|
|
100
100
|
* has nothing to contribute after the deadline simply doesn't settle and is
|
|
@@ -42,6 +42,11 @@ export interface BuildOAuthAuthorizeUrlParams {
|
|
|
42
42
|
state: string;
|
|
43
43
|
/** The PKCE `codeChallenge` from {@link generatePkcePair}. */
|
|
44
44
|
codeChallenge: string;
|
|
45
|
+
/**
|
|
46
|
+
* OAuth `prompt` parameter. Use `none` for silent cross-origin session restore
|
|
47
|
+
* (no UI when the IdP hub already has a session + grant).
|
|
48
|
+
*/
|
|
49
|
+
prompt?: 'none' | 'login' | 'consent';
|
|
45
50
|
}
|
|
46
51
|
/**
|
|
47
52
|
* Compute the PKCE S256 `code_challenge` for a given verifier:
|
|
@@ -72,3 +77,25 @@ export declare function generateOAuthState(): Promise<string>;
|
|
|
72
77
|
* All values are percent-encoded by `URL.searchParams`.
|
|
73
78
|
*/
|
|
74
79
|
export declare function buildOAuthAuthorizeUrl(params: BuildOAuthAuthorizeUrlParams): string;
|
|
80
|
+
/** `sessionStorage` key for the OAuth CSRF `state` across an authorize redirect. */
|
|
81
|
+
export declare const OXY_OAUTH_STATE_STORAGE_KEY = "oxy_oauth_state";
|
|
82
|
+
/** `sessionStorage` key for the PKCE `code_verifier` across an authorize redirect. */
|
|
83
|
+
export declare const OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = "oxy_oauth_code_verifier";
|
|
84
|
+
/** `sessionStorage` key — at most one silent OAuth attempt per navigation. */
|
|
85
|
+
export declare const OXY_SILENT_OAUTH_ATTEMPTED_KEY = "oxy.silent_oauth_attempted";
|
|
86
|
+
/** `sessionStorage` key — blocks further cross-origin auto-restore in this tab. */
|
|
87
|
+
export declare const OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY = "oxy.cross_origin_restore_attempted";
|
|
88
|
+
/**
|
|
89
|
+
* Normalize a redirect URI to its origin. Official Oxy apps register apex
|
|
90
|
+
* origins (`https://inbox.oxy.so`) — never path-qualified URLs.
|
|
91
|
+
*/
|
|
92
|
+
export declare function normalizeOAuthRedirectUri(input: string): string;
|
|
93
|
+
/** Persist the OAuth handshake for a full-page redirect return (web only). */
|
|
94
|
+
export declare function persistOAuthHandshake(state: string, codeVerifier: string): boolean;
|
|
95
|
+
/** Read the persisted OAuth handshake, or `null` when absent. */
|
|
96
|
+
export declare function readOAuthHandshake(): {
|
|
97
|
+
state: string;
|
|
98
|
+
codeVerifier: string;
|
|
99
|
+
} | null;
|
|
100
|
+
/** Drop persisted OAuth handshake keys after a successful or aborted return. */
|
|
101
|
+
export declare function clearOAuthHandshake(): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official first-party web origin allowlist — shared by hub-ticket issuance,
|
|
3
|
+
* OAuth redirect validation, and cross-origin session restore.
|
|
4
|
+
*/
|
|
5
|
+
export declare function buildIdpHubOrigin(): string;
|
|
6
|
+
/** Whether the current web origin is the central IdP hub (`auth.oxy.so`). */
|
|
7
|
+
export declare function isIdpHubOrigin(): boolean;
|
|
8
|
+
/** Whether an origin belongs to the official Oxy web ecosystem. */
|
|
9
|
+
export declare function isOfficialWebOrigin(origin: string): boolean;
|
|
10
|
+
/** Normalize and validate a return URL against official origins. Returns origin only. */
|
|
11
|
+
export declare function normalizeOfficialReturnOrigin(raw: string): string | null;
|
|
12
|
+
/** Validate a hub-sync return URL; returns the full normalized URL string. */
|
|
13
|
+
export declare function parseHubSyncReturnUrl(raw: string | null): string | null;
|
|
14
|
+
/** Build auth.oxy.so/sync redirect URL with a one-time hub ticket. */
|
|
15
|
+
export declare function buildHubSyncUrl(ticket: string, returnUrl?: string): string;
|
|
16
|
+
/** @deprecated Use {@link isOfficialWebOrigin}. */
|
|
17
|
+
export declare const isAllowedDeviceJoinOrigin: typeof isOfficialWebOrigin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.2.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",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
|
-
"@oxyhq/contracts": "^0.13.
|
|
97
|
+
"@oxyhq/contracts": "^0.13.1",
|
|
98
98
|
"@oxyhq/protocol": "^0.1.3",
|
|
99
99
|
"bip39": "^3.1.0",
|
|
100
100
|
"buffer": "^6.0.3",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { OxyServices } from '../../OxyServices';
|
|
10
10
|
import type { DeviceTokenMintResponse } from '@oxyhq/contracts';
|
|
11
11
|
import type { SessionLoginResponse } from '../../models/session';
|
|
12
|
-
import { runSessionColdBoot } from '../
|
|
12
|
+
import { runSessionColdBoot } from '../sessionColdBoot';
|
|
13
13
|
import { createMemoryAuthStateStore, type PersistedAuthState } from '../../session/authStateStore';
|
|
14
14
|
|
|
15
15
|
interface OxyOverrides {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Device-first session cold boot for every consumer.
|
|
3
3
|
*
|
|
4
4
|
* On a fresh page load / app launch this resolves the device's session in a
|
|
5
5
|
* deterministic order, built on the pure `runColdBoot` primitive. It NEVER
|
|
@@ -139,7 +139,7 @@ export async function runSessionColdBoot(
|
|
|
139
139
|
// Transient (network / 5xx): keep the secret; a later attempt can succeed.
|
|
140
140
|
logger.debug(
|
|
141
141
|
'device-secret mint failed (transient) — keeping secret',
|
|
142
|
-
{ component: '
|
|
142
|
+
{ component: 'sessionColdBoot', method: 'device-secret-mint' },
|
|
143
143
|
error,
|
|
144
144
|
);
|
|
145
145
|
return { kind: 'skip' };
|
package/src/index.ts
CHANGED
|
@@ -523,14 +523,38 @@ export {
|
|
|
523
523
|
generatePkcePair,
|
|
524
524
|
DEFAULT_OAUTH_SCOPE,
|
|
525
525
|
OXY_AUTHORIZE_URL,
|
|
526
|
+
OXY_OAUTH_STATE_STORAGE_KEY,
|
|
527
|
+
OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY,
|
|
528
|
+
OXY_SILENT_OAUTH_ATTEMPTED_KEY,
|
|
529
|
+
OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY,
|
|
530
|
+
normalizeOAuthRedirectUri,
|
|
531
|
+
persistOAuthHandshake,
|
|
532
|
+
readOAuthHandshake,
|
|
533
|
+
clearOAuthHandshake,
|
|
526
534
|
} from './utils/oauthPkce';
|
|
527
535
|
export type { PkcePair, BuildOAuthAuthorizeUrlParams } from './utils/oauthPkce';
|
|
528
536
|
|
|
537
|
+
export {
|
|
538
|
+
buildIdpHubOrigin,
|
|
539
|
+
buildHubSyncUrl,
|
|
540
|
+
isIdpHubOrigin,
|
|
541
|
+
isOfficialWebOrigin,
|
|
542
|
+
isAllowedDeviceJoinOrigin,
|
|
543
|
+
normalizeOfficialReturnOrigin,
|
|
544
|
+
parseHubSyncReturnUrl,
|
|
545
|
+
} from './utils/officialOrigins';
|
|
546
|
+
|
|
547
|
+
export {
|
|
548
|
+
syncHubAfterSignIn,
|
|
549
|
+
redeemHubTicketOnHub,
|
|
550
|
+
} from './session/hubSync';
|
|
551
|
+
export type { SyncHubAfterSignInOptions } from './session/hubSync';
|
|
552
|
+
|
|
529
553
|
// ---------------------------------------------------------------------------
|
|
530
554
|
// Session sync (device-scoped multi-account session client)
|
|
531
555
|
// ---------------------------------------------------------------------------
|
|
532
556
|
export { SessionClient } from './session/SessionClient';
|
|
533
|
-
export type { TokenTransport, SessionClientHost, SessionClientOptions } from './session/SessionClient';
|
|
557
|
+
export type { TokenTransport, SessionClientHost, SessionClientOptions, DeviceCredential } from './session/SessionClient';
|
|
534
558
|
// The injectable socket factory type: consumers that bundle socket.io-client
|
|
535
559
|
// (services/auth-sdk) pass its `io` export as `socketFactory` so realtime sync
|
|
536
560
|
// never relies on core's lazy dynamic import of a bare specifier.
|
|
@@ -609,12 +633,12 @@ export {
|
|
|
609
633
|
} from './session/refresh';
|
|
610
634
|
export type { RefreshDeps, TokenRefreshSchedulerHandle } from './session/refresh';
|
|
611
635
|
|
|
612
|
-
export { runSessionColdBoot } from './boot/
|
|
636
|
+
export { runSessionColdBoot } from './boot/sessionColdBoot';
|
|
613
637
|
export type {
|
|
614
638
|
RunSessionColdBootOptions,
|
|
615
639
|
SignedOutReason,
|
|
616
640
|
DeviceBootSession,
|
|
617
|
-
} from './boot/
|
|
641
|
+
} from './boot/sessionColdBoot';
|
|
618
642
|
|
|
619
643
|
// API response contracts (request/response Zod schemas + inferred types) live in
|
|
620
644
|
// `@oxyhq/contracts` — the single source of truth shared by the backend and every
|
|
@@ -1136,7 +1136,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1136
1136
|
async passwordSignIn(
|
|
1137
1137
|
identifier: string,
|
|
1138
1138
|
password: string,
|
|
1139
|
-
options: { deviceName?: string; deviceFingerprint?: string } = {},
|
|
1139
|
+
options: { deviceName?: string; deviceFingerprint?: string; deviceId?: string } = {},
|
|
1140
1140
|
): Promise<LoginResult> {
|
|
1141
1141
|
try {
|
|
1142
1142
|
const res = await this.makeRequest<unknown>('POST', '/auth/login', {
|
|
@@ -1144,6 +1144,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1144
1144
|
password,
|
|
1145
1145
|
deviceName: options.deviceName,
|
|
1146
1146
|
deviceFingerprint: options.deviceFingerprint,
|
|
1147
|
+
...(options.deviceId ? { deviceId: options.deviceId } : {}),
|
|
1147
1148
|
}, { cache: false });
|
|
1148
1149
|
const parsed = safeParseContract(loginResultSchema, res);
|
|
1149
1150
|
if (!parsed) {
|
|
@@ -1170,6 +1171,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1170
1171
|
token?: string;
|
|
1171
1172
|
backupCode?: string;
|
|
1172
1173
|
deviceName?: string;
|
|
1174
|
+
deviceId?: string;
|
|
1173
1175
|
}): Promise<LoginSessionResult> {
|
|
1174
1176
|
try {
|
|
1175
1177
|
const res = await this.makeRequest<unknown>('POST', '/security/2fa/verify-login', {
|
|
@@ -1177,6 +1179,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1177
1179
|
token: params.token,
|
|
1178
1180
|
backupCode: params.backupCode,
|
|
1179
1181
|
deviceName: params.deviceName,
|
|
1182
|
+
...(params.deviceId ? { deviceId: params.deviceId } : {}),
|
|
1180
1183
|
}, { cache: false });
|
|
1181
1184
|
const parsed = safeParseContract(loginResultSchema, res);
|
|
1182
1185
|
if (!parsed || 'twoFactorRequired' in parsed) {
|
|
@@ -1190,5 +1193,71 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
1190
1193
|
throw this.handleError(error);
|
|
1191
1194
|
}
|
|
1192
1195
|
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Exchange an OAuth authorization code (returned to the RP redirect URI
|
|
1199
|
+
* after password sign-in at auth.oxy.so) for a device-first session.
|
|
1200
|
+
* Public first-party clients use PKCE (`codeVerifier`); the access token is
|
|
1201
|
+
* planted immediately on success.
|
|
1202
|
+
*/
|
|
1203
|
+
async exchangeOAuthCode(params: {
|
|
1204
|
+
code: string;
|
|
1205
|
+
clientId: string;
|
|
1206
|
+
redirectUri: string;
|
|
1207
|
+
codeVerifier: string;
|
|
1208
|
+
}): Promise<LoginSessionResult> {
|
|
1209
|
+
try {
|
|
1210
|
+
const res = await this.makeRequest<unknown>('POST', '/auth/oauth/token', {
|
|
1211
|
+
code: params.code,
|
|
1212
|
+
clientId: params.clientId,
|
|
1213
|
+
redirectUri: params.redirectUri,
|
|
1214
|
+
codeVerifier: params.codeVerifier,
|
|
1215
|
+
}, { cache: false });
|
|
1216
|
+
const payload =
|
|
1217
|
+
(res as { data?: Record<string, unknown> }).data ??
|
|
1218
|
+
(res as Record<string, unknown>);
|
|
1219
|
+
if (!payload || typeof payload !== 'object') {
|
|
1220
|
+
throw new Error('auth/oauth/token returned an unexpected response shape');
|
|
1221
|
+
}
|
|
1222
|
+
const record = payload as Record<string, unknown>;
|
|
1223
|
+
const accessToken = (record.access_token ?? record.accessToken) as string | undefined;
|
|
1224
|
+
const sessionId = (record.session_id ?? record.sessionId) as string | undefined;
|
|
1225
|
+
const deviceId = (record.deviceId ?? record.device_id) as string | undefined;
|
|
1226
|
+
const deviceSecret = (record.deviceSecret ?? record.device_secret) as string | undefined;
|
|
1227
|
+
const userRaw = record.user;
|
|
1228
|
+
if (!sessionId || !deviceId || !userRaw || typeof userRaw !== 'object') {
|
|
1229
|
+
throw new Error('auth/oauth/token returned an incomplete session payload');
|
|
1230
|
+
}
|
|
1231
|
+
const userObj = userRaw as Record<string, unknown>;
|
|
1232
|
+
const userId = userObj.id as string | undefined;
|
|
1233
|
+
if (!userId) {
|
|
1234
|
+
throw new Error('auth/oauth/token returned a session without user.id');
|
|
1235
|
+
}
|
|
1236
|
+
const expiresInSec =
|
|
1237
|
+
typeof record.expires_in === 'number'
|
|
1238
|
+
? record.expires_in
|
|
1239
|
+
: typeof record.expiresIn === 'number'
|
|
1240
|
+
? record.expiresIn
|
|
1241
|
+
: 15 * 60;
|
|
1242
|
+
const expiresAt = new Date(Date.now() + expiresInSec * 1000).toISOString();
|
|
1243
|
+
if (accessToken) {
|
|
1244
|
+
this.setTokens(accessToken);
|
|
1245
|
+
}
|
|
1246
|
+
return {
|
|
1247
|
+
sessionId,
|
|
1248
|
+
deviceId,
|
|
1249
|
+
expiresAt,
|
|
1250
|
+
accessToken,
|
|
1251
|
+
deviceSecret,
|
|
1252
|
+
user: {
|
|
1253
|
+
id: userId,
|
|
1254
|
+
username: typeof userObj.username === 'string' ? userObj.username : undefined,
|
|
1255
|
+
avatar: typeof userObj.avatar === 'string' ? userObj.avatar : undefined,
|
|
1256
|
+
},
|
|
1257
|
+
};
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
throw this.handleError(error);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1193
1262
|
};
|
|
1194
1263
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Device-first token mint mixin.
|
|
3
3
|
*
|
|
4
4
|
* The client half of the zero-cookie device transport: the single network call
|
|
5
|
-
* the cold boot (`
|
|
5
|
+
* the cold boot (`sessionColdBoot`) and the unified re-mint handler (`refresh.ts`)
|
|
6
6
|
* make to turn a first-party `deviceId` + `deviceSecret` into a fresh access
|
|
7
7
|
* token. The response is validated against the `@oxyhq/contracts`
|
|
8
8
|
* `deviceTokenMintResponseSchema`, so producer (oxy-api) and consumer cannot
|
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import {
|
|
17
17
|
deviceTokenMintResponseSchema,
|
|
18
|
+
deviceHubTicketIssueResponseSchema,
|
|
19
|
+
deviceHubTicketRedeemResponseSchema,
|
|
18
20
|
safeParseContract,
|
|
19
21
|
type DeviceTokenMintResponse,
|
|
22
|
+
type DeviceHubTicketIssueResponse,
|
|
23
|
+
type DeviceHubTicketRedeemResponse,
|
|
20
24
|
} from '@oxyhq/contracts';
|
|
21
25
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
22
26
|
|
|
@@ -57,5 +61,46 @@ export function OxyServicesDeviceBootMixin<T extends typeof OxyServicesBase>(Bas
|
|
|
57
61
|
throw this.handleError(error);
|
|
58
62
|
}
|
|
59
63
|
}
|
|
64
|
+
|
|
65
|
+
/** Mint a one-time hub sync ticket (bearer required). */
|
|
66
|
+
async issueHubTicket(returnOrigin: string): Promise<DeviceHubTicketIssueResponse> {
|
|
67
|
+
try {
|
|
68
|
+
const res = await this.makeRequest<unknown>(
|
|
69
|
+
'POST',
|
|
70
|
+
'/session/device/hub-ticket',
|
|
71
|
+
{ returnOrigin },
|
|
72
|
+
{ cache: false },
|
|
73
|
+
);
|
|
74
|
+
const parsed = safeParseContract(deviceHubTicketIssueResponseSchema, res);
|
|
75
|
+
if (!parsed) {
|
|
76
|
+
throw new Error('session/device/hub-ticket returned an unexpected response shape');
|
|
77
|
+
}
|
|
78
|
+
return parsed;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw this.handleError(error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Redeem a hub sync ticket for a fresh device secret (public). */
|
|
85
|
+
async redeemHubTicket(
|
|
86
|
+
ticket: string,
|
|
87
|
+
returnOrigin: string,
|
|
88
|
+
): Promise<DeviceHubTicketRedeemResponse> {
|
|
89
|
+
try {
|
|
90
|
+
const res = await this.makeRequest<unknown>(
|
|
91
|
+
'POST',
|
|
92
|
+
'/session/device/redeem-ticket',
|
|
93
|
+
{ ticket, returnOrigin },
|
|
94
|
+
{ cache: false, skipAuth: true },
|
|
95
|
+
);
|
|
96
|
+
const parsed = safeParseContract(deviceHubTicketRedeemResponseSchema, res);
|
|
97
|
+
if (!parsed) {
|
|
98
|
+
throw new Error('session/device/redeem-ticket returned an unexpected response shape');
|
|
99
|
+
}
|
|
100
|
+
return parsed;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw this.handleError(error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
60
105
|
};
|
|
61
106
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -69,3 +69,11 @@ export { verifySecret } from './verifySecret';
|
|
|
69
69
|
// Pure host handling (no browser deps), so it is safe on the server subpath and
|
|
70
70
|
// lets `@oxyhq/api` derive `auth.<apex>` without duplicating PSL logic.
|
|
71
71
|
export { registrableApex } from '../utils/registrableApex';
|
|
72
|
+
export {
|
|
73
|
+
buildIdpHubOrigin,
|
|
74
|
+
buildHubSyncUrl,
|
|
75
|
+
isIdpHubOrigin,
|
|
76
|
+
isOfficialWebOrigin,
|
|
77
|
+
normalizeOfficialReturnOrigin,
|
|
78
|
+
parseHubSyncReturnUrl,
|
|
79
|
+
} from '../utils/officialOrigins';
|