@oxyhq/core 12.10.6 → 12.11.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 (139) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +66 -6
  3. package/dist/cjs/boot/sessionColdBoot.js +142 -37
  4. package/dist/cjs/i18n/locales/ar-SA.json +13 -1
  5. package/dist/cjs/i18n/locales/ca-ES.json +13 -1
  6. package/dist/cjs/i18n/locales/de-DE.json +13 -1
  7. package/dist/cjs/i18n/locales/en-US.json +13 -4
  8. package/dist/cjs/i18n/locales/es-ES.json +13 -4
  9. package/dist/cjs/i18n/locales/fr-FR.json +13 -1
  10. package/dist/cjs/i18n/locales/it-IT.json +13 -1
  11. package/dist/cjs/i18n/locales/ja-JP.json +13 -1
  12. package/dist/cjs/i18n/locales/ko-KR.json +13 -1
  13. package/dist/cjs/i18n/locales/locales/ar-SA.json +13 -1
  14. package/dist/cjs/i18n/locales/locales/ca-ES.json +13 -1
  15. package/dist/cjs/i18n/locales/locales/de-DE.json +13 -1
  16. package/dist/cjs/i18n/locales/locales/en-US.json +13 -4
  17. package/dist/cjs/i18n/locales/locales/es-ES.json +13 -4
  18. package/dist/cjs/i18n/locales/locales/fr-FR.json +13 -1
  19. package/dist/cjs/i18n/locales/locales/it-IT.json +13 -1
  20. package/dist/cjs/i18n/locales/locales/ja-JP.json +13 -1
  21. package/dist/cjs/i18n/locales/locales/ko-KR.json +13 -1
  22. package/dist/cjs/i18n/locales/locales/pt-PT.json +13 -1
  23. package/dist/cjs/i18n/locales/locales/zh-CN.json +13 -1
  24. package/dist/cjs/i18n/locales/pt-PT.json +13 -1
  25. package/dist/cjs/i18n/locales/zh-CN.json +13 -1
  26. package/dist/cjs/index.js +31 -5
  27. package/dist/cjs/mixins/OxyServices.auth.js +257 -17
  28. package/dist/cjs/mixins/OxyServices.deviceBoot.js +54 -3
  29. package/dist/cjs/mixins/OxyServices.notifications.js +63 -0
  30. package/dist/cjs/mixins/OxyServices.privacy.js +6 -0
  31. package/dist/cjs/mixins/OxyServices.user.js +1 -0
  32. package/dist/cjs/mixins/index.js +5 -0
  33. package/dist/cjs/session/SessionClient.js +40 -4
  34. package/dist/cjs/session/accountDialogController.js +288 -41
  35. package/dist/cjs/session/identityPin.js +256 -0
  36. package/dist/cjs/session/identitySession.js +135 -0
  37. package/dist/cjs/session/projectSessionState.js +54 -15
  38. package/dist/cjs/session/refresh.js +73 -8
  39. package/dist/cjs/utils/commonsDelivery.js +59 -0
  40. package/dist/cjs/utils/oauthPkce.js +33 -3
  41. package/dist/esm/.tsbuildinfo +1 -1
  42. package/dist/esm/HttpService.js +66 -6
  43. package/dist/esm/boot/sessionColdBoot.js +142 -37
  44. package/dist/esm/i18n/locales/ar-SA.json +13 -1
  45. package/dist/esm/i18n/locales/ca-ES.json +13 -1
  46. package/dist/esm/i18n/locales/de-DE.json +13 -1
  47. package/dist/esm/i18n/locales/en-US.json +13 -4
  48. package/dist/esm/i18n/locales/es-ES.json +13 -4
  49. package/dist/esm/i18n/locales/fr-FR.json +13 -1
  50. package/dist/esm/i18n/locales/it-IT.json +13 -1
  51. package/dist/esm/i18n/locales/ja-JP.json +13 -1
  52. package/dist/esm/i18n/locales/ko-KR.json +13 -1
  53. package/dist/esm/i18n/locales/locales/ar-SA.json +13 -1
  54. package/dist/esm/i18n/locales/locales/ca-ES.json +13 -1
  55. package/dist/esm/i18n/locales/locales/de-DE.json +13 -1
  56. package/dist/esm/i18n/locales/locales/en-US.json +13 -4
  57. package/dist/esm/i18n/locales/locales/es-ES.json +13 -4
  58. package/dist/esm/i18n/locales/locales/fr-FR.json +13 -1
  59. package/dist/esm/i18n/locales/locales/it-IT.json +13 -1
  60. package/dist/esm/i18n/locales/locales/ja-JP.json +13 -1
  61. package/dist/esm/i18n/locales/locales/ko-KR.json +13 -1
  62. package/dist/esm/i18n/locales/locales/pt-PT.json +13 -1
  63. package/dist/esm/i18n/locales/locales/zh-CN.json +13 -1
  64. package/dist/esm/i18n/locales/pt-PT.json +13 -1
  65. package/dist/esm/i18n/locales/zh-CN.json +13 -1
  66. package/dist/esm/index.js +14 -1
  67. package/dist/esm/mixins/OxyServices.auth.js +257 -17
  68. package/dist/esm/mixins/OxyServices.deviceBoot.js +52 -3
  69. package/dist/esm/mixins/OxyServices.notifications.js +60 -0
  70. package/dist/esm/mixins/OxyServices.privacy.js +6 -0
  71. package/dist/esm/mixins/OxyServices.user.js +1 -0
  72. package/dist/esm/mixins/index.js +5 -0
  73. package/dist/esm/session/SessionClient.js +40 -4
  74. package/dist/esm/session/accountDialogController.js +288 -41
  75. package/dist/esm/session/identityPin.js +249 -0
  76. package/dist/esm/session/identitySession.js +131 -0
  77. package/dist/esm/session/projectSessionState.js +54 -15
  78. package/dist/esm/session/refresh.js +73 -8
  79. package/dist/esm/utils/commonsDelivery.js +54 -0
  80. package/dist/esm/utils/oauthPkce.js +31 -2
  81. package/dist/types/.tsbuildinfo +1 -1
  82. package/dist/types/HttpService.d.ts +18 -1
  83. package/dist/types/boot/sessionColdBoot.d.ts +38 -1
  84. package/dist/types/index.d.ts +12 -4
  85. package/dist/types/mixins/OxyServices.auth.d.ts +262 -11
  86. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +31 -1
  87. package/dist/types/mixins/OxyServices.notifications.d.ts +143 -0
  88. package/dist/types/mixins/OxyServices.user.d.ts +2 -0
  89. package/dist/types/mixins/index.d.ts +2 -1
  90. package/dist/types/session/SessionClient.d.ts +28 -0
  91. package/dist/types/session/accountDialogController.d.ts +187 -9
  92. package/dist/types/session/identityPin.d.ts +87 -0
  93. package/dist/types/session/identitySession.d.ts +105 -0
  94. package/dist/types/session/projectSessionState.d.ts +21 -19
  95. package/dist/types/session/refresh.d.ts +32 -1
  96. package/dist/types/utils/commonsDelivery.d.ts +105 -0
  97. package/dist/types/utils/oauthPkce.d.ts +21 -1
  98. package/package.json +3 -3
  99. package/src/HttpService.ts +67 -6
  100. package/src/__tests__/inSessionRefresh.test.ts +67 -0
  101. package/src/boot/__tests__/sessionColdBoot.identity.test.ts +393 -0
  102. package/src/boot/sessionColdBoot.ts +183 -37
  103. package/src/i18n/locales/ar-SA.json +13 -1
  104. package/src/i18n/locales/ca-ES.json +13 -1
  105. package/src/i18n/locales/de-DE.json +13 -1
  106. package/src/i18n/locales/en-US.json +13 -4
  107. package/src/i18n/locales/es-ES.json +13 -4
  108. package/src/i18n/locales/fr-FR.json +13 -1
  109. package/src/i18n/locales/it-IT.json +13 -1
  110. package/src/i18n/locales/ja-JP.json +13 -1
  111. package/src/i18n/locales/ko-KR.json +13 -1
  112. package/src/i18n/locales/pt-PT.json +13 -1
  113. package/src/i18n/locales/zh-CN.json +13 -1
  114. package/src/index.ts +49 -0
  115. package/src/mixins/OxyServices.auth.ts +464 -24
  116. package/src/mixins/OxyServices.deviceBoot.ts +54 -2
  117. package/src/mixins/OxyServices.notifications.ts +142 -0
  118. package/src/mixins/OxyServices.privacy.ts +6 -0
  119. package/src/mixins/OxyServices.user.ts +3 -0
  120. package/src/mixins/__tests__/commonsSignIn.test.ts +754 -2
  121. package/src/mixins/__tests__/deviceTokenMintPinned.test.ts +113 -0
  122. package/src/mixins/__tests__/preSessionSkipAuth.test.ts +18 -0
  123. package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +2 -0
  124. package/src/mixins/index.ts +6 -0
  125. package/src/session/SessionClient.ts +57 -4
  126. package/src/session/__tests__/SessionClient.identityPin.test.ts +175 -0
  127. package/src/session/__tests__/accountDialogController.test.ts +812 -1
  128. package/src/session/__tests__/identityPin.test.ts +131 -0
  129. package/src/session/__tests__/identitySession.test.ts +194 -0
  130. package/src/session/__tests__/projectSessionState.test.ts +88 -0
  131. package/src/session/__tests__/refresh.identityPin.test.ts +306 -0
  132. package/src/session/accountDialogController.ts +435 -44
  133. package/src/session/identityPin.ts +310 -0
  134. package/src/session/identitySession.ts +206 -0
  135. package/src/session/projectSessionState.ts +61 -13
  136. package/src/session/refresh.ts +103 -9
  137. package/src/utils/__tests__/oauthPkce.test.ts +51 -0
  138. package/src/utils/commonsDelivery.ts +132 -0
  139. package/src/utils/oauthPkce.ts +56 -3
@@ -89,7 +89,15 @@ export declare class HttpService {
89
89
  private logger;
90
90
  private config;
91
91
  private tokenRefreshPromise;
92
- private tokenRefreshCooldownUntil;
92
+ /**
93
+ * Epoch ms of the last FAILED refresh (0 = none since the last success). The
94
+ * post-failure cooldown is measured from here; its length depends on whether
95
+ * the current token is still valid ({@link TOKEN_REFRESH_COOLDOWN_MS}) or
96
+ * already expired ({@link EXPIRED_TOKEN_REFRESH_COOLDOWN_MS}), so an expired
97
+ * token recovers promptly the instant it crosses `exp` — without storing a
98
+ * fixed deadline that could not shrink once the token expired mid-cooldown.
99
+ */
100
+ private lastRefreshFailureAt;
93
101
  private authRefreshHandler;
94
102
  private accessTokenProvider;
95
103
  private deviceSecretMintInFlight;
@@ -224,6 +232,15 @@ export declare class HttpService {
224
232
  */
225
233
  private getAuthHeader;
226
234
  refreshAccessToken(reason: AuthRefreshReason): Promise<string | null>;
235
+ /**
236
+ * Whether the CURRENT stored access token is already past its `exp`. Drives
237
+ * the shorter post-failure refresh cooldown ({@link EXPIRED_TOKEN_REFRESH_COOLDOWN_MS}):
238
+ * a still-valid (near-expiry) token can wait out the full cooldown, but an
239
+ * expired one must re-mint promptly. Returns `false` for an absent or
240
+ * opaque/no-`exp` token — no proof it is expired, so keep the conservative
241
+ * (longer) cooldown and avoid an unnecessary retry loop.
242
+ */
243
+ private isAccessTokenExpired;
227
244
  /**
228
245
  * PROCESS-WIDE single-flight for the rotating device-secret mint
229
246
  * (`POST /session/device/token`).
@@ -15,14 +15,34 @@
15
15
  * origin persisted a `deviceId` + `deviceSecret`, mint a short access token
16
16
  * with a single bearer-less POST to `/session/device/token` (no cookie, no
17
17
  * navigation) and rotate the secret in-use.
18
- * 3. `shared-key-signin` (native) — re-mint from the shared-keychain identity.
18
+ * 3. `shared-key-signin` (native, ACCOUNT mode) — re-mint from the
19
+ * shared-keychain identity — OR `identity-key-signin` (IDENTITY mode) —
20
+ * re-mint from THIS device's primary identity key.
19
21
  * 4. Signed out.
20
22
  *
23
+ * Two session modes (see {@link RunSessionColdBootOptions.sessionMode}):
24
+ * - `account` (default) — the device's ACTIVE account owns the session. Every
25
+ * Oxy app but the identity vault boots this way; behaviour is unchanged.
26
+ * - `identity` — the owner of the local PRIMARY identity key owns the session,
27
+ * permanently, regardless of which account the device is switched to. Each
28
+ * step above is bound to the persisted identity pin, and the shared-keychain
29
+ * lane is replaced by the primary-key one (the shared slot is a CROSS-APP
30
+ * slot that may hold a different identity).
31
+ *
21
32
  * ESM-safe (no `require()`); no react/react-native/expo imports.
22
33
  */
23
34
  import { type ColdBootOutcome } from '../utils/coldBoot';
35
+ import { type IdentityBinding } from '../session/identitySession';
24
36
  import type { OxyServices } from '../OxyServices';
25
37
  import type { AuthStateStore } from '../session/authStateStore';
38
+ /**
39
+ * Who owns the session this boot resolves.
40
+ *
41
+ * - `account` — the device's active account (every ordinary Oxy app).
42
+ * - `identity` — the owner of the device's primary identity key (the identity
43
+ * vault). Requires {@link RunSessionColdBootOptions.identity}.
44
+ */
45
+ export type SessionMode = 'account' | 'identity';
26
46
  /** The winning session shape a cold-boot step reports. */
27
47
  export interface DeviceBootSession {
28
48
  sessionId: string;
@@ -71,6 +91,23 @@ export interface RunSessionColdBootOptions {
71
91
  * so a flaky probe can never falsely skip a real sign-in.
72
92
  */
73
93
  isOffline?: () => boolean;
94
+ /**
95
+ * Who owns the resolved session. Defaults to `'account'` — the device's active
96
+ * account — which is the behaviour every ordinary Oxy app has today and which
97
+ * this option leaves byte-for-byte unchanged.
98
+ *
99
+ * `'identity'` binds the boot to the owner of this device's PRIMARY identity
100
+ * key and REQUIRES {@link identity}. When it is missing the boot refuses to
101
+ * run any lane (an identity-bound client silently falling back to the device's
102
+ * active account is precisely the bug this mode exists to prevent) and
103
+ * resolves signed out.
104
+ */
105
+ sessionMode?: SessionMode;
106
+ /**
107
+ * The identity binding (pin store + key/signature access) used by
108
+ * `sessionMode: 'identity'`. Ignored in `'account'` mode.
109
+ */
110
+ identity?: IdentityBinding;
74
111
  }
75
112
  /**
76
113
  * Run the device-first cold boot. Resolves to the `runColdBoot` outcome and, as
@@ -23,8 +23,11 @@ export type { LinkedHttpClient } from './OxyServices.base';
23
23
  export type { AuthRefreshReason, AuthRefreshHandler } from './HttpService';
24
24
  export { ServiceCredentialMismatchError, } from './mixins/OxyServices.auth';
25
25
  export { getCommonsApprovalBlockingReason, parseCommonsApprovalExpiresAt, } from './utils/commonsApproval';
26
+ export { selectCommonsDelivery, pushTargetsFromDelivery, commonsDeliveryPlatform } from './utils/commonsDelivery';
27
+ export type { CommonsDeliveryFacts, CommonsDeliveryPlatform, CommonsDeliveryRoute, } from './utils/commonsDelivery';
26
28
  export type { ServiceTokenResponse } from './mixins/OxyServices.auth';
27
- export type { CommonsSignInHandle, CommonsSignInStatus, CommonsApprovalInfo, CommonsSignInActionResult, } from './mixins/OxyServices.auth';
29
+ export type { CommonsSignInHandle, CommonsSignInStatus, CommonsSignInPurpose, CommonsOAuthContext, CommonsApprovalInfo, CommonsApprovalSubjectAccount, CommonsSignInActionResult, CommonsDenyReason, CommonsOAuthFinalizeResult, CommonsDeliveryResult, } from './mixins/OxyServices.auth';
30
+ export type { PushTokenPlatform, RegisterPushTokenInput, } from './mixins/OxyServices.notifications';
28
31
  export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
29
32
  export type { ContactDiscoveryMatch, ContactDiscoveryResponse, } from './mixins/OxyServices.contacts';
30
33
  export type { InitDeviceTransferResult, DeviceTransferOutcome, } from './mixins/OxyServices.deviceTransfer';
@@ -93,7 +96,7 @@ export { CENTRAL_IDP_APEX } from './utils/authWebUrl';
93
96
  export { isOxyRpOrigin } from './utils/webauthnOrigin';
94
97
  export { runColdBoot } from './utils/coldBoot';
95
98
  export type { ColdBootStep, ColdBootStepResult, ColdBootSession, ColdBootSkip, ColdBootOutcome, RunColdBootOptions, } from './utils/coldBoot';
96
- 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, OXY_OAUTH_RETURN_PATH_STORAGE_KEY, normalizeOAuthRedirectUri, persistOAuthHandshake, readOAuthHandshake, clearOAuthHandshake, persistOAuthReturnPath, consumeOAuthReturnPath, } from './utils/oauthPkce';
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_SILENT_OAUTH_ATTEMPTED_KEY, OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY, OXY_OAUTH_RETURN_PATH_STORAGE_KEY, normalizeOAuthRedirectUri, canonicalizeOAuthRedirectUri, persistOAuthHandshake, readOAuthHandshake, clearOAuthHandshake, persistOAuthReturnPath, consumeOAuthReturnPath, } from './utils/oauthPkce';
97
100
  export type { PkcePair, BuildOAuthAuthorizeUrlParams } from './utils/oauthPkce';
98
101
  export { buildIdpHubOrigin, buildHubSyncUrl, isIdpHubOrigin, isLoopbackOrigin, isOfficialWebOrigin, isAllowedDeviceJoinOrigin, normalizeOfficialReturnOrigin, parseHubSyncReturnUrl, } from './utils/officialOrigins';
99
102
  export { syncHubAfterSignIn, redeemHubTicketOnHub, } from './session/hubSync';
@@ -107,13 +110,18 @@ export { deviceStateToClientSessions, activeSessionIdOf, activeUserOf, accountId
107
110
  export { projectSwitchableAccounts, switchableAccountIds, } from './session/accountProjection';
108
111
  export type { SwitchableAccount, SwitchableAccountUser, ProjectSwitchableAccountsInput, } from './session/accountProjection';
109
112
  export { AccountDialogController, createAccountDialogController, } from './session/accountDialogController';
110
- export type { AccountDialogControllerOptions, AccountDialogSnapshot, AccountDialogView, CommonsAvailability, PopupWindowHandle, SignInFlowPhase, SignInFlowState, } from './session/accountDialogController';
113
+ export type { AccountDialogControllerOptions, AccountDialogSnapshot, AccountDialogView, CommonsAvailability, PopupWindowHandle, SignInFlowPhase, SignInFlowState, SignInProgress, } from './session/accountDialogController';
111
114
  export { createWebAuthStateStore, createNativeAuthStateStore, createMemoryAuthStateStore, AUTH_STATE_STORAGE_KEY, } from './session/authStateStore';
112
115
  export type { PersistedAuthState, AuthStateStore, NativeKeyValueStorage, } from './session/authStateStore';
116
+ export { createWebIdentityPinStore, createNativeIdentityPinStore, createMemoryIdentityPinStore, identityPinMatches, IDENTITY_PIN_STORAGE_KEY, } from './session/identityPin';
117
+ export type { IdentityPin, IdentityPinStore } from './session/identityPin';
118
+ export { resolveIdentityPin, establishIdentitySession, } from './session/identitySession';
119
+ export type { IdentityBinding, IdentityRequestOptions, EstablishedIdentitySession, } from './session/identitySession';
120
+ export { AccountNotOnDeviceError } from './mixins/OxyServices.deviceBoot';
113
121
  export { refreshPersistedSession, refreshDeviceSecretArm, createAuthRefreshHandler, installAuthRefreshHandler, startTokenRefreshScheduler, TOKEN_REFRESH_LEAD_MS, } from './session/refresh';
114
122
  export type { RefreshDeps, TokenRefreshSchedulerHandle, DeviceSecretMintOutcome } from './session/refresh';
115
123
  export { runSessionColdBoot } from './boot/sessionColdBoot';
116
- export type { RunSessionColdBootOptions, SignedOutReason, DeviceBootSession, } from './boot/sessionColdBoot';
124
+ export type { RunSessionColdBootOptions, SessionMode, SignedOutReason, DeviceBootSession, } from './boot/sessionColdBoot';
117
125
  export { packageInfo } from './constants/version';
118
126
  import { OxyServices } from './OxyServices';
119
127
  export default OxyServices;
@@ -32,12 +32,57 @@ export interface PublicKeyCheckResponse {
32
32
  registered: boolean;
33
33
  message: string;
34
34
  }
35
+ /**
36
+ * How a "Sign in with Oxy" request finalizes once the approver authorizes it.
37
+ *
38
+ * ONE request (`AuthSession`) serves every delivery surface — popup, push, QR,
39
+ * deep link — so the purpose describes the FINALIZATION, never the transport:
40
+ *
41
+ * - `device_sign_in` — the classic device flow. The initiator exchanges its
42
+ * secret `sessionToken` for the first access token via `claimSessionByToken`.
43
+ * - `oauth_authorization` — the request additionally carries an OAuth binding
44
+ * ({@link CommonsOAuthContext}), so it finalizes into a single-use
45
+ * authorization CODE via {@link OxyServicesAuthMixin.finalizeCommonsOAuth}.
46
+ * The caller still performs the PKCE token exchange itself.
47
+ */
48
+ export type CommonsSignInPurpose = 'device_sign_in' | 'oauth_authorization';
49
+ /**
50
+ * OAuth binding attached to a "Sign in with Oxy" request so a single
51
+ * `AuthSession` can finalize into a standard OAuth authorization code instead of
52
+ * a device-flow session.
53
+ *
54
+ * Only the minimum request binding is carried here — everything else (the app's
55
+ * name, icon, registered redirect URIs, trust flags) is owned server-side by the
56
+ * `Application` the `clientId` resolves to and is never client-supplied.
57
+ *
58
+ * The PKCE `codeVerifier` NEVER appears here: only its S256 `codeChallenge`
59
+ * crosses the wire, exactly as in the redirect flow. The RP-owned OAuth `state`
60
+ * also stays with the relying party, which validates it locally.
61
+ */
62
+ export interface CommonsOAuthContext {
63
+ /** Exact registered redirect URI the authorization code will be returned to. */
64
+ redirectUri: string;
65
+ /** PKCE `BASE64URL(SHA-256(codeVerifier))` (RFC 7636 §4.2); the verifier stays client-side. */
66
+ codeChallenge: string;
67
+ /** PKCE transformation method. Always `S256` — `plain` is not accepted. */
68
+ codeChallengeMethod: 'S256';
69
+ /** Space-delimited OAuth scope string; the server normalizes and validates it. */
70
+ scope?: string;
71
+ /**
72
+ * Optional delegated account the application will act AS (an organization or
73
+ * project the identity is a member of). The identity approving the request
74
+ * does not change; the server verifies the identity's permission to act as
75
+ * this account before finalizing.
76
+ */
77
+ subjectAccountId?: string;
78
+ }
35
79
  /**
36
80
  * Handle returned by {@link OxyServicesAuthMixin.startCommonsSignIn} for a
37
81
  * relying-party app initiating a "Sign in with Oxy" flow.
38
82
  *
39
83
  * `sessionToken` is the SECRET, high-entropy device-flow credential — it stays
40
- * on the initiating client, is exchanged once via `claimSessionByToken`, and is
84
+ * on the initiating client, is exchanged once via `claimSessionByToken` (device
85
+ * sign-in) or {@link OxyServicesAuthMixin.finalizeCommonsOAuth} (OAuth), and is
41
86
  * NEVER placed in the QR/deep-link. `authorizeCode` is the PUBLIC handle carried
42
87
  * in `qrPayload`; the approver (Commons) resolves it via
43
88
  * {@link OxyServicesAuthMixin.getCommonsApprovalInfo}.
@@ -54,16 +99,77 @@ export interface CommonsSignInHandle {
54
99
  /** Session lifecycle status as reported by the server (e.g. `'pending'`). */
55
100
  status: string;
56
101
  }
57
- /** Poll result for a "Sign in with Oxy" device-flow session (`GET /auth/session/status`). */
102
+ /**
103
+ * Poll result for a "Sign in with Oxy" device-flow session
104
+ * (`GET /auth/session/status`).
105
+ *
106
+ * The authoritative state machine stays small — `pending → authorized →
107
+ * consumed`, plus `cancelled` / `expired` — and lives in `status`. Delivery
108
+ * PROGRESS (`pushSentAt`, `openedAt`) is carried as timestamps beside it, never
109
+ * as competing statuses, so a progress signal can never be mistaken for an
110
+ * authorization.
111
+ */
58
112
  export interface CommonsSignInStatus {
59
113
  /** True once an approver has authorized the session. */
60
114
  authorized: boolean;
61
- /** The authorized session id (present once `authorized`). */
115
+ /** The authorized session id (present once `authorized` for device sign-in). */
62
116
  sessionId?: string;
63
117
  /** The approving identity's public key (present once `authorized`). */
64
118
  publicKey?: string;
65
119
  /** Lifecycle status (`'pending'` | `'authorized'` | `'cancelled'` | `'expired'`). */
66
120
  status?: string;
121
+ /**
122
+ * How this request finalizes. Unrecognized/missing values degrade to
123
+ * `device_sign_in` so an older API never misroutes an OAuth finalize.
124
+ */
125
+ purpose?: CommonsSignInPurpose;
126
+ /**
127
+ * ISO-8601 timestamp of when the request was pushed to a known Commons
128
+ * installation, or `null` when no push has been sent (including on a server
129
+ * that predates delivery progress). Progress only — it never implies the push
130
+ * was received, opened, or approved.
131
+ */
132
+ pushSentAt: string | null;
133
+ /**
134
+ * ISO-8601 timestamp of when the approval route was opened in Commons, or
135
+ * `null` when it has not been opened. Reported by the approver via
136
+ * {@link OxyServicesAuthMixin.markCommonsApprovalOpened}; it is an
137
+ * un-authenticated progress hint used only to advance the waiting UI, and is
138
+ * NEVER evidence that the request was approved.
139
+ */
140
+ openedAt: string | null;
141
+ }
142
+ /**
143
+ * Outcome of asking Oxy to deliver a pending sign-in request to the identity's
144
+ * known Commons installations (`POST /auth/session/deliver/:authorizeCode`).
145
+ *
146
+ * `targets: 0` is a NORMAL outcome, not a failure: it simply means no capable
147
+ * Commons installation is registered, so push is not a usable route and the
148
+ * caller shows the QR instead. Feed `targets` into `selectCommonsDelivery`
149
+ * (`utils/commonsDelivery`) rather than branching on it ad hoc.
150
+ */
151
+ export interface CommonsDeliveryResult {
152
+ /** Whether the server dispatched the request to at least one installation. */
153
+ delivered: boolean;
154
+ /** How many eligible Commons installations it was dispatched to (`0` is normal). */
155
+ targets: number;
156
+ }
157
+ /**
158
+ * The account an application will act AS once the request is approved, when the
159
+ * request delegates to an organization/project rather than the approver's own
160
+ * personal account. Resolved and sanitized server-side from the request's
161
+ * `subjectAccountId`, so it is safe to display in the approval UI.
162
+ *
163
+ * The identity approving stays the identity: Commons renders this as a distinct
164
+ * "will act as" line, never as a change of who is signing.
165
+ */
166
+ export interface CommonsApprovalSubjectAccount {
167
+ /** The delegated account's id. */
168
+ id: string;
169
+ /** The delegated account's handle. */
170
+ username: string;
171
+ /** Optional human-readable name; absent when the account has no real name. */
172
+ displayName?: string;
67
173
  }
68
174
  /**
69
175
  * Server-resolved approval context shown by the approver (Commons) before
@@ -85,6 +191,32 @@ export interface CommonsApprovalInfo {
85
191
  * "not verified") by {@link OxyServicesAuthMixin.getCommonsApprovalInfo}.
86
192
  */
87
193
  originVerified: boolean;
194
+ /**
195
+ * COARSE, display-only label of the client that STARTED the request
196
+ * (`"Chrome on Windows"`), resolved server-side from the requesting browser —
197
+ * NEVER from the QR payload. Render it verbatim as a secondary line under the
198
+ * origin; it is the whole descriptor the platform has (no raw User-Agent, no
199
+ * IP, no location is ever collected for it).
200
+ *
201
+ * `null` whenever the server has no browser context to describe: native
202
+ * requesters, unidentifiable User-Agents, and any API that predates the field.
203
+ * Omit the line entirely in that case — never substitute a guess.
204
+ */
205
+ requesterLabel: string | null;
206
+ /**
207
+ * How this request finalizes. Always present — an unrecognized or missing
208
+ * server value degrades to `'device_sign_in'`, the behaviour every server has
209
+ * always had, so an older API never makes the approver believe it is granting
210
+ * an OAuth authorization.
211
+ */
212
+ purpose: CommonsSignInPurpose;
213
+ /**
214
+ * The delegated account the application will act as, or `null` when the
215
+ * request is for the approver's own account. Always present — a missing or
216
+ * malformed server value degrades to `null` (fail-safe to "no delegation"),
217
+ * so a partial payload can never imply a broader grant than was requested.
218
+ */
219
+ subjectAccount: CommonsApprovalSubjectAccount | null;
88
220
  /** Server-authoritative expiry (epoch ms or ISO-8601 string from the API). */
89
221
  expiresAt: number | string;
90
222
  /** Session lifecycle status. */
@@ -94,6 +226,33 @@ export interface CommonsApprovalInfo {
94
226
  export interface CommonsSignInActionResult {
95
227
  success: boolean;
96
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
+ /**
241
+ * Result of finalizing an approved, OAuth-bound "Sign in with Oxy" request.
242
+ *
243
+ * This is an authorization CODE, not a session: the caller still performs the
244
+ * standard PKCE token exchange (`exchangeOAuthCode`) with the `codeVerifier` it
245
+ * has held all along. No access token, refresh token, or device secret is ever
246
+ * produced by finalization.
247
+ */
248
+ export interface CommonsOAuthFinalizeResult {
249
+ /** Single-use OAuth authorization code. */
250
+ code: string;
251
+ /** The exact registered redirect URI the request was bound to. */
252
+ redirectUri: string;
253
+ /** Lifetime of the authorization code, in seconds. */
254
+ expiresIn: number;
255
+ }
97
256
  export interface ServiceTokenResponse {
98
257
  token: string;
99
258
  expiresIn: number;
@@ -372,33 +531,120 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
372
531
  * returns the server-issued public `authorizeCode` + ready-to-render
373
532
  * `qrPayload`. Render the QR (web) / open the deep-link (same-device); the
374
533
  * approver resolves the code and authorizes. Then poll with
375
- * {@link pollCommonsSignIn} and, on `authorized`, exchange the
376
- * `sessionToken` via the existing `claimSessionByToken`.
534
+ * {@link pollCommonsSignIn} and finalize.
535
+ *
536
+ * ONE request serves every delivery surface, and how it finalizes is decided
537
+ * here by whether an OAuth binding is attached:
538
+ * - no `oauth` (the default): a `device_sign_in` request — on `authorized`,
539
+ * exchange the `sessionToken` via the existing `claimSessionByToken`.
540
+ * - with `oauth`: an `oauth_authorization` request — on `authorized`, call
541
+ * {@link finalizeCommonsOAuth} with the same `sessionToken` to mint the
542
+ * single-use authorization code, then exchange it with PKCE.
377
543
  *
378
544
  * @param params.clientId - The RP's registered OAuth client id
379
545
  * (ApplicationCredential publicKey); required so the server can resolve the
380
546
  * requesting application's identity.
547
+ * @param params.oauth - Optional OAuth binding ({@link CommonsOAuthContext}).
548
+ * Carries only the redirect URI, the PKCE S256 challenge, the requested
549
+ * scope, and an optional delegated `subjectAccountId` — never the PKCE
550
+ * verifier, the OAuth `state`, or any token.
381
551
  */
382
552
  startCommonsSignIn(params: {
383
553
  clientId: string;
554
+ oauth?: CommonsOAuthContext;
384
555
  }): Promise<CommonsSignInHandle>;
385
556
  /**
386
557
  * MECHANISM B (relying party) — poll a device-flow session for approval.
387
558
  *
388
559
  * Backstop for the auth socket. On `authorized` (with a `sessionId`), the
389
- * caller exchanges the secret `sessionToken` via the existing
390
- * `claimSessionByToken` to mint the first access token.
560
+ * caller finalizes: `claimSessionByToken` for a `device_sign_in` request,
561
+ * {@link finalizeCommonsOAuth} for an `oauth_authorization` one.
562
+ *
563
+ * Every field is narrowed fail-safe. `authorized` counts only as a literal
564
+ * `true`, the identifiers only as non-empty strings, and the delivery
565
+ * progress timestamps degrade to `null` when absent or unparseable — so a
566
+ * partial or older-API payload can advance the waiting UI at most, never
567
+ * make it believe a request was approved.
391
568
  *
392
569
  * @param sessionToken - The secret token from {@link startCommonsSignIn}.
393
570
  */
394
571
  pollCommonsSignIn(sessionToken: string): Promise<CommonsSignInStatus>;
572
+ /**
573
+ * MECHANISM B (relying party) — ask Oxy to DELIVER a pending sign-in request
574
+ * to the identity's known Commons installations.
575
+ *
576
+ * This is the automatic half of "one intention, one primary action": rather
577
+ * than offering the user a menu of transports, the caller asks for delivery
578
+ * and lets the answer pick the route. Pass the returned `targets` to
579
+ * `selectCommonsDelivery` (`utils/commonsDelivery`) — `targets: 0` means no
580
+ * capable Commons installation is registered, which is a NORMAL outcome that
581
+ * resolves to the QR route, not an error to surface.
582
+ *
583
+ * **Requires a bearer.** Delivery is only allowed when Oxy already knows the
584
+ * intended identity from a trusted authenticated context — a request that
585
+ * merely carries a username or email typed into an unauthenticated browser
586
+ * must never be able to ring somebody's phone.
587
+ *
588
+ * The push it sends carries only `{ type, approvalUrl }` where the URL holds
589
+ * the public `authorizeCode` — no display data, no secrets. Commons resolves
590
+ * everything it shows from `getCommonsApprovalInfo`.
591
+ *
592
+ * @param authorizeCode - The public code from {@link startCommonsSignIn}.
593
+ */
594
+ deliverCommonsSignIn(authorizeCode: string): Promise<CommonsDeliveryResult>;
595
+ /**
596
+ * MECHANISM B (approver / Commons) — report that the approval route was
597
+ * OPENED, so the waiting relying party can show "Opened in Commons".
598
+ *
599
+ * Progress only. It is idempotent, applies to a `pending` request alone, and
600
+ * records a timestamp (`openedAt`) — it never approves, authorizes, or
601
+ * advances the authorization state machine. Public, like the other approver
602
+ * handles: the approver has only the public `authorizeCode` at this point
603
+ * and has not yet signed anything.
604
+ *
605
+ * Best-effort by nature — a failure here costs the user only a progress
606
+ * line, so callers are free to ignore a rejection and continue to the
607
+ * approval screen.
608
+ *
609
+ * @param authorizeCode - The public code scanned from the QR / deep-link / push.
610
+ */
611
+ markCommonsApprovalOpened(authorizeCode: string): Promise<void>;
612
+ /**
613
+ * MECHANISM B (relying party) — finalize an APPROVED, OAuth-bound request
614
+ * into a single-use OAuth authorization code.
615
+ *
616
+ * The OAuth counterpart of `claimSessionByToken`: same secret credential,
617
+ * same single-use semantics, different output. Call it once the request the
618
+ * RP started with an `oauth` binding reports `authorized`; the server
619
+ * atomically mints exactly ONE `AuthCode` bound to the redirect URI, PKCE
620
+ * challenge, scopes, approving identity, and any delegated subject account
621
+ * the request was created with. A second call cannot mint another code.
622
+ *
623
+ * The result is an authorization CODE, never a token — the caller completes
624
+ * the flow with the ordinary PKCE exchange (`exchangeOAuthCode`) using the
625
+ * `codeVerifier` it never sent anywhere. Nothing here is exposed to the
626
+ * popup: the code travels back through the registered callback, and the
627
+ * main window owns the verifier.
628
+ *
629
+ * Like `claimSessionByToken`, this needs no Authorization header — the
630
+ * high-entropy SECRET `sessionToken` IS the credential. Never pass the
631
+ * public `authorizeCode` here; it is the approver's handle, not the
632
+ * initiator's. Every server-side failure (wrong/expired/already-finalized
633
+ * request, non-OAuth purpose, missing permission for the delegated account)
634
+ * surfaces as one generic error, so nothing about the request's state can be
635
+ * probed from outside.
636
+ *
637
+ * @param sessionToken - The secret token from {@link startCommonsSignIn}.
638
+ */
639
+ finalizeCommonsOAuth(sessionToken: string): Promise<CommonsOAuthFinalizeResult>;
395
640
  /**
396
641
  * MECHANISM B (approver / Commons) — resolve the TRUSTED identity of a
397
642
  * sign-in request from its public `authorizeCode`.
398
643
  *
399
- * The returned `application` is resolved server-side and is the only safe
400
- * thing to display in the approval UI — NEVER trust the app/name/origin
401
- * strings carried in the QR payload. Public (no auth required).
644
+ * The returned `application` and `subjectAccount` are resolved server-side
645
+ * and are the only safe things to display in the approval UI — NEVER trust
646
+ * the app/name/origin strings carried in the QR payload. Public (no auth
647
+ * required).
402
648
  *
403
649
  * @param authorizeCode - The public code scanned from the QR / deep-link.
404
650
  */
@@ -426,8 +672,13 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
426
672
  * device-flow session so the RP stops waiting.
427
673
  *
428
674
  * @param authorizeCode - The public code being denied.
675
+ * @param reason - Optional closed-set reason ({@link CommonsDenyReason}).
676
+ * Pass `'not_me'` ONLY when the user actually reported the request as one
677
+ * they did not start — the server records it as a suspicious denial rather
678
+ * than an ordinary cancel. Omitting it sends the exact body this endpoint
679
+ * has always received.
429
680
  */
430
- denyCommonsSignIn(authorizeCode: string): Promise<CommonsSignInActionResult>;
681
+ denyCommonsSignIn(authorizeCode: string, reason?: CommonsDenyReason): Promise<CommonsSignInActionResult>;
431
682
  /**
432
683
  * Internal: decode (without verifying) the `sessionId` claim from a
433
684
  * server-signed access token. The server already verified the signature;
@@ -15,6 +15,24 @@
15
15
  */
16
16
  import { type DeviceTokenMintResponse, type DeviceHubTicketIssueResponse, type DeviceHubTicketRedeemResponse } from '@oxyhq/contracts';
17
17
  import type { OxyServicesBase } from '../OxyServices.base';
18
+ /**
19
+ * The server's `401 account_not_on_device` for a PINNED mint: the requested
20
+ * `accountId` is not (or is no longer) a live account of this device session.
21
+ *
22
+ * Distinguished from every other mint 401 because the remedy is different: the
23
+ * device secret is FINE — it is the identity binding that went stale (the
24
+ * account was signed out on this device, or revoked). An identity-bound caller
25
+ * must re-establish its session from the local key rather than drop/clear the
26
+ * device credential.
27
+ */
28
+ export declare class AccountNotOnDeviceError extends Error {
29
+ readonly accountId: string;
30
+ readonly cause?: unknown | undefined;
31
+ readonly name = "AccountNotOnDeviceError";
32
+ /** HTTP status of the originating response; mirrors the ApiError shape. */
33
+ readonly status = 401;
34
+ constructor(accountId: string, cause?: unknown | undefined);
35
+ }
18
36
  export declare function OxyServicesDeviceBootMixin<T extends typeof OxyServicesBase>(Base: T): {
19
37
  new (...args: any[]): {
20
38
  /**
@@ -38,9 +56,21 @@ export declare function OxyServicesDeviceBootMixin<T extends typeof OxyServicesB
38
56
  * boot's worst-case time-to-route. A transient failure surfaces once and the
39
57
  * scheduler/401 path retries it later.
40
58
  *
59
+ * `options.accountId` PINS the mint to one account of the device instead of
60
+ * whichever account is currently active. It exists for identity-bound
61
+ * clients (Commons), whose authenticated user is fixed by a local
62
+ * cryptographic key and must never follow an account switch made by another
63
+ * app on the same device. The server never mutates `activeAccountId` for a
64
+ * pinned mint — the returned `state` still reports the device's true active
65
+ * account — and rejects a non-member/dead account with
66
+ * `401 account_not_on_device`, surfaced here as {@link AccountNotOnDeviceError}.
67
+ *
68
+ * @throws {AccountNotOnDeviceError} when a pinned mint's account is not on the device.
41
69
  * @throws if the response does not match {@link deviceTokenMintResponseSchema}.
42
70
  */
43
- mintFromDeviceSecret(deviceId: string, deviceSecret: string): Promise<DeviceTokenMintResponse>;
71
+ mintFromDeviceSecret(deviceId: string, deviceSecret: string, options?: {
72
+ accountId?: string;
73
+ }): Promise<DeviceTokenMintResponse>;
44
74
  /** Mint a one-time hub sync ticket (bearer required). */
45
75
  issueHubTicket(returnOrigin: string): Promise<DeviceHubTicketIssueResponse>;
46
76
  /** Redeem a hub sync ticket for a fresh device secret (public). */