@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.
Files changed (76) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/.tsbuildinfo +1 -1
  3. package/dist/cjs/boot/{coldBootV2.js → sessionColdBoot.js} +15 -2
  4. package/dist/cjs/index.js +27 -6
  5. package/dist/cjs/mixins/OxyServices.accounts.js +3 -0
  6. package/dist/cjs/mixins/OxyServices.auth.js +61 -0
  7. package/dist/cjs/mixins/OxyServices.deviceBoot.js +29 -1
  8. package/dist/cjs/server/index.js +8 -1
  9. package/dist/cjs/session/SessionClient.js +57 -25
  10. package/dist/cjs/session/accountDialogController.js +21 -9
  11. package/dist/cjs/session/authStateStore.js +13 -7
  12. package/dist/cjs/session/hubSync.js +55 -0
  13. package/dist/cjs/session/sessionClientHost.js +5 -0
  14. package/dist/cjs/utils/coldBoot.js +1 -1
  15. package/dist/cjs/utils/oauthPkce.js +65 -1
  16. package/dist/cjs/utils/officialOrigins.js +128 -0
  17. package/dist/esm/.tsbuildinfo +1 -1
  18. package/dist/esm/boot/{coldBootV2.js → sessionColdBoot.js} +15 -2
  19. package/dist/esm/index.js +5 -2
  20. package/dist/esm/mixins/OxyServices.accounts.js +1 -0
  21. package/dist/esm/mixins/OxyServices.auth.js +61 -0
  22. package/dist/esm/mixins/OxyServices.deviceBoot.js +30 -2
  23. package/dist/esm/server/index.js +1 -0
  24. package/dist/esm/session/SessionClient.js +57 -25
  25. package/dist/esm/session/accountDialogController.js +21 -9
  26. package/dist/esm/session/authStateStore.js +13 -7
  27. package/dist/esm/session/hubSync.js +51 -0
  28. package/dist/esm/session/sessionClientHost.js +5 -0
  29. package/dist/esm/utils/coldBoot.js +1 -1
  30. package/dist/esm/utils/oauthPkce.js +60 -0
  31. package/dist/esm/utils/officialOrigins.js +119 -0
  32. package/dist/types/.tsbuildinfo +1 -1
  33. package/dist/types/boot/{coldBootV2.d.ts → sessionColdBoot.d.ts} +1 -1
  34. package/dist/types/index.d.ts +9 -5
  35. package/dist/types/mixins/OxyServices.accounts.d.ts +8 -0
  36. package/dist/types/mixins/OxyServices.auth.d.ts +15 -0
  37. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +6 -2
  38. package/dist/types/models/interfaces.d.ts +3 -1
  39. package/dist/types/models/session.d.ts +6 -0
  40. package/dist/types/server/index.d.ts +1 -0
  41. package/dist/types/session/SessionClient.d.ts +6 -0
  42. package/dist/types/session/accountDialogController.d.ts +9 -1
  43. package/dist/types/session/authStateStore.d.ts +1 -1
  44. package/dist/types/session/hubSync.d.ts +20 -0
  45. package/dist/types/session/sessionClientHost.d.ts +2 -1
  46. package/dist/types/utils/coldBoot.d.ts +2 -2
  47. package/dist/types/utils/oauthPkce.d.ts +27 -0
  48. package/dist/types/utils/officialOrigins.d.ts +17 -0
  49. package/package.json +2 -2
  50. package/src/boot/__tests__/{coldBootV2.test.ts → sessionColdBoot.test.ts} +21 -1
  51. package/src/boot/{coldBootV2.ts → sessionColdBoot.ts} +15 -2
  52. package/src/index.ts +30 -3
  53. package/src/mixins/OxyServices.accounts.ts +9 -0
  54. package/src/mixins/OxyServices.auth.ts +72 -1
  55. package/src/mixins/OxyServices.deviceBoot.ts +46 -1
  56. package/src/models/interfaces.ts +3 -1
  57. package/src/models/session.ts +6 -0
  58. package/src/server/index.ts +8 -0
  59. package/src/session/SessionClient.ts +68 -26
  60. package/src/session/__tests__/SessionClient.additive.test.ts +1 -0
  61. package/src/session/__tests__/SessionClient.broadcastChannel.test.ts +1 -0
  62. package/src/session/__tests__/SessionClient.diagnostics.test.ts +1 -0
  63. package/src/session/__tests__/SessionClient.rest.test.ts +1 -0
  64. package/src/session/__tests__/SessionClient.socket.test.ts +1 -0
  65. package/src/session/__tests__/SessionClient.socketFactory.test.ts +1 -0
  66. package/src/session/__tests__/SessionClient.state.test.ts +1 -0
  67. package/src/session/__tests__/accountDialogController.test.ts +46 -7
  68. package/src/session/__tests__/sessionIntegration.test.ts +7 -0
  69. package/src/session/accountDialogController.ts +45 -11
  70. package/src/session/authStateStore.ts +18 -9
  71. package/src/session/hubSync.ts +79 -0
  72. package/src/session/sessionClientHost.ts +10 -2
  73. package/src/utils/__tests__/officialOrigins.test.ts +74 -0
  74. package/src/utils/coldBoot.ts +2 -2
  75. package/src/utils/oauthPkce.ts +71 -0
  76. package/src/utils/officialOrigins.ts +124 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * coldBootV2 — one device-first cold boot for every consumer.
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
@@ -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/coldBootV2';
103
- export type { RunSessionColdBootOptions, SignedOutReason, DeviceBootSession, } from './boot/coldBootV2';
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 (`coldBootV2`) and the unified re-mint handler (`refresh.ts`)
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
- }): string;
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 (`coldBootV2`) and the unified re-mint
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/coldBootV2.ts`) is the current
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. `coldBootV2`'s
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.0",
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.0",
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 '../coldBootV2';
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
- * coldBootV2 — one device-first cold boot for every consumer.
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: 'coldBootV2', method: 'device-secret-mint' },
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/coldBootV2';
639
+ export { runSessionColdBoot } from './boot/sessionColdBoot';
613
640
  export type {
614
641
  RunSessionColdBootOptions,
615
642
  SignedOutReason,
616
643
  DeviceBootSession,
617
- } from './boot/coldBootV2';
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. */