@oxyhq/core 9.1.0 → 9.2.1
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} +15 -2
- package/dist/cjs/index.js +27 -6
- package/dist/cjs/mixins/OxyServices.accounts.js +3 -0
- 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 +21 -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} +15 -2
- package/dist/esm/index.js +5 -2
- package/dist/esm/mixins/OxyServices.accounts.js +1 -0
- 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 +21 -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 +9 -5
- package/dist/types/mixins/OxyServices.accounts.d.ts +8 -0
- package/dist/types/mixins/OxyServices.auth.d.ts +15 -0
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +6 -2
- package/dist/types/models/interfaces.d.ts +3 -1
- package/dist/types/models/session.d.ts +6 -0
- 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} +21 -1
- package/src/boot/{coldBootV2.ts → sessionColdBoot.ts} +15 -2
- package/src/index.ts +30 -3
- package/src/mixins/OxyServices.accounts.ts +9 -0
- package/src/mixins/OxyServices.auth.ts +72 -1
- package/src/mixins/OxyServices.deviceBoot.ts +46 -1
- package/src/models/interfaces.ts +3 -1
- package/src/models/session.ts +6 -0
- 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 +46 -7
- package/src/session/__tests__/sessionIntegration.test.ts +7 -0
- package/src/session/accountDialogController.ts +45 -11
- 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
|
@@ -34,7 +34,8 @@ export type { CanonicalUserHandleInput, UserHandleInput } from './utils/userHand
|
|
|
34
34
|
export { normalizeProfileLinks } from './utils/profileLinks';
|
|
35
35
|
export type { ProfileLink, ProfileLinkMetadata } from './utils/profileLinks';
|
|
36
36
|
export type { PublicApplication, ConnectedApp, } from './mixins/OxyServices.connectedApps';
|
|
37
|
-
export type { AccountKind, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, AccountCredentialType, AccountCredentialEnvironment, AccountCredentialStatus, AccountCredential, AccountCredentialWithSecret, RotateAccountCredentialResult, ListAccountsOptions, CreateAccountInput, UpdateAccountInput, InviteAccountMemberInput, UpdateAccountMemberInput, TransferAccountOwnershipInput, CreateAccountCredentialInput, AccountSuccessResult, SwitchAccountResult, Application, ApplicationType, ApplicationStatus, ApplicationCredential, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, } from './mixins/OxyServices.accounts';
|
|
37
|
+
export type { AccountKind, OrganizationCategory, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, AccountCredentialType, AccountCredentialEnvironment, AccountCredentialStatus, AccountCredential, AccountCredentialWithSecret, RotateAccountCredentialResult, ListAccountsOptions, CreateAccountInput, UpdateAccountInput, InviteAccountMemberInput, UpdateAccountMemberInput, TransferAccountOwnershipInput, CreateAccountCredentialInput, AccountSuccessResult, SwitchAccountResult, Application, ApplicationType, ApplicationStatus, ApplicationCredential, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, } from './mixins/OxyServices.accounts';
|
|
38
|
+
export { ORGANIZATION_CATEGORIES } from './mixins/OxyServices.accounts';
|
|
38
39
|
export type { ReputationCategory, TrustTier, ReputationTransactionStatus, ReputationTargetEntityType, ReputationDisputeStatus, ReputationInfluenceContext, ReputationTransaction, ReputationBalanceBreakdown, ReputationInfluence, ReputationReliability, ReputationBalance, ReputationDispute, ReputationRule, ReputationLeaderboardEntry, ReputationInfluenceResult, ReverseReputationTransactionResult, AwardReputationInput, CreateReputationDisputeInput, ResolveReputationDisputeInput, UpsertReputationRuleInput, ReverseReputationTransactionInput, } from './mixins/OxyServices.reputation';
|
|
39
40
|
export { buildUserDid } from './mixins/OxyServices.identity';
|
|
40
41
|
export type { IdentityRecordType, UnlinkableAuthMethodType, LinkAuthMethodResult, PublishRecordResult, VerifyRecordResult, VerifyDomainResult, RemoveDomainResult, } from './mixins/OxyServices.identity';
|
|
@@ -83,10 +84,13 @@ export { registrableApex } from './utils/registrableApex';
|
|
|
83
84
|
export { CENTRAL_IDP_APEX } from './utils/authWebUrl';
|
|
84
85
|
export { runColdBoot } from './utils/coldBoot';
|
|
85
86
|
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';
|
|
87
|
+
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
88
|
export type { PkcePair, BuildOAuthAuthorizeUrlParams } from './utils/oauthPkce';
|
|
89
|
+
export { buildIdpHubOrigin, buildHubSyncUrl, isIdpHubOrigin, isOfficialWebOrigin, isAllowedDeviceJoinOrigin, normalizeOfficialReturnOrigin, parseHubSyncReturnUrl, } from './utils/officialOrigins';
|
|
90
|
+
export { syncHubAfterSignIn, redeemHubTicketOnHub, } from './session/hubSync';
|
|
91
|
+
export type { SyncHubAfterSignInOptions } from './session/hubSync';
|
|
88
92
|
export { SessionClient } from './session/SessionClient';
|
|
89
|
-
export type { TokenTransport, SessionClientHost, SessionClientOptions } from './session/SessionClient';
|
|
93
|
+
export type { TokenTransport, SessionClientHost, SessionClientOptions, DeviceCredential } from './session/SessionClient';
|
|
90
94
|
export type { SocketIOFactory, MinimalSocket } from './session/socketLoader';
|
|
91
95
|
export { createSessionClientHost } from './session/sessionClientHost';
|
|
92
96
|
export { createSessionClient } from './session/createSessionClient';
|
|
@@ -99,8 +103,8 @@ export { createWebAuthStateStore, createNativeAuthStateStore, createMemoryAuthSt
|
|
|
99
103
|
export type { PersistedAuthState, AuthStateStore, NativeKeyValueStorage, } from './session/authStateStore';
|
|
100
104
|
export { refreshPersistedSession, createAuthRefreshHandler, installAuthRefreshHandler, startTokenRefreshScheduler, TOKEN_REFRESH_LEAD_MS, } from './session/refresh';
|
|
101
105
|
export type { RefreshDeps, TokenRefreshSchedulerHandle } from './session/refresh';
|
|
102
|
-
export { runSessionColdBoot } from './boot/
|
|
103
|
-
export type { RunSessionColdBootOptions, SignedOutReason, DeviceBootSession, } from './boot/
|
|
106
|
+
export { runSessionColdBoot } from './boot/sessionColdBoot';
|
|
107
|
+
export type { RunSessionColdBootOptions, SignedOutReason, DeviceBootSession, } from './boot/sessionColdBoot';
|
|
104
108
|
export { packageInfo } from './constants/version';
|
|
105
109
|
import { OxyServices } from './OxyServices';
|
|
106
110
|
export default OxyServices;
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
* registers the switched session into the operator's device-set directly).
|
|
34
34
|
*/
|
|
35
35
|
import type { User } from '../models/interfaces';
|
|
36
|
+
import type { OrganizationCategory } from '@oxyhq/contracts';
|
|
36
37
|
import type { SessionLoginResponse } from '../models/session';
|
|
37
38
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
38
39
|
/**
|
|
@@ -42,6 +43,9 @@ import type { OxyServicesBase } from '../OxyServices.base';
|
|
|
42
43
|
* and have no direct login.
|
|
43
44
|
*/
|
|
44
45
|
export type AccountKind = 'personal' | 'organization' | 'project' | 'bot';
|
|
46
|
+
/** Real-estate / team taxonomy for `kind: 'organization'` accounts. */
|
|
47
|
+
export type { OrganizationCategory } from '@oxyhq/contracts';
|
|
48
|
+
export { ORGANIZATION_CATEGORIES } from '@oxyhq/contracts';
|
|
45
49
|
/**
|
|
46
50
|
* The calling user's relationship to an account node, as resolved by the API:
|
|
47
51
|
* - `self` — the caller's own personal (root) account.
|
|
@@ -143,6 +147,8 @@ export interface CreateAccountInput {
|
|
|
143
147
|
};
|
|
144
148
|
bio?: string;
|
|
145
149
|
avatar?: string;
|
|
150
|
+
/** Meaningful only when `kind` is `organization`. */
|
|
151
|
+
organizationCategory?: OrganizationCategory;
|
|
146
152
|
}
|
|
147
153
|
/** Input accepted by `updateAccount`. Tree placement changes go through `/move`. */
|
|
148
154
|
export interface UpdateAccountInput {
|
|
@@ -153,6 +159,8 @@ export interface UpdateAccountInput {
|
|
|
153
159
|
};
|
|
154
160
|
bio?: string | null;
|
|
155
161
|
avatar?: string | null;
|
|
162
|
+
/** Clears the category when `null`; only valid on `kind: 'organization'`. */
|
|
163
|
+
organizationCategory?: OrganizationCategory | null;
|
|
156
164
|
}
|
|
157
165
|
/** Input accepted by `inviteAccountMember`. The owner role cannot be invited. */
|
|
158
166
|
export interface InviteAccountMemberInput {
|
|
@@ -319,6 +319,7 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
319
319
|
deviceId: string;
|
|
320
320
|
expiresAt: string;
|
|
321
321
|
user: User;
|
|
322
|
+
deviceSecret?: string;
|
|
322
323
|
}>;
|
|
323
324
|
/**
|
|
324
325
|
* MECHANISM A — same-device shared-keychain SSO.
|
|
@@ -477,6 +478,7 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
477
478
|
passwordSignIn(identifier: string, password: string, options?: {
|
|
478
479
|
deviceName?: string;
|
|
479
480
|
deviceFingerprint?: string;
|
|
481
|
+
deviceId?: string;
|
|
480
482
|
}): Promise<LoginResult>;
|
|
481
483
|
/**
|
|
482
484
|
* Complete a 2FA-gated sign-in started by {@link passwordSignIn}. Presents
|
|
@@ -490,6 +492,19 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
490
492
|
token?: string;
|
|
491
493
|
backupCode?: string;
|
|
492
494
|
deviceName?: string;
|
|
495
|
+
deviceId?: string;
|
|
496
|
+
}): Promise<LoginSessionResult>;
|
|
497
|
+
/**
|
|
498
|
+
* Exchange an OAuth authorization code (returned to the RP redirect URI
|
|
499
|
+
* after password sign-in at auth.oxy.so) for a device-first session.
|
|
500
|
+
* Public first-party clients use PKCE (`codeVerifier`); the access token is
|
|
501
|
+
* planted immediately on success.
|
|
502
|
+
*/
|
|
503
|
+
exchangeOAuthCode(params: {
|
|
504
|
+
code: string;
|
|
505
|
+
clientId: string;
|
|
506
|
+
redirectUri: string;
|
|
507
|
+
codeVerifier: string;
|
|
493
508
|
}): Promise<LoginSessionResult>;
|
|
494
509
|
httpService: import("../HttpService").HttpService;
|
|
495
510
|
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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UserNameResponse } from '@oxyhq/contracts';
|
|
1
|
+
import type { OrganizationCategory, UserNameResponse } from '@oxyhq/contracts';
|
|
2
2
|
export interface OxyConfig {
|
|
3
3
|
baseURL: string;
|
|
4
4
|
cloudURL?: string;
|
|
@@ -129,6 +129,8 @@ export interface User {
|
|
|
129
129
|
};
|
|
130
130
|
isManagedAccount?: boolean;
|
|
131
131
|
managedBy?: string;
|
|
132
|
+
/** Real-estate taxonomy when this user is a `kind: 'organization'` account. */
|
|
133
|
+
organizationCategory?: OrganizationCategory;
|
|
132
134
|
notificationPreferences?: NotificationPreferences;
|
|
133
135
|
userPreferences?: UserPreferences;
|
|
134
136
|
[key: string]: unknown;
|
|
@@ -31,4 +31,10 @@ export interface SessionLoginResponse {
|
|
|
31
31
|
user: MinimalUserData;
|
|
32
32
|
/** JWT access token for API authentication */
|
|
33
33
|
accessToken?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Rotating zero-cookie device credential minted on sign-in / claim. Persisted
|
|
36
|
+
* first-party alongside `deviceId` so cold boot can re-mint via
|
|
37
|
+
* `POST /session/device/token`.
|
|
38
|
+
*/
|
|
39
|
+
deviceSecret?: string;
|
|
34
40
|
}
|
|
@@ -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.1
|
|
3
|
+
"version": "9.2.1",
|
|
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 {
|
|
@@ -258,6 +258,26 @@ describe('runSessionColdBoot — shared-key-signin (native)', () => {
|
|
|
258
258
|
);
|
|
259
259
|
});
|
|
260
260
|
|
|
261
|
+
it('persists deviceSecret from shared-key verifyChallenge for the next mint lane', async () => {
|
|
262
|
+
const store = createMemoryAuthStateStore();
|
|
263
|
+
const signInWithSharedIdentity = jest.fn(async () => ({
|
|
264
|
+
...sharedSession,
|
|
265
|
+
deviceSecret: 'shared-mint-secret',
|
|
266
|
+
}));
|
|
267
|
+
const { oxy } = makeOxy({ signInWithSharedIdentity });
|
|
268
|
+
|
|
269
|
+
await runSessionColdBoot({ oxy, store, platform: NATIVE });
|
|
270
|
+
|
|
271
|
+
const persisted = await store.load();
|
|
272
|
+
expect(persisted).toMatchObject({
|
|
273
|
+
sessionId: 'sess-shared',
|
|
274
|
+
userId: 'user-shared',
|
|
275
|
+
deviceId: 'dev-1',
|
|
276
|
+
deviceSecret: 'shared-mint-secret',
|
|
277
|
+
accessToken: 'access-shared',
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
261
281
|
it('does NOT run the shared-key lane on web', async () => {
|
|
262
282
|
const store = createMemoryAuthStateStore();
|
|
263
283
|
const signInWithSharedIdentity = jest.fn(async () => sharedSession);
|
|
@@ -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' };
|
|
@@ -156,6 +156,19 @@ export async function runSessionColdBoot(
|
|
|
156
156
|
if (!session?.accessToken) {
|
|
157
157
|
return { kind: 'skip' };
|
|
158
158
|
}
|
|
159
|
+
// `verifyChallenge` mints a rotating deviceSecret; persist it so the next
|
|
160
|
+
// boot can use the faster device-secret-mint lane (sockets + tab-focus
|
|
161
|
+
// re-mint depend on the credential being in the store).
|
|
162
|
+
if (session.deviceId && session.deviceSecret) {
|
|
163
|
+
await store.save({
|
|
164
|
+
sessionId: session.sessionId,
|
|
165
|
+
userId: session.user.id,
|
|
166
|
+
deviceId: session.deviceId,
|
|
167
|
+
deviceSecret: session.deviceSecret,
|
|
168
|
+
accessToken: session.accessToken,
|
|
169
|
+
expiresAt: session.expiresAt,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
159
172
|
return {
|
|
160
173
|
kind: 'session',
|
|
161
174
|
session: {
|
package/src/index.ts
CHANGED
|
@@ -86,6 +86,7 @@ export type {
|
|
|
86
86
|
// ---------------------------------------------------------------------------
|
|
87
87
|
export type {
|
|
88
88
|
AccountKind,
|
|
89
|
+
OrganizationCategory,
|
|
89
90
|
AccountRelationship,
|
|
90
91
|
AccountRole,
|
|
91
92
|
AccountMemberStatus,
|
|
@@ -127,6 +128,8 @@ export type {
|
|
|
127
128
|
ApplicationUsageStats,
|
|
128
129
|
} from './mixins/OxyServices.accounts';
|
|
129
130
|
|
|
131
|
+
export { ORGANIZATION_CATEGORIES } from './mixins/OxyServices.accounts';
|
|
132
|
+
|
|
130
133
|
// ---------------------------------------------------------------------------
|
|
131
134
|
// Reputation (Oxy Trust: ledger, balances, disputes, rules, influence)
|
|
132
135
|
// ---------------------------------------------------------------------------
|
|
@@ -523,14 +526,38 @@ export {
|
|
|
523
526
|
generatePkcePair,
|
|
524
527
|
DEFAULT_OAUTH_SCOPE,
|
|
525
528
|
OXY_AUTHORIZE_URL,
|
|
529
|
+
OXY_OAUTH_STATE_STORAGE_KEY,
|
|
530
|
+
OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY,
|
|
531
|
+
OXY_SILENT_OAUTH_ATTEMPTED_KEY,
|
|
532
|
+
OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY,
|
|
533
|
+
normalizeOAuthRedirectUri,
|
|
534
|
+
persistOAuthHandshake,
|
|
535
|
+
readOAuthHandshake,
|
|
536
|
+
clearOAuthHandshake,
|
|
526
537
|
} from './utils/oauthPkce';
|
|
527
538
|
export type { PkcePair, BuildOAuthAuthorizeUrlParams } from './utils/oauthPkce';
|
|
528
539
|
|
|
540
|
+
export {
|
|
541
|
+
buildIdpHubOrigin,
|
|
542
|
+
buildHubSyncUrl,
|
|
543
|
+
isIdpHubOrigin,
|
|
544
|
+
isOfficialWebOrigin,
|
|
545
|
+
isAllowedDeviceJoinOrigin,
|
|
546
|
+
normalizeOfficialReturnOrigin,
|
|
547
|
+
parseHubSyncReturnUrl,
|
|
548
|
+
} from './utils/officialOrigins';
|
|
549
|
+
|
|
550
|
+
export {
|
|
551
|
+
syncHubAfterSignIn,
|
|
552
|
+
redeemHubTicketOnHub,
|
|
553
|
+
} from './session/hubSync';
|
|
554
|
+
export type { SyncHubAfterSignInOptions } from './session/hubSync';
|
|
555
|
+
|
|
529
556
|
// ---------------------------------------------------------------------------
|
|
530
557
|
// Session sync (device-scoped multi-account session client)
|
|
531
558
|
// ---------------------------------------------------------------------------
|
|
532
559
|
export { SessionClient } from './session/SessionClient';
|
|
533
|
-
export type { TokenTransport, SessionClientHost, SessionClientOptions } from './session/SessionClient';
|
|
560
|
+
export type { TokenTransport, SessionClientHost, SessionClientOptions, DeviceCredential } from './session/SessionClient';
|
|
534
561
|
// The injectable socket factory type: consumers that bundle socket.io-client
|
|
535
562
|
// (services/auth-sdk) pass its `io` export as `socketFactory` so realtime sync
|
|
536
563
|
// never relies on core's lazy dynamic import of a bare specifier.
|
|
@@ -609,12 +636,12 @@ export {
|
|
|
609
636
|
} from './session/refresh';
|
|
610
637
|
export type { RefreshDeps, TokenRefreshSchedulerHandle } from './session/refresh';
|
|
611
638
|
|
|
612
|
-
export { runSessionColdBoot } from './boot/
|
|
639
|
+
export { runSessionColdBoot } from './boot/sessionColdBoot';
|
|
613
640
|
export type {
|
|
614
641
|
RunSessionColdBootOptions,
|
|
615
642
|
SignedOutReason,
|
|
616
643
|
DeviceBootSession,
|
|
617
|
-
} from './boot/
|
|
644
|
+
} from './boot/sessionColdBoot';
|
|
618
645
|
|
|
619
646
|
// API response contracts (request/response Zod schemas + inferred types) live in
|
|
620
647
|
// `@oxyhq/contracts` — the single source of truth shared by the backend and every
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
* registers the switched session into the operator's device-set directly).
|
|
34
34
|
*/
|
|
35
35
|
import type { User } from '../models/interfaces';
|
|
36
|
+
import type { OrganizationCategory } from '@oxyhq/contracts';
|
|
36
37
|
import type { SessionLoginResponse } from '../models/session';
|
|
37
38
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
38
39
|
import { normalizeUserIdentity } from '../utils/userIdentity';
|
|
@@ -50,6 +51,10 @@ import { CACHE_TIMES } from './mixinHelpers';
|
|
|
50
51
|
*/
|
|
51
52
|
export type AccountKind = 'personal' | 'organization' | 'project' | 'bot';
|
|
52
53
|
|
|
54
|
+
/** Real-estate / team taxonomy for `kind: 'organization'` accounts. */
|
|
55
|
+
export type { OrganizationCategory } from '@oxyhq/contracts';
|
|
56
|
+
export { ORGANIZATION_CATEGORIES } from '@oxyhq/contracts';
|
|
57
|
+
|
|
53
58
|
/**
|
|
54
59
|
* The calling user's relationship to an account node, as resolved by the API:
|
|
55
60
|
* - `self` — the caller's own personal (root) account.
|
|
@@ -155,6 +160,8 @@ export interface CreateAccountInput {
|
|
|
155
160
|
name?: { first?: string; last?: string };
|
|
156
161
|
bio?: string;
|
|
157
162
|
avatar?: string;
|
|
163
|
+
/** Meaningful only when `kind` is `organization`. */
|
|
164
|
+
organizationCategory?: OrganizationCategory;
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
/** Input accepted by `updateAccount`. Tree placement changes go through `/move`. */
|
|
@@ -163,6 +170,8 @@ export interface UpdateAccountInput {
|
|
|
163
170
|
name?: { first?: string; last?: string };
|
|
164
171
|
bio?: string | null;
|
|
165
172
|
avatar?: string | null;
|
|
173
|
+
/** Clears the category when `null`; only valid on `kind: 'organization'`. */
|
|
174
|
+
organizationCategory?: OrganizationCategory | null;
|
|
166
175
|
}
|
|
167
176
|
|
|
168
177
|
/** Input accepted by `inviteAccountMember`. The owner role cannot be invited. */
|