@oxyhq/core 12.11.1 → 13.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 +2 -12
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +0 -31
- package/dist/cjs/server/index.js +1 -6
- package/dist/cjs/server/rateLimit.js +3 -0
- package/dist/cjs/session/accountDialogController.js +6 -8
- package/dist/cjs/utils/oauthPkce.js +1 -5
- package/dist/cjs/utils/officialOrigins.js +3 -73
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +2 -3
- package/dist/esm/mixins/OxyServices.deviceBoot.js +1 -32
- package/dist/esm/server/index.js +1 -1
- package/dist/esm/server/rateLimit.js +3 -0
- package/dist/esm/session/accountDialogController.js +6 -8
- package/dist/esm/utils/oauthPkce.js +0 -4
- package/dist/esm/utils/officialOrigins.js +3 -68
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +3 -5
- package/dist/types/mixins/OxyServices.auth.d.ts +1 -12
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +1 -5
- package/dist/types/server/index.d.ts +1 -1
- package/dist/types/session/accountDialogController.d.ts +9 -15
- package/dist/types/utils/oauthPkce.d.ts +11 -7
- package/dist/types/utils/officialOrigins.d.ts +3 -13
- package/package.json +3 -3
- package/src/index.ts +6 -14
- package/src/mixins/OxyServices.auth.ts +6 -13
- package/src/mixins/OxyServices.deviceBoot.ts +0 -47
- package/src/mixins/__tests__/commonsSignIn.test.ts +9 -2
- package/src/server/index.ts +1 -8
- package/src/server/rateLimit.ts +3 -0
- package/src/session/__tests__/accountDialogController.test.ts +3 -5
- package/src/session/accountDialogController.ts +12 -18
- package/src/utils/__tests__/officialOrigins.test.ts +0 -57
- package/src/utils/oauthPkce.ts +11 -9
- package/src/utils/officialOrigins.ts +3 -70
- package/dist/cjs/session/hubSync.js +0 -55
- package/dist/esm/session/hubSync.js +0 -51
- package/dist/types/session/hubSync.d.ts +0 -20
- package/src/session/__tests__/hubSync.test.ts +0 -51
- package/src/session/hubSync.ts +0 -79
package/dist/types/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export { getCommonsApprovalBlockingReason, parseCommonsApprovalExpiresAt, } from
|
|
|
26
26
|
export { selectCommonsDelivery, pushTargetsFromDelivery, commonsDeliveryPlatform } from './utils/commonsDelivery';
|
|
27
27
|
export type { CommonsDeliveryFacts, CommonsDeliveryPlatform, CommonsDeliveryRoute, } from './utils/commonsDelivery';
|
|
28
28
|
export type { ServiceTokenResponse } from './mixins/OxyServices.auth';
|
|
29
|
-
export type { CommonsSignInHandle, CommonsSignInStatus, CommonsSignInPurpose, CommonsOAuthContext, CommonsApprovalInfo, CommonsApprovalSubjectAccount, CommonsSignInActionResult,
|
|
29
|
+
export type { CommonsSignInHandle, CommonsSignInStatus, CommonsSignInPurpose, CommonsOAuthContext, CommonsApprovalInfo, CommonsApprovalSubjectAccount, CommonsSignInActionResult, CommonsOAuthFinalizeResult, CommonsDeliveryResult, } from './mixins/OxyServices.auth';
|
|
30
30
|
export type { PushTokenPlatform, RegisterPushTokenInput, } from './mixins/OxyServices.notifications';
|
|
31
31
|
export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
|
|
32
32
|
export type { ContactDiscoveryMatch, ContactDiscoveryResponse, } from './mixins/OxyServices.contacts';
|
|
@@ -96,11 +96,9 @@ export { CENTRAL_IDP_APEX } from './utils/authWebUrl';
|
|
|
96
96
|
export { isOxyRpOrigin } from './utils/webauthnOrigin';
|
|
97
97
|
export { runColdBoot } from './utils/coldBoot';
|
|
98
98
|
export type { ColdBootStep, ColdBootStepResult, ColdBootSession, ColdBootSkip, ColdBootOutcome, RunColdBootOptions, } from './utils/coldBoot';
|
|
99
|
-
export { buildOAuthAuthorizeUrl, computeCodeChallenge, generateOAuthState, generatePkcePair, DEFAULT_OAUTH_SCOPE, OXY_AUTHORIZE_URL, OXY_OAUTH_STATE_STORAGE_KEY, OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY, OXY_OAUTH_REDIRECT_URI_STORAGE_KEY,
|
|
99
|
+
export { buildOAuthAuthorizeUrl, computeCodeChallenge, generateOAuthState, generatePkcePair, DEFAULT_OAUTH_SCOPE, OXY_AUTHORIZE_URL, OXY_OAUTH_STATE_STORAGE_KEY, OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY, OXY_OAUTH_REDIRECT_URI_STORAGE_KEY, OXY_OAUTH_RETURN_PATH_STORAGE_KEY, normalizeOAuthRedirectUri, canonicalizeOAuthRedirectUri, persistOAuthHandshake, readOAuthHandshake, clearOAuthHandshake, persistOAuthReturnPath, consumeOAuthReturnPath, } from './utils/oauthPkce';
|
|
100
100
|
export type { PkcePair, BuildOAuthAuthorizeUrlParams } from './utils/oauthPkce';
|
|
101
|
-
export {
|
|
102
|
-
export { syncHubAfterSignIn, redeemHubTicketOnHub, } from './session/hubSync';
|
|
103
|
-
export type { SyncHubAfterSignInOptions } from './session/hubSync';
|
|
101
|
+
export { isLoopbackOrigin, isOfficialWebOrigin, isAllowedDeviceJoinOrigin, } from './utils/officialOrigins';
|
|
104
102
|
export { SessionClient } from './session/SessionClient';
|
|
105
103
|
export type { TokenTransport, SessionClientHost, SessionClientOptions, DeviceCredential, SessionStateOrigin } from './session/SessionClient';
|
|
106
104
|
export type { SocketIOFactory, MinimalSocket } from './session/socketLoader';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Supports password-based login (email/username) and public key challenge-response.
|
|
5
5
|
*/
|
|
6
6
|
import type { User } from '../models/interfaces';
|
|
7
|
-
import type { LoginResult, LoginSessionResult } from '@oxyhq/contracts';
|
|
7
|
+
import type { LoginResult, LoginSessionResult, CommonsDenyReason } from '@oxyhq/contracts';
|
|
8
8
|
import type { SessionLoginResponse } from '../models/session';
|
|
9
9
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
10
10
|
import type { PublicApplication } from './OxyServices.connectedApps';
|
|
@@ -226,17 +226,6 @@ export interface CommonsApprovalInfo {
|
|
|
226
226
|
export interface CommonsSignInActionResult {
|
|
227
227
|
success: boolean;
|
|
228
228
|
}
|
|
229
|
-
/**
|
|
230
|
-
* Why the approver denied a "Sign in with Oxy" request. A CLOSED set — the deny
|
|
231
|
-
* endpoint is unauthenticated, so it accepts no free-form text:
|
|
232
|
-
*
|
|
233
|
-
* - `'declined'` the approver rejected a request they recognised ("Not now").
|
|
234
|
-
* - `'not_me'` the approver did not start the request ("This wasn't me").
|
|
235
|
-
* The only value that records the denial as suspicious rather
|
|
236
|
-
* than an ordinary cancel, so a UI may only offer it where the
|
|
237
|
-
* user genuinely said so.
|
|
238
|
-
*/
|
|
239
|
-
export type CommonsDenyReason = 'declined' | 'not_me';
|
|
240
229
|
/**
|
|
241
230
|
* Result of finalizing an approved, OAuth-bound "Sign in with Oxy" request.
|
|
242
231
|
*
|
|
@@ -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
|
|
16
|
+
import { type DeviceTokenMintResponse } from '@oxyhq/contracts';
|
|
17
17
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
18
18
|
/**
|
|
19
19
|
* The server's `401 account_not_on_device` for a PINNED mint: the requested
|
|
@@ -71,10 +71,6 @@ export declare function OxyServicesDeviceBootMixin<T extends typeof OxyServicesB
|
|
|
71
71
|
mintFromDeviceSecret(deviceId: string, deviceSecret: string, options?: {
|
|
72
72
|
accountId?: string;
|
|
73
73
|
}): Promise<DeviceTokenMintResponse>;
|
|
74
|
-
/** Mint a one-time hub sync ticket (bearer required). */
|
|
75
|
-
issueHubTicket(returnOrigin: string): Promise<DeviceHubTicketIssueResponse>;
|
|
76
|
-
/** Redeem a hub sync ticket for a fresh device secret (public). */
|
|
77
|
-
redeemHubTicket(ticket: string, returnOrigin: string): Promise<DeviceHubTicketRedeemResponse>;
|
|
78
74
|
httpService: import("../HttpService").HttpService;
|
|
79
75
|
cloudURL: string;
|
|
80
76
|
config: import("../OxyServices.base").OxyConfig;
|
|
@@ -24,4 +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 {
|
|
27
|
+
export { isOfficialWebOrigin } from '../utils/officialOrigins';
|
|
@@ -231,21 +231,17 @@ export interface AccountDialogControllerOptions {
|
|
|
231
231
|
* `SessionClient.registerAndActivate` (registration + activation only — no
|
|
232
232
|
* provider-side durable persist/hydration).
|
|
233
233
|
*
|
|
234
|
-
* This is the SIGN-IN commit:
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* origin legitimately needs that. An account SWITCH does NOT — see
|
|
238
|
-
* {@link commitSwitchedSession}.
|
|
234
|
+
* This is the SIGN-IN commit: registers the session into the host's device
|
|
235
|
+
* set with durable persist + profile hydration. An account SWITCH uses
|
|
236
|
+
* {@link commitSwitchedSession} instead — see below.
|
|
239
237
|
*/
|
|
240
238
|
commitSession?: (session: SessionLoginResponse) => Promise<void>;
|
|
241
239
|
/**
|
|
242
240
|
* Commit a minted graph SWITCH session into the host's session set — same
|
|
243
241
|
* device-first registration + durable persist + profile hydration as
|
|
244
|
-
* {@link commitSession}, but IN-PLACE: it must NOT
|
|
245
|
-
*
|
|
246
|
-
* device
|
|
247
|
-
* re-syncing is redundant and a full-page redirect on switch is the exact
|
|
248
|
-
* regression this separation prevents. Cross-tab/app propagation of the switch
|
|
242
|
+
* {@link commitSession}, but IN-PLACE: it must NOT re-run sign-in side effects
|
|
243
|
+
* that belong only to a fresh authorization (for example, a redundant full
|
|
244
|
+
* device-set reconcile on switch). Cross-tab/app propagation of the switch
|
|
249
245
|
* still happens instantly via the server's device-scoped `session_state` /
|
|
250
246
|
* `session_accounts_changed` socket broadcast — no navigation required.
|
|
251
247
|
*
|
|
@@ -600,11 +596,9 @@ export declare class AccountDialogController {
|
|
|
600
596
|
* consumer's commit funnel (durable persist + hydration); falls back to
|
|
601
597
|
* `SessionClient.registerAndActivate` (registration + activation only).
|
|
602
598
|
*
|
|
603
|
-
* A SWITCH (`opts.fromSwitch`) uses the IN-PLACE `commitSwitchedSession` funnel
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
* switch funnel is not wired it falls back to the sign-in funnel, then to
|
|
607
|
-
* `registerAndActivate`.
|
|
599
|
+
* A SWITCH (`opts.fromSwitch`) uses the IN-PLACE `commitSwitchedSession` funnel;
|
|
600
|
+
* a SIGN-IN uses `commitSession`. When the switch funnel is not wired it falls
|
|
601
|
+
* back to the sign-in funnel, then to `registerAndActivate`.
|
|
608
602
|
*/
|
|
609
603
|
private commitAuthorizedSession;
|
|
610
604
|
private failSignIn;
|
|
@@ -43,10 +43,18 @@ export interface BuildOAuthAuthorizeUrlParams {
|
|
|
43
43
|
/** The PKCE `codeChallenge` from {@link generatePkcePair}. */
|
|
44
44
|
codeChallenge: string;
|
|
45
45
|
/**
|
|
46
|
-
* OAuth `prompt` parameter
|
|
47
|
-
*
|
|
46
|
+
* OAuth `prompt` parameter — `login` forces a fresh authentication even when
|
|
47
|
+
* the IdP already has a session, `consent` forces the consent screen even for
|
|
48
|
+
* an already-granted scope. Both are ordinary OAuth/OIDC and are here for
|
|
49
|
+
* third-party relying parties building their own authorize link.
|
|
50
|
+
*
|
|
51
|
+
* `'none'` is deliberately NOT in this union. It is the silent-SSO value, and
|
|
52
|
+
* it was the only value this SDK ever sent — from the cold-boot cross-origin
|
|
53
|
+
* restore deleted in #691 phase 7b. Accepting it again would hand consumers a
|
|
54
|
+
* one-line rebuild of the automatic, gesture-less full-page bounce to the IdP
|
|
55
|
+
* that the popup transport exists to eliminate.
|
|
48
56
|
*/
|
|
49
|
-
prompt?: '
|
|
57
|
+
prompt?: 'login' | 'consent';
|
|
50
58
|
/**
|
|
51
59
|
* How the IdP should deliver the authorization response. Omitted (the
|
|
52
60
|
* default) means the ordinary top-level redirect back to `redirectUri`.
|
|
@@ -95,10 +103,6 @@ export declare const OXY_OAUTH_STATE_STORAGE_KEY = "oxy_oauth_state";
|
|
|
95
103
|
export declare const OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = "oxy_oauth_code_verifier";
|
|
96
104
|
/** `sessionStorage` key — the exact `redirect_uri` sent on the authorize request. */
|
|
97
105
|
export declare const OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = "oxy.oauth_redirect_uri";
|
|
98
|
-
/** `sessionStorage` key — at most one silent OAuth attempt per navigation. */
|
|
99
|
-
export declare const OXY_SILENT_OAUTH_ATTEMPTED_KEY = "oxy.silent_oauth_attempted";
|
|
100
|
-
/** `sessionStorage` key — blocks further cross-origin auto-restore in this tab. */
|
|
101
|
-
export declare const OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY = "oxy.cross_origin_restore_attempted";
|
|
102
106
|
/**
|
|
103
107
|
* `sessionStorage` key for the in-app path to return to after an authorize
|
|
104
108
|
* round trip. See {@link persistOAuthReturnPath}.
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Official first-party web origin allowlist — shared by
|
|
3
|
-
*
|
|
2
|
+
* Official first-party web origin allowlist — shared by OAuth redirect
|
|
3
|
+
* validation and the server-side trusted-origin checks.
|
|
4
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
5
|
/**
|
|
9
6
|
* Whether an origin is a loopback / local-dev origin (`localhost`, `127.0.0.1`,
|
|
10
|
-
* or `[::1]` on any port, http or https).
|
|
11
|
-
* hosted IdP for cross-origin session restore.
|
|
7
|
+
* or `[::1]` on any port, http or https).
|
|
12
8
|
*/
|
|
13
9
|
export declare function isLoopbackOrigin(origin: string): boolean;
|
|
14
10
|
/** Whether an origin belongs to the official Oxy web ecosystem. */
|
|
15
11
|
export declare function isOfficialWebOrigin(origin: string): boolean;
|
|
16
|
-
/** Normalize and validate a return URL against official origins. Returns origin only. */
|
|
17
|
-
export declare function normalizeOfficialReturnOrigin(raw: string): string | null;
|
|
18
|
-
/** Validate a hub-sync return URL; returns the full normalized URL string. */
|
|
19
|
-
export declare function parseHubSyncReturnUrl(raw: string | null): string | null;
|
|
20
|
-
/** Build auth.oxy.so/sync redirect URL with a one-time hub ticket. */
|
|
21
|
-
export declare function buildHubSyncUrl(ticket: string, returnUrl?: string): string;
|
|
22
12
|
/** @deprecated Use {@link isOfficialWebOrigin}. */
|
|
23
13
|
export declare const isAllowedDeviceJoinOrigin: typeof isOfficialWebOrigin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "13.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",
|
|
@@ -115,8 +115,8 @@
|
|
|
115
115
|
"dependencies": {
|
|
116
116
|
"@noble/ciphers": "^1.3.0",
|
|
117
117
|
"@noble/hashes": "^1.8.0",
|
|
118
|
-
"@oxyhq/contracts": "^0.
|
|
119
|
-
"@oxyhq/protocol": "^0.1.
|
|
118
|
+
"@oxyhq/contracts": "^0.19.0",
|
|
119
|
+
"@oxyhq/protocol": "^0.1.6",
|
|
120
120
|
"bip39": "^3.1.0",
|
|
121
121
|
"buffer": "^6.0.3",
|
|
122
122
|
"elliptic": "^6.6.1",
|
package/src/index.ts
CHANGED
|
@@ -59,10 +59,15 @@ export type {
|
|
|
59
59
|
CommonsApprovalInfo,
|
|
60
60
|
CommonsApprovalSubjectAccount,
|
|
61
61
|
CommonsSignInActionResult,
|
|
62
|
-
CommonsDenyReason,
|
|
63
62
|
CommonsOAuthFinalizeResult,
|
|
64
63
|
CommonsDeliveryResult,
|
|
65
64
|
} from './mixins/OxyServices.auth';
|
|
65
|
+
// `denyCommonsSignIn`'s reason parameter is typed by `CommonsDenyReason`, which
|
|
66
|
+
// is a WIRE contract shared with the API (the request schema of
|
|
67
|
+
// `POST /auth/session/deny/:authorizeCode` and the persisted
|
|
68
|
+
// `AuthSession.deniedReason` enum read the same declaration). It lives in
|
|
69
|
+
// `@oxyhq/contracts` and is NOT re-exported here: consumers import API contract
|
|
70
|
+
// types directly from `@oxyhq/contracts`, per the package boundary rule.
|
|
66
71
|
// Push-token registration (Expo push tokens — never raw APNs/FCM device tokens).
|
|
67
72
|
export type {
|
|
68
73
|
PushTokenPlatform,
|
|
@@ -601,8 +606,6 @@ export {
|
|
|
601
606
|
OXY_OAUTH_STATE_STORAGE_KEY,
|
|
602
607
|
OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY,
|
|
603
608
|
OXY_OAUTH_REDIRECT_URI_STORAGE_KEY,
|
|
604
|
-
OXY_SILENT_OAUTH_ATTEMPTED_KEY,
|
|
605
|
-
OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY,
|
|
606
609
|
OXY_OAUTH_RETURN_PATH_STORAGE_KEY,
|
|
607
610
|
normalizeOAuthRedirectUri,
|
|
608
611
|
canonicalizeOAuthRedirectUri,
|
|
@@ -615,22 +618,11 @@ export {
|
|
|
615
618
|
export type { PkcePair, BuildOAuthAuthorizeUrlParams } from './utils/oauthPkce';
|
|
616
619
|
|
|
617
620
|
export {
|
|
618
|
-
buildIdpHubOrigin,
|
|
619
|
-
buildHubSyncUrl,
|
|
620
|
-
isIdpHubOrigin,
|
|
621
621
|
isLoopbackOrigin,
|
|
622
622
|
isOfficialWebOrigin,
|
|
623
623
|
isAllowedDeviceJoinOrigin,
|
|
624
|
-
normalizeOfficialReturnOrigin,
|
|
625
|
-
parseHubSyncReturnUrl,
|
|
626
624
|
} from './utils/officialOrigins';
|
|
627
625
|
|
|
628
|
-
export {
|
|
629
|
-
syncHubAfterSignIn,
|
|
630
|
-
redeemHubTicketOnHub,
|
|
631
|
-
} from './session/hubSync';
|
|
632
|
-
export type { SyncHubAfterSignInOptions } from './session/hubSync';
|
|
633
|
-
|
|
634
626
|
// ---------------------------------------------------------------------------
|
|
635
627
|
// Session sync (device-scoped multi-account session client)
|
|
636
628
|
// ---------------------------------------------------------------------------
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* Supports password-based login (email/username) and public key challenge-response.
|
|
5
5
|
*/
|
|
6
6
|
import type { User } from '../models/interfaces';
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
UserNameResponse,
|
|
9
|
+
LoginResult,
|
|
10
|
+
LoginSessionResult,
|
|
11
|
+
CommonsDenyReason,
|
|
12
|
+
} from '@oxyhq/contracts';
|
|
8
13
|
import { loginResultSchema, safeParseContract } from '@oxyhq/contracts';
|
|
9
14
|
import type { SessionLoginResponse } from '../models/session';
|
|
10
15
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
@@ -336,18 +341,6 @@ export interface CommonsSignInActionResult {
|
|
|
336
341
|
success: boolean;
|
|
337
342
|
}
|
|
338
343
|
|
|
339
|
-
/**
|
|
340
|
-
* Why the approver denied a "Sign in with Oxy" request. A CLOSED set — the deny
|
|
341
|
-
* endpoint is unauthenticated, so it accepts no free-form text:
|
|
342
|
-
*
|
|
343
|
-
* - `'declined'` the approver rejected a request they recognised ("Not now").
|
|
344
|
-
* - `'not_me'` the approver did not start the request ("This wasn't me").
|
|
345
|
-
* The only value that records the denial as suspicious rather
|
|
346
|
-
* than an ordinary cancel, so a UI may only offer it where the
|
|
347
|
-
* user genuinely said so.
|
|
348
|
-
*/
|
|
349
|
-
export type CommonsDenyReason = 'declined' | 'not_me';
|
|
350
|
-
|
|
351
344
|
/**
|
|
352
345
|
* Result of finalizing an approved, OAuth-bound "Sign in with Oxy" request.
|
|
353
346
|
*
|
|
@@ -15,12 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import {
|
|
17
17
|
deviceTokenMintResponseSchema,
|
|
18
|
-
deviceHubTicketIssueResponseSchema,
|
|
19
|
-
deviceHubTicketRedeemResponseSchema,
|
|
20
18
|
safeParseContract,
|
|
21
19
|
type DeviceTokenMintResponse,
|
|
22
|
-
type DeviceHubTicketIssueResponse,
|
|
23
|
-
type DeviceHubTicketRedeemResponse,
|
|
24
20
|
} from '@oxyhq/contracts';
|
|
25
21
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
26
22
|
|
|
@@ -124,48 +120,5 @@ export function OxyServicesDeviceBootMixin<T extends typeof OxyServicesBase>(Bas
|
|
|
124
120
|
throw normalized;
|
|
125
121
|
}
|
|
126
122
|
}
|
|
127
|
-
|
|
128
|
-
/** Mint a one-time hub sync ticket (bearer required). */
|
|
129
|
-
async issueHubTicket(returnOrigin: string): Promise<DeviceHubTicketIssueResponse> {
|
|
130
|
-
try {
|
|
131
|
-
const res = await this.makeRequest<unknown>(
|
|
132
|
-
'POST',
|
|
133
|
-
'/session/device/hub-ticket',
|
|
134
|
-
{ returnOrigin },
|
|
135
|
-
{ cache: false },
|
|
136
|
-
);
|
|
137
|
-
const parsed = safeParseContract(deviceHubTicketIssueResponseSchema, res);
|
|
138
|
-
if (!parsed) {
|
|
139
|
-
throw new Error('session/device/hub-ticket returned an unexpected response shape');
|
|
140
|
-
}
|
|
141
|
-
return parsed;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
throw this.handleError(error);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Redeem a hub sync ticket for a fresh device secret (public). */
|
|
148
|
-
async redeemHubTicket(
|
|
149
|
-
ticket: string,
|
|
150
|
-
returnOrigin: string,
|
|
151
|
-
): Promise<DeviceHubTicketRedeemResponse> {
|
|
152
|
-
try {
|
|
153
|
-
const res = await this.makeRequest<unknown>(
|
|
154
|
-
'POST',
|
|
155
|
-
'/session/device/redeem-ticket',
|
|
156
|
-
{ ticket, returnOrigin },
|
|
157
|
-
// Public device-hub sync mint (bearer-less). Same control-plane class as
|
|
158
|
-
// the device-secret mint — bypassQueue so it never waits for a slot.
|
|
159
|
-
{ cache: false, skipAuth: true, bypassQueue: true },
|
|
160
|
-
);
|
|
161
|
-
const parsed = safeParseContract(deviceHubTicketRedeemResponseSchema, res);
|
|
162
|
-
if (!parsed) {
|
|
163
|
-
throw new Error('session/device/redeem-ticket returned an unexpected response shape');
|
|
164
|
-
}
|
|
165
|
-
return parsed;
|
|
166
|
-
} catch (error) {
|
|
167
|
-
throw this.handleError(error);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
123
|
};
|
|
171
124
|
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* degrade-to-QR behaviour every ambiguous input must produce.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { CommonsDenyReason } from '@oxyhq/contracts';
|
|
16
|
+
import { COMMONS_DENY_REASONS } from '@oxyhq/contracts';
|
|
15
17
|
import type { SessionLoginResponse } from '../../models/session';
|
|
16
18
|
import type { ChallengeResponse } from '../OxyServices.auth';
|
|
17
19
|
import type { CommonsDeliveryPlatform } from '../../utils/commonsDelivery';
|
|
@@ -769,12 +771,17 @@ describe('OxyServices — "Sign in with Oxy" handoff', () => {
|
|
|
769
771
|
);
|
|
770
772
|
});
|
|
771
773
|
|
|
772
|
-
|
|
774
|
+
// Iterating the CONTRACT set (rather than a local literal) is the point: the
|
|
775
|
+
// SDK, the API request schema and the persisted `AuthSession.deniedReason`
|
|
776
|
+
// enum all read this one declaration, so a value added on the server side
|
|
777
|
+
// without an SDK release — or the reverse — cannot go unnoticed here.
|
|
778
|
+
it.each([...COMMONS_DENY_REASONS])(
|
|
773
779
|
'sends the closed-set reason %s in the body',
|
|
774
780
|
async (reason) => {
|
|
775
781
|
makeRequestSpy.mockResolvedValue({ success: true });
|
|
776
782
|
|
|
777
|
-
|
|
783
|
+
const typedReason: CommonsDenyReason = reason;
|
|
784
|
+
await oxy.denyCommonsSignIn('code-1', typedReason);
|
|
778
785
|
|
|
779
786
|
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
780
787
|
'POST',
|
package/src/server/index.ts
CHANGED
|
@@ -71,11 +71,4 @@ export { verifySecret } from './verifySecret';
|
|
|
71
71
|
// Pure host handling (no browser deps), so it is safe on the server subpath and
|
|
72
72
|
// lets `@oxyhq/api` derive `auth.<apex>` without duplicating PSL logic.
|
|
73
73
|
export { registrableApex } from '../utils/registrableApex';
|
|
74
|
-
export {
|
|
75
|
-
buildIdpHubOrigin,
|
|
76
|
-
buildHubSyncUrl,
|
|
77
|
-
isIdpHubOrigin,
|
|
78
|
-
isOfficialWebOrigin,
|
|
79
|
-
normalizeOfficialReturnOrigin,
|
|
80
|
-
parseHubSyncReturnUrl,
|
|
81
|
-
} from '../utils/officialOrigins';
|
|
74
|
+
export { isOfficialWebOrigin } from '../utils/officialOrigins';
|
package/src/server/rateLimit.ts
CHANGED
|
@@ -278,6 +278,9 @@ export function createOxyRateLimit(
|
|
|
278
278
|
standardHeaders: true,
|
|
279
279
|
legacyHeaders: false,
|
|
280
280
|
skip,
|
|
281
|
+
// hashAnonymousIp already buckets IPv6 to /56 before HMAC; disable the v8
|
|
282
|
+
// static source scan that false-positives on req.ip (ERR_ERL_KEY_GEN_IPV6).
|
|
283
|
+
validate: { keyGeneratorIpFallback: false },
|
|
281
284
|
});
|
|
282
285
|
|
|
283
286
|
return (req, res, next) => {
|
|
@@ -433,11 +433,9 @@ describe('AccountDialogController — switchTo (uniform switch)', () => {
|
|
|
433
433
|
expect(commitSession.mock.calls[0][0]).toMatchObject({ sessionId: 'sess-org', accessToken: 'access-org' });
|
|
434
434
|
});
|
|
435
435
|
|
|
436
|
-
it('commits a graph switch via the IN-PLACE commitSwitchedSession — never
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
// commitSwitchedSession (in-place) and NEVER commitSession (which may
|
|
440
|
-
// redirect on an official web origin).
|
|
436
|
+
it('commits a graph switch via the IN-PLACE commitSwitchedSession — never commitSession', async () => {
|
|
437
|
+
// An account switch must use commitSwitchedSession (in-place) and NEVER
|
|
438
|
+
// commitSession when both funnels are wired.
|
|
441
439
|
const oxy = makeOxy();
|
|
442
440
|
const sc = new TestSessionClient(host());
|
|
443
441
|
sc.set(state([{ accountId: 'a1', sessionId: 's1' }], 'a1'));
|
|
@@ -302,21 +302,17 @@ export interface AccountDialogControllerOptions {
|
|
|
302
302
|
* `SessionClient.registerAndActivate` (registration + activation only — no
|
|
303
303
|
* provider-side durable persist/hydration).
|
|
304
304
|
*
|
|
305
|
-
* This is the SIGN-IN commit:
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* origin legitimately needs that. An account SWITCH does NOT — see
|
|
309
|
-
* {@link commitSwitchedSession}.
|
|
305
|
+
* This is the SIGN-IN commit: registers the session into the host's device
|
|
306
|
+
* set with durable persist + profile hydration. An account SWITCH uses
|
|
307
|
+
* {@link commitSwitchedSession} instead — see below.
|
|
310
308
|
*/
|
|
311
309
|
commitSession?: (session: SessionLoginResponse) => Promise<void>;
|
|
312
310
|
/**
|
|
313
311
|
* Commit a minted graph SWITCH session into the host's session set — same
|
|
314
312
|
* device-first registration + durable persist + profile hydration as
|
|
315
|
-
* {@link commitSession}, but IN-PLACE: it must NOT
|
|
316
|
-
*
|
|
317
|
-
* device
|
|
318
|
-
* re-syncing is redundant and a full-page redirect on switch is the exact
|
|
319
|
-
* regression this separation prevents. Cross-tab/app propagation of the switch
|
|
313
|
+
* {@link commitSession}, but IN-PLACE: it must NOT re-run sign-in side effects
|
|
314
|
+
* that belong only to a fresh authorization (for example, a redundant full
|
|
315
|
+
* device-set reconcile on switch). Cross-tab/app propagation of the switch
|
|
320
316
|
* still happens instantly via the server's device-scoped `session_state` /
|
|
321
317
|
* `session_accounts_changed` socket broadcast — no navigation required.
|
|
322
318
|
*
|
|
@@ -811,9 +807,9 @@ export class AccountDialogController {
|
|
|
811
807
|
accessToken: result.accessToken,
|
|
812
808
|
},
|
|
813
809
|
result.user,
|
|
814
|
-
// A switch is IN-PLACE:
|
|
815
|
-
//
|
|
816
|
-
//
|
|
810
|
+
// A switch is IN-PLACE: use the switch commit funnel (not sign-in).
|
|
811
|
+
// Cross-tab/app propagation rides the server's `session_state` socket
|
|
812
|
+
// broadcast, not a navigation.
|
|
817
813
|
{ fromSwitch: true },
|
|
818
814
|
);
|
|
819
815
|
}
|
|
@@ -1349,11 +1345,9 @@ export class AccountDialogController {
|
|
|
1349
1345
|
* consumer's commit funnel (durable persist + hydration); falls back to
|
|
1350
1346
|
* `SessionClient.registerAndActivate` (registration + activation only).
|
|
1351
1347
|
*
|
|
1352
|
-
* A SWITCH (`opts.fromSwitch`) uses the IN-PLACE `commitSwitchedSession` funnel
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
1355
|
-
* switch funnel is not wired it falls back to the sign-in funnel, then to
|
|
1356
|
-
* `registerAndActivate`.
|
|
1348
|
+
* A SWITCH (`opts.fromSwitch`) uses the IN-PLACE `commitSwitchedSession` funnel;
|
|
1349
|
+
* a SIGN-IN uses `commitSession`. When the switch funnel is not wired it falls
|
|
1350
|
+
* back to the sign-in funnel, then to `registerAndActivate`.
|
|
1357
1351
|
*/
|
|
1358
1352
|
private async commitAuthorizedSession(
|
|
1359
1353
|
session: SessionLoginResponse,
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
buildHubSyncUrl,
|
|
3
|
-
buildIdpHubOrigin,
|
|
4
2
|
isAllowedDeviceJoinOrigin,
|
|
5
|
-
isIdpHubOrigin,
|
|
6
3
|
isLoopbackOrigin,
|
|
7
4
|
isOfficialWebOrigin,
|
|
8
|
-
normalizeOfficialReturnOrigin,
|
|
9
|
-
parseHubSyncReturnUrl,
|
|
10
5
|
} from '../officialOrigins';
|
|
11
6
|
|
|
12
7
|
describe('officialOrigins', () => {
|
|
13
|
-
it('builds the IdP hub origin', () => {
|
|
14
|
-
expect(buildIdpHubOrigin()).toBe('https://auth.oxy.so');
|
|
15
|
-
});
|
|
16
|
-
|
|
17
8
|
it('allows official first-party origins', () => {
|
|
18
9
|
expect(isOfficialWebOrigin('https://inbox.oxy.so')).toBe(true);
|
|
19
10
|
expect(isOfficialWebOrigin('https://mention.earth')).toBe(true);
|
|
@@ -37,52 +28,4 @@ describe('officialOrigins', () => {
|
|
|
37
28
|
expect(isAllowedDeviceJoinOrigin('https://accounts.oxy.so')).toBe(true);
|
|
38
29
|
expect(isAllowedDeviceJoinOrigin('https://evil.example')).toBe(false);
|
|
39
30
|
});
|
|
40
|
-
|
|
41
|
-
it('normalizes return origins to origin only', () => {
|
|
42
|
-
expect(normalizeOfficialReturnOrigin('https://accounts.oxy.so/settings')).toBe(
|
|
43
|
-
'https://accounts.oxy.so',
|
|
44
|
-
);
|
|
45
|
-
expect(normalizeOfficialReturnOrigin('https://evil.example/')).toBeNull();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('parses hub-sync return URLs', () => {
|
|
49
|
-
expect(parseHubSyncReturnUrl('https://inbox.oxy.so/messages')).toBe(
|
|
50
|
-
'https://inbox.oxy.so/messages',
|
|
51
|
-
);
|
|
52
|
-
expect(parseHubSyncReturnUrl('https://evil.example/')).toBeNull();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('builds hub sync URLs with ticket and optional return', () => {
|
|
56
|
-
const url = new URL(buildHubSyncUrl('tk-abc', 'https://accounts.oxy.so/'));
|
|
57
|
-
expect(url.pathname).toBe('/sync');
|
|
58
|
-
expect(url.searchParams.get('ticket')).toBe('tk-abc');
|
|
59
|
-
expect(url.searchParams.get('return')).toBe('https://accounts.oxy.so/');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe('isIdpHubOrigin', () => {
|
|
63
|
-
const originalLocation = globalThis.location;
|
|
64
|
-
|
|
65
|
-
afterEach(() => {
|
|
66
|
-
Object.defineProperty(globalThis, 'location', {
|
|
67
|
-
configurable: true,
|
|
68
|
-
value: originalLocation,
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('returns true on auth.oxy.so', () => {
|
|
73
|
-
Object.defineProperty(globalThis, 'location', {
|
|
74
|
-
configurable: true,
|
|
75
|
-
value: { href: 'https://auth.oxy.so/sync' },
|
|
76
|
-
});
|
|
77
|
-
expect(isIdpHubOrigin()).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('returns false on satellite origins', () => {
|
|
81
|
-
Object.defineProperty(globalThis, 'location', {
|
|
82
|
-
configurable: true,
|
|
83
|
-
value: { href: 'https://inbox.oxy.so/' },
|
|
84
|
-
});
|
|
85
|
-
expect(isIdpHubOrigin()).toBe(false);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
31
|
});
|
package/src/utils/oauthPkce.ts
CHANGED
|
@@ -62,10 +62,18 @@ export interface BuildOAuthAuthorizeUrlParams {
|
|
|
62
62
|
/** The PKCE `codeChallenge` from {@link generatePkcePair}. */
|
|
63
63
|
codeChallenge: string;
|
|
64
64
|
/**
|
|
65
|
-
* OAuth `prompt` parameter
|
|
66
|
-
*
|
|
65
|
+
* OAuth `prompt` parameter — `login` forces a fresh authentication even when
|
|
66
|
+
* the IdP already has a session, `consent` forces the consent screen even for
|
|
67
|
+
* an already-granted scope. Both are ordinary OAuth/OIDC and are here for
|
|
68
|
+
* third-party relying parties building their own authorize link.
|
|
69
|
+
*
|
|
70
|
+
* `'none'` is deliberately NOT in this union. It is the silent-SSO value, and
|
|
71
|
+
* it was the only value this SDK ever sent — from the cold-boot cross-origin
|
|
72
|
+
* restore deleted in #691 phase 7b. Accepting it again would hand consumers a
|
|
73
|
+
* one-line rebuild of the automatic, gesture-less full-page bounce to the IdP
|
|
74
|
+
* that the popup transport exists to eliminate.
|
|
67
75
|
*/
|
|
68
|
-
prompt?: '
|
|
76
|
+
prompt?: 'login' | 'consent';
|
|
69
77
|
/**
|
|
70
78
|
* How the IdP should deliver the authorization response. Omitted (the
|
|
71
79
|
* default) means the ordinary top-level redirect back to `redirectUri`.
|
|
@@ -220,12 +228,6 @@ export const OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = 'oxy_oauth_code_verifier';
|
|
|
220
228
|
/** `sessionStorage` key — the exact `redirect_uri` sent on the authorize request. */
|
|
221
229
|
export const OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = 'oxy.oauth_redirect_uri';
|
|
222
230
|
|
|
223
|
-
/** `sessionStorage` key — at most one silent OAuth attempt per navigation. */
|
|
224
|
-
export const OXY_SILENT_OAUTH_ATTEMPTED_KEY = 'oxy.silent_oauth_attempted';
|
|
225
|
-
|
|
226
|
-
/** `sessionStorage` key — blocks further cross-origin auto-restore in this tab. */
|
|
227
|
-
export const OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY = 'oxy.cross_origin_restore_attempted';
|
|
228
|
-
|
|
229
231
|
/**
|
|
230
232
|
* `sessionStorage` key for the in-app path to return to after an authorize
|
|
231
233
|
* round trip. See {@link persistOAuthReturnPath}.
|