@oxyhq/core 12.10.5 → 12.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/boot/sessionColdBoot.js +142 -37
- package/dist/cjs/i18n/locales/ar-SA.json +13 -1
- package/dist/cjs/i18n/locales/ca-ES.json +13 -1
- package/dist/cjs/i18n/locales/de-DE.json +13 -1
- package/dist/cjs/i18n/locales/en-US.json +13 -4
- package/dist/cjs/i18n/locales/es-ES.json +13 -4
- package/dist/cjs/i18n/locales/fr-FR.json +13 -1
- package/dist/cjs/i18n/locales/it-IT.json +13 -1
- package/dist/cjs/i18n/locales/ja-JP.json +13 -1
- package/dist/cjs/i18n/locales/ko-KR.json +13 -1
- package/dist/cjs/i18n/locales/locales/ar-SA.json +13 -1
- package/dist/cjs/i18n/locales/locales/ca-ES.json +13 -1
- package/dist/cjs/i18n/locales/locales/de-DE.json +13 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +13 -4
- package/dist/cjs/i18n/locales/locales/es-ES.json +13 -4
- package/dist/cjs/i18n/locales/locales/fr-FR.json +13 -1
- package/dist/cjs/i18n/locales/locales/it-IT.json +13 -1
- package/dist/cjs/i18n/locales/locales/ja-JP.json +13 -1
- package/dist/cjs/i18n/locales/locales/ko-KR.json +13 -1
- package/dist/cjs/i18n/locales/locales/pt-PT.json +13 -1
- package/dist/cjs/i18n/locales/locales/zh-CN.json +13 -1
- package/dist/cjs/i18n/locales/pt-PT.json +13 -1
- package/dist/cjs/i18n/locales/zh-CN.json +13 -1
- package/dist/cjs/index.js +31 -5
- package/dist/cjs/mixins/OxyServices.auth.js +257 -17
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +54 -3
- package/dist/cjs/mixins/OxyServices.notifications.js +63 -0
- package/dist/cjs/mixins/index.js +5 -0
- package/dist/cjs/session/SessionClient.js +40 -4
- package/dist/cjs/session/accountDialogController.js +288 -41
- package/dist/cjs/session/hubSync.js +1 -1
- package/dist/cjs/session/identityPin.js +256 -0
- package/dist/cjs/session/identitySession.js +135 -0
- package/dist/cjs/session/projectSessionState.js +54 -15
- package/dist/cjs/session/refresh.js +73 -8
- package/dist/cjs/utils/commonsDelivery.js +59 -0
- package/dist/cjs/utils/oauthPkce.js +33 -3
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/boot/sessionColdBoot.js +142 -37
- package/dist/esm/i18n/locales/ar-SA.json +13 -1
- package/dist/esm/i18n/locales/ca-ES.json +13 -1
- package/dist/esm/i18n/locales/de-DE.json +13 -1
- package/dist/esm/i18n/locales/en-US.json +13 -4
- package/dist/esm/i18n/locales/es-ES.json +13 -4
- package/dist/esm/i18n/locales/fr-FR.json +13 -1
- package/dist/esm/i18n/locales/it-IT.json +13 -1
- package/dist/esm/i18n/locales/ja-JP.json +13 -1
- package/dist/esm/i18n/locales/ko-KR.json +13 -1
- package/dist/esm/i18n/locales/locales/ar-SA.json +13 -1
- package/dist/esm/i18n/locales/locales/ca-ES.json +13 -1
- package/dist/esm/i18n/locales/locales/de-DE.json +13 -1
- package/dist/esm/i18n/locales/locales/en-US.json +13 -4
- package/dist/esm/i18n/locales/locales/es-ES.json +13 -4
- package/dist/esm/i18n/locales/locales/fr-FR.json +13 -1
- package/dist/esm/i18n/locales/locales/it-IT.json +13 -1
- package/dist/esm/i18n/locales/locales/ja-JP.json +13 -1
- package/dist/esm/i18n/locales/locales/ko-KR.json +13 -1
- package/dist/esm/i18n/locales/locales/pt-PT.json +13 -1
- package/dist/esm/i18n/locales/locales/zh-CN.json +13 -1
- package/dist/esm/i18n/locales/pt-PT.json +13 -1
- package/dist/esm/i18n/locales/zh-CN.json +13 -1
- package/dist/esm/index.js +14 -1
- package/dist/esm/mixins/OxyServices.auth.js +257 -17
- package/dist/esm/mixins/OxyServices.deviceBoot.js +52 -3
- package/dist/esm/mixins/OxyServices.notifications.js +60 -0
- package/dist/esm/mixins/index.js +5 -0
- package/dist/esm/session/SessionClient.js +40 -4
- package/dist/esm/session/accountDialogController.js +288 -41
- package/dist/esm/session/hubSync.js +1 -1
- package/dist/esm/session/identityPin.js +249 -0
- package/dist/esm/session/identitySession.js +131 -0
- package/dist/esm/session/projectSessionState.js +54 -15
- package/dist/esm/session/refresh.js +73 -8
- package/dist/esm/utils/commonsDelivery.js +54 -0
- package/dist/esm/utils/oauthPkce.js +31 -2
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/boot/sessionColdBoot.d.ts +38 -1
- package/dist/types/index.d.ts +12 -4
- package/dist/types/mixins/OxyServices.auth.d.ts +262 -11
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +31 -1
- package/dist/types/mixins/OxyServices.notifications.d.ts +143 -0
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/session/SessionClient.d.ts +28 -0
- package/dist/types/session/accountDialogController.d.ts +187 -9
- package/dist/types/session/identityPin.d.ts +87 -0
- package/dist/types/session/identitySession.d.ts +105 -0
- package/dist/types/session/projectSessionState.d.ts +21 -19
- package/dist/types/session/refresh.d.ts +32 -1
- package/dist/types/utils/commonsDelivery.d.ts +105 -0
- package/dist/types/utils/oauthPkce.d.ts +21 -1
- package/package.json +2 -2
- package/src/boot/__tests__/sessionColdBoot.identity.test.ts +393 -0
- package/src/boot/sessionColdBoot.ts +183 -37
- package/src/i18n/locales/ar-SA.json +13 -1
- package/src/i18n/locales/ca-ES.json +13 -1
- package/src/i18n/locales/de-DE.json +13 -1
- package/src/i18n/locales/en-US.json +13 -4
- package/src/i18n/locales/es-ES.json +13 -4
- package/src/i18n/locales/fr-FR.json +13 -1
- package/src/i18n/locales/it-IT.json +13 -1
- package/src/i18n/locales/ja-JP.json +13 -1
- package/src/i18n/locales/ko-KR.json +13 -1
- package/src/i18n/locales/pt-PT.json +13 -1
- package/src/i18n/locales/zh-CN.json +13 -1
- package/src/index.ts +49 -0
- package/src/mixins/OxyServices.auth.ts +464 -24
- package/src/mixins/OxyServices.deviceBoot.ts +54 -2
- package/src/mixins/OxyServices.notifications.ts +142 -0
- package/src/mixins/__tests__/commonsSignIn.test.ts +754 -2
- package/src/mixins/__tests__/deviceTokenMintPinned.test.ts +113 -0
- package/src/mixins/__tests__/preSessionSkipAuth.test.ts +18 -0
- package/src/mixins/index.ts +6 -0
- package/src/session/SessionClient.ts +57 -4
- package/src/session/__tests__/SessionClient.identityPin.test.ts +175 -0
- package/src/session/__tests__/accountDialogController.test.ts +812 -1
- package/src/session/__tests__/hubSync.test.ts +51 -0
- package/src/session/__tests__/identityPin.test.ts +131 -0
- package/src/session/__tests__/identitySession.test.ts +194 -0
- package/src/session/__tests__/projectSessionState.test.ts +88 -0
- package/src/session/__tests__/refresh.identityPin.test.ts +306 -0
- package/src/session/accountDialogController.ts +435 -44
- package/src/session/hubSync.ts +1 -1
- package/src/session/identityPin.ts +310 -0
- package/src/session/identitySession.ts +206 -0
- package/src/session/projectSessionState.ts +61 -13
- package/src/session/refresh.ts +103 -9
- package/src/utils/__tests__/oauthPkce.test.ts +51 -0
- package/src/utils/commonsDelivery.ts +132 -0
- package/src/utils/oauthPkce.ts +56 -3
|
@@ -124,7 +124,19 @@
|
|
|
124
124
|
"commonsNotInstalled": "Commonsがありませんか?アプリを入手してOxy IDでサインインしましょう。",
|
|
125
125
|
"showQrAnyway": "別のデバイスにCommonsがあります",
|
|
126
126
|
"switchWhileSignedInAs": "アカウントを切り替え({{name}} としてログイン中)",
|
|
127
|
-
"manageOnDevice": "このデバイスのアカウントを管理"
|
|
127
|
+
"manageOnDevice": "このデバイスのアカウントを管理",
|
|
128
|
+
"continueWithOxy": "Oxyで続ける",
|
|
129
|
+
"havingTrouble": "うまくいきませんか?",
|
|
130
|
+
"progress": {
|
|
131
|
+
"preparing": "リクエストを準備しています",
|
|
132
|
+
"awaitingApproval": "承認を待っています",
|
|
133
|
+
"scanWithCommons": "スマートフォンのCommonsでスキャンしてください",
|
|
134
|
+
"continueInCommons": "Commonsで続けてください",
|
|
135
|
+
"checkCommons": "スマートフォンのCommonsを確認してください",
|
|
136
|
+
"openedInCommons": "Commonsで開きました",
|
|
137
|
+
"confirming": "本人確認をしています",
|
|
138
|
+
"confirmed": "本人確認が完了しました"
|
|
139
|
+
}
|
|
128
140
|
},
|
|
129
141
|
"common": {
|
|
130
142
|
"actions": {
|
|
@@ -124,7 +124,19 @@
|
|
|
124
124
|
"commonsNotInstalled": "Commons가 없으신가요? 앱을 다운로드하여 Oxy ID로 로그인하세요.",
|
|
125
125
|
"showQrAnyway": "다른 기기에 Commons가 있습니다",
|
|
126
126
|
"switchWhileSignedInAs": "계정 전환, {{name}}(으)로 로그인됨",
|
|
127
|
-
"manageOnDevice": "이 기기의 계정 관리"
|
|
127
|
+
"manageOnDevice": "이 기기의 계정 관리",
|
|
128
|
+
"continueWithOxy": "Oxy로 계속하기",
|
|
129
|
+
"havingTrouble": "문제가 있으신가요?",
|
|
130
|
+
"progress": {
|
|
131
|
+
"preparing": "요청 준비 중",
|
|
132
|
+
"awaitingApproval": "승인 대기 중",
|
|
133
|
+
"scanWithCommons": "휴대폰의 Commons로 스캔하세요",
|
|
134
|
+
"continueInCommons": "Commons에서 계속하세요",
|
|
135
|
+
"checkCommons": "휴대폰에서 Commons를 확인하세요",
|
|
136
|
+
"openedInCommons": "Commons에서 열림",
|
|
137
|
+
"confirming": "신원 확인 중",
|
|
138
|
+
"confirmed": "신원 확인 완료"
|
|
139
|
+
}
|
|
128
140
|
},
|
|
129
141
|
"common": {
|
|
130
142
|
"actions": {
|
|
@@ -124,7 +124,19 @@
|
|
|
124
124
|
"commonsNotInstalled": "Não tem o Commons? Obtenha a app para iniciar sessão com o seu Oxy ID.",
|
|
125
125
|
"showQrAnyway": "Tenho o Commons noutro dispositivo",
|
|
126
126
|
"switchWhileSignedInAs": "Mudar de conta, com sessão iniciada como {{name}}",
|
|
127
|
-
"manageOnDevice": "Gerir contas neste dispositivo"
|
|
127
|
+
"manageOnDevice": "Gerir contas neste dispositivo",
|
|
128
|
+
"continueWithOxy": "Continuar com Oxy",
|
|
129
|
+
"havingTrouble": "Algum problema?",
|
|
130
|
+
"progress": {
|
|
131
|
+
"preparing": "A preparar o pedido",
|
|
132
|
+
"awaitingApproval": "A aguardar aprovação",
|
|
133
|
+
"scanWithCommons": "Digitalize com o Commons no seu telemóvel",
|
|
134
|
+
"continueInCommons": "Continue no Commons",
|
|
135
|
+
"checkCommons": "Verifique o Commons no seu telemóvel",
|
|
136
|
+
"openedInCommons": "Aberto no Commons",
|
|
137
|
+
"confirming": "A confirmar a identidade",
|
|
138
|
+
"confirmed": "Identidade confirmada"
|
|
139
|
+
}
|
|
128
140
|
},
|
|
129
141
|
"common": {
|
|
130
142
|
"actions": {
|
|
@@ -124,7 +124,19 @@
|
|
|
124
124
|
"commonsNotInstalled": "没有 Commons?获取应用以使用您的 Oxy ID 登录。",
|
|
125
125
|
"showQrAnyway": "我在另一台设备上有 Commons",
|
|
126
126
|
"switchWhileSignedInAs": "切换账号,当前登录为 {{name}}",
|
|
127
|
-
"manageOnDevice": "管理此设备上的账号"
|
|
127
|
+
"manageOnDevice": "管理此设备上的账号",
|
|
128
|
+
"continueWithOxy": "使用 Oxy 继续",
|
|
129
|
+
"havingTrouble": "遇到问题?",
|
|
130
|
+
"progress": {
|
|
131
|
+
"preparing": "正在准备请求",
|
|
132
|
+
"awaitingApproval": "正在等待批准",
|
|
133
|
+
"scanWithCommons": "请用手机上的 Commons 扫描",
|
|
134
|
+
"continueInCommons": "请在 Commons 中继续",
|
|
135
|
+
"checkCommons": "请查看手机上的 Commons",
|
|
136
|
+
"openedInCommons": "已在 Commons 中打开",
|
|
137
|
+
"confirming": "正在确认身份",
|
|
138
|
+
"confirmed": "身份已确认"
|
|
139
|
+
}
|
|
128
140
|
},
|
|
129
141
|
"common": {
|
|
130
142
|
"actions": {
|
|
@@ -124,7 +124,19 @@
|
|
|
124
124
|
"commonsNotInstalled": "Não tem o Commons? Obtenha a app para iniciar sessão com o seu Oxy ID.",
|
|
125
125
|
"showQrAnyway": "Tenho o Commons noutro dispositivo",
|
|
126
126
|
"switchWhileSignedInAs": "Mudar de conta, com sessão iniciada como {{name}}",
|
|
127
|
-
"manageOnDevice": "Gerir contas neste dispositivo"
|
|
127
|
+
"manageOnDevice": "Gerir contas neste dispositivo",
|
|
128
|
+
"continueWithOxy": "Continuar com Oxy",
|
|
129
|
+
"havingTrouble": "Algum problema?",
|
|
130
|
+
"progress": {
|
|
131
|
+
"preparing": "A preparar o pedido",
|
|
132
|
+
"awaitingApproval": "A aguardar aprovação",
|
|
133
|
+
"scanWithCommons": "Digitalize com o Commons no seu telemóvel",
|
|
134
|
+
"continueInCommons": "Continue no Commons",
|
|
135
|
+
"checkCommons": "Verifique o Commons no seu telemóvel",
|
|
136
|
+
"openedInCommons": "Aberto no Commons",
|
|
137
|
+
"confirming": "A confirmar a identidade",
|
|
138
|
+
"confirmed": "Identidade confirmada"
|
|
139
|
+
}
|
|
128
140
|
},
|
|
129
141
|
"common": {
|
|
130
142
|
"actions": {
|
|
@@ -124,7 +124,19 @@
|
|
|
124
124
|
"commonsNotInstalled": "没有 Commons?获取应用以使用您的 Oxy ID 登录。",
|
|
125
125
|
"showQrAnyway": "我在另一台设备上有 Commons",
|
|
126
126
|
"switchWhileSignedInAs": "切换账号,当前登录为 {{name}}",
|
|
127
|
-
"manageOnDevice": "管理此设备上的账号"
|
|
127
|
+
"manageOnDevice": "管理此设备上的账号",
|
|
128
|
+
"continueWithOxy": "使用 Oxy 继续",
|
|
129
|
+
"havingTrouble": "遇到问题?",
|
|
130
|
+
"progress": {
|
|
131
|
+
"preparing": "正在准备请求",
|
|
132
|
+
"awaitingApproval": "正在等待批准",
|
|
133
|
+
"scanWithCommons": "请用手机上的 Commons 扫描",
|
|
134
|
+
"continueInCommons": "请在 Commons 中继续",
|
|
135
|
+
"checkCommons": "请查看手机上的 Commons",
|
|
136
|
+
"openedInCommons": "已在 Commons 中打开",
|
|
137
|
+
"confirming": "正在确认身份",
|
|
138
|
+
"confirmed": "身份已确认"
|
|
139
|
+
}
|
|
128
140
|
},
|
|
129
141
|
"common": {
|
|
130
142
|
"actions": {
|
package/dist/esm/index.js
CHANGED
|
@@ -28,6 +28,9 @@ export { OXY_CLOUD_URL, oxyClient } from './OxyServices.js';
|
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
29
|
export { ServiceCredentialMismatchError, } from './mixins/OxyServices.auth.js';
|
|
30
30
|
export { getCommonsApprovalBlockingReason, parseCommonsApprovalExpiresAt, } from './utils/commonsApproval.js';
|
|
31
|
+
// Automatic "Sign in with Oxy" delivery selection — ONE pure decision that maps
|
|
32
|
+
// the caller's facts onto exactly one primary route (open Commons / await push / QR).
|
|
33
|
+
export { selectCommonsDelivery, pushTargetsFromDelivery, commonsDeliveryPlatform } from './utils/commonsDelivery.js';
|
|
31
34
|
export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData.js';
|
|
32
35
|
// ---------------------------------------------------------------------------
|
|
33
36
|
// User identity and handles
|
|
@@ -145,7 +148,7 @@ export { runColdBoot } from './utils/coldBoot.js';
|
|
|
145
148
|
// OAuth 2.0 Authorization Code + PKCE helpers ("Sign in with Oxy" third party).
|
|
146
149
|
// Standard OAuth against auth.oxy.so/authorize — no FedCM/cookies/SSO bounce.
|
|
147
150
|
// ---------------------------------------------------------------------------
|
|
148
|
-
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.js';
|
|
151
|
+
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.js';
|
|
149
152
|
export { buildIdpHubOrigin, buildHubSyncUrl, isIdpHubOrigin, isLoopbackOrigin, isOfficialWebOrigin, isAllowedDeviceJoinOrigin, normalizeOfficialReturnOrigin, parseHubSyncReturnUrl, } from './utils/officialOrigins.js';
|
|
150
153
|
export { syncHubAfterSignIn, redeemHubTicketOnHub, } from './session/hubSync.js';
|
|
151
154
|
// ---------------------------------------------------------------------------
|
|
@@ -180,6 +183,16 @@ export { AccountDialogController, createAccountDialogController, } from './sessi
|
|
|
180
183
|
// via `POST /session/device/token`.
|
|
181
184
|
// ---------------------------------------------------------------------------
|
|
182
185
|
export { createWebAuthStateStore, createNativeAuthStateStore, createMemoryAuthStateStore, AUTH_STATE_STORAGE_KEY, } from './session/authStateStore.js';
|
|
186
|
+
// Identity-bound sessions (the identity vault). The pin is the durable
|
|
187
|
+
// `{publicKey, accountId}` binding between this device's PRIMARY identity key
|
|
188
|
+
// and the account it authenticates as; it is what keeps such a client from
|
|
189
|
+
// following the device's mutable `activeAccountId`.
|
|
190
|
+
export { createWebIdentityPinStore, createNativeIdentityPinStore, createMemoryIdentityPinStore, identityPinMatches, IDENTITY_PIN_STORAGE_KEY, } from './session/identityPin.js';
|
|
191
|
+
export { resolveIdentityPin, establishIdentitySession, } from './session/identitySession.js';
|
|
192
|
+
// The typed `401 account_not_on_device` from a PINNED device-secret mint: the
|
|
193
|
+
// pinned account left this device's session set, so the caller re-establishes
|
|
194
|
+
// the identity session instead of dropping a healthy device credential.
|
|
195
|
+
export { AccountNotOnDeviceError } from './mixins/OxyServices.deviceBoot.js';
|
|
183
196
|
export { refreshPersistedSession, refreshDeviceSecretArm, createAuthRefreshHandler, installAuthRefreshHandler, startTokenRefreshScheduler, TOKEN_REFRESH_LEAD_MS, } from './session/refresh.js';
|
|
184
197
|
export { runSessionColdBoot } from './boot/sessionColdBoot.js';
|
|
185
198
|
// API response contracts (request/response Zod schemas + inferred types) live in
|
|
@@ -12,6 +12,61 @@ import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/use
|
|
|
12
12
|
* returned `expiresAt` is authoritative; this is only the client-proposed value.
|
|
13
13
|
*/
|
|
14
14
|
const COMMONS_SIGN_IN_EXPIRY_MS = 5 * 60 * 1000;
|
|
15
|
+
/**
|
|
16
|
+
* @internal Narrow an untrusted `requesterLabel` from the approve-info response.
|
|
17
|
+
*
|
|
18
|
+
* Returns the trimmed label only when the server sent a real, non-empty string;
|
|
19
|
+
* everything else — absent (an API that predates the field), `null` (a native
|
|
20
|
+
* requester the server could not describe), or a non-string — degrades to
|
|
21
|
+
* `null`, so the approval UI drops the line instead of rendering a blank or a
|
|
22
|
+
* coerced value under the app it is about to authorize.
|
|
23
|
+
*/
|
|
24
|
+
function parseCommonsRequesterLabel(value) {
|
|
25
|
+
if (typeof value !== 'string') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const label = value.trim();
|
|
29
|
+
return label.length > 0 ? label : null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @internal Narrow an untrusted `subjectAccount` from the approve-info response.
|
|
33
|
+
*
|
|
34
|
+
* Returns `null` unless the value is an object carrying a non-empty string `id`
|
|
35
|
+
* AND `username` — the two fields the approval UI needs to name the delegated
|
|
36
|
+
* account. A half-populated object is rejected whole rather than rendered with
|
|
37
|
+
* blanks, and `displayName` is only carried through when it is a string.
|
|
38
|
+
*/
|
|
39
|
+
function parseCommonsSubjectAccount(value) {
|
|
40
|
+
if (value === null || typeof value !== 'object') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const raw = value;
|
|
44
|
+
const { id, username, displayName } = raw;
|
|
45
|
+
if (typeof id !== 'string' || !id || typeof username !== 'string' || !username) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
username,
|
|
51
|
+
...(typeof displayName === 'string' ? { displayName } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @internal Narrow an untrusted delivery-progress timestamp from the status
|
|
56
|
+
* response.
|
|
57
|
+
*
|
|
58
|
+
* Returns the ISO-8601 string unchanged when it is a real, parseable instant,
|
|
59
|
+
* and `null` for everything else — absent (an older API that has no delivery
|
|
60
|
+
* progress at all), empty, non-string, or unparseable. Progress is advisory, so
|
|
61
|
+
* degrading to "no progress yet" is always safe; surfacing a garbage timestamp
|
|
62
|
+
* to the waiting UI is not.
|
|
63
|
+
*/
|
|
64
|
+
function parseCommonsProgressTimestamp(value) {
|
|
65
|
+
if (typeof value !== 'string' || !value) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return Number.isFinite(Date.parse(value)) ? value : null;
|
|
69
|
+
}
|
|
15
70
|
/**
|
|
16
71
|
* Sentinel error raised when getServiceToken() is called with a known apiKey
|
|
17
72
|
* but a non-matching secret. Indicates either credential drift in the caller
|
|
@@ -459,11 +514,16 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
459
514
|
// A. Same-device shared-keychain SSO (`signInWithSharedIdentity`): a
|
|
460
515
|
// sibling native app silently mints its own session from the shared
|
|
461
516
|
// identity key. No user interaction.
|
|
462
|
-
// B. QR / app-to-app handoff: a relying party
|
|
463
|
-
// `
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
517
|
+
// B. QR / app-to-app / popup / push handoff: a relying party
|
|
518
|
+
// (`startCommonsSignIn` + `pollCommonsSignIn`, finalized with either
|
|
519
|
+
// `claimSessionByToken` or `finalizeCommonsOAuth`) and the approver /
|
|
520
|
+
// Commons (`getCommonsApprovalInfo` + `approveCommonsSignIn` /
|
|
521
|
+
// `denyCommonsSignIn`). The approver signs with its PRIMARY local key;
|
|
522
|
+
// the RP never sees the private key.
|
|
523
|
+
//
|
|
524
|
+
// ONE request serves every delivery surface. The delivery surface is
|
|
525
|
+
// not part of the model: only the request's purpose is, and it is fixed
|
|
526
|
+
// at creation by whether an OAuth binding was attached.
|
|
467
527
|
// =======================================================================
|
|
468
528
|
/**
|
|
469
529
|
* MECHANISM A — same-device shared-keychain SSO.
|
|
@@ -514,12 +574,23 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
514
574
|
* returns the server-issued public `authorizeCode` + ready-to-render
|
|
515
575
|
* `qrPayload`. Render the QR (web) / open the deep-link (same-device); the
|
|
516
576
|
* approver resolves the code and authorizes. Then poll with
|
|
517
|
-
* {@link pollCommonsSignIn} and
|
|
518
|
-
*
|
|
577
|
+
* {@link pollCommonsSignIn} and finalize.
|
|
578
|
+
*
|
|
579
|
+
* ONE request serves every delivery surface, and how it finalizes is decided
|
|
580
|
+
* here by whether an OAuth binding is attached:
|
|
581
|
+
* - no `oauth` (the default): a `device_sign_in` request — on `authorized`,
|
|
582
|
+
* exchange the `sessionToken` via the existing `claimSessionByToken`.
|
|
583
|
+
* - with `oauth`: an `oauth_authorization` request — on `authorized`, call
|
|
584
|
+
* {@link finalizeCommonsOAuth} with the same `sessionToken` to mint the
|
|
585
|
+
* single-use authorization code, then exchange it with PKCE.
|
|
519
586
|
*
|
|
520
587
|
* @param params.clientId - The RP's registered OAuth client id
|
|
521
588
|
* (ApplicationCredential publicKey); required so the server can resolve the
|
|
522
589
|
* requesting application's identity.
|
|
590
|
+
* @param params.oauth - Optional OAuth binding ({@link CommonsOAuthContext}).
|
|
591
|
+
* Carries only the redirect URI, the PKCE S256 challenge, the requested
|
|
592
|
+
* scope, and an optional delegated `subjectAccountId` — never the PKCE
|
|
593
|
+
* verifier, the OAuth `state`, or any token.
|
|
523
594
|
*/
|
|
524
595
|
async startCommonsSignIn(params) {
|
|
525
596
|
try {
|
|
@@ -528,7 +599,15 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
528
599
|
// QR. Reuses the platform-safe random generator.
|
|
529
600
|
const sessionToken = await SignatureService.generateChallenge();
|
|
530
601
|
const expiresAt = Date.now() + COMMONS_SIGN_IN_EXPIRY_MS;
|
|
531
|
-
const res = await this.makeRequest('POST', '/auth/session/create', {
|
|
602
|
+
const res = await this.makeRequest('POST', '/auth/session/create', {
|
|
603
|
+
sessionToken,
|
|
604
|
+
expiresAt,
|
|
605
|
+
clientId: params.clientId,
|
|
606
|
+
// Omitted entirely when absent so the device-sign-in body stays
|
|
607
|
+
// exactly what it has always been (the server reads presence, not a
|
|
608
|
+
// null, to decide the request's purpose).
|
|
609
|
+
...(params.oauth ? { oauth: params.oauth } : {}),
|
|
610
|
+
},
|
|
532
611
|
// Public/pre-session (no bearer): skip the preflight so a stale
|
|
533
612
|
// near-expiry token cannot re-enter refreshAccessToken while the
|
|
534
613
|
// refresh handler is already in flight (self-await hang).
|
|
@@ -549,18 +628,162 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
549
628
|
* MECHANISM B (relying party) — poll a device-flow session for approval.
|
|
550
629
|
*
|
|
551
630
|
* Backstop for the auth socket. On `authorized` (with a `sessionId`), the
|
|
552
|
-
* caller
|
|
553
|
-
*
|
|
631
|
+
* caller finalizes: `claimSessionByToken` for a `device_sign_in` request,
|
|
632
|
+
* {@link finalizeCommonsOAuth} for an `oauth_authorization` one.
|
|
633
|
+
*
|
|
634
|
+
* Every field is narrowed fail-safe. `authorized` counts only as a literal
|
|
635
|
+
* `true`, the identifiers only as non-empty strings, and the delivery
|
|
636
|
+
* progress timestamps degrade to `null` when absent or unparseable — so a
|
|
637
|
+
* partial or older-API payload can advance the waiting UI at most, never
|
|
638
|
+
* make it believe a request was approved.
|
|
554
639
|
*
|
|
555
640
|
* @param sessionToken - The secret token from {@link startCommonsSignIn}.
|
|
556
641
|
*/
|
|
557
642
|
async pollCommonsSignIn(sessionToken) {
|
|
558
643
|
try {
|
|
559
|
-
|
|
644
|
+
const res = await this.makeRequest('GET', `/auth/session/status/${encodeURIComponent(sessionToken)}`, undefined,
|
|
560
645
|
// Public/pre-session (no bearer): a preflight here is wrong per se and,
|
|
561
646
|
// if ever reached while a refresh is pending, would re-enter
|
|
562
647
|
// refreshAccessToken and await the very promise it runs inside.
|
|
563
648
|
{ cache: false, retry: false, skipAuth: true });
|
|
649
|
+
if (res === null || typeof res !== 'object') {
|
|
650
|
+
throw new Error('auth/session/status returned an unexpected response shape');
|
|
651
|
+
}
|
|
652
|
+
const { authorized, sessionId, publicKey, status, purpose, pushSentAt, openedAt } = res;
|
|
653
|
+
return {
|
|
654
|
+
authorized: authorized === true,
|
|
655
|
+
...(typeof sessionId === 'string' && sessionId ? { sessionId } : {}),
|
|
656
|
+
...(typeof publicKey === 'string' && publicKey ? { publicKey } : {}),
|
|
657
|
+
...(typeof status === 'string' && status ? { status } : {}),
|
|
658
|
+
purpose: purpose === 'oauth_authorization' ? 'oauth_authorization' : 'device_sign_in',
|
|
659
|
+
pushSentAt: parseCommonsProgressTimestamp(pushSentAt),
|
|
660
|
+
openedAt: parseCommonsProgressTimestamp(openedAt),
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
throw this.handleError(error);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* MECHANISM B (relying party) — ask Oxy to DELIVER a pending sign-in request
|
|
669
|
+
* to the identity's known Commons installations.
|
|
670
|
+
*
|
|
671
|
+
* This is the automatic half of "one intention, one primary action": rather
|
|
672
|
+
* than offering the user a menu of transports, the caller asks for delivery
|
|
673
|
+
* and lets the answer pick the route. Pass the returned `targets` to
|
|
674
|
+
* `selectCommonsDelivery` (`utils/commonsDelivery`) — `targets: 0` means no
|
|
675
|
+
* capable Commons installation is registered, which is a NORMAL outcome that
|
|
676
|
+
* resolves to the QR route, not an error to surface.
|
|
677
|
+
*
|
|
678
|
+
* **Requires a bearer.** Delivery is only allowed when Oxy already knows the
|
|
679
|
+
* intended identity from a trusted authenticated context — a request that
|
|
680
|
+
* merely carries a username or email typed into an unauthenticated browser
|
|
681
|
+
* must never be able to ring somebody's phone.
|
|
682
|
+
*
|
|
683
|
+
* The push it sends carries only `{ type, approvalUrl }` where the URL holds
|
|
684
|
+
* the public `authorizeCode` — no display data, no secrets. Commons resolves
|
|
685
|
+
* everything it shows from `getCommonsApprovalInfo`.
|
|
686
|
+
*
|
|
687
|
+
* @param authorizeCode - The public code from {@link startCommonsSignIn}.
|
|
688
|
+
*/
|
|
689
|
+
async deliverCommonsSignIn(authorizeCode) {
|
|
690
|
+
try {
|
|
691
|
+
const res = await this.makeRequest('POST', `/auth/session/deliver/${encodeURIComponent(authorizeCode)}`, undefined,
|
|
692
|
+
// Bearer REQUIRED (unlike its public siblings): the identity to
|
|
693
|
+
// deliver to comes from the authenticated caller, never the code.
|
|
694
|
+
{ cache: false });
|
|
695
|
+
if (res === null || typeof res !== 'object') {
|
|
696
|
+
throw new Error('auth/session/deliver returned an unexpected response shape');
|
|
697
|
+
}
|
|
698
|
+
const { delivered, targets } = res;
|
|
699
|
+
// Both fields drive the route choice, so a partial payload is rejected
|
|
700
|
+
// outright rather than defaulted into a route the server never chose.
|
|
701
|
+
if (typeof delivered !== 'boolean' ||
|
|
702
|
+
typeof targets !== 'number' ||
|
|
703
|
+
!Number.isInteger(targets) ||
|
|
704
|
+
targets < 0) {
|
|
705
|
+
throw new Error('auth/session/deliver returned an incomplete delivery result');
|
|
706
|
+
}
|
|
707
|
+
return { delivered, targets };
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
throw this.handleError(error);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* MECHANISM B (approver / Commons) — report that the approval route was
|
|
715
|
+
* OPENED, so the waiting relying party can show "Opened in Commons".
|
|
716
|
+
*
|
|
717
|
+
* Progress only. It is idempotent, applies to a `pending` request alone, and
|
|
718
|
+
* records a timestamp (`openedAt`) — it never approves, authorizes, or
|
|
719
|
+
* advances the authorization state machine. Public, like the other approver
|
|
720
|
+
* handles: the approver has only the public `authorizeCode` at this point
|
|
721
|
+
* and has not yet signed anything.
|
|
722
|
+
*
|
|
723
|
+
* Best-effort by nature — a failure here costs the user only a progress
|
|
724
|
+
* line, so callers are free to ignore a rejection and continue to the
|
|
725
|
+
* approval screen.
|
|
726
|
+
*
|
|
727
|
+
* @param authorizeCode - The public code scanned from the QR / deep-link / push.
|
|
728
|
+
*/
|
|
729
|
+
async markCommonsApprovalOpened(authorizeCode) {
|
|
730
|
+
try {
|
|
731
|
+
await this.makeRequest('POST', `/auth/session/opened/${encodeURIComponent(authorizeCode)}`, undefined,
|
|
732
|
+
// Public (no bearer) — skip the preflight, exactly like approve-info.
|
|
733
|
+
{ cache: false, skipAuth: true });
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
throw this.handleError(error);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* MECHANISM B (relying party) — finalize an APPROVED, OAuth-bound request
|
|
741
|
+
* into a single-use OAuth authorization code.
|
|
742
|
+
*
|
|
743
|
+
* The OAuth counterpart of `claimSessionByToken`: same secret credential,
|
|
744
|
+
* same single-use semantics, different output. Call it once the request the
|
|
745
|
+
* RP started with an `oauth` binding reports `authorized`; the server
|
|
746
|
+
* atomically mints exactly ONE `AuthCode` bound to the redirect URI, PKCE
|
|
747
|
+
* challenge, scopes, approving identity, and any delegated subject account
|
|
748
|
+
* the request was created with. A second call cannot mint another code.
|
|
749
|
+
*
|
|
750
|
+
* The result is an authorization CODE, never a token — the caller completes
|
|
751
|
+
* the flow with the ordinary PKCE exchange (`exchangeOAuthCode`) using the
|
|
752
|
+
* `codeVerifier` it never sent anywhere. Nothing here is exposed to the
|
|
753
|
+
* popup: the code travels back through the registered callback, and the
|
|
754
|
+
* main window owns the verifier.
|
|
755
|
+
*
|
|
756
|
+
* Like `claimSessionByToken`, this needs no Authorization header — the
|
|
757
|
+
* high-entropy SECRET `sessionToken` IS the credential. Never pass the
|
|
758
|
+
* public `authorizeCode` here; it is the approver's handle, not the
|
|
759
|
+
* initiator's. Every server-side failure (wrong/expired/already-finalized
|
|
760
|
+
* request, non-OAuth purpose, missing permission for the delegated account)
|
|
761
|
+
* surfaces as one generic error, so nothing about the request's state can be
|
|
762
|
+
* probed from outside.
|
|
763
|
+
*
|
|
764
|
+
* @param sessionToken - The secret token from {@link startCommonsSignIn}.
|
|
765
|
+
*/
|
|
766
|
+
async finalizeCommonsOAuth(sessionToken) {
|
|
767
|
+
try {
|
|
768
|
+
const res = await this.makeRequest('POST', `/auth/session/finalize/${encodeURIComponent(sessionToken)}`, undefined,
|
|
769
|
+
// Body-authenticated by the path's secret token (no bearer) — skip the
|
|
770
|
+
// preflight, exactly like the device-flow claim.
|
|
771
|
+
{ cache: false, skipAuth: true });
|
|
772
|
+
if (res === null || typeof res !== 'object') {
|
|
773
|
+
throw new Error('auth/session/finalize returned an unexpected response shape');
|
|
774
|
+
}
|
|
775
|
+
const { code, redirectUri, expiresIn } = res;
|
|
776
|
+
// All three fields are load-bearing for the exchange that follows, so a
|
|
777
|
+
// partial payload is rejected outright rather than returned half-parsed.
|
|
778
|
+
if (typeof code !== 'string' ||
|
|
779
|
+
!code ||
|
|
780
|
+
typeof redirectUri !== 'string' ||
|
|
781
|
+
!redirectUri ||
|
|
782
|
+
typeof expiresIn !== 'number' ||
|
|
783
|
+
!Number.isFinite(expiresIn)) {
|
|
784
|
+
throw new Error('auth/session/finalize returned an incomplete authorization code');
|
|
785
|
+
}
|
|
786
|
+
return { code, redirectUri, expiresIn };
|
|
564
787
|
}
|
|
565
788
|
catch (error) {
|
|
566
789
|
throw this.handleError(error);
|
|
@@ -570,9 +793,10 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
570
793
|
* MECHANISM B (approver / Commons) — resolve the TRUSTED identity of a
|
|
571
794
|
* sign-in request from its public `authorizeCode`.
|
|
572
795
|
*
|
|
573
|
-
* The returned `application`
|
|
574
|
-
*
|
|
575
|
-
* strings carried in the QR payload. Public (no auth
|
|
796
|
+
* The returned `application` and `subjectAccount` are resolved server-side
|
|
797
|
+
* and are the only safe things to display in the approval UI — NEVER trust
|
|
798
|
+
* the app/name/origin strings carried in the QR payload. Public (no auth
|
|
799
|
+
* required).
|
|
576
800
|
*
|
|
577
801
|
* @param authorizeCode - The public code scanned from the QR / deep-link.
|
|
578
802
|
*/
|
|
@@ -590,6 +814,17 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
590
814
|
// missing or non-boolean value (older server, malformed response)
|
|
591
815
|
// coerces to `false` so a stale server can never imply trust.
|
|
592
816
|
originVerified: raw.originVerified === true,
|
|
817
|
+
// Same discipline: a missing/blank/non-string label degrades to null
|
|
818
|
+
// (an older API, or a native requester with no browser to describe),
|
|
819
|
+
// and the approver simply omits the "where from" line.
|
|
820
|
+
requesterLabel: parseCommonsRequesterLabel(raw.requesterLabel),
|
|
821
|
+
// Same discipline: only the literal OAuth purpose opts into OAuth
|
|
822
|
+
// finalization. Anything else — including a server that predates this
|
|
823
|
+
// field — is the plain device sign-in it has always been.
|
|
824
|
+
purpose: raw.purpose === 'oauth_authorization' ? 'oauth_authorization' : 'device_sign_in',
|
|
825
|
+
// A missing/partial delegated account degrades to "no delegation"
|
|
826
|
+
// rather than a half-rendered "will act as" line.
|
|
827
|
+
subjectAccount: parseCommonsSubjectAccount(raw.subjectAccount),
|
|
593
828
|
expiresAt: raw.expiresAt,
|
|
594
829
|
status: raw.status,
|
|
595
830
|
};
|
|
@@ -641,10 +876,15 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
641
876
|
* device-flow session so the RP stops waiting.
|
|
642
877
|
*
|
|
643
878
|
* @param authorizeCode - The public code being denied.
|
|
879
|
+
* @param reason - Optional closed-set reason ({@link CommonsDenyReason}).
|
|
880
|
+
* Pass `'not_me'` ONLY when the user actually reported the request as one
|
|
881
|
+
* they did not start — the server records it as a suspicious denial rather
|
|
882
|
+
* than an ordinary cancel. Omitting it sends the exact body this endpoint
|
|
883
|
+
* has always received.
|
|
644
884
|
*/
|
|
645
|
-
async denyCommonsSignIn(authorizeCode) {
|
|
885
|
+
async denyCommonsSignIn(authorizeCode, reason) {
|
|
646
886
|
try {
|
|
647
|
-
return await this.makeRequest('POST', `/auth/session/deny/${encodeURIComponent(authorizeCode)}`, undefined,
|
|
887
|
+
return await this.makeRequest('POST', `/auth/session/deny/${encodeURIComponent(authorizeCode)}`, reason ? { reason } : undefined,
|
|
648
888
|
// Public (no auth required) — skip the bearer preflight.
|
|
649
889
|
{ cache: false, skipAuth: true });
|
|
650
890
|
}
|
|
@@ -907,7 +1147,7 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
907
1147
|
const deviceId = (record.deviceId ?? record.device_id);
|
|
908
1148
|
const deviceSecret = (record.deviceSecret ?? record.device_secret);
|
|
909
1149
|
const userRaw = record.user;
|
|
910
|
-
if (!sessionId || !deviceId || !userRaw || typeof userRaw !== 'object') {
|
|
1150
|
+
if (!sessionId || !deviceId || !deviceSecret || !userRaw || typeof userRaw !== 'object') {
|
|
911
1151
|
throw new Error('auth/oauth/token returned an incomplete session payload');
|
|
912
1152
|
}
|
|
913
1153
|
const userObj = userRaw;
|
|
@@ -14,6 +14,40 @@
|
|
|
14
14
|
* primitive can be reused from either without double-planting.
|
|
15
15
|
*/
|
|
16
16
|
import { deviceTokenMintResponseSchema, deviceHubTicketIssueResponseSchema, deviceHubTicketRedeemResponseSchema, safeParseContract, } from '@oxyhq/contracts';
|
|
17
|
+
/**
|
|
18
|
+
* The server's `401 account_not_on_device` for a PINNED mint: the requested
|
|
19
|
+
* `accountId` is not (or is no longer) a live account of this device session.
|
|
20
|
+
*
|
|
21
|
+
* Distinguished from every other mint 401 because the remedy is different: the
|
|
22
|
+
* device secret is FINE — it is the identity binding that went stale (the
|
|
23
|
+
* account was signed out on this device, or revoked). An identity-bound caller
|
|
24
|
+
* must re-establish its session from the local key rather than drop/clear the
|
|
25
|
+
* device credential.
|
|
26
|
+
*/
|
|
27
|
+
export class AccountNotOnDeviceError extends Error {
|
|
28
|
+
constructor(accountId, cause) {
|
|
29
|
+
super(`account_not_on_device: ${accountId} is not a live account of this device session`);
|
|
30
|
+
this.accountId = accountId;
|
|
31
|
+
this.cause = cause;
|
|
32
|
+
this.name = 'AccountNotOnDeviceError';
|
|
33
|
+
/** HTTP status of the originating response; mirrors the ApiError shape. */
|
|
34
|
+
this.status = 401;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Structural (never `instanceof`) read of a normalized mint error: the thrown
|
|
39
|
+
* value may be a plain ApiError-shaped object or come from another realm.
|
|
40
|
+
*/
|
|
41
|
+
function isAccountNotOnDevice(error) {
|
|
42
|
+
if (!error || typeof error !== 'object') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const { status, message } = error;
|
|
46
|
+
if (status !== 401) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return typeof message === 'string' && message.includes('account_not_on_device');
|
|
50
|
+
}
|
|
17
51
|
export function OxyServicesDeviceBootMixin(Base) {
|
|
18
52
|
return class extends Base {
|
|
19
53
|
/**
|
|
@@ -37,11 +71,22 @@ export function OxyServicesDeviceBootMixin(Base) {
|
|
|
37
71
|
* boot's worst-case time-to-route. A transient failure surfaces once and the
|
|
38
72
|
* scheduler/401 path retries it later.
|
|
39
73
|
*
|
|
74
|
+
* `options.accountId` PINS the mint to one account of the device instead of
|
|
75
|
+
* whichever account is currently active. It exists for identity-bound
|
|
76
|
+
* clients (Commons), whose authenticated user is fixed by a local
|
|
77
|
+
* cryptographic key and must never follow an account switch made by another
|
|
78
|
+
* app on the same device. The server never mutates `activeAccountId` for a
|
|
79
|
+
* pinned mint — the returned `state` still reports the device's true active
|
|
80
|
+
* account — and rejects a non-member/dead account with
|
|
81
|
+
* `401 account_not_on_device`, surfaced here as {@link AccountNotOnDeviceError}.
|
|
82
|
+
*
|
|
83
|
+
* @throws {AccountNotOnDeviceError} when a pinned mint's account is not on the device.
|
|
40
84
|
* @throws if the response does not match {@link deviceTokenMintResponseSchema}.
|
|
41
85
|
*/
|
|
42
|
-
async mintFromDeviceSecret(deviceId, deviceSecret) {
|
|
86
|
+
async mintFromDeviceSecret(deviceId, deviceSecret, options) {
|
|
87
|
+
const accountId = options?.accountId;
|
|
43
88
|
try {
|
|
44
|
-
const res = await this.makeRequest('POST', '/session/device/token', { deviceId, deviceSecret },
|
|
89
|
+
const res = await this.makeRequest('POST', '/session/device/token', { deviceId, deviceSecret, ...(accountId ? { accountId } : {}) },
|
|
45
90
|
// `bypassQueue`: this mint is the control-plane call the auth lane
|
|
46
91
|
// depends on — it must run even when every RequestQueue slot is parked
|
|
47
92
|
// awaiting it, or the whole client deadlocks. See RequestOptions.bypassQueue.
|
|
@@ -53,7 +98,11 @@ export function OxyServicesDeviceBootMixin(Base) {
|
|
|
53
98
|
return parsed;
|
|
54
99
|
}
|
|
55
100
|
catch (error) {
|
|
56
|
-
|
|
101
|
+
const normalized = this.handleError(error);
|
|
102
|
+
if (accountId && isAccountNotOnDevice(normalized)) {
|
|
103
|
+
throw new AccountNotOnDeviceError(accountId, normalized);
|
|
104
|
+
}
|
|
105
|
+
throw normalized;
|
|
57
106
|
}
|
|
58
107
|
}
|
|
59
108
|
/** Mint a one-time hub sync ticket (bearer required). */
|