@oxy.so/core 1.18.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -7
- package/dist/cjs/HttpService.js +159 -111
- package/dist/cjs/OxyServices.errors.js +48 -1
- package/dist/cjs/OxyServices.js +263 -60
- package/dist/cjs/api/accounts.js +231 -0
- package/dist/cjs/api/agency.js +102 -0
- package/dist/cjs/api/appData.js +45 -0
- package/dist/cjs/api/apps.js +197 -0
- package/dist/cjs/api/assets.js +329 -0
- package/dist/cjs/api/auth.js +900 -0
- package/dist/cjs/api/billing.js +44 -0
- package/dist/cjs/api/civic.js +289 -0
- package/dist/cjs/api/contacts.js +16 -0
- package/dist/cjs/api/devices.js +351 -0
- package/dist/cjs/api/follows.js +240 -0
- package/dist/cjs/api/identity.js +569 -0
- package/dist/cjs/api/linkedAccounts.js +48 -0
- package/dist/cjs/api/nodes.js +75 -0
- package/dist/cjs/api/notifications.js +75 -0
- package/dist/cjs/api/privacy.js +105 -0
- package/dist/cjs/api/reputation.js +74 -0
- package/dist/cjs/api/session.js +177 -0
- package/dist/cjs/api/store.js +199 -0
- package/dist/cjs/api/topics.js +66 -0
- package/dist/cjs/api/users.js +309 -0
- package/dist/cjs/boot/sessionColdBoot.js +4 -4
- package/dist/cjs/civic/index.js +16 -0
- package/dist/cjs/civic/payloads.js +156 -0
- package/dist/cjs/client/context.js +2 -0
- package/dist/cjs/crypto/identityLink.js +5 -5
- package/dist/cjs/crypto/index.js +44 -0
- package/dist/cjs/crypto/internal.js +37 -0
- package/dist/cjs/crypto/keyManager.js +2 -2
- package/dist/cjs/crypto/polyfill.js +9 -80
- package/dist/cjs/crypto/recoveryPhrase.js +1 -0
- package/dist/cjs/crypto/registrationPow.js +1 -1
- package/dist/cjs/crypto/signatureService.js +1 -0
- package/dist/cjs/i18n/accountCategoryLabels.js +3 -3
- package/dist/cjs/i18n/accountRoleLabels.js +7 -7
- package/dist/cjs/i18n/index.js +130 -52
- package/dist/cjs/i18n/locales/{ar-SA.json → ar-SA.js} +134 -52
- package/dist/cjs/i18n/locales/{ca-ES.json → ca-ES.js} +134 -52
- package/dist/{esm/i18n/locales/de-DE.json → cjs/i18n/locales/de-DE.js} +136 -54
- package/dist/{esm/i18n/locales/en-US.json → cjs/i18n/locales/en-US.js} +163 -245
- package/dist/{esm/i18n/locales/es-ES.json → cjs/i18n/locales/es-ES.js} +163 -245
- package/dist/cjs/i18n/locales/{fr-FR.json → fr-FR.js} +134 -52
- package/dist/cjs/i18n/locales/{it-IT.json → it-IT.js} +134 -52
- package/dist/{esm/i18n/locales/ja-JP.json → cjs/i18n/locales/ja-JP.js} +134 -52
- package/dist/cjs/i18n/locales/{ko-KR.json → ko-KR.js} +134 -52
- package/dist/{esm/i18n/locales/pt-PT.json → cjs/i18n/locales/pt-PT.js} +136 -54
- package/dist/{esm/i18n/locales/zh-CN.json → cjs/i18n/locales/zh-CN.js} +135 -53
- package/dist/cjs/i18n/reputationCategoryLabels.js +2 -2
- package/dist/cjs/i18n/trustTierLabels.js +2 -2
- package/dist/cjs/index.js +55 -263
- package/dist/cjs/inference/index.js +21 -0
- package/dist/cjs/server/OxyServer.js +521 -0
- package/dist/cjs/server/auth.js +2 -2
- package/dist/cjs/server/index.js +23 -6
- package/dist/cjs/server/middleware.js +1154 -0
- package/dist/cjs/server/namespaces.js +214 -0
- package/dist/cjs/server/rateLimit.js +2 -2
- package/dist/cjs/server/requesterAssertion.js +5 -5
- package/dist/cjs/server/securityHeaders.js +3 -3
- package/dist/cjs/server/traffic.js +7 -6
- package/dist/cjs/server/trafficNodeHttp.js +9 -3
- package/dist/cjs/server/userInvalidation.js +5 -5
- package/dist/cjs/session/accountDialogController.js +23 -23
- package/dist/cjs/session/authStateStore.js +2 -2
- package/dist/cjs/session/identitySession.js +3 -3
- package/dist/cjs/session/index.js +131 -0
- package/dist/cjs/session/refresh.js +37 -30
- package/dist/cjs/session/sessionClientHost.js +5 -5
- package/dist/cjs/session/sharedDeviceCredential.js +3 -2
- package/dist/cjs/shared/utils/networkUtils.js +2 -2
- package/dist/cjs/utils/accountCacheSweep.js +11 -15
- package/dist/cjs/utils/asyncUtils.js +22 -163
- package/dist/cjs/utils/authHelpers.js +2 -2
- package/dist/cjs/utils/authWebUrl.js +3 -4
- package/dist/cjs/utils/avatarUtils.js +2 -2
- package/dist/cjs/utils/cache.js +32 -2
- package/dist/cjs/utils/cacheKey.js +5 -10
- package/dist/cjs/utils/deviceManager.js +17 -16
- package/dist/cjs/utils/edgeRegion.js +2 -3
- package/dist/cjs/utils/identityCacheSweep.js +6 -10
- package/dist/cjs/utils/oxyServiceEnvironment.js +3 -7
- package/dist/cjs/utils/requestUtils.js +40 -33
- package/dist/cjs/utils/sessionUtils.js +0 -2
- package/dist/cjs/utils/tokenClaims.js +34 -0
- package/dist/esm/HttpService.js +160 -112
- package/dist/esm/OxyServices.errors.js +45 -0
- package/dist/esm/OxyServices.js +262 -57
- package/dist/esm/api/accounts.js +218 -0
- package/dist/esm/api/agency.js +95 -0
- package/dist/esm/api/appData.js +40 -0
- package/dist/esm/api/apps.js +191 -0
- package/dist/esm/api/assets.js +324 -0
- package/dist/esm/api/auth.js +858 -0
- package/dist/esm/api/billing.js +40 -0
- package/dist/esm/api/civic.js +250 -0
- package/dist/esm/api/contacts.js +12 -0
- package/dist/esm/api/devices.js +346 -0
- package/dist/esm/api/follows.js +236 -0
- package/dist/esm/api/identity.js +527 -0
- package/dist/esm/api/linkedAccounts.js +44 -0
- package/dist/esm/api/nodes.js +71 -0
- package/dist/esm/api/notifications.js +71 -0
- package/dist/esm/api/privacy.js +101 -0
- package/dist/esm/api/reputation.js +70 -0
- package/dist/esm/api/session.js +173 -0
- package/dist/esm/api/store.js +192 -0
- package/dist/esm/api/topics.js +62 -0
- package/dist/esm/api/users.js +272 -0
- package/dist/esm/boot/sessionColdBoot.js +4 -4
- package/dist/esm/civic/index.js +10 -0
- package/dist/esm/civic/payloads.js +118 -0
- package/dist/esm/client/context.js +1 -0
- package/dist/esm/crypto/identityLink.js +5 -5
- package/dist/esm/crypto/index.js +24 -0
- package/dist/esm/crypto/internal.js +19 -0
- package/dist/esm/crypto/keyManager.js +2 -2
- package/dist/esm/crypto/polyfill.js +10 -81
- package/dist/esm/crypto/recoveryPhrase.js +1 -0
- package/dist/esm/crypto/registrationPow.js +1 -1
- package/dist/esm/crypto/signatureService.js +1 -0
- package/dist/esm/i18n/accountCategoryLabels.js +2 -2
- package/dist/esm/i18n/accountRoleLabels.js +1 -1
- package/dist/esm/i18n/index.js +93 -52
- package/dist/esm/i18n/locales/{ar-SA.json → ar-SA.js} +132 -52
- package/dist/esm/i18n/locales/{ca-ES.json → ca-ES.js} +132 -52
- package/dist/{cjs/i18n/locales/de-DE.json → esm/i18n/locales/de-DE.js} +134 -54
- package/dist/{cjs/i18n/locales/en-US.json → esm/i18n/locales/en-US.js} +161 -245
- package/dist/{cjs/i18n/locales/es-ES.json → esm/i18n/locales/es-ES.js} +161 -245
- package/dist/esm/i18n/locales/{fr-FR.json → fr-FR.js} +132 -52
- package/dist/esm/i18n/locales/{it-IT.json → it-IT.js} +132 -52
- package/dist/{cjs/i18n/locales/ja-JP.json → esm/i18n/locales/ja-JP.js} +132 -52
- package/dist/esm/i18n/locales/{ko-KR.json → ko-KR.js} +132 -52
- package/dist/{cjs/i18n/locales/pt-PT.json → esm/i18n/locales/pt-PT.js} +134 -54
- package/dist/{cjs/i18n/locales/zh-CN.json → esm/i18n/locales/zh-CN.js} +133 -53
- package/dist/esm/i18n/reputationCategoryLabels.js +1 -1
- package/dist/esm/i18n/trustTierLabels.js +1 -1
- package/dist/esm/index.js +30 -166
- package/dist/esm/inference/index.js +13 -0
- package/dist/esm/server/OxyServer.js +516 -0
- package/dist/esm/server/auth.js +2 -2
- package/dist/esm/server/index.js +10 -5
- package/dist/esm/server/middleware.js +1147 -0
- package/dist/esm/server/namespaces.js +206 -0
- package/dist/esm/server/rateLimit.js +2 -2
- package/dist/esm/server/requesterAssertion.js +5 -5
- package/dist/esm/server/securityHeaders.js +3 -3
- package/dist/esm/server/traffic.js +7 -6
- package/dist/esm/server/trafficNodeHttp.js +9 -3
- package/dist/esm/server/userInvalidation.js +5 -5
- package/dist/esm/session/accountDialogController.js +23 -23
- package/dist/esm/session/authStateStore.js +2 -2
- package/dist/esm/session/identitySession.js +3 -3
- package/dist/esm/session/index.js +82 -0
- package/dist/esm/session/refresh.js +37 -30
- package/dist/esm/session/sessionClientHost.js +5 -5
- package/dist/esm/session/sharedDeviceCredential.js +3 -2
- package/dist/esm/shared/utils/networkUtils.js +2 -2
- package/dist/esm/utils/accountCacheSweep.js +11 -15
- package/dist/esm/utils/asyncUtils.js +22 -151
- package/dist/esm/utils/authHelpers.js +2 -2
- package/dist/esm/utils/authWebUrl.js +3 -4
- package/dist/esm/utils/avatarUtils.js +2 -2
- package/dist/esm/utils/cache.js +32 -2
- package/dist/esm/utils/cacheKey.js +5 -10
- package/dist/esm/utils/deviceManager.js +17 -16
- package/dist/esm/utils/edgeRegion.js +1 -2
- package/dist/esm/utils/identityCacheSweep.js +6 -10
- package/dist/esm/utils/oxyServiceEnvironment.js +3 -7
- package/dist/esm/utils/requestUtils.js +40 -33
- package/dist/esm/utils/sessionUtils.js +0 -2
- package/dist/esm/utils/tokenClaims.js +31 -0
- package/dist/types/HttpService.d.ts +26 -9
- package/dist/types/OxyServices.d.ts +160 -131
- package/dist/types/OxyServices.errors.d.ts +42 -0
- package/dist/types/api/accounts.d.ts +415 -0
- package/dist/types/api/agency.d.ts +135 -0
- package/dist/types/api/appData.d.ts +27 -0
- package/dist/types/api/apps.d.ts +433 -0
- package/dist/types/api/assets.d.ts +174 -0
- package/dist/types/api/auth.d.ts +766 -0
- package/dist/types/api/billing.d.ts +113 -0
- package/dist/types/api/civic.d.ts +203 -0
- package/dist/types/api/contacts.d.ts +31 -0
- package/{src/mixins/OxyServices.deviceBoot.ts → dist/types/api/devices.d.ts} +130 -110
- package/dist/types/api/follows.d.ts +236 -0
- package/dist/types/api/identity.d.ts +275 -0
- package/dist/types/api/linkedAccounts.d.ts +48 -0
- package/dist/types/api/nodes.d.ts +102 -0
- package/dist/types/api/notifications.d.ts +77 -0
- package/dist/types/api/privacy.d.ts +43 -0
- package/dist/types/api/reputation.d.ts +50 -0
- package/dist/types/api/session.d.ts +85 -0
- package/dist/types/api/store.d.ts +278 -0
- package/dist/types/api/topics.d.ts +34 -0
- package/dist/types/api/users.d.ts +134 -0
- package/dist/types/boot/sessionColdBoot.d.ts +1 -1
- package/dist/types/civic/index.d.ts +10 -0
- package/dist/types/civic/payloads.d.ts +83 -0
- package/dist/types/client/context.d.ts +33 -0
- package/dist/types/crypto/identityLink.d.ts +5 -5
- package/dist/types/crypto/index.d.ts +24 -0
- package/dist/types/crypto/internal.d.ts +19 -0
- package/dist/types/crypto/polyfill.d.ts +6 -9
- package/dist/types/crypto/registrationPow.d.ts +1 -1
- package/dist/types/i18n/accountCategoryLabels.d.ts +1 -1
- package/dist/types/i18n/accountRoleLabels.d.ts +1 -1
- package/dist/types/i18n/index.d.ts +20 -1
- package/dist/types/i18n/locales/ar-SA.d.ts +386 -0
- package/dist/types/i18n/locales/ca-ES.d.ts +386 -0
- package/dist/types/i18n/locales/de-DE.d.ts +386 -0
- package/dist/types/i18n/locales/en-US.d.ts +2034 -0
- package/dist/types/i18n/locales/es-ES.d.ts +2034 -0
- package/dist/types/i18n/locales/fr-FR.d.ts +386 -0
- package/dist/types/i18n/locales/it-IT.d.ts +386 -0
- package/dist/types/i18n/locales/ja-JP.d.ts +386 -0
- package/dist/types/i18n/locales/ko-KR.d.ts +386 -0
- package/dist/types/i18n/locales/pt-PT.d.ts +386 -0
- package/dist/types/i18n/locales/zh-CN.d.ts +386 -0
- package/dist/types/index.d.ts +46 -95
- package/dist/types/inference/index.d.ts +22 -0
- package/dist/types/models/interfaces.d.ts +1 -22
- package/dist/types/server/OxyServer.d.ts +183 -0
- package/dist/types/server/auth.d.ts +10 -7
- package/dist/types/server/index.d.ts +10 -4
- package/dist/types/server/middleware.d.ts +291 -0
- package/dist/types/server/namespaces.d.ts +98 -0
- package/dist/types/server/rateLimit.d.ts +7 -5
- package/dist/types/server/requesterAssertion.d.ts +14 -11
- package/dist/types/server/securityHeaders.d.ts +4 -5
- package/dist/types/server/userInvalidation.d.ts +10 -8
- package/dist/types/session/accountDelegationAccess.d.ts +1 -1
- package/dist/types/session/accountDialogController.d.ts +8 -8
- package/dist/types/session/accountSwitchTargets.d.ts +1 -1
- package/dist/types/session/authStateStore.d.ts +7 -6
- package/dist/types/session/index.d.ts +40 -0
- package/dist/types/utils/accountCacheSweep.d.ts +7 -10
- package/dist/types/utils/apiUtils.d.ts +3 -3
- package/dist/types/utils/asyncUtils.d.ts +11 -58
- package/dist/types/utils/authWebUrl.d.ts +3 -4
- package/dist/types/utils/avatarUtils.d.ts +5 -6
- package/dist/types/utils/cache.d.ts +11 -3
- package/dist/types/utils/commonsApproval.d.ts +1 -1
- package/dist/types/utils/edgeRegion.d.ts +1 -2
- package/dist/types/utils/identityCacheSweep.d.ts +6 -4
- package/dist/types/utils/oauthPkce.d.ts +2 -2
- package/dist/types/utils/oxyServiceEnvironment.d.ts +3 -7
- package/dist/types/utils/requestUtils.d.ts +18 -14
- package/dist/types/utils/tokenClaims.d.ts +9 -0
- package/package.json +68 -7
- package/src/HttpService.ts +166 -131
- package/src/OxyServices.errors.ts +73 -0
- package/src/OxyServices.ts +289 -164
- package/src/api/accounts.ts +579 -0
- package/src/api/agency.ts +277 -0
- package/src/api/appData.ts +63 -0
- package/src/api/apps.ts +596 -0
- package/src/api/assets.ts +416 -0
- package/src/api/auth.ts +1457 -0
- package/src/api/billing.ts +144 -0
- package/src/api/civic.ts +428 -0
- package/src/api/contacts.ts +41 -0
- package/src/api/devices.ts +438 -0
- package/src/api/follows.ts +414 -0
- package/src/api/identity.ts +727 -0
- package/src/api/linkedAccounts.ts +78 -0
- package/src/api/nodes.ts +169 -0
- package/src/api/notifications.ts +135 -0
- package/src/api/privacy.ts +120 -0
- package/src/api/reputation.ts +134 -0
- package/src/api/session.ts +210 -0
- package/src/api/store.ts +435 -0
- package/src/api/topics.ts +83 -0
- package/src/api/users.ts +357 -0
- package/src/boot/sessionColdBoot.ts +4 -4
- package/src/civic/index.ts +12 -0
- package/src/civic/payloads.ts +175 -0
- package/src/client/context.ts +34 -0
- package/src/crypto/identityLink.ts +5 -5
- package/src/crypto/index.ts +39 -0
- package/src/crypto/internal.ts +20 -0
- package/src/crypto/keyManager.ts +2 -2
- package/src/crypto/polyfill.ts +9 -89
- package/src/crypto/recoveryPhrase.ts +1 -0
- package/src/crypto/registrationPow.ts +1 -1
- package/src/crypto/signatureService.ts +1 -0
- package/src/i18n/accountCategoryLabels.ts +2 -2
- package/src/i18n/accountRoleLabels.ts +2 -2
- package/src/i18n/index.ts +105 -54
- package/src/i18n/locales/{ar-SA.json → ar-SA.ts} +132 -52
- package/src/i18n/locales/{ca-ES.json → ca-ES.ts} +132 -52
- package/{dist/cjs/i18n/locales/locales/de-DE.json → src/i18n/locales/de-DE.ts} +134 -54
- package/src/i18n/locales/{en-US.json → en-US.ts} +161 -245
- package/{dist/cjs/i18n/locales/locales/es-ES.json → src/i18n/locales/es-ES.ts} +161 -245
- package/src/i18n/locales/{fr-FR.json → fr-FR.ts} +132 -52
- package/src/i18n/locales/{it-IT.json → it-IT.ts} +132 -52
- package/{dist/cjs/i18n/locales/locales/ja-JP.json → src/i18n/locales/ja-JP.ts} +132 -52
- package/src/i18n/locales/{ko-KR.json → ko-KR.ts} +132 -52
- package/src/i18n/locales/{pt-PT.json → pt-PT.ts} +134 -54
- package/src/i18n/locales/{zh-CN.json → zh-CN.ts} +133 -53
- package/src/i18n/reputationCategoryLabels.ts +1 -1
- package/src/i18n/trustTierLabels.ts +1 -1
- package/src/index.ts +170 -501
- package/src/inference/index.ts +48 -0
- package/src/models/interfaces.ts +1 -22
- package/src/server/OxyServer.ts +637 -0
- package/src/server/auth.ts +12 -8
- package/src/server/index.ts +28 -5
- package/src/server/middleware.ts +1520 -0
- package/src/server/namespaces.ts +257 -0
- package/src/server/rateLimit.ts +10 -9
- package/src/server/requesterAssertion.ts +13 -10
- package/src/server/securityHeaders.ts +7 -8
- package/src/server/traffic.ts +6 -6
- package/src/server/trafficNodeHttp.ts +7 -3
- package/src/server/userInvalidation.ts +9 -9
- package/src/session/accountDelegationAccess.ts +1 -1
- package/src/session/accountDialogController.ts +24 -24
- package/src/session/accountSwitchTargets.ts +1 -1
- package/src/session/authStateStore.ts +9 -8
- package/src/session/identitySession.ts +3 -3
- package/src/session/index.ts +210 -0
- package/src/session/refresh.ts +36 -30
- package/src/session/sessionClientHost.ts +5 -5
- package/src/session/sharedDeviceCredential.ts +3 -2
- package/src/shared/utils/networkUtils.ts +2 -2
- package/src/utils/accountCacheSweep.ts +11 -15
- package/src/utils/apiUtils.ts +3 -3
- package/src/utils/asyncUtils.ts +38 -235
- package/src/utils/authHelpers.ts +2 -2
- package/src/utils/authWebUrl.ts +3 -4
- package/src/utils/avatarUtils.ts +4 -7
- package/src/utils/cache.ts +36 -5
- package/src/utils/cacheKey.ts +5 -9
- package/src/utils/commonsApproval.ts +1 -1
- package/src/utils/deviceManager.ts +17 -16
- package/src/utils/edgeRegion.ts +1 -2
- package/src/utils/identityCacheSweep.ts +7 -12
- package/src/utils/oauthPkce.ts +2 -2
- package/src/utils/oxyServiceEnvironment.ts +3 -7
- package/src/utils/requestUtils.ts +48 -44
- package/src/utils/sessionUtils.ts +0 -2
- package/src/utils/tokenClaims.ts +38 -0
- package/dist/cjs/.tsbuildinfo +0 -1
- package/dist/cjs/OxyServices.base.js +0 -392
- package/dist/cjs/i18n/locales/locales/ar-SA.json +0 -306
- package/dist/cjs/i18n/locales/locales/ca-ES.json +0 -306
- package/dist/cjs/i18n/locales/locales/en-US.json +0 -2127
- package/dist/cjs/i18n/locales/locales/fr-FR.json +0 -306
- package/dist/cjs/i18n/locales/locales/it-IT.json +0 -306
- package/dist/cjs/i18n/locales/locales/ko-KR.json +0 -306
- package/dist/cjs/i18n/locales/locales/pt-PT.json +0 -306
- package/dist/cjs/i18n/locales/locales/zh-CN.json +0 -306
- package/dist/cjs/mixins/OxyServices.accounts.js +0 -555
- package/dist/cjs/mixins/OxyServices.agency.js +0 -154
- package/dist/cjs/mixins/OxyServices.analytics.js +0 -49
- package/dist/cjs/mixins/OxyServices.appData.js +0 -108
- package/dist/cjs/mixins/OxyServices.assets.js +0 -854
- package/dist/cjs/mixins/OxyServices.auth.js +0 -1495
- package/dist/cjs/mixins/OxyServices.chains.js +0 -73
- package/dist/cjs/mixins/OxyServices.civic.js +0 -611
- package/dist/cjs/mixins/OxyServices.connectedApps.js +0 -95
- package/dist/cjs/mixins/OxyServices.contacts.js +0 -50
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +0 -191
- package/dist/cjs/mixins/OxyServices.devices.js +0 -116
- package/dist/cjs/mixins/OxyServices.features.js +0 -335
- package/dist/cjs/mixins/OxyServices.followGraph.js +0 -221
- package/dist/cjs/mixins/OxyServices.identity.js +0 -562
- package/dist/cjs/mixins/OxyServices.identityBackup.js +0 -172
- package/dist/cjs/mixins/OxyServices.inference.js +0 -59
- package/dist/cjs/mixins/OxyServices.language.js +0 -125
- package/dist/cjs/mixins/OxyServices.linkedAccounts.js +0 -79
- package/dist/cjs/mixins/OxyServices.location.js +0 -43
- package/dist/cjs/mixins/OxyServices.nodes.js +0 -175
- package/dist/cjs/mixins/OxyServices.notifications.js +0 -63
- package/dist/cjs/mixins/OxyServices.payment.js +0 -158
- package/dist/cjs/mixins/OxyServices.privacy.js +0 -199
- package/dist/cjs/mixins/OxyServices.reputation.js +0 -289
- package/dist/cjs/mixins/OxyServices.security.js +0 -89
- package/dist/cjs/mixins/OxyServices.store.js +0 -266
- package/dist/cjs/mixins/OxyServices.topics.js +0 -137
- package/dist/cjs/mixins/OxyServices.user.js +0 -954
- package/dist/cjs/mixins/OxyServices.utility.js +0 -1402
- package/dist/cjs/mixins/index.js +0 -136
- package/dist/cjs/mixins/mixinHelpers.js +0 -53
- package/dist/cjs/utils/webauthnOrigin.js +0 -51
- package/dist/esm/.tsbuildinfo +0 -1
- package/dist/esm/OxyServices.base.js +0 -388
- package/dist/esm/i18n/locales/locales/ar-SA.json +0 -306
- package/dist/esm/i18n/locales/locales/ca-ES.json +0 -306
- package/dist/esm/i18n/locales/locales/de-DE.json +0 -306
- package/dist/esm/i18n/locales/locales/en-US.json +0 -2127
- package/dist/esm/i18n/locales/locales/es-ES.json +0 -2127
- package/dist/esm/i18n/locales/locales/fr-FR.json +0 -306
- package/dist/esm/i18n/locales/locales/it-IT.json +0 -306
- package/dist/esm/i18n/locales/locales/ja-JP.json +0 -306
- package/dist/esm/i18n/locales/locales/ko-KR.json +0 -306
- package/dist/esm/i18n/locales/locales/pt-PT.json +0 -306
- package/dist/esm/i18n/locales/locales/zh-CN.json +0 -306
- package/dist/esm/mixins/OxyServices.accounts.js +0 -543
- package/dist/esm/mixins/OxyServices.agency.js +0 -151
- package/dist/esm/mixins/OxyServices.analytics.js +0 -46
- package/dist/esm/mixins/OxyServices.appData.js +0 -103
- package/dist/esm/mixins/OxyServices.assets.js +0 -851
- package/dist/esm/mixins/OxyServices.auth.js +0 -1455
- package/dist/esm/mixins/OxyServices.chains.js +0 -70
- package/dist/esm/mixins/OxyServices.civic.js +0 -605
- package/dist/esm/mixins/OxyServices.connectedApps.js +0 -92
- package/dist/esm/mixins/OxyServices.contacts.js +0 -47
- package/dist/esm/mixins/OxyServices.deviceBoot.js +0 -186
- package/dist/esm/mixins/OxyServices.devices.js +0 -113
- package/dist/esm/mixins/OxyServices.features.js +0 -332
- package/dist/esm/mixins/OxyServices.followGraph.js +0 -218
- package/dist/esm/mixins/OxyServices.identity.js +0 -558
- package/dist/esm/mixins/OxyServices.identityBackup.js +0 -169
- package/dist/esm/mixins/OxyServices.inference.js +0 -56
- package/dist/esm/mixins/OxyServices.language.js +0 -122
- package/dist/esm/mixins/OxyServices.linkedAccounts.js +0 -76
- package/dist/esm/mixins/OxyServices.location.js +0 -40
- package/dist/esm/mixins/OxyServices.nodes.js +0 -172
- package/dist/esm/mixins/OxyServices.notifications.js +0 -60
- package/dist/esm/mixins/OxyServices.payment.js +0 -155
- package/dist/esm/mixins/OxyServices.privacy.js +0 -196
- package/dist/esm/mixins/OxyServices.reputation.js +0 -286
- package/dist/esm/mixins/OxyServices.security.js +0 -86
- package/dist/esm/mixins/OxyServices.store.js +0 -263
- package/dist/esm/mixins/OxyServices.topics.js +0 -134
- package/dist/esm/mixins/OxyServices.user.js +0 -951
- package/dist/esm/mixins/OxyServices.utility.js +0 -1397
- package/dist/esm/mixins/index.js +0 -133
- package/dist/esm/mixins/mixinHelpers.js +0 -48
- package/dist/esm/utils/webauthnOrigin.js +0 -48
- package/dist/types/.tsbuildinfo +0 -1
- package/dist/types/OxyServices.base.d.ts +0 -189
- package/dist/types/mixins/OxyServices.accounts.d.ts +0 -868
- package/dist/types/mixins/OxyServices.agency.d.ts +0 -176
- package/dist/types/mixins/OxyServices.analytics.d.ts +0 -72
- package/dist/types/mixins/OxyServices.appData.d.ts +0 -108
- package/dist/types/mixins/OxyServices.assets.d.ts +0 -371
- package/dist/types/mixins/OxyServices.auth.d.ts +0 -998
- package/dist/types/mixins/OxyServices.chains.d.ts +0 -156
- package/dist/types/mixins/OxyServices.civic.d.ts +0 -520
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -184
- package/dist/types/mixins/OxyServices.contacts.d.ts +0 -100
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +0 -185
- package/dist/types/mixins/OxyServices.devices.d.ts +0 -103
- package/dist/types/mixins/OxyServices.features.d.ts +0 -250
- package/dist/types/mixins/OxyServices.followGraph.d.ts +0 -223
- package/dist/types/mixins/OxyServices.identity.d.ts +0 -337
- package/dist/types/mixins/OxyServices.identityBackup.d.ts +0 -129
- package/dist/types/mixins/OxyServices.inference.d.ts +0 -95
- package/dist/types/mixins/OxyServices.language.d.ts +0 -87
- package/dist/types/mixins/OxyServices.linkedAccounts.d.ts +0 -107
- package/dist/types/mixins/OxyServices.location.d.ts +0 -70
- package/dist/types/mixins/OxyServices.nodes.d.ts +0 -198
- package/dist/types/mixins/OxyServices.notifications.d.ts +0 -143
- package/dist/types/mixins/OxyServices.payment.d.ts +0 -117
- package/dist/types/mixins/OxyServices.privacy.d.ts +0 -146
- package/dist/types/mixins/OxyServices.reputation.d.ts +0 -202
- package/dist/types/mixins/OxyServices.security.d.ts +0 -84
- package/dist/types/mixins/OxyServices.store.d.ts +0 -334
- package/dist/types/mixins/OxyServices.topics.d.ts +0 -110
- package/dist/types/mixins/OxyServices.user.d.ts +0 -600
- package/dist/types/mixins/OxyServices.utility.d.ts +0 -526
- package/dist/types/mixins/index.d.ts +0 -87
- package/dist/types/mixins/mixinHelpers.d.ts +0 -31
- package/dist/types/utils/webauthnOrigin.d.ts +0 -32
- package/src/OxyServices.base.ts +0 -480
- package/src/__tests__/authSocket.test.ts +0 -96
- package/src/__tests__/fixEsmImports.test.ts +0 -132
- package/src/__tests__/httpServiceAbort.test.ts +0 -308
- package/src/__tests__/httpServiceActivityId.test.ts +0 -49
- package/src/__tests__/httpServiceAuthSelfAwait.test.ts +0 -108
- package/src/__tests__/httpServiceCache.test.ts +0 -318
- package/src/__tests__/httpServiceFormEncoded.test.ts +0 -139
- package/src/__tests__/httpServiceQueueDeadlock.test.ts +0 -128
- package/src/__tests__/httpServiceRateLimitedRefresh.test.ts +0 -160
- package/src/__tests__/httpServiceUnwrapEnvelope.test.ts +0 -115
- package/src/__tests__/httpServiceWrites.test.ts +0 -175
- package/src/__tests__/inSessionRefresh.test.ts +0 -313
- package/src/__tests__/linkedClient.test.ts +0 -363
- package/src/__tests__/nearExpiryMintStorm.test.ts +0 -165
- package/src/__tests__/parseHttpErrorBody.test.ts +0 -116
- package/src/__tests__/publicInterfaceParity.test.ts +0 -75
- package/src/__tests__/serverValueImportsDeclared.test.ts +0 -120
- package/src/__tests__/serviceIdentityWhenAnonymous.test.ts +0 -159
- package/src/__tests__/serviceTokenWorkloadFallback.test.ts +0 -85
- package/src/__tests__/userIdentity.test.ts +0 -143
- package/src/boot/__tests__/sessionColdBoot.identity.test.ts +0 -393
- package/src/boot/__tests__/sessionColdBoot.sharedDevice.test.ts +0 -325
- package/src/boot/__tests__/sessionColdBoot.test.ts +0 -570
- package/src/crypto/__tests__/backupMaterial.test.ts +0 -86
- package/src/crypto/__tests__/cryptoPrimitives.test.ts +0 -226
- package/src/crypto/__tests__/identityLink.test.ts +0 -18
- package/src/crypto/__tests__/identityMocks.ts +0 -125
- package/src/crypto/__tests__/identityProof.test.ts +0 -81
- package/src/crypto/__tests__/keyManager.atomicity.test.ts +0 -272
- package/src/crypto/__tests__/keyManager.cacheSafety.test.ts +0 -175
- package/src/crypto/__tests__/keyManager.deviceBackup.test.ts +0 -310
- package/src/crypto/__tests__/keyManager.identityStatus.test.ts +0 -217
- package/src/crypto/__tests__/keyManager.recoveryLadder.test.ts +0 -179
- package/src/crypto/__tests__/keyManager.recoveryMnemonic.test.ts +0 -138
- package/src/crypto/__tests__/keyManager.sharedSlotSync.test.ts +0 -150
- package/src/crypto/__tests__/keyManager.storageMigration.test.ts +0 -227
- package/src/crypto/__tests__/keyManager.test.ts +0 -329
- package/src/crypto/__tests__/polyfill.test.ts +0 -109
- package/src/crypto/__tests__/polyfillOrder.test.ts +0 -114
- package/src/crypto/__tests__/recoveryPhrase.test.ts +0 -61
- package/src/crypto/__tests__/registrationPow.test.ts +0 -61
- package/src/crypto/__tests__/scopedSeed.test.ts +0 -126
- package/src/crypto/__tests__/signChallengeShared.test.ts +0 -63
- package/src/crypto/__tests__/signedRecord.test.ts +0 -230
- package/src/i18n/__tests__/accountCategoryLabels.test.ts +0 -62
- package/src/i18n/__tests__/accountRoleLabels.test.ts +0 -54
- package/src/i18n/__tests__/reputationCategoryLabels.test.ts +0 -56
- package/src/i18n/__tests__/trustTierLabels.test.ts +0 -47
- package/src/i18n/locales/de-DE.json +0 -306
- package/src/i18n/locales/es-ES.json +0 -2127
- package/src/i18n/locales/ja-JP.json +0 -306
- package/src/inference/__tests__/OxyInferenceClient.test.ts +0 -899
- package/src/logger/__tests__/logger.test.ts +0 -207
- package/src/mixins/OxyServices.accounts.ts +0 -1330
- package/src/mixins/OxyServices.agency.ts +0 -353
- package/src/mixins/OxyServices.analytics.ts +0 -53
- package/src/mixins/OxyServices.appData.ts +0 -158
- package/src/mixins/OxyServices.assets.ts +0 -964
- package/src/mixins/OxyServices.auth.ts +0 -2083
- package/src/mixins/OxyServices.chains.ts +0 -134
- package/src/mixins/OxyServices.civic.ts +0 -945
- package/src/mixins/OxyServices.connectedApps.ts +0 -212
- package/src/mixins/OxyServices.contacts.ts +0 -73
- package/src/mixins/OxyServices.devices.ts +0 -120
- package/src/mixins/OxyServices.features.ts +0 -454
- package/src/mixins/OxyServices.followGraph.ts +0 -293
- package/src/mixins/OxyServices.identity.ts +0 -787
- package/src/mixins/OxyServices.identityBackup.ts +0 -250
- package/src/mixins/OxyServices.inference.ts +0 -57
- package/src/mixins/OxyServices.language.ts +0 -144
- package/src/mixins/OxyServices.linkedAccounts.ts +0 -129
- package/src/mixins/OxyServices.location.ts +0 -46
- package/src/mixins/OxyServices.nodes.ts +0 -337
- package/src/mixins/OxyServices.notifications.ts +0 -142
- package/src/mixins/OxyServices.payment.ts +0 -163
- package/src/mixins/OxyServices.privacy.ts +0 -219
- package/src/mixins/OxyServices.reputation.ts +0 -436
- package/src/mixins/OxyServices.security.ts +0 -110
- package/src/mixins/OxyServices.store.ts +0 -585
- package/src/mixins/OxyServices.topics.ts +0 -169
- package/src/mixins/OxyServices.user.ts +0 -1208
- package/src/mixins/OxyServices.utility.ts +0 -1803
- package/src/mixins/__tests__/OxyServices.civic.test.ts +0 -1095
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +0 -124
- package/src/mixins/__tests__/OxyServices.identity.test.ts +0 -301
- package/src/mixins/__tests__/OxyServices.nodes.test.ts +0 -341
- package/src/mixins/__tests__/OxyServices.rotateKey.test.ts +0 -323
- package/src/mixins/__tests__/OxyServices.serviceAssetMetadata.test.ts +0 -192
- package/src/mixins/__tests__/OxyServices.serviceAssetMetadataBySha.test.ts +0 -135
- package/src/mixins/__tests__/OxyServices.serviceLinkedDownloadUrl.test.ts +0 -139
- package/src/mixins/__tests__/accountEvents.test.ts +0 -158
- package/src/mixins/__tests__/accounts.test.ts +0 -727
- package/src/mixins/__tests__/agency.test.ts +0 -120
- package/src/mixins/__tests__/appData.test.ts +0 -203
- package/src/mixins/__tests__/assetCredentials.test.ts +0 -47
- package/src/mixins/__tests__/assetUpload.test.ts +0 -191
- package/src/mixins/__tests__/authRefusalObservability.test.ts +0 -396
- package/src/mixins/__tests__/backgroundCredential.httpIntegration.test.ts +0 -130
- package/src/mixins/__tests__/chains.test.ts +0 -113
- package/src/mixins/__tests__/commonsSignIn.test.ts +0 -1147
- package/src/mixins/__tests__/connectedApps.test.ts +0 -146
- package/src/mixins/__tests__/deviceTokenMintPinned.test.ts +0 -113
- package/src/mixins/__tests__/discoveryErrorHandling.test.ts +0 -265
- package/src/mixins/__tests__/followCacheInvalidation.test.ts +0 -173
- package/src/mixins/__tests__/followGraph.test.ts +0 -147
- package/src/mixins/__tests__/followGraphPagination.test.ts +0 -271
- package/src/mixins/__tests__/getFileDownloadUrl.test.ts +0 -346
- package/src/mixins/__tests__/getFollowStatuses.test.ts +0 -95
- package/src/mixins/__tests__/getUsersByIds.test.ts +0 -176
- package/src/mixins/__tests__/identityBackup.test.ts +0 -315
- package/src/mixins/__tests__/identityLink.test.ts +0 -89
- package/src/mixins/__tests__/identityWriteCacheInvalidation.test.ts +0 -407
- package/src/mixins/__tests__/inferenceFactory.test.ts +0 -58
- package/src/mixins/__tests__/linkedAccounts.test.ts +0 -109
- package/src/mixins/__tests__/notifications.read.test.ts +0 -82
- package/src/mixins/__tests__/onTokensChanged.test.ts +0 -129
- package/src/mixins/__tests__/preSessionSkipAuth.test.ts +0 -284
- package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +0 -155
- package/src/mixins/__tests__/reputation.test.ts +0 -522
- package/src/mixins/__tests__/serviceAuth.test.ts +0 -889
- package/src/mixins/__tests__/serviceTokenJwks.test.ts +0 -208
- package/src/mixins/__tests__/store.test.ts +0 -304
- package/src/mixins/__tests__/topics.test.ts +0 -155
- package/src/mixins/__tests__/userReadCacheBypass.test.ts +0 -129
- package/src/mixins/__tests__/userTokenAuth.test.ts +0 -746
- package/src/mixins/__tests__/verifyChallenge.test.ts +0 -161
- package/src/mixins/__tests__/verifyServiceActingAs.test.ts +0 -167
- package/src/mixins/__tests__/webauthnAuth.test.ts +0 -279
- package/src/mixins/index.ts +0 -203
- package/src/mixins/mixinHelpers.ts +0 -69
- package/src/server/__tests__/auth.test.ts +0 -78
- package/src/server/__tests__/capabilityTicket.test.ts +0 -111
- package/src/server/__tests__/cors.socket.test.ts +0 -225
- package/src/server/__tests__/cors.test.ts +0 -158
- package/src/server/__tests__/rateLimit.test.ts +0 -257
- package/src/server/__tests__/requesterAssertion.test.ts +0 -359
- package/src/server/__tests__/safeFetch.test.ts +0 -232
- package/src/server/__tests__/securityHeaders.test.ts +0 -387
- package/src/server/__tests__/serviceTokenAttribution.test.ts +0 -396
- package/src/server/__tests__/traffic.test.ts +0 -180
- package/src/server/__tests__/trafficNodeHttp.test.ts +0 -38
- package/src/server/__tests__/userInvalidation.test.ts +0 -203
- package/src/server/__tests__/verifySecret.test.ts +0 -40
- package/src/server/__tests__/workloadIdentity.client.test.ts +0 -16
- package/src/server/__tests__/workloadIdentity.test.ts +0 -128
- package/src/session/__tests__/SessionClient.additive.test.ts +0 -153
- package/src/session/__tests__/SessionClient.broadcastChannel.test.ts +0 -114
- package/src/session/__tests__/SessionClient.diagnostics.test.ts +0 -91
- package/src/session/__tests__/SessionClient.directory.test.ts +0 -688
- package/src/session/__tests__/SessionClient.httpIntegration.test.ts +0 -65
- package/src/session/__tests__/SessionClient.identityPin.test.ts +0 -175
- package/src/session/__tests__/SessionClient.rest.test.ts +0 -119
- package/src/session/__tests__/SessionClient.serverEvents.test.ts +0 -72
- package/src/session/__tests__/SessionClient.socket.test.ts +0 -202
- package/src/session/__tests__/SessionClient.socketFactory.test.ts +0 -81
- package/src/session/__tests__/SessionClient.state.test.ts +0 -78
- package/src/session/__tests__/SessionClient.switchTokenOrder.test.ts +0 -170
- package/src/session/__tests__/accountDelegationAccess.test.ts +0 -104
- package/src/session/__tests__/accountDialogController.test.ts +0 -2502
- package/src/session/__tests__/accountDialogShape.test.ts +0 -120
- package/src/session/__tests__/accountSwitchTargets.test.ts +0 -137
- package/src/session/__tests__/authStateStore.test.ts +0 -451
- package/src/session/__tests__/deviceDirectory.test.ts +0 -422
- package/src/session/__tests__/deviceSwitcherRows.test.ts +0 -223
- package/src/session/__tests__/identityPin.test.ts +0 -131
- package/src/session/__tests__/identitySession.test.ts +0 -194
- package/src/session/__tests__/projectSessionState.test.ts +0 -105
- package/src/session/__tests__/refresh.identityPin.test.ts +0 -307
- package/src/session/__tests__/refresh.test.ts +0 -606
- package/src/session/__tests__/sessionIntegration.test.ts +0 -209
- package/src/session/__tests__/sharedDeviceCredential.test.ts +0 -300
- package/src/utils/__tests__/accountUtils.test.ts +0 -151
- package/src/utils/__tests__/activityId.test.ts +0 -45
- package/src/utils/__tests__/asyncUtils.test.ts +0 -279
- package/src/utils/__tests__/authWebUrl.test.ts +0 -15
- package/src/utils/__tests__/cacheKey.test.ts +0 -0
- package/src/utils/__tests__/cacheLifecycle.test.ts +0 -65
- package/src/utils/__tests__/coldBoot.test.ts +0 -496
- package/src/utils/__tests__/commonsApproval.test.ts +0 -60
- package/src/utils/__tests__/identityCacheSweep.test.ts +0 -151
- package/src/utils/__tests__/languageUtils.test.ts +0 -244
- package/src/utils/__tests__/oauthPkce.test.ts +0 -303
- package/src/utils/__tests__/officialOrigins.test.ts +0 -31
- package/src/utils/__tests__/oxyServiceEnvironment.test.ts +0 -7
- package/src/utils/__tests__/profileLinks.test.ts +0 -180
- package/src/utils/__tests__/redactUrl.test.ts +0 -33
- package/src/utils/__tests__/registrableApex.test.ts +0 -62
- package/src/utils/__tests__/requestUtilsAbort.test.ts +0 -155
- package/src/utils/__tests__/textNormalization.test.ts +0 -205
- package/src/utils/__tests__/validationUtils.test.ts +0 -703
- package/src/utils/__tests__/webauthnOrigin.test.ts +0 -83
- package/src/utils/webauthnOrigin.ts +0 -52
|
@@ -1,2083 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authentication Methods Mixin
|
|
3
|
-
*
|
|
4
|
-
* Supports password-based login (email/username) and public key challenge-response.
|
|
5
|
-
*/
|
|
6
|
-
import type { User } from '../models/interfaces';
|
|
7
|
-
import type {
|
|
8
|
-
UserNameResponse,
|
|
9
|
-
LoginResult,
|
|
10
|
-
CommonsDenyReason,
|
|
11
|
-
} from '@oxy.so/contracts';
|
|
12
|
-
import {
|
|
13
|
-
emailVerificationConfirmResponseSchema,
|
|
14
|
-
emailVerificationStartResponseSchema,
|
|
15
|
-
loginResultSchema,
|
|
16
|
-
safeParseContract,
|
|
17
|
-
type EmailVerificationConfirmResponse,
|
|
18
|
-
type EmailVerificationStartRequest,
|
|
19
|
-
type EmailVerificationStartResponse,
|
|
20
|
-
} from '@oxy.so/contracts';
|
|
21
|
-
import type { SessionLoginResponse } from '../models/session';
|
|
22
|
-
import type { OxyServicesBase } from '../OxyServices.base';
|
|
23
|
-
import type { PublicApplication } from './OxyServices.connectedApps';
|
|
24
|
-
export {
|
|
25
|
-
getCommonsApprovalBlockingReason,
|
|
26
|
-
parseCommonsApprovalExpiresAt,
|
|
27
|
-
} from '../utils/commonsApproval';
|
|
28
|
-
import { OxyAuthenticationError } from '../OxyServices.errors';
|
|
29
|
-
import { KeyManager } from '../crypto/keyManager';
|
|
30
|
-
import { solveRegistrationPow } from '../crypto/registrationPow';
|
|
31
|
-
import { SignatureService } from '../crypto/signatureService';
|
|
32
|
-
import { loadNodeCrypto } from '@oxy.so/protocol';
|
|
33
|
-
import { logger } from '../logger';
|
|
34
|
-
import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity';
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Default lifetime of a "Sign in with Oxy" device-flow session / authorize code.
|
|
38
|
-
* Matches the authorize-code TTL the server enforces (5 minutes). The server's
|
|
39
|
-
* returned `expiresAt` is authoritative; this is only the client-proposed value.
|
|
40
|
-
*/
|
|
41
|
-
const COMMONS_SIGN_IN_EXPIRY_MS = 5 * 60 * 1000;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Fallback access-token lifetime used only if the token endpoint ever omits the
|
|
45
|
-
* RFC 6749 `expires_in` member. Matches the server's current 15-minute access
|
|
46
|
-
* token; the server's value always wins when present.
|
|
47
|
-
*/
|
|
48
|
-
const DEFAULT_ACCESS_TOKEN_TTL_SECONDS = 15 * 60;
|
|
49
|
-
|
|
50
|
-
export interface ChallengeResponse {
|
|
51
|
-
challenge: string;
|
|
52
|
-
expiresAt: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface RegistrationRequest {
|
|
56
|
-
publicKey: string;
|
|
57
|
-
username: string;
|
|
58
|
-
email?: string;
|
|
59
|
-
signature: string;
|
|
60
|
-
timestamp: number;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface ChallengeVerifyRequest {
|
|
64
|
-
publicKey: string;
|
|
65
|
-
challenge: string;
|
|
66
|
-
signature: string;
|
|
67
|
-
timestamp: number;
|
|
68
|
-
deviceName?: string;
|
|
69
|
-
deviceFingerprint?: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface PublicKeyCheckResponse {
|
|
73
|
-
registered: boolean;
|
|
74
|
-
message: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** OpenID Connect userinfo claims returned by `GET /auth/oauth/userinfo`. */
|
|
78
|
-
export interface OAuthUserInfoResponse {
|
|
79
|
-
sub: string;
|
|
80
|
-
preferred_username?: string;
|
|
81
|
-
name?: string;
|
|
82
|
-
picture?: string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* The session an OAuth authorization-code exchange yields.
|
|
87
|
-
*
|
|
88
|
-
* Deliberately NOT `LoginSessionResult`. That type mirrors the API's
|
|
89
|
-
* `buildSessionAuthResponse`, which every FIRST-PARTY sign-in lane emits, and it
|
|
90
|
-
* requires `deviceId` because those lanes always join the origin's DeviceSession.
|
|
91
|
-
* `POST /auth/oauth/token` is the RFC 6749 token endpoint and serves third
|
|
92
|
-
* parties, whose grant is deliberately ISOLATED: an untrusted application must be
|
|
93
|
-
* able to receive a session carrying NO DeviceSession credential at all.
|
|
94
|
-
*
|
|
95
|
-
* Both device fields are therefore optional here, and a response omitting them is
|
|
96
|
-
* a well-formed device-less grant rather than a malformed payload. What that
|
|
97
|
-
* costs the session is spelled out on `exchangeOAuthCode` below.
|
|
98
|
-
*/
|
|
99
|
-
export interface OAuthTokenExchangeResult {
|
|
100
|
-
sessionId: string;
|
|
101
|
-
/** ISO-8601 expiry of {@link accessToken}, derived from RFC 6749 `expires_in`. */
|
|
102
|
-
expiresAt: string;
|
|
103
|
-
accessToken?: string;
|
|
104
|
-
/**
|
|
105
|
-
* The DeviceSession this grant joined, when the server issued one. ABSENT for
|
|
106
|
-
* an isolated third-party grant — never assume a string.
|
|
107
|
-
*/
|
|
108
|
-
deviceId?: string;
|
|
109
|
-
/**
|
|
110
|
-
* The zero-cookie mint credential for {@link deviceId}. Present only alongside
|
|
111
|
-
* it; absent for an isolated third-party grant.
|
|
112
|
-
*/
|
|
113
|
-
deviceSecret?: string;
|
|
114
|
-
user: {
|
|
115
|
-
id: string;
|
|
116
|
-
username?: string;
|
|
117
|
-
avatar?: string;
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ===========================================================================
|
|
122
|
-
// "Sign in with Oxy" — cross-device QR / app-to-app handoff (Workstream C)
|
|
123
|
-
// ===========================================================================
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* How a "Sign in with Oxy" request finalizes once the approver authorizes it.
|
|
127
|
-
*
|
|
128
|
-
* ONE request (`AuthSession`) serves every delivery surface — popup, push, QR,
|
|
129
|
-
* deep link — so the purpose describes the FINALIZATION, never the transport:
|
|
130
|
-
*
|
|
131
|
-
* - `device_sign_in` — the classic device flow. The initiator exchanges its
|
|
132
|
-
* secret `sessionToken` for the first access token via `claimSessionByToken`.
|
|
133
|
-
* - `oauth_authorization` — the request additionally carries an OAuth binding
|
|
134
|
-
* ({@link CommonsOAuthContext}), so it finalizes into a single-use
|
|
135
|
-
* authorization CODE via {@link OxyServicesAuthMixin.finalizeCommonsOAuth}.
|
|
136
|
-
* The caller still performs the PKCE token exchange itself.
|
|
137
|
-
*/
|
|
138
|
-
export type CommonsSignInPurpose = 'device_sign_in' | 'oauth_authorization';
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* OAuth binding attached to a "Sign in with Oxy" request so a single
|
|
142
|
-
* `AuthSession` can finalize into a standard OAuth authorization code instead of
|
|
143
|
-
* a device-flow session.
|
|
144
|
-
*
|
|
145
|
-
* Only the minimum request binding is carried here — everything else (the app's
|
|
146
|
-
* name, icon, registered redirect URIs, trust flags) is owned server-side by the
|
|
147
|
-
* `Application` the `clientId` resolves to and is never client-supplied.
|
|
148
|
-
*
|
|
149
|
-
* The PKCE `codeVerifier` NEVER appears here: only its S256 `codeChallenge`
|
|
150
|
-
* crosses the wire, exactly as in the redirect flow. The RP-owned OAuth `state`
|
|
151
|
-
* also stays with the relying party, which validates it locally.
|
|
152
|
-
*/
|
|
153
|
-
export interface CommonsOAuthContext {
|
|
154
|
-
/** Exact registered redirect URI the authorization code will be returned to. */
|
|
155
|
-
redirectUri: string;
|
|
156
|
-
/** PKCE `BASE64URL(SHA-256(codeVerifier))` (RFC 7636 §4.2); the verifier stays client-side. */
|
|
157
|
-
codeChallenge: string;
|
|
158
|
-
/** PKCE transformation method. Always `S256` — `plain` is not accepted. */
|
|
159
|
-
codeChallengeMethod: 'S256';
|
|
160
|
-
/** Space-delimited OAuth scope string; the server normalizes and validates it. */
|
|
161
|
-
scope?: string;
|
|
162
|
-
/**
|
|
163
|
-
* Optional delegated account the application will act AS (an organization or
|
|
164
|
-
* project the identity is a member of). The identity approving the request
|
|
165
|
-
* does not change; the server verifies the identity's permission to act as
|
|
166
|
-
* this account before finalizing.
|
|
167
|
-
*/
|
|
168
|
-
subjectAccountId?: string;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Handle returned by {@link OxyServicesAuthMixin.startCommonsSignIn} for a
|
|
173
|
-
* relying-party app initiating a "Sign in with Oxy" flow.
|
|
174
|
-
*
|
|
175
|
-
* `sessionToken` is the SECRET, high-entropy device-flow credential — it stays
|
|
176
|
-
* on the initiating client, is exchanged once via `claimSessionByToken` (device
|
|
177
|
-
* sign-in) or {@link OxyServicesAuthMixin.finalizeCommonsOAuth} (OAuth), and is
|
|
178
|
-
* NEVER placed in the QR/deep-link. `authorizeCode` is the PUBLIC handle carried
|
|
179
|
-
* in `qrPayload`; the approver (Commons) resolves it via
|
|
180
|
-
* {@link OxyServicesAuthMixin.getCommonsApprovalInfo}.
|
|
181
|
-
*/
|
|
182
|
-
export interface CommonsSignInHandle {
|
|
183
|
-
/** Secret device-flow token (held by the initiator; exchanged via `claimSessionByToken`). */
|
|
184
|
-
sessionToken: string;
|
|
185
|
-
/** Public, single-use authorize code carried in the QR / deep-link. */
|
|
186
|
-
authorizeCode: string;
|
|
187
|
-
/** Ready-to-render deep-link / universal-link string (`oxycommons://approve?...`). */
|
|
188
|
-
qrPayload: string;
|
|
189
|
-
/** Server-authoritative expiry (epoch milliseconds). */
|
|
190
|
-
expiresAt: number;
|
|
191
|
-
/** Session lifecycle status as reported by the server (e.g. `'pending'`). */
|
|
192
|
-
status: string;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Poll result for a "Sign in with Oxy" device-flow session
|
|
197
|
-
* (`GET /auth/session/status`).
|
|
198
|
-
*
|
|
199
|
-
* The authoritative state machine stays small — `pending → authorized →
|
|
200
|
-
* consumed`, plus `cancelled` / `expired` — and lives in `status`. Delivery
|
|
201
|
-
* PROGRESS (`pushSentAt`, `openedAt`) is carried as timestamps beside it, never
|
|
202
|
-
* as competing statuses, so a progress signal can never be mistaken for an
|
|
203
|
-
* authorization.
|
|
204
|
-
*/
|
|
205
|
-
export interface CommonsSignInStatus {
|
|
206
|
-
/** True once an approver has authorized the session. */
|
|
207
|
-
authorized: boolean;
|
|
208
|
-
/** The authorized session id (present once `authorized` for device sign-in). */
|
|
209
|
-
sessionId?: string;
|
|
210
|
-
/** The approving identity's public key (present once `authorized`). */
|
|
211
|
-
publicKey?: string;
|
|
212
|
-
/** Lifecycle status (`'pending'` | `'authorized'` | `'cancelled'` | `'expired'`). */
|
|
213
|
-
status?: string;
|
|
214
|
-
/**
|
|
215
|
-
* How this request finalizes. Unrecognized/missing values degrade to
|
|
216
|
-
* `device_sign_in` so an older API never misroutes an OAuth finalize.
|
|
217
|
-
*/
|
|
218
|
-
purpose?: CommonsSignInPurpose;
|
|
219
|
-
/**
|
|
220
|
-
* ISO-8601 timestamp of when the request was pushed to a known Commons
|
|
221
|
-
* installation, or `null` when no push has been sent (including on a server
|
|
222
|
-
* that predates delivery progress). Progress only — it never implies the push
|
|
223
|
-
* was received, opened, or approved.
|
|
224
|
-
*/
|
|
225
|
-
pushSentAt: string | null;
|
|
226
|
-
/**
|
|
227
|
-
* ISO-8601 timestamp of when the approval route was opened in Commons, or
|
|
228
|
-
* `null` when it has not been opened. Reported by the approver via
|
|
229
|
-
* {@link OxyServicesAuthMixin.markCommonsApprovalOpened}; it is an
|
|
230
|
-
* un-authenticated progress hint used only to advance the waiting UI, and is
|
|
231
|
-
* NEVER evidence that the request was approved.
|
|
232
|
-
*/
|
|
233
|
-
openedAt: string | null;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Outcome of asking Oxy to deliver a pending sign-in request to the identity's
|
|
238
|
-
* known Commons installations (`POST /auth/session/deliver/:authorizeCode`).
|
|
239
|
-
*
|
|
240
|
-
* `targets: 0` is a NORMAL outcome, not a failure: it simply means no capable
|
|
241
|
-
* Commons installation is registered, so push is not a usable route and the
|
|
242
|
-
* caller shows the QR instead. Feed `targets` into `selectCommonsDelivery`
|
|
243
|
-
* (`utils/commonsDelivery`) rather than branching on it ad hoc.
|
|
244
|
-
*/
|
|
245
|
-
export interface CommonsDeliveryResult {
|
|
246
|
-
/** Whether the server dispatched the request to at least one installation. */
|
|
247
|
-
delivered: boolean;
|
|
248
|
-
/** How many eligible Commons installations it was dispatched to (`0` is normal). */
|
|
249
|
-
targets: number;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* The account an application will act AS once the request is approved, when the
|
|
254
|
-
* request delegates to an organization/project rather than the approver's own
|
|
255
|
-
* personal account. Resolved and sanitized server-side from the request's
|
|
256
|
-
* `subjectAccountId`, so it is safe to display in the approval UI.
|
|
257
|
-
*
|
|
258
|
-
* The identity approving stays the identity: Commons renders this as a distinct
|
|
259
|
-
* "will act as" line, never as a change of who is signing.
|
|
260
|
-
*/
|
|
261
|
-
export interface CommonsApprovalSubjectAccount {
|
|
262
|
-
/** The delegated account's id. */
|
|
263
|
-
id: string;
|
|
264
|
-
/** The delegated account's handle. */
|
|
265
|
-
username: string;
|
|
266
|
-
/** Optional human-readable name; absent when the account has no real name. */
|
|
267
|
-
displayName?: string;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Server-resolved approval context shown by the approver (Commons) before
|
|
272
|
-
* authorizing — the TRUSTED identity of the requesting app, resolved from the
|
|
273
|
-
* `authorizeCode` server-side (never from the QR string).
|
|
274
|
-
*/
|
|
275
|
-
export interface CommonsApprovalInfo {
|
|
276
|
-
/** Sanitized, display-safe identity of the requesting application. */
|
|
277
|
-
application: PublicApplication | null;
|
|
278
|
-
/** OAuth scopes the application is requesting. */
|
|
279
|
-
scopes: string[];
|
|
280
|
-
/** The origin the session is bound to (the RP web origin), when applicable. */
|
|
281
|
-
boundOrigin?: string;
|
|
282
|
-
/**
|
|
283
|
-
* Server-authoritative anti-phishing flag: `true` only when this device-flow
|
|
284
|
-
* sign-in was started from a verified, registered origin of a trusted app.
|
|
285
|
-
* The approver (Commons) shows a warning when this is `false`. Always present
|
|
286
|
-
* — a missing/non-boolean server value is coerced to `false` (fail-safe to
|
|
287
|
-
* "not verified") by {@link OxyServicesAuthMixin.getCommonsApprovalInfo}.
|
|
288
|
-
*/
|
|
289
|
-
originVerified: boolean;
|
|
290
|
-
/**
|
|
291
|
-
* COARSE, display-only label of the client that STARTED the request
|
|
292
|
-
* (`"Chrome on Windows"`), resolved server-side from the requesting browser —
|
|
293
|
-
* NEVER from the QR payload. Render it verbatim as a secondary line under the
|
|
294
|
-
* origin; it is the whole descriptor the platform has (no raw User-Agent, no
|
|
295
|
-
* IP, no location is ever collected for it).
|
|
296
|
-
*
|
|
297
|
-
* `null` whenever the server has no browser context to describe: native
|
|
298
|
-
* requesters, unidentifiable User-Agents, and any API that predates the field.
|
|
299
|
-
* Omit the line entirely in that case — never substitute a guess.
|
|
300
|
-
*/
|
|
301
|
-
requesterLabel: string | null;
|
|
302
|
-
/**
|
|
303
|
-
* How this request finalizes. Always present — an unrecognized or missing
|
|
304
|
-
* server value degrades to `'device_sign_in'`, the behaviour every server has
|
|
305
|
-
* always had, so an older API never makes the approver believe it is granting
|
|
306
|
-
* an OAuth authorization.
|
|
307
|
-
*/
|
|
308
|
-
purpose: CommonsSignInPurpose;
|
|
309
|
-
/**
|
|
310
|
-
* The delegated account the application will act as, or `null` when the
|
|
311
|
-
* request is for the approver's own account. Always present — a missing or
|
|
312
|
-
* malformed server value degrades to `null` (fail-safe to "no delegation"),
|
|
313
|
-
* so a partial payload can never imply a broader grant than was requested.
|
|
314
|
-
*/
|
|
315
|
-
subjectAccount: CommonsApprovalSubjectAccount | null;
|
|
316
|
-
/** Server-authoritative expiry (epoch ms or ISO-8601 string from the API). */
|
|
317
|
-
expiresAt: number | string;
|
|
318
|
-
/** Session lifecycle status. */
|
|
319
|
-
status: string;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* @internal Raw server response of `GET /auth/session/approve-info/:code`.
|
|
324
|
-
* `originVerified`, `purpose` and `subjectAccount` are typed loosely here
|
|
325
|
-
* because older servers may omit them (or send an unexpected shape); the SDK
|
|
326
|
-
* narrows each one fail-safe when mapping into {@link CommonsApprovalInfo}.
|
|
327
|
-
*/
|
|
328
|
-
interface CommonsApprovalInfoResponse {
|
|
329
|
-
application: PublicApplication | null;
|
|
330
|
-
scopes: string[];
|
|
331
|
-
boundOrigin?: string;
|
|
332
|
-
originVerified?: unknown;
|
|
333
|
-
requesterLabel?: unknown;
|
|
334
|
-
purpose?: unknown;
|
|
335
|
-
subjectAccount?: unknown;
|
|
336
|
-
expiresAt: number | string;
|
|
337
|
-
status: string;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* @internal Narrow an untrusted `requesterLabel` from the approve-info response.
|
|
342
|
-
*
|
|
343
|
-
* Returns the trimmed label only when the server sent a real, non-empty string;
|
|
344
|
-
* everything else — absent (an API that predates the field), `null` (a native
|
|
345
|
-
* requester the server could not describe), or a non-string — degrades to
|
|
346
|
-
* `null`, so the approval UI drops the line instead of rendering a blank or a
|
|
347
|
-
* coerced value under the app it is about to authorize.
|
|
348
|
-
*/
|
|
349
|
-
function parseCommonsRequesterLabel(value: unknown): string | null {
|
|
350
|
-
if (typeof value !== 'string') {
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
const label = value.trim();
|
|
354
|
-
return label.length > 0 ? label : null;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* @internal Narrow an untrusted `subjectAccount` from the approve-info response.
|
|
359
|
-
*
|
|
360
|
-
* Returns `null` unless the value is an object carrying a non-empty string `id`
|
|
361
|
-
* AND `username` — the two fields the approval UI needs to name the delegated
|
|
362
|
-
* account. A half-populated object is rejected whole rather than rendered with
|
|
363
|
-
* blanks, and `displayName` is only carried through when it is a string.
|
|
364
|
-
*/
|
|
365
|
-
function parseCommonsSubjectAccount(value: unknown): CommonsApprovalSubjectAccount | null {
|
|
366
|
-
if (value === null || typeof value !== 'object') {
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
const raw = value as Record<string, unknown>;
|
|
370
|
-
const { id, username, displayName } = raw;
|
|
371
|
-
if (typeof id !== 'string' || !id || typeof username !== 'string' || !username) {
|
|
372
|
-
return null;
|
|
373
|
-
}
|
|
374
|
-
return {
|
|
375
|
-
id,
|
|
376
|
-
username,
|
|
377
|
-
...(typeof displayName === 'string' ? { displayName } : {}),
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* @internal Narrow an untrusted delivery-progress timestamp from the status
|
|
383
|
-
* response.
|
|
384
|
-
*
|
|
385
|
-
* Returns the ISO-8601 string unchanged when it is a real, parseable instant,
|
|
386
|
-
* and `null` for everything else — absent (an older API that has no delivery
|
|
387
|
-
* progress at all), empty, non-string, or unparseable. Progress is advisory, so
|
|
388
|
-
* degrading to "no progress yet" is always safe; surfacing a garbage timestamp
|
|
389
|
-
* to the waiting UI is not.
|
|
390
|
-
*/
|
|
391
|
-
function parseCommonsProgressTimestamp(value: unknown): string | null {
|
|
392
|
-
if (typeof value !== 'string' || !value) {
|
|
393
|
-
return null;
|
|
394
|
-
}
|
|
395
|
-
return Number.isFinite(Date.parse(value)) ? value : null;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/** Result of approving / denying a "Sign in with Oxy" request. */
|
|
399
|
-
export interface CommonsSignInActionResult {
|
|
400
|
-
success: boolean;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Result of finalizing an approved, OAuth-bound "Sign in with Oxy" request.
|
|
405
|
-
*
|
|
406
|
-
* This is an authorization CODE, not a session: the caller still performs the
|
|
407
|
-
* standard PKCE token exchange (`exchangeOAuthCode`) with the `codeVerifier` it
|
|
408
|
-
* has held all along. No access token, refresh token, or device secret is ever
|
|
409
|
-
* produced by finalization.
|
|
410
|
-
*/
|
|
411
|
-
export interface CommonsOAuthFinalizeResult {
|
|
412
|
-
/** Single-use OAuth authorization code. */
|
|
413
|
-
code: string;
|
|
414
|
-
/** The exact registered redirect URI the request was bound to. */
|
|
415
|
-
redirectUri: string;
|
|
416
|
-
/** Lifetime of the authorization code, in seconds. */
|
|
417
|
-
expiresIn: number;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/** @internal Response shape of the extended `POST /auth/session/create`. */
|
|
421
|
-
interface CommonsSessionCreateResponse {
|
|
422
|
-
authorizeCode: string;
|
|
423
|
-
qrPayload: string;
|
|
424
|
-
status: string;
|
|
425
|
-
/** Optional server-authoritative expiry; falls back to the client-proposed value. */
|
|
426
|
-
expiresAt?: number;
|
|
427
|
-
/** Optional server echo of the session token (the client-supplied value is authoritative). */
|
|
428
|
-
sessionToken?: string;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
export interface ServiceTokenResponse {
|
|
432
|
-
token: string;
|
|
433
|
-
expiresIn: number;
|
|
434
|
-
appName: string;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* One cache entry per (apiKey hash) → issued token + the secret that produced it.
|
|
439
|
-
* The secret is kept around in raw Buffer form so we can perform a
|
|
440
|
-
* constant-time compare against any reused credential pair — this prevents an
|
|
441
|
-
* attacker who learned a victim's apiKey from receiving the victim's cached
|
|
442
|
-
* service token by simply guessing the secret.
|
|
443
|
-
*
|
|
444
|
-
* @internal
|
|
445
|
-
*/
|
|
446
|
-
interface ServiceTokenCacheEntry {
|
|
447
|
-
token: string;
|
|
448
|
-
/** Expiry as ms since epoch */
|
|
449
|
-
expiresAt: number;
|
|
450
|
-
/** Raw secret stored as Buffer for constant-time comparison on cache hit */
|
|
451
|
-
secretBuf: Buffer;
|
|
452
|
-
/** In-flight refresh promise (deduplicates concurrent callers) */
|
|
453
|
-
pending: Promise<string> | null;
|
|
454
|
-
/**
|
|
455
|
-
* The raw apiKey that produced this entry. Retained so a targeted, fully
|
|
456
|
-
* synchronous `invalidateServiceToken(apiKey)` can locate the entry without
|
|
457
|
-
* re-deriving the async `SHA-256(apiKey)` Map key. Never logged or returned.
|
|
458
|
-
*/
|
|
459
|
-
apiKey: string;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Sentinel error raised when getServiceToken() is called with a known apiKey
|
|
464
|
-
* but a non-matching secret. Indicates either credential drift in the caller
|
|
465
|
-
* or a cross-tenant cache lookup attempt. Surface as a 401-equivalent.
|
|
466
|
-
*/
|
|
467
|
-
export class ServiceCredentialMismatchError extends Error {
|
|
468
|
-
constructor() {
|
|
469
|
-
super('Service credential mismatch: provided secret does not match the secret stored for this apiKey');
|
|
470
|
-
this.name = 'ServiceCredentialMismatchError';
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* How long a failed service-token mint sends session-less requests anonymous
|
|
476
|
-
* before the next attempt, under `serviceIdentity: 'when-anonymous'`.
|
|
477
|
-
*/
|
|
478
|
-
export const ANONYMOUS_SERVICE_TOKEN_RETRY_MS = 30_000;
|
|
479
|
-
|
|
480
|
-
export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
481
|
-
return class extends Base {
|
|
482
|
-
/**
|
|
483
|
-
* Per-credential token cache.
|
|
484
|
-
*
|
|
485
|
-
* Keyed by SHA-256(apiKey). Each entry carries:
|
|
486
|
-
* - the issued service JWT
|
|
487
|
-
* - its expiry timestamp
|
|
488
|
-
* - the secret that produced it (Buffer for constant-time compare)
|
|
489
|
-
* - an optional in-flight promise to deduplicate concurrent refreshes
|
|
490
|
-
*
|
|
491
|
-
* The previous implementation kept ONE token/exp pair per OxyServices
|
|
492
|
-
* instance. That meant calling `getServiceToken(keyA, secretA)` populated
|
|
493
|
-
* the cache, and a subsequent `getServiceToken(keyB, secretB)` (different
|
|
494
|
-
* tenant) would receive tenant A's token. This is fixed by routing every
|
|
495
|
-
* lookup through the Map.
|
|
496
|
-
*
|
|
497
|
-
* @internal
|
|
498
|
-
*/
|
|
499
|
-
_serviceTokenCache = new Map<string, ServiceTokenCacheEntry>();
|
|
500
|
-
|
|
501
|
-
/** @internal Raw apiKey stored by configureServiceAuth() for use by getServiceToken() */
|
|
502
|
-
_serviceApiKey: string | null = null;
|
|
503
|
-
/** @internal Raw apiSecret stored by configureServiceAuth() for use by getServiceToken() */
|
|
504
|
-
_serviceApiSecret: string | null = null;
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Before this instant, a request without a user session does not try to
|
|
508
|
-
* mint a service token and goes out anonymous. Set after a failed mint; see
|
|
509
|
-
* {@link _serviceTokenForAnonymousRequest}.
|
|
510
|
-
*
|
|
511
|
-
* @internal
|
|
512
|
-
*/
|
|
513
|
-
_anonymousServiceTokenRetryAt = 0;
|
|
514
|
-
|
|
515
|
-
constructor(...args: any[]) {
|
|
516
|
-
super(...(args as [any]));
|
|
517
|
-
if (this.config.serviceIdentity === 'when-anonymous') {
|
|
518
|
-
this.httpService.setAnonymousAuthProvider(() => this._serviceTokenForAnonymousRequest());
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* The bearer for a request with no user session, under `serviceIdentity:
|
|
524
|
-
* 'when-anonymous'`: this process's service token, or `null` to send the
|
|
525
|
-
* request anonymous as before.
|
|
526
|
-
*
|
|
527
|
-
* `null`, never a throw, whenever there is no token to offer:
|
|
528
|
-
* - no key pair and no attestable workload (a local checkout) — asking
|
|
529
|
-
* costs nothing and there is nothing to ask for;
|
|
530
|
-
* - the mint failed. The request still goes out, anonymous, and so does
|
|
531
|
-
* every request for the next {@link ANONYMOUS_SERVICE_TOKEN_RETRY_MS}, so
|
|
532
|
-
* an Oxy that is refusing attestation (seen for ~2.5 minutes after a
|
|
533
|
-
* deploy on 2026-09-25) is asked once per window rather than once per
|
|
534
|
-
* read, and no read waits on a mint that just failed.
|
|
535
|
-
*
|
|
536
|
-
* The token itself comes from {@link getServiceToken}: cached for its
|
|
537
|
-
* lifetime and single-flight, so concurrent reads share one mint.
|
|
538
|
-
*
|
|
539
|
-
* @internal
|
|
540
|
-
*/
|
|
541
|
-
async _serviceTokenForAnonymousRequest(): Promise<string | null> {
|
|
542
|
-
if (Date.now() < this._anonymousServiceTokenRetryAt) return null;
|
|
543
|
-
const hasKeyPair = Boolean(this._serviceApiKey && this._serviceApiSecret);
|
|
544
|
-
if (!hasKeyPair && !(await this._canUseWorkloadIdentity())) return null;
|
|
545
|
-
try {
|
|
546
|
-
return await this.getServiceToken();
|
|
547
|
-
} catch (error) {
|
|
548
|
-
this._anonymousServiceTokenRetryAt = Date.now() + ANONYMOUS_SERVICE_TOKEN_RETRY_MS;
|
|
549
|
-
logger.warn('[oxy.auth] No service token for a request without a session; sending it anonymous', {
|
|
550
|
-
component: 'auth',
|
|
551
|
-
method: '_serviceTokenForAnonymousRequest',
|
|
552
|
-
retryInMs: ANONYMOUS_SERVICE_TOKEN_RETRY_MS,
|
|
553
|
-
error: error instanceof Error ? error.message : String(error),
|
|
554
|
-
});
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Hash an apiKey into a stable Map cache key. Uses Node's SHA-256 — service
|
|
561
|
-
* tokens are only ever issued by a Node host (the SDK on web/RN never has
|
|
562
|
-
* the apiSecret in the first place), so we can rely on Node crypto here.
|
|
563
|
-
*
|
|
564
|
-
* @internal
|
|
565
|
-
*/
|
|
566
|
-
async _hashApiKey(apiKey: string): Promise<string> {
|
|
567
|
-
const nodeCrypto = await loadNodeCrypto();
|
|
568
|
-
return nodeCrypto.createHash('sha256').update(apiKey).digest('hex');
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Configure service credentials for internal service-to-service communication.
|
|
573
|
-
* Call this once at startup so that getServiceToken() and makeServiceRequest()
|
|
574
|
-
* can automatically obtain and refresh tokens.
|
|
575
|
-
*
|
|
576
|
-
* Calling this with credentials that differ from a previously-configured pair
|
|
577
|
-
* is allowed — each `(apiKey, apiSecret)` pair is cached independently, so
|
|
578
|
-
* legitimate multi-tenant hosts that need to switch credentials cannot leak
|
|
579
|
-
* one tenant's token to another tenant on the same instance.
|
|
580
|
-
*
|
|
581
|
-
* @param apiKey - Application credential public key (oxy_dk_*)
|
|
582
|
-
* @param apiSecret - Application credential secret
|
|
583
|
-
*/
|
|
584
|
-
configureServiceAuth(apiKey: string, apiSecret: string): void {
|
|
585
|
-
this._serviceApiKey = apiKey;
|
|
586
|
-
this._serviceApiSecret = apiSecret;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Get a service token for internal service-to-service communication.
|
|
591
|
-
* Tokens are short-lived (1h) and automatically cached/refreshed per
|
|
592
|
-
* `(apiKey, apiSecret)` pair.
|
|
593
|
-
*
|
|
594
|
-
* Concurrent callers for the same credential pair share a single in-flight
|
|
595
|
-
* request to avoid hammering `/auth/service-token` when the cache is empty
|
|
596
|
-
* or expired.
|
|
597
|
-
*
|
|
598
|
-
* **Security guarantee:** if the cache already holds a token for this
|
|
599
|
-
* apiKey but the supplied apiSecret does not constant-time match the
|
|
600
|
-
* secret that originally produced that token, this method throws
|
|
601
|
-
* `ServiceCredentialMismatchError` instead of returning the cached token.
|
|
602
|
-
* This prevents an attacker who learned a peer's apiKey from extracting
|
|
603
|
-
* their service token by polling with a wrong secret.
|
|
604
|
-
*
|
|
605
|
-
* @param apiKey - Application credential public key (optional if configureServiceAuth was called)
|
|
606
|
-
* @param apiSecret - Application credential secret (optional if configureServiceAuth was called)
|
|
607
|
-
*/
|
|
608
|
-
async getServiceToken(apiKey?: string, apiSecret?: string): Promise<string> {
|
|
609
|
-
const key = apiKey || this._serviceApiKey;
|
|
610
|
-
const secret = apiSecret || this._serviceApiSecret;
|
|
611
|
-
|
|
612
|
-
if (!key || !secret) {
|
|
613
|
-
/**
|
|
614
|
-
* No credential configured — so prove what this process IS instead
|
|
615
|
-
* (ADR 0026).
|
|
616
|
-
*
|
|
617
|
-
* This is what lets an official service drop its api key and secret
|
|
618
|
-
* WITHOUT a code change: remove the two environment variables, and the
|
|
619
|
-
* next token comes from the workload path. It is deliberately the
|
|
620
|
-
* fallback rather than the preference, so a deployment that still has a
|
|
621
|
-
* credential keeps using it and the migration is one service at a time.
|
|
622
|
-
*
|
|
623
|
-
* A process with no credential AND no attestation still gets the
|
|
624
|
-
* original error: "nothing to authenticate with" is the truth there, and
|
|
625
|
-
* a local checkout must not be told to go looking for a container
|
|
626
|
-
* credentials endpoint that is not there.
|
|
627
|
-
*/
|
|
628
|
-
if (await this._canUseWorkloadIdentity()) {
|
|
629
|
-
return this._getWorkloadServiceToken();
|
|
630
|
-
}
|
|
631
|
-
throw new Error('Service credentials not provided. Call configureServiceAuth() or pass apiKey and apiSecret.');
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const cacheKey = await this._hashApiKey(key);
|
|
635
|
-
const now = Date.now();
|
|
636
|
-
const providedSecretBuf = Buffer.from(secret, 'utf8');
|
|
637
|
-
|
|
638
|
-
let entry = this._serviceTokenCache.get(cacheKey);
|
|
639
|
-
|
|
640
|
-
// Verify the secret on every cache hit, regardless of token freshness.
|
|
641
|
-
// Constant-time compare prevents timing oracles on the stored secret.
|
|
642
|
-
if (entry) {
|
|
643
|
-
const nodeCrypto = await loadNodeCrypto();
|
|
644
|
-
const storedSecretBuf = entry.secretBuf;
|
|
645
|
-
const lengthMatch = storedSecretBuf.length === providedSecretBuf.length;
|
|
646
|
-
// Always run timingSafeEqual on equal-length inputs to keep timing flat.
|
|
647
|
-
// When lengths differ, run against a zero-padded copy of the same length
|
|
648
|
-
// to avoid an early-return timing signal.
|
|
649
|
-
const compareBuf = lengthMatch
|
|
650
|
-
? providedSecretBuf
|
|
651
|
-
: Buffer.alloc(storedSecretBuf.length);
|
|
652
|
-
const compareResult = nodeCrypto.timingSafeEqual(storedSecretBuf, compareBuf);
|
|
653
|
-
if (!lengthMatch || !compareResult) {
|
|
654
|
-
logger.warn('[oxy.auth] Service token cache hit with mismatched secret', {
|
|
655
|
-
component: 'auth',
|
|
656
|
-
method: 'getServiceToken',
|
|
657
|
-
});
|
|
658
|
-
throw new ServiceCredentialMismatchError();
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Return cached token if still valid (with 60s buffer for clock drift)
|
|
662
|
-
if (entry.token && entry.expiresAt > now + 60_000) {
|
|
663
|
-
return entry.token;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// If a fetch is already in-flight for this credential, share its result
|
|
667
|
-
if (entry.pending) {
|
|
668
|
-
return entry.pending;
|
|
669
|
-
}
|
|
670
|
-
} else {
|
|
671
|
-
// First time seeing this apiKey on this instance — seed an empty entry
|
|
672
|
-
// so concurrent callers serialize on the same promise.
|
|
673
|
-
entry = {
|
|
674
|
-
token: '',
|
|
675
|
-
expiresAt: 0,
|
|
676
|
-
secretBuf: providedSecretBuf,
|
|
677
|
-
pending: null,
|
|
678
|
-
apiKey: key,
|
|
679
|
-
};
|
|
680
|
-
this._serviceTokenCache.set(cacheKey, entry);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const pending = this._doFetchServiceToken(key, secret, cacheKey, providedSecretBuf);
|
|
684
|
-
entry.pending = pending;
|
|
685
|
-
try {
|
|
686
|
-
return await pending;
|
|
687
|
-
} catch (error) {
|
|
688
|
-
// Do not retain unauthenticated cache entries. If the initial
|
|
689
|
-
// /auth/service-token request fails (for example, wrong apiSecret),
|
|
690
|
-
// leaving the pre-seeded empty entry would cause later calls with the
|
|
691
|
-
// real secret for the same apiKey to fail locally as a credential
|
|
692
|
-
// mismatch without ever contacting the server. Keep previously-issued
|
|
693
|
-
// stale tokens on refresh failures, but remove never-authenticated
|
|
694
|
-
// entries.
|
|
695
|
-
const failed = this._serviceTokenCache.get(cacheKey);
|
|
696
|
-
if (failed?.pending === pending && !failed.token) {
|
|
697
|
-
this._serviceTokenCache.delete(cacheKey);
|
|
698
|
-
}
|
|
699
|
-
throw error;
|
|
700
|
-
} finally {
|
|
701
|
-
// Clear the in-flight slot; the entry itself (with fresh token / expiry)
|
|
702
|
-
// is updated inside _doFetchServiceToken before we land here.
|
|
703
|
-
const settled = this._serviceTokenCache.get(cacheKey);
|
|
704
|
-
if (settled?.pending === pending) {
|
|
705
|
-
settled.pending = null;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
/**
|
|
711
|
-
* Whether this process can prove what it is (ADR 0026).
|
|
712
|
-
*
|
|
713
|
-
* `#workload-identity` (package.json `imports`) selects a client
|
|
714
|
-
* implementation for native and browser bundlers, and the signer only for
|
|
715
|
-
* Node hosts. The runtime Node guard avoids calling attestation on clients;
|
|
716
|
-
* the module boundary keeps node:crypto out of their bundle graphs (a guard
|
|
717
|
-
* alone cannot). It is a package import, not a self-reference to an export
|
|
718
|
-
* subpath: a self-reference only resolves where `@oxy.so/core` is reachable
|
|
719
|
-
* through `node_modules`, which bundlers inside this workspace are not.
|
|
720
|
-
*
|
|
721
|
-
* @internal
|
|
722
|
-
*/
|
|
723
|
-
async _canUseWorkloadIdentity(): Promise<boolean> {
|
|
724
|
-
if (typeof process === 'undefined' || !process.versions?.node) return false;
|
|
725
|
-
try {
|
|
726
|
-
const { canAttestWorkloadIdentity } = await import('#workload-identity');
|
|
727
|
-
return canAttestWorkloadIdentity();
|
|
728
|
-
} catch {
|
|
729
|
-
// A bundler that dropped the server subpath, or a runtime without it.
|
|
730
|
-
// "We cannot attest here" is the correct reading, and the caller falls
|
|
731
|
-
// back to the credential error it would have thrown anyway.
|
|
732
|
-
return false;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* A service token obtained by attestation, cached like a credential's.
|
|
738
|
-
*
|
|
739
|
-
* The cache key is a constant, not a credential hash: a process has exactly
|
|
740
|
-
* one identity, so there is nothing to key on and nothing to keep separate.
|
|
741
|
-
* The secret-comparison guard the credential path applies has nothing to
|
|
742
|
-
* compare here, which is the point — there is no secret.
|
|
743
|
-
*
|
|
744
|
-
* @internal
|
|
745
|
-
*/
|
|
746
|
-
async _getWorkloadServiceToken(): Promise<string> {
|
|
747
|
-
const cacheKey = 'workload-identity';
|
|
748
|
-
const entry = this._serviceTokenCache.get(cacheKey);
|
|
749
|
-
const now = Date.now();
|
|
750
|
-
if (entry?.token && entry.expiresAt > now + 60_000) return entry.token;
|
|
751
|
-
if (entry?.pending) return entry.pending;
|
|
752
|
-
|
|
753
|
-
const { requestWorkloadServiceToken } = await import('#workload-identity');
|
|
754
|
-
const seeded = entry ?? {
|
|
755
|
-
token: '',
|
|
756
|
-
expiresAt: 0,
|
|
757
|
-
secretBuf: Buffer.alloc(0),
|
|
758
|
-
pending: null,
|
|
759
|
-
apiKey: cacheKey,
|
|
760
|
-
};
|
|
761
|
-
this._serviceTokenCache.set(cacheKey, seeded);
|
|
762
|
-
|
|
763
|
-
const pending = (async () => {
|
|
764
|
-
const granted = await requestWorkloadServiceToken({ baseUrl: this.getBaseURL() });
|
|
765
|
-
const current = this._serviceTokenCache.get(cacheKey);
|
|
766
|
-
if (current) {
|
|
767
|
-
current.token = granted.token;
|
|
768
|
-
current.expiresAt = Date.now() + granted.expiresIn * 1000;
|
|
769
|
-
}
|
|
770
|
-
return granted.token;
|
|
771
|
-
})();
|
|
772
|
-
seeded.pending = pending;
|
|
773
|
-
|
|
774
|
-
try {
|
|
775
|
-
return await pending;
|
|
776
|
-
} catch (error) {
|
|
777
|
-
// Never keep an entry that never held a token: the next caller must try
|
|
778
|
-
// again rather than inherit a failure.
|
|
779
|
-
const failed = this._serviceTokenCache.get(cacheKey);
|
|
780
|
-
if (failed?.pending === pending && !failed.token) this._serviceTokenCache.delete(cacheKey);
|
|
781
|
-
throw error;
|
|
782
|
-
} finally {
|
|
783
|
-
const settled = this._serviceTokenCache.get(cacheKey);
|
|
784
|
-
if (settled?.pending === pending) settled.pending = null;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
* Perform the actual /auth/service-token request and cache the result.
|
|
790
|
-
* Separated so getServiceToken() can deduplicate concurrent calls.
|
|
791
|
-
* @internal
|
|
792
|
-
*/
|
|
793
|
-
async _doFetchServiceToken(
|
|
794
|
-
key: string,
|
|
795
|
-
secret: string,
|
|
796
|
-
cacheKey: string,
|
|
797
|
-
secretBuf: Buffer,
|
|
798
|
-
): Promise<string> {
|
|
799
|
-
const response = await this.makeRequest<ServiceTokenResponse>(
|
|
800
|
-
'POST',
|
|
801
|
-
'/auth/service-token',
|
|
802
|
-
{ apiKey: key, apiSecret: secret },
|
|
803
|
-
{ cache: false, retry: false, skipAuth: true }
|
|
804
|
-
);
|
|
805
|
-
|
|
806
|
-
const expiresAt = Date.now() + response.expiresIn * 1000;
|
|
807
|
-
// Update the entry in-place so any caller that already grabbed a reference
|
|
808
|
-
// (via `_serviceTokenCache.get(...)`) sees the fresh state.
|
|
809
|
-
const entry = this._serviceTokenCache.get(cacheKey);
|
|
810
|
-
if (entry) {
|
|
811
|
-
entry.token = response.token;
|
|
812
|
-
entry.expiresAt = expiresAt;
|
|
813
|
-
entry.secretBuf = secretBuf;
|
|
814
|
-
} else {
|
|
815
|
-
this._serviceTokenCache.set(cacheKey, {
|
|
816
|
-
token: response.token,
|
|
817
|
-
expiresAt,
|
|
818
|
-
secretBuf,
|
|
819
|
-
pending: null,
|
|
820
|
-
apiKey: key,
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
return response.token;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
/**
|
|
828
|
-
* Invalidate cached service token(s), forcing the next `getServiceToken()`
|
|
829
|
-
* call to mint a fresh token from `/auth/service-token`.
|
|
830
|
-
*
|
|
831
|
-
* `getServiceToken()` only refreshes on expiry (with a 60s clock-drift
|
|
832
|
-
* buffer), so a credential that is revoked or rotated mid-run — surfaced as
|
|
833
|
-
* a 401 on a downstream service request — cannot otherwise be recovered
|
|
834
|
-
* within the same process: the still-unexpired cached token keeps being
|
|
835
|
-
* returned. Call this after such a 401 to clear the stale entry; the very
|
|
836
|
-
* next `getServiceToken()` for that credential re-mints.
|
|
837
|
-
*
|
|
838
|
-
* Fully synchronous and deterministic: the call completes before it
|
|
839
|
-
* returns, so a `getServiceToken()` issued immediately afterwards is
|
|
840
|
-
* guaranteed to see the cleared cache and mint anew.
|
|
841
|
-
*
|
|
842
|
-
* @param apiKey - When provided, clears only the cache entry for that
|
|
843
|
-
* specific apiKey. When omitted, clears the entry for the credential set
|
|
844
|
-
* via `configureServiceAuth()`; if neither is available (no key to
|
|
845
|
-
* target), clears the entire cache. Passing no argument is the common
|
|
846
|
-
* case for hosts that configured a single service credential at startup.
|
|
847
|
-
*
|
|
848
|
-
* The cache Map is keyed by an asynchronously-computed `SHA-256(apiKey)`
|
|
849
|
-
* that cannot be reproduced synchronously, so a targeted clear scans the
|
|
850
|
-
* entries and removes the one whose stored raw `apiKey` matches — keeping
|
|
851
|
-
* this method synchronous. The fully-untargeted call (no argument and no
|
|
852
|
-
* configured key) clears every entry, which is safe because each credential
|
|
853
|
-
* pair is independently re-minted on its next request.
|
|
854
|
-
*/
|
|
855
|
-
invalidateServiceToken(apiKey?: string): void {
|
|
856
|
-
const targetKey = apiKey ?? this._serviceApiKey;
|
|
857
|
-
|
|
858
|
-
// No specific credential to target — clear everything. The next
|
|
859
|
-
// getServiceToken() for any credential re-mints from scratch.
|
|
860
|
-
if (!targetKey) {
|
|
861
|
-
this._serviceTokenCache.clear();
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
for (const [cacheKey, entry] of this._serviceTokenCache) {
|
|
866
|
-
if (entry.apiKey === targetKey) {
|
|
867
|
-
this._serviceTokenCache.delete(cacheKey);
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
/**
|
|
874
|
-
* Make an authenticated request on behalf of a user using a service token.
|
|
875
|
-
* Automatically obtains/refreshes the service token.
|
|
876
|
-
*
|
|
877
|
-
* @param method - HTTP method
|
|
878
|
-
* @param url - API endpoint URL
|
|
879
|
-
* @param data - Request body or query params
|
|
880
|
-
* @param userId - Optional user ID to act on behalf of (sent as X-Oxy-User-Id)
|
|
881
|
-
*/
|
|
882
|
-
async makeServiceRequest<R = any>(
|
|
883
|
-
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
884
|
-
url: string,
|
|
885
|
-
data?: any,
|
|
886
|
-
userId?: string
|
|
887
|
-
): Promise<R> {
|
|
888
|
-
const token = await this.getServiceToken();
|
|
889
|
-
|
|
890
|
-
const headers: Record<string, string> = {
|
|
891
|
-
Authorization: `Bearer ${token}`,
|
|
892
|
-
};
|
|
893
|
-
if (userId) {
|
|
894
|
-
headers['X-Oxy-User-Id'] = userId;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
return this.makeRequest<R>(method, url, data, { headers, cache: false });
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
/**
|
|
901
|
-
* Register a new identity with public key authentication
|
|
902
|
-
* Identity is purely cryptographic - username and profile data are optional
|
|
903
|
-
*
|
|
904
|
-
* @param publicKey - The user's ECDSA public key (hex)
|
|
905
|
-
* @param signature - Signature of the registration request
|
|
906
|
-
* @param timestamp - Timestamp when the signature was created
|
|
907
|
-
*/
|
|
908
|
-
async register(
|
|
909
|
-
publicKey: string,
|
|
910
|
-
signature: string,
|
|
911
|
-
timestamp: number
|
|
912
|
-
): Promise<{ message: string; user: User }> {
|
|
913
|
-
try {
|
|
914
|
-
// Advisory for now (server soft-enforces — see
|
|
915
|
-
// `SessionController.register`), but solved unconditionally so every
|
|
916
|
-
// client is already sending it once the server starts requiring it.
|
|
917
|
-
const powNonce = await solveRegistrationPow(publicKey, timestamp);
|
|
918
|
-
|
|
919
|
-
const res = await this.makeRequest<{ message: string; user: User }>('POST', '/auth/register', {
|
|
920
|
-
publicKey,
|
|
921
|
-
signature,
|
|
922
|
-
timestamp,
|
|
923
|
-
powNonce,
|
|
924
|
-
}, { cache: false, skipAuth: true });
|
|
925
|
-
|
|
926
|
-
if (!res || (typeof res === 'object' && Object.keys(res).length === 0)) {
|
|
927
|
-
throw new OxyAuthenticationError('Registration failed', 'REGISTER_FAILED', 400);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
return res;
|
|
931
|
-
} catch (error) {
|
|
932
|
-
throw this.handleError(error);
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
/**
|
|
937
|
-
* Request an authentication challenge
|
|
938
|
-
* The client must sign this challenge with their private key
|
|
939
|
-
*
|
|
940
|
-
* @param publicKey - The user's public key
|
|
941
|
-
* @param requestOptions - Optional per-call transport overrides (`retry`,
|
|
942
|
-
* `timeout`). Interactive callers omit it (defaults keep retries); the
|
|
943
|
-
* cold-boot `shared-key-signin` step passes `{ retry: false }` so a slow
|
|
944
|
-
* network cannot multiply boot latency via the inner retry loop.
|
|
945
|
-
*/
|
|
946
|
-
async requestChallenge(
|
|
947
|
-
publicKey: string,
|
|
948
|
-
requestOptions?: { retry?: boolean; timeout?: number },
|
|
949
|
-
): Promise<ChallengeResponse> {
|
|
950
|
-
try {
|
|
951
|
-
return await this.makeRequest<ChallengeResponse>('POST', '/auth/challenge', {
|
|
952
|
-
publicKey,
|
|
953
|
-
}, { cache: false, skipAuth: true, ...requestOptions });
|
|
954
|
-
} catch (error) {
|
|
955
|
-
throw this.handleError(error);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
/**
|
|
960
|
-
* Verify a signed challenge and create a session
|
|
961
|
-
*
|
|
962
|
-
* @param publicKey - The user's public key
|
|
963
|
-
* @param challenge - The challenge string from requestChallenge
|
|
964
|
-
* @param signature - Signature of the auth message
|
|
965
|
-
* @param timestamp - Timestamp when the signature was created
|
|
966
|
-
* @param deviceName - Optional device name
|
|
967
|
-
* @param deviceFingerprint - Optional device fingerprint
|
|
968
|
-
* @param requestOptions - Optional per-call transport overrides (`retry`,
|
|
969
|
-
* `timeout`). Interactive callers omit it (defaults keep retries); the
|
|
970
|
-
* cold-boot `shared-key-signin` step passes `{ retry: false }` so a slow
|
|
971
|
-
* network cannot multiply boot latency via the inner retry loop.
|
|
972
|
-
* @param options.plantTokens - Install the returned bearer on this client
|
|
973
|
-
* (default `true`). A caller that must decide LATER whether the session is
|
|
974
|
-
* still wanted (the account dialog, whose user may have cancelled while the
|
|
975
|
-
* request was in flight) passes `false` and plants it itself.
|
|
976
|
-
*/
|
|
977
|
-
async verifyChallenge(
|
|
978
|
-
publicKey: string,
|
|
979
|
-
challenge: string,
|
|
980
|
-
signature: string,
|
|
981
|
-
timestamp: number,
|
|
982
|
-
deviceName?: string,
|
|
983
|
-
deviceFingerprint?: string,
|
|
984
|
-
requestOptions?: { retry?: boolean; timeout?: number },
|
|
985
|
-
options: { plantTokens?: boolean } = {},
|
|
986
|
-
): Promise<SessionLoginResponse> {
|
|
987
|
-
try {
|
|
988
|
-
const res = await this.makeRequest<SessionLoginResponse>('POST', '/auth/verify', {
|
|
989
|
-
publicKey,
|
|
990
|
-
challenge,
|
|
991
|
-
signature,
|
|
992
|
-
timestamp,
|
|
993
|
-
deviceName,
|
|
994
|
-
deviceFingerprint,
|
|
995
|
-
}, { cache: false, skipAuth: true, ...requestOptions });
|
|
996
|
-
|
|
997
|
-
// Plant the freshly-minted tokens, mirroring `claimSessionByToken`.
|
|
998
|
-
// `/auth/verify` returns the first access token (and refresh token) in
|
|
999
|
-
// its body, so installing it here means callers get an authenticated
|
|
1000
|
-
// client without a second round-trip. Refresh stays in the httpOnly
|
|
1001
|
-
// cookie slot set by the API.
|
|
1002
|
-
if (res?.accessToken && options.plantTokens !== false) {
|
|
1003
|
-
this.setTokens(res.accessToken);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
return {
|
|
1007
|
-
...res,
|
|
1008
|
-
user: normalizeUserIdentity(res.user),
|
|
1009
|
-
};
|
|
1010
|
-
} catch (error) {
|
|
1011
|
-
throw this.handleError(error);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
/**
|
|
1016
|
-
* Check if a public key is already registered
|
|
1017
|
-
*/
|
|
1018
|
-
async checkPublicKeyRegistered(publicKey: string): Promise<PublicKeyCheckResponse> {
|
|
1019
|
-
try {
|
|
1020
|
-
return await this.makeRequest<PublicKeyCheckResponse>(
|
|
1021
|
-
'GET',
|
|
1022
|
-
`/auth/check-publickey/${encodeURIComponent(publicKey)}`,
|
|
1023
|
-
undefined,
|
|
1024
|
-
{ cache: false, skipAuth: true }
|
|
1025
|
-
);
|
|
1026
|
-
} catch (error) {
|
|
1027
|
-
throw this.handleError(error);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
/**
|
|
1032
|
-
* Get user by public key
|
|
1033
|
-
*/
|
|
1034
|
-
async getUserByPublicKey(publicKey: string): Promise<User> {
|
|
1035
|
-
try {
|
|
1036
|
-
const user = await this.makeRequest<User>(
|
|
1037
|
-
'GET',
|
|
1038
|
-
`/auth/user/${encodeURIComponent(publicKey)}`,
|
|
1039
|
-
undefined,
|
|
1040
|
-
{
|
|
1041
|
-
cache: true,
|
|
1042
|
-
cacheTTL: 2 * 60 * 1000,
|
|
1043
|
-
// Public lookup by public key (pre-session) — skip the bearer preflight.
|
|
1044
|
-
skipAuth: true,
|
|
1045
|
-
},
|
|
1046
|
-
);
|
|
1047
|
-
return normalizeUserIdentity(user);
|
|
1048
|
-
} catch (error) {
|
|
1049
|
-
throw this.handleError(error);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
/**
|
|
1054
|
-
* Get user by session ID
|
|
1055
|
-
*/
|
|
1056
|
-
async getUserBySession(sessionId: string): Promise<User> {
|
|
1057
|
-
try {
|
|
1058
|
-
const user = await this.makeRequest<User>('GET', `/session/user/${sessionId}`, undefined, {
|
|
1059
|
-
cache: true,
|
|
1060
|
-
cacheTTL: 2 * 60 * 1000,
|
|
1061
|
-
});
|
|
1062
|
-
return normalizeUserIdentity(user);
|
|
1063
|
-
} catch (error) {
|
|
1064
|
-
throw this.handleError(error);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
/**
|
|
1069
|
-
* Batch get multiple user profiles by session IDs
|
|
1070
|
-
*/
|
|
1071
|
-
async getUsersBySessions(sessionIds: string[]): Promise<Array<{ sessionId: string; user: User | null }>> {
|
|
1072
|
-
try {
|
|
1073
|
-
if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
|
|
1074
|
-
return [];
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
const uniqueSessionIds = Array.from(new Set(sessionIds)).sort();
|
|
1078
|
-
|
|
1079
|
-
const users = await this.makeRequest<Array<{ sessionId: string; user: User | null }>>(
|
|
1080
|
-
'POST',
|
|
1081
|
-
'/session/users/batch',
|
|
1082
|
-
{ sessionIds: uniqueSessionIds },
|
|
1083
|
-
{
|
|
1084
|
-
cache: true,
|
|
1085
|
-
cacheTTL: 2 * 60 * 1000,
|
|
1086
|
-
deduplicate: true,
|
|
1087
|
-
}
|
|
1088
|
-
);
|
|
1089
|
-
return users.map((entry) => ({
|
|
1090
|
-
...entry,
|
|
1091
|
-
user: normalizeUserIdentityOrNull(entry.user),
|
|
1092
|
-
}));
|
|
1093
|
-
} catch (error) {
|
|
1094
|
-
throw this.handleError(error);
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
/**
|
|
1099
|
-
* Exchange a device-flow sessionToken for the first access token.
|
|
1100
|
-
*
|
|
1101
|
-
* The originating client holds a 128-bit `sessionToken` that nobody
|
|
1102
|
-
* else has seen — it was generated client-side, sent once on
|
|
1103
|
-
* `POST /auth/session/create`, and is never echoed back. After
|
|
1104
|
-
* another authenticated device approves the session via
|
|
1105
|
-
* `POST /auth/session/authorize/{sessionToken}` (bearer-authed) and
|
|
1106
|
-
* the auth socket / poll loop notifies this client, the client
|
|
1107
|
-
* exchanges its `sessionToken` here for the first access token,
|
|
1108
|
-
* refresh token, sessionId, and the authorized user.
|
|
1109
|
-
*
|
|
1110
|
-
* This call requires no Authorization header — the high-entropy
|
|
1111
|
-
* `sessionToken` IS the credential (RFC 8628 §3.4). The exchange is
|
|
1112
|
-
* single-use; replay attempts are rejected with 401.
|
|
1113
|
-
*
|
|
1114
|
-
* @param sessionToken - The same sessionToken the SDK passed to
|
|
1115
|
-
* `POST /auth/session/create` at the start of the flow.
|
|
1116
|
-
* @param options.deviceFingerprint - Optional fingerprint of the
|
|
1117
|
-
* originating client device.
|
|
1118
|
-
* @param options.plantTokens - Install the claimed bearer on this client
|
|
1119
|
-
* (default `true`). Pass `false` to install it yourself once you know the
|
|
1120
|
-
* session is still wanted — see `verifyChallenge`.
|
|
1121
|
-
*/
|
|
1122
|
-
async claimSessionByToken(
|
|
1123
|
-
sessionToken: string,
|
|
1124
|
-
options: { deviceFingerprint?: string; plantTokens?: boolean } = {}
|
|
1125
|
-
): Promise<{
|
|
1126
|
-
accessToken: string;
|
|
1127
|
-
sessionId: string;
|
|
1128
|
-
deviceId: string;
|
|
1129
|
-
expiresAt: string;
|
|
1130
|
-
user: User;
|
|
1131
|
-
deviceSecret?: string;
|
|
1132
|
-
}> {
|
|
1133
|
-
try {
|
|
1134
|
-
const res = await this.makeRequest<{
|
|
1135
|
-
accessToken: string;
|
|
1136
|
-
sessionId: string;
|
|
1137
|
-
deviceId: string;
|
|
1138
|
-
expiresAt: string;
|
|
1139
|
-
user: User;
|
|
1140
|
-
deviceSecret?: string;
|
|
1141
|
-
}>(
|
|
1142
|
-
'POST',
|
|
1143
|
-
'/auth/session/claim',
|
|
1144
|
-
{
|
|
1145
|
-
sessionToken,
|
|
1146
|
-
...(options.deviceFingerprint ? { deviceFingerprint: options.deviceFingerprint } : {}),
|
|
1147
|
-
},
|
|
1148
|
-
// Body-authenticated device-flow claim (no bearer) — skip the preflight.
|
|
1149
|
-
{ cache: false, retry: false, skipAuth: true }
|
|
1150
|
-
);
|
|
1151
|
-
|
|
1152
|
-
if (options.plantTokens !== false) {
|
|
1153
|
-
this.setTokens(res.accessToken);
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
return res;
|
|
1157
|
-
} catch (error) {
|
|
1158
|
-
throw this.handleError(error);
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// =======================================================================
|
|
1163
|
-
// "Sign in with Oxy" — handoff (Workstream C)
|
|
1164
|
-
//
|
|
1165
|
-
// Two mechanisms share the same challenge/verify + device-flow primitives:
|
|
1166
|
-
// A. Same-device shared-keychain SSO (`signInWithSharedIdentity`): a
|
|
1167
|
-
// sibling native app silently mints its own session from the shared
|
|
1168
|
-
// identity key. No user interaction.
|
|
1169
|
-
// B. QR / app-to-app / popup / push handoff: a relying party
|
|
1170
|
-
// (`startCommonsSignIn` + `pollCommonsSignIn`, finalized with either
|
|
1171
|
-
// `claimSessionByToken` or `finalizeCommonsOAuth`) and the approver /
|
|
1172
|
-
// Commons (`getCommonsApprovalInfo` + `approveCommonsSignIn` /
|
|
1173
|
-
// `denyCommonsSignIn`). The approver signs with its PRIMARY local key;
|
|
1174
|
-
// the RP never sees the private key.
|
|
1175
|
-
//
|
|
1176
|
-
// ONE request serves every delivery surface. The delivery surface is
|
|
1177
|
-
// not part of the model: only the request's purpose is, and it is fixed
|
|
1178
|
-
// at creation by whether an OAuth binding was attached.
|
|
1179
|
-
// =======================================================================
|
|
1180
|
-
|
|
1181
|
-
/**
|
|
1182
|
-
* MECHANISM A — same-device shared-keychain SSO.
|
|
1183
|
-
*
|
|
1184
|
-
* Native-only. If this device holds a shared identity (the cross-app
|
|
1185
|
-
* `group.so.oxy.shared` keychain key), prove control of it and mint a
|
|
1186
|
-
* session: `requestChallenge(sharedPublicKey)` → `signChallengeWithSharedKey`
|
|
1187
|
-
* → `verifyChallenge` (which plants the tokens). Returns `null` on web or
|
|
1188
|
-
* when no shared identity is present — never throws for the absent-identity
|
|
1189
|
-
* case, so a cold-boot caller can fall through to the next step.
|
|
1190
|
-
*
|
|
1191
|
-
* The cold-boot wiring that CALLS this lives in `OxyContext`
|
|
1192
|
-
* (`@oxy.so/services`); this method just performs the exchange.
|
|
1193
|
-
*
|
|
1194
|
-
* @param opts.requestOptions - Optional per-call transport overrides
|
|
1195
|
-
* (`retry`, `timeout`) forwarded to BOTH the `requestChallenge` and
|
|
1196
|
-
* `verifyChallenge` round-trips. Interactive flows omit it (defaults keep
|
|
1197
|
-
* retries); the cold-boot `shared-key-signin` step passes `{ retry: false }`
|
|
1198
|
-
* so this network step cannot multiply boot latency via the inner retry
|
|
1199
|
-
* loop. The token-refresh scheduler / 401 lane still retry later.
|
|
1200
|
-
* @param opts.plantTokens - Forwarded to `verifyChallenge` (default `true`).
|
|
1201
|
-
*/
|
|
1202
|
-
async signInWithSharedIdentity(
|
|
1203
|
-
opts: {
|
|
1204
|
-
deviceName?: string;
|
|
1205
|
-
deviceFingerprint?: string;
|
|
1206
|
-
requestOptions?: { retry?: boolean; timeout?: number };
|
|
1207
|
-
plantTokens?: boolean;
|
|
1208
|
-
} = {}
|
|
1209
|
-
): Promise<SessionLoginResponse | null> {
|
|
1210
|
-
try {
|
|
1211
|
-
// `hasSharedIdentity()` already returns false on web (the shared
|
|
1212
|
-
// keychain is native-only), so this short-circuits the web case without
|
|
1213
|
-
// a wasted challenge round-trip.
|
|
1214
|
-
if (!(await KeyManager.hasSharedIdentity())) {
|
|
1215
|
-
return null;
|
|
1216
|
-
}
|
|
1217
|
-
const sharedPublicKey = await KeyManager.getSharedPublicKey();
|
|
1218
|
-
if (!sharedPublicKey) {
|
|
1219
|
-
return null;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
const { challenge } = await this.requestChallenge(sharedPublicKey, opts.requestOptions);
|
|
1223
|
-
const signed = await SignatureService.signChallengeWithSharedKey(challenge);
|
|
1224
|
-
|
|
1225
|
-
// `signed.challenge` carries the SIGNATURE (mirrors `signChallenge`).
|
|
1226
|
-
return await this.verifyChallenge(
|
|
1227
|
-
signed.publicKey,
|
|
1228
|
-
challenge,
|
|
1229
|
-
signed.challenge,
|
|
1230
|
-
signed.timestamp,
|
|
1231
|
-
opts.deviceName,
|
|
1232
|
-
opts.deviceFingerprint,
|
|
1233
|
-
opts.requestOptions,
|
|
1234
|
-
{ plantTokens: opts.plantTokens },
|
|
1235
|
-
);
|
|
1236
|
-
} catch (error) {
|
|
1237
|
-
throw this.handleError(error);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
/**
|
|
1242
|
-
* MECHANISM B (relying party) — begin a "Sign in with Oxy" handoff.
|
|
1243
|
-
*
|
|
1244
|
-
* Generates a secret device-flow `sessionToken` client-side (it never
|
|
1245
|
-
* appears in the QR), registers it with `POST /auth/session/create`, and
|
|
1246
|
-
* returns the server-issued public `authorizeCode` + ready-to-render
|
|
1247
|
-
* `qrPayload`. Render the QR (web) / open the deep-link (same-device); the
|
|
1248
|
-
* approver resolves the code and authorizes. Then poll with
|
|
1249
|
-
* {@link pollCommonsSignIn} and finalize.
|
|
1250
|
-
*
|
|
1251
|
-
* ONE request serves every delivery surface, and how it finalizes is decided
|
|
1252
|
-
* here by whether an OAuth binding is attached:
|
|
1253
|
-
* - no `oauth` (the default): a `device_sign_in` request — on `authorized`,
|
|
1254
|
-
* exchange the `sessionToken` via the existing `claimSessionByToken`.
|
|
1255
|
-
* - with `oauth`: an `oauth_authorization` request — on `authorized`, call
|
|
1256
|
-
* {@link finalizeCommonsOAuth} with the same `sessionToken` to mint the
|
|
1257
|
-
* single-use authorization code, then exchange it with PKCE.
|
|
1258
|
-
*
|
|
1259
|
-
* @param params.clientId - The RP's registered OAuth client id
|
|
1260
|
-
* (ApplicationCredential publicKey); required so the server can resolve the
|
|
1261
|
-
* requesting application's identity.
|
|
1262
|
-
* @param params.oauth - Optional OAuth binding ({@link CommonsOAuthContext}).
|
|
1263
|
-
* Carries only the redirect URI, the PKCE S256 challenge, the requested
|
|
1264
|
-
* scope, and an optional delegated `subjectAccountId` — never the PKCE
|
|
1265
|
-
* verifier, the OAuth `state`, or any token.
|
|
1266
|
-
*/
|
|
1267
|
-
async startCommonsSignIn(params: {
|
|
1268
|
-
clientId: string;
|
|
1269
|
-
oauth?: CommonsOAuthContext;
|
|
1270
|
-
}): Promise<CommonsSignInHandle> {
|
|
1271
|
-
try {
|
|
1272
|
-
// High-entropy opaque secret token (256-bit hex). Generated client-side
|
|
1273
|
-
// and held only here; the server stores it but never returns it in the
|
|
1274
|
-
// QR. Reuses the platform-safe random generator.
|
|
1275
|
-
const sessionToken = await SignatureService.generateChallenge();
|
|
1276
|
-
const expiresAt = Date.now() + COMMONS_SIGN_IN_EXPIRY_MS;
|
|
1277
|
-
|
|
1278
|
-
const res = await this.makeRequest<CommonsSessionCreateResponse>(
|
|
1279
|
-
'POST',
|
|
1280
|
-
'/auth/session/create',
|
|
1281
|
-
{
|
|
1282
|
-
sessionToken,
|
|
1283
|
-
expiresAt,
|
|
1284
|
-
clientId: params.clientId,
|
|
1285
|
-
// Omitted entirely when absent so the device-sign-in body stays
|
|
1286
|
-
// exactly what it has always been (the server reads presence, not a
|
|
1287
|
-
// null, to decide the request's purpose).
|
|
1288
|
-
...(params.oauth ? { oauth: params.oauth } : {}),
|
|
1289
|
-
},
|
|
1290
|
-
// Public/pre-session (no bearer): skip the preflight so a stale
|
|
1291
|
-
// near-expiry token cannot re-enter refreshAccessToken while the
|
|
1292
|
-
// refresh handler is already in flight (self-await hang).
|
|
1293
|
-
{ cache: false, skipAuth: true },
|
|
1294
|
-
);
|
|
1295
|
-
|
|
1296
|
-
return {
|
|
1297
|
-
sessionToken,
|
|
1298
|
-
authorizeCode: res.authorizeCode,
|
|
1299
|
-
qrPayload: res.qrPayload,
|
|
1300
|
-
expiresAt: res.expiresAt ?? expiresAt,
|
|
1301
|
-
status: res.status,
|
|
1302
|
-
};
|
|
1303
|
-
} catch (error) {
|
|
1304
|
-
throw this.handleError(error);
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
/**
|
|
1309
|
-
* MECHANISM B (relying party) — poll a device-flow session for approval.
|
|
1310
|
-
*
|
|
1311
|
-
* Backstop for the auth socket. On `authorized` (with a `sessionId`), the
|
|
1312
|
-
* caller finalizes: `claimSessionByToken` for a `device_sign_in` request,
|
|
1313
|
-
* {@link finalizeCommonsOAuth} for an `oauth_authorization` one.
|
|
1314
|
-
*
|
|
1315
|
-
* Every field is narrowed fail-safe. `authorized` counts only as a literal
|
|
1316
|
-
* `true`, the identifiers only as non-empty strings, and the delivery
|
|
1317
|
-
* progress timestamps degrade to `null` when absent or unparseable — so a
|
|
1318
|
-
* partial or older-API payload can advance the waiting UI at most, never
|
|
1319
|
-
* make it believe a request was approved.
|
|
1320
|
-
*
|
|
1321
|
-
* @param sessionToken - The secret token from {@link startCommonsSignIn}.
|
|
1322
|
-
*/
|
|
1323
|
-
async pollCommonsSignIn(sessionToken: string): Promise<CommonsSignInStatus> {
|
|
1324
|
-
try {
|
|
1325
|
-
const res = await this.makeRequest<unknown>(
|
|
1326
|
-
'GET',
|
|
1327
|
-
`/auth/session/status/${encodeURIComponent(sessionToken)}`,
|
|
1328
|
-
undefined,
|
|
1329
|
-
// Public/pre-session (no bearer): a preflight here is wrong per se and,
|
|
1330
|
-
// if ever reached while a refresh is pending, would re-enter
|
|
1331
|
-
// refreshAccessToken and await the very promise it runs inside.
|
|
1332
|
-
{ cache: false, retry: false, skipAuth: true }
|
|
1333
|
-
);
|
|
1334
|
-
|
|
1335
|
-
if (res === null || typeof res !== 'object') {
|
|
1336
|
-
throw new Error('auth/session/status returned an unexpected response shape');
|
|
1337
|
-
}
|
|
1338
|
-
const { authorized, sessionId, publicKey, status, purpose, pushSentAt, openedAt } =
|
|
1339
|
-
res as Record<string, unknown>;
|
|
1340
|
-
|
|
1341
|
-
return {
|
|
1342
|
-
authorized: authorized === true,
|
|
1343
|
-
...(typeof sessionId === 'string' && sessionId ? { sessionId } : {}),
|
|
1344
|
-
...(typeof publicKey === 'string' && publicKey ? { publicKey } : {}),
|
|
1345
|
-
...(typeof status === 'string' && status ? { status } : {}),
|
|
1346
|
-
purpose: purpose === 'oauth_authorization' ? 'oauth_authorization' : 'device_sign_in',
|
|
1347
|
-
pushSentAt: parseCommonsProgressTimestamp(pushSentAt),
|
|
1348
|
-
openedAt: parseCommonsProgressTimestamp(openedAt),
|
|
1349
|
-
};
|
|
1350
|
-
} catch (error) {
|
|
1351
|
-
throw this.handleError(error);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
/**
|
|
1356
|
-
* MECHANISM B (relying party) — ask Oxy to DELIVER a pending sign-in request
|
|
1357
|
-
* to the identity's known Commons installations.
|
|
1358
|
-
*
|
|
1359
|
-
* This is the automatic half of "one intention, one primary action": rather
|
|
1360
|
-
* than offering the user a menu of transports, the caller asks for delivery
|
|
1361
|
-
* and lets the answer pick the route. Pass the returned `targets` to
|
|
1362
|
-
* `selectCommonsDelivery` (`utils/commonsDelivery`) — `targets: 0` means no
|
|
1363
|
-
* capable Commons installation is registered, which is a NORMAL outcome that
|
|
1364
|
-
* resolves to the QR route, not an error to surface.
|
|
1365
|
-
*
|
|
1366
|
-
* **Requires a bearer.** Delivery is only allowed when Oxy already knows the
|
|
1367
|
-
* intended identity from a trusted authenticated context — a request that
|
|
1368
|
-
* merely carries a username or email typed into an unauthenticated browser
|
|
1369
|
-
* must never be able to ring somebody's phone.
|
|
1370
|
-
*
|
|
1371
|
-
* The push it sends carries only `{ type, approvalUrl }` where the URL holds
|
|
1372
|
-
* the public `authorizeCode` — no display data, no secrets. Commons resolves
|
|
1373
|
-
* everything it shows from `getCommonsApprovalInfo`.
|
|
1374
|
-
*
|
|
1375
|
-
* @param authorizeCode - The public code from {@link startCommonsSignIn}.
|
|
1376
|
-
*/
|
|
1377
|
-
async deliverCommonsSignIn(authorizeCode: string): Promise<CommonsDeliveryResult> {
|
|
1378
|
-
try {
|
|
1379
|
-
const res = await this.makeRequest<unknown>(
|
|
1380
|
-
'POST',
|
|
1381
|
-
`/auth/session/deliver/${encodeURIComponent(authorizeCode)}`,
|
|
1382
|
-
undefined,
|
|
1383
|
-
// Bearer REQUIRED (unlike its public siblings): the identity to
|
|
1384
|
-
// deliver to comes from the authenticated caller, never the code.
|
|
1385
|
-
{ cache: false },
|
|
1386
|
-
);
|
|
1387
|
-
|
|
1388
|
-
if (res === null || typeof res !== 'object') {
|
|
1389
|
-
throw new Error('auth/session/deliver returned an unexpected response shape');
|
|
1390
|
-
}
|
|
1391
|
-
const { delivered, targets } = res as Record<string, unknown>;
|
|
1392
|
-
// Both fields drive the route choice, so a partial payload is rejected
|
|
1393
|
-
// outright rather than defaulted into a route the server never chose.
|
|
1394
|
-
if (
|
|
1395
|
-
typeof delivered !== 'boolean' ||
|
|
1396
|
-
typeof targets !== 'number' ||
|
|
1397
|
-
!Number.isInteger(targets) ||
|
|
1398
|
-
targets < 0
|
|
1399
|
-
) {
|
|
1400
|
-
throw new Error('auth/session/deliver returned an incomplete delivery result');
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
return { delivered, targets };
|
|
1404
|
-
} catch (error) {
|
|
1405
|
-
throw this.handleError(error);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
/**
|
|
1410
|
-
* MECHANISM B (approver / Commons) — report that the approval route was
|
|
1411
|
-
* OPENED, so the waiting relying party can show "Opened in Commons".
|
|
1412
|
-
*
|
|
1413
|
-
* Progress only. It is idempotent, applies to a `pending` request alone, and
|
|
1414
|
-
* records a timestamp (`openedAt`) — it never approves, authorizes, or
|
|
1415
|
-
* advances the authorization state machine. Public, like the other approver
|
|
1416
|
-
* handles: the approver has only the public `authorizeCode` at this point
|
|
1417
|
-
* and has not yet signed anything.
|
|
1418
|
-
*
|
|
1419
|
-
* Best-effort by nature — a failure here costs the user only a progress
|
|
1420
|
-
* line, so callers are free to ignore a rejection and continue to the
|
|
1421
|
-
* approval screen.
|
|
1422
|
-
*
|
|
1423
|
-
* @param authorizeCode - The public code scanned from the QR / deep-link / push.
|
|
1424
|
-
*/
|
|
1425
|
-
async markCommonsApprovalOpened(authorizeCode: string): Promise<void> {
|
|
1426
|
-
try {
|
|
1427
|
-
await this.makeRequest<unknown>(
|
|
1428
|
-
'POST',
|
|
1429
|
-
`/auth/session/opened/${encodeURIComponent(authorizeCode)}`,
|
|
1430
|
-
undefined,
|
|
1431
|
-
// Public (no bearer) — skip the preflight, exactly like approve-info.
|
|
1432
|
-
{ cache: false, skipAuth: true },
|
|
1433
|
-
);
|
|
1434
|
-
} catch (error) {
|
|
1435
|
-
throw this.handleError(error);
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
/**
|
|
1440
|
-
* MECHANISM B (relying party) — finalize an APPROVED, OAuth-bound request
|
|
1441
|
-
* into a single-use OAuth authorization code.
|
|
1442
|
-
*
|
|
1443
|
-
* The OAuth counterpart of `claimSessionByToken`: same secret credential,
|
|
1444
|
-
* same single-use semantics, different output. Call it once the request the
|
|
1445
|
-
* RP started with an `oauth` binding reports `authorized`; the server
|
|
1446
|
-
* atomically mints exactly ONE `AuthCode` bound to the redirect URI, PKCE
|
|
1447
|
-
* challenge, scopes, approving identity, and any delegated subject account
|
|
1448
|
-
* the request was created with. A second call cannot mint another code.
|
|
1449
|
-
*
|
|
1450
|
-
* The result is an authorization CODE, never a token — the caller completes
|
|
1451
|
-
* the flow with the ordinary PKCE exchange (`exchangeOAuthCode`) using the
|
|
1452
|
-
* `codeVerifier` it never sent anywhere. Nothing here is exposed to the
|
|
1453
|
-
* popup: the code travels back through the registered callback, and the
|
|
1454
|
-
* main window owns the verifier.
|
|
1455
|
-
*
|
|
1456
|
-
* Like `claimSessionByToken`, this needs no Authorization header — the
|
|
1457
|
-
* high-entropy SECRET `sessionToken` IS the credential. Never pass the
|
|
1458
|
-
* public `authorizeCode` here; it is the approver's handle, not the
|
|
1459
|
-
* initiator's. Every server-side failure (wrong/expired/already-finalized
|
|
1460
|
-
* request, non-OAuth purpose, missing permission for the delegated account)
|
|
1461
|
-
* surfaces as one generic error, so nothing about the request's state can be
|
|
1462
|
-
* probed from outside.
|
|
1463
|
-
*
|
|
1464
|
-
* @param sessionToken - The secret token from {@link startCommonsSignIn}.
|
|
1465
|
-
*/
|
|
1466
|
-
async finalizeCommonsOAuth(sessionToken: string): Promise<CommonsOAuthFinalizeResult> {
|
|
1467
|
-
try {
|
|
1468
|
-
const res = await this.makeRequest<unknown>(
|
|
1469
|
-
'POST',
|
|
1470
|
-
`/auth/session/finalize/${encodeURIComponent(sessionToken)}`,
|
|
1471
|
-
undefined,
|
|
1472
|
-
// Body-authenticated by the path's secret token (no bearer) — skip the
|
|
1473
|
-
// preflight, exactly like the device-flow claim.
|
|
1474
|
-
{ cache: false, skipAuth: true },
|
|
1475
|
-
);
|
|
1476
|
-
|
|
1477
|
-
if (res === null || typeof res !== 'object') {
|
|
1478
|
-
throw new Error('auth/session/finalize returned an unexpected response shape');
|
|
1479
|
-
}
|
|
1480
|
-
const { code, redirectUri, expiresIn } = res as Record<string, unknown>;
|
|
1481
|
-
// All three fields are load-bearing for the exchange that follows, so a
|
|
1482
|
-
// partial payload is rejected outright rather than returned half-parsed.
|
|
1483
|
-
if (
|
|
1484
|
-
typeof code !== 'string' ||
|
|
1485
|
-
!code ||
|
|
1486
|
-
typeof redirectUri !== 'string' ||
|
|
1487
|
-
!redirectUri ||
|
|
1488
|
-
typeof expiresIn !== 'number' ||
|
|
1489
|
-
!Number.isFinite(expiresIn)
|
|
1490
|
-
) {
|
|
1491
|
-
throw new Error('auth/session/finalize returned an incomplete authorization code');
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
return { code, redirectUri, expiresIn };
|
|
1495
|
-
} catch (error) {
|
|
1496
|
-
throw this.handleError(error);
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
/**
|
|
1501
|
-
* MECHANISM B (approver / Commons) — resolve the TRUSTED identity of a
|
|
1502
|
-
* sign-in request from its public `authorizeCode`.
|
|
1503
|
-
*
|
|
1504
|
-
* The returned `application` and `subjectAccount` are resolved server-side
|
|
1505
|
-
* and are the only safe things to display in the approval UI — NEVER trust
|
|
1506
|
-
* the app/name/origin strings carried in the QR payload. Public (no auth
|
|
1507
|
-
* required).
|
|
1508
|
-
*
|
|
1509
|
-
* @param authorizeCode - The public code scanned from the QR / deep-link.
|
|
1510
|
-
*/
|
|
1511
|
-
async getCommonsApprovalInfo(authorizeCode: string): Promise<CommonsApprovalInfo> {
|
|
1512
|
-
try {
|
|
1513
|
-
const raw = await this.makeRequest<CommonsApprovalInfoResponse>(
|
|
1514
|
-
'GET',
|
|
1515
|
-
`/auth/session/approve-info/${encodeURIComponent(authorizeCode)}`,
|
|
1516
|
-
undefined,
|
|
1517
|
-
// Public (no auth required) — skip the bearer preflight (avoids the
|
|
1518
|
-
// pre-session self-await class).
|
|
1519
|
-
{ cache: false, skipAuth: true }
|
|
1520
|
-
);
|
|
1521
|
-
return {
|
|
1522
|
-
application: raw.application,
|
|
1523
|
-
scopes: raw.scopes,
|
|
1524
|
-
boundOrigin: raw.boundOrigin,
|
|
1525
|
-
// Fail-safe: only a literal boolean `true` counts as verified. A
|
|
1526
|
-
// missing or non-boolean value (older server, malformed response)
|
|
1527
|
-
// coerces to `false` so a stale server can never imply trust.
|
|
1528
|
-
originVerified: raw.originVerified === true,
|
|
1529
|
-
// Same discipline: a missing/blank/non-string label degrades to null
|
|
1530
|
-
// (an older API, or a native requester with no browser to describe),
|
|
1531
|
-
// and the approver simply omits the "where from" line.
|
|
1532
|
-
requesterLabel: parseCommonsRequesterLabel(raw.requesterLabel),
|
|
1533
|
-
// Same discipline: only the literal OAuth purpose opts into OAuth
|
|
1534
|
-
// finalization. Anything else — including a server that predates this
|
|
1535
|
-
// field — is the plain device sign-in it has always been.
|
|
1536
|
-
purpose: raw.purpose === 'oauth_authorization' ? 'oauth_authorization' : 'device_sign_in',
|
|
1537
|
-
// A missing/partial delegated account degrades to "no delegation"
|
|
1538
|
-
// rather than a half-rendered "will act as" line.
|
|
1539
|
-
subjectAccount: parseCommonsSubjectAccount(raw.subjectAccount),
|
|
1540
|
-
expiresAt: raw.expiresAt,
|
|
1541
|
-
status: raw.status,
|
|
1542
|
-
};
|
|
1543
|
-
} catch (error) {
|
|
1544
|
-
throw this.handleError(error);
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
/**
|
|
1549
|
-
* MECHANISM B (approver / Commons) — approve a sign-in request by signing a
|
|
1550
|
-
* fresh challenge with the PRIMARY local identity key.
|
|
1551
|
-
*
|
|
1552
|
-
* Commons holds the user's identity as its primary key (not the shared
|
|
1553
|
-
* key), so this uses `signChallenge`. The signed-but-cookieless authorize
|
|
1554
|
-
* endpoint resolves the user from the verified signer — the RP that started
|
|
1555
|
-
* the flow then claims its session. Native-only (requires a local identity).
|
|
1556
|
-
*
|
|
1557
|
-
* @param params.authorizeCode - The public code being approved.
|
|
1558
|
-
* @param params.deviceName - Optional human-readable device label.
|
|
1559
|
-
* @param params.deviceFingerprint - Optional device fingerprint.
|
|
1560
|
-
*/
|
|
1561
|
-
async approveCommonsSignIn(params: {
|
|
1562
|
-
authorizeCode: string;
|
|
1563
|
-
deviceName?: string;
|
|
1564
|
-
deviceFingerprint?: string;
|
|
1565
|
-
}): Promise<CommonsSignInActionResult> {
|
|
1566
|
-
try {
|
|
1567
|
-
const publicKey = await KeyManager.getPublicKey();
|
|
1568
|
-
if (!publicKey) {
|
|
1569
|
-
throw new Error('No identity found on this device. Create or import an identity first.');
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
const { challenge } = await this.requestChallenge(publicKey);
|
|
1573
|
-
const signed = await SignatureService.signChallenge(challenge);
|
|
1574
|
-
|
|
1575
|
-
return await this.makeRequest<CommonsSignInActionResult>(
|
|
1576
|
-
'POST',
|
|
1577
|
-
`/auth/session/authorize-signed/${encodeURIComponent(params.authorizeCode)}`,
|
|
1578
|
-
{
|
|
1579
|
-
// `signed.challenge` carries the SIGNATURE; `challenge` is the
|
|
1580
|
-
// original server-issued challenge string.
|
|
1581
|
-
publicKey: signed.publicKey,
|
|
1582
|
-
challenge,
|
|
1583
|
-
signature: signed.challenge,
|
|
1584
|
-
timestamp: signed.timestamp,
|
|
1585
|
-
...(params.deviceName ? { deviceName: params.deviceName } : {}),
|
|
1586
|
-
...(params.deviceFingerprint ? { deviceFingerprint: params.deviceFingerprint } : {}),
|
|
1587
|
-
},
|
|
1588
|
-
// Key-signed, cookieless (no bearer) — skip the preflight.
|
|
1589
|
-
{ cache: false, skipAuth: true }
|
|
1590
|
-
);
|
|
1591
|
-
} catch (error) {
|
|
1592
|
-
throw this.handleError(error);
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
/**
|
|
1597
|
-
* MECHANISM B (approver / Commons) — deny a sign-in request, cancelling the
|
|
1598
|
-
* device-flow session so the RP stops waiting.
|
|
1599
|
-
*
|
|
1600
|
-
* @param authorizeCode - The public code being denied.
|
|
1601
|
-
* @param reason - Optional closed-set reason ({@link CommonsDenyReason}).
|
|
1602
|
-
* Pass `'not_me'` ONLY when the user actually reported the request as one
|
|
1603
|
-
* they did not start — the server records it as a suspicious denial rather
|
|
1604
|
-
* than an ordinary cancel. Omitting it sends the exact body this endpoint
|
|
1605
|
-
* has always received.
|
|
1606
|
-
*/
|
|
1607
|
-
async denyCommonsSignIn(
|
|
1608
|
-
authorizeCode: string,
|
|
1609
|
-
reason?: CommonsDenyReason,
|
|
1610
|
-
): Promise<CommonsSignInActionResult> {
|
|
1611
|
-
try {
|
|
1612
|
-
return await this.makeRequest<CommonsSignInActionResult>(
|
|
1613
|
-
'POST',
|
|
1614
|
-
`/auth/session/deny/${encodeURIComponent(authorizeCode)}`,
|
|
1615
|
-
reason ? { reason } : undefined,
|
|
1616
|
-
// Public (no auth required) — skip the bearer preflight.
|
|
1617
|
-
{ cache: false, skipAuth: true }
|
|
1618
|
-
);
|
|
1619
|
-
} catch (error) {
|
|
1620
|
-
throw this.handleError(error);
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
/**
|
|
1625
|
-
* Internal: decode (without verifying) the `sessionId` claim from a
|
|
1626
|
-
* server-signed access token. The server already verified the signature;
|
|
1627
|
-
* the client only reads the claim to drive multi-session state.
|
|
1628
|
-
*
|
|
1629
|
-
* @internal
|
|
1630
|
-
*/
|
|
1631
|
-
_decodeSessionIdFromAccessToken(token: string): string | null {
|
|
1632
|
-
if (!token || typeof token !== 'string') {
|
|
1633
|
-
return null;
|
|
1634
|
-
}
|
|
1635
|
-
const segments = token.split('.');
|
|
1636
|
-
if (segments.length !== 3) {
|
|
1637
|
-
return null;
|
|
1638
|
-
}
|
|
1639
|
-
const payloadSegment = segments[1];
|
|
1640
|
-
if (!payloadSegment) {
|
|
1641
|
-
return null;
|
|
1642
|
-
}
|
|
1643
|
-
try {
|
|
1644
|
-
const base64 = payloadSegment.replace(/-/g, '+').replace(/_/g, '/');
|
|
1645
|
-
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
|
|
1646
|
-
if (typeof atob !== 'function') {
|
|
1647
|
-
return null;
|
|
1648
|
-
}
|
|
1649
|
-
const json = decodeURIComponent(
|
|
1650
|
-
atob(padded)
|
|
1651
|
-
.split('')
|
|
1652
|
-
.map((char) => `%${`00${char.charCodeAt(0).toString(16)}`.slice(-2)}`)
|
|
1653
|
-
.join(''),
|
|
1654
|
-
);
|
|
1655
|
-
const parsed: unknown = JSON.parse(json);
|
|
1656
|
-
if (parsed === null || typeof parsed !== 'object') {
|
|
1657
|
-
return null;
|
|
1658
|
-
}
|
|
1659
|
-
const claims = parsed as Record<string, unknown>;
|
|
1660
|
-
return typeof claims.sessionId === 'string' ? claims.sessionId : null;
|
|
1661
|
-
} catch {
|
|
1662
|
-
return null;
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
/**
|
|
1667
|
-
* Get sessions by session ID
|
|
1668
|
-
*/
|
|
1669
|
-
async getSessionsBySessionId(sessionId: string): Promise<any[]> {
|
|
1670
|
-
try {
|
|
1671
|
-
return await this.makeRequest('GET', `/session/sessions/${sessionId}`, undefined, {
|
|
1672
|
-
cache: false,
|
|
1673
|
-
});
|
|
1674
|
-
} catch (error) {
|
|
1675
|
-
throw this.handleError(error);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
/**
|
|
1680
|
-
* Logout from a specific session
|
|
1681
|
-
*/
|
|
1682
|
-
async logoutSession(sessionId: string, targetSessionId?: string): Promise<void> {
|
|
1683
|
-
try {
|
|
1684
|
-
const url = targetSessionId
|
|
1685
|
-
? `/session/logout/${sessionId}/${targetSessionId}`
|
|
1686
|
-
: `/session/logout/${sessionId}`;
|
|
1687
|
-
|
|
1688
|
-
await this.makeRequest('POST', url, undefined, { cache: false });
|
|
1689
|
-
} catch (error) {
|
|
1690
|
-
throw this.handleError(error);
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
/**
|
|
1695
|
-
* Logout from all sessions
|
|
1696
|
-
*/
|
|
1697
|
-
async logoutAllSessions(sessionId: string): Promise<void> {
|
|
1698
|
-
try {
|
|
1699
|
-
await this.makeRequest('POST', `/session/logout-all/${sessionId}`, undefined, { cache: false });
|
|
1700
|
-
} catch (error) {
|
|
1701
|
-
throw this.handleError(error);
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
/**
|
|
1706
|
-
* Validate session
|
|
1707
|
-
*/
|
|
1708
|
-
async validateSession(
|
|
1709
|
-
sessionId: string,
|
|
1710
|
-
options: {
|
|
1711
|
-
deviceFingerprint?: string;
|
|
1712
|
-
useHeaderValidation?: boolean;
|
|
1713
|
-
} = {}
|
|
1714
|
-
): Promise<{
|
|
1715
|
-
valid: boolean;
|
|
1716
|
-
expiresAt: string;
|
|
1717
|
-
lastActivity: string;
|
|
1718
|
-
user: User;
|
|
1719
|
-
sessionId?: string;
|
|
1720
|
-
source?: string;
|
|
1721
|
-
}> {
|
|
1722
|
-
try {
|
|
1723
|
-
const urlParams: Record<string, string> = {};
|
|
1724
|
-
if (options.deviceFingerprint) urlParams.deviceFingerprint = options.deviceFingerprint;
|
|
1725
|
-
if (options.useHeaderValidation) urlParams.useHeaderValidation = 'true';
|
|
1726
|
-
const validation = await this.makeRequest<{
|
|
1727
|
-
valid: boolean;
|
|
1728
|
-
expiresAt: string;
|
|
1729
|
-
lastActivity: string;
|
|
1730
|
-
user: User;
|
|
1731
|
-
sessionId?: string;
|
|
1732
|
-
source?: string;
|
|
1733
|
-
}>('GET', `/session/validate/${sessionId}`, urlParams, { cache: false });
|
|
1734
|
-
return {
|
|
1735
|
-
...validation,
|
|
1736
|
-
user: normalizeUserIdentity(validation.user),
|
|
1737
|
-
};
|
|
1738
|
-
} catch (error) {
|
|
1739
|
-
// Session is invalid — clear any cached user data for this session (#196)
|
|
1740
|
-
this.clearCacheEntry(`GET:/session/user/${sessionId}`);
|
|
1741
|
-
throw this.handleError(error);
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
/**
|
|
1746
|
-
* Check username availability
|
|
1747
|
-
*/
|
|
1748
|
-
async checkUsernameAvailability(username: string): Promise<{ available: boolean; message: string }> {
|
|
1749
|
-
try {
|
|
1750
|
-
// Public availability lookup (pre-session) — skip the bearer preflight.
|
|
1751
|
-
return await this.makeRequest('GET', `/auth/check-username/${username}`, undefined, { cache: false, skipAuth: true });
|
|
1752
|
-
} catch (error) {
|
|
1753
|
-
throw this.handleError(error);
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
/**
|
|
1758
|
-
* Send a 6-digit code to an email (ADR 0029 D3): the recovery email of a
|
|
1759
|
-
* new passkey account (`purpose: 'signup'`), or — named by username or
|
|
1760
|
-
* email — the recovery email of an account being recovered
|
|
1761
|
-
* (`purpose: 'recovery'`). The answer is the same whether or not an
|
|
1762
|
-
* account exists; nothing is revealed about who has one.
|
|
1763
|
-
*/
|
|
1764
|
-
async startEmailVerification(request: EmailVerificationStartRequest): Promise<EmailVerificationStartResponse> {
|
|
1765
|
-
try {
|
|
1766
|
-
const res = await this.makeRequest<unknown>('POST', '/auth/email/verify/start', request, {
|
|
1767
|
-
cache: false,
|
|
1768
|
-
skipAuth: true,
|
|
1769
|
-
});
|
|
1770
|
-
const parsed = safeParseContract(emailVerificationStartResponseSchema, res);
|
|
1771
|
-
if (!parsed) throw new Error('auth/email/verify/start returned an unexpected response shape');
|
|
1772
|
-
return parsed;
|
|
1773
|
-
} catch (error) {
|
|
1774
|
-
throw this.handleError(error);
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
/**
|
|
1779
|
-
* Confirm the code {@link startEmailVerification} sent. Resolves to a
|
|
1780
|
-
* short-lived one-use ticket: a sign-up passes it (with the email) to
|
|
1781
|
-
* {@link webauthnRegisterVerify}; a recovery passes it as `recoveryTicket`
|
|
1782
|
-
* to {@link webauthnRegisterOptions} and {@link webauthnRegisterVerify}.
|
|
1783
|
-
*/
|
|
1784
|
-
async confirmEmailVerification(verificationId: string, code: string): Promise<EmailVerificationConfirmResponse> {
|
|
1785
|
-
try {
|
|
1786
|
-
const res = await this.makeRequest<unknown>(
|
|
1787
|
-
'POST',
|
|
1788
|
-
'/auth/email/verify/confirm',
|
|
1789
|
-
{ verificationId, code },
|
|
1790
|
-
{ cache: false, skipAuth: true },
|
|
1791
|
-
);
|
|
1792
|
-
const parsed = safeParseContract(emailVerificationConfirmResponseSchema, res);
|
|
1793
|
-
if (!parsed) throw new Error('auth/email/verify/confirm returned an unexpected response shape');
|
|
1794
|
-
return parsed;
|
|
1795
|
-
} catch (error) {
|
|
1796
|
-
throw this.handleError(error);
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
/**
|
|
1801
|
-
* Begin a WebAuthn / passkey REGISTRATION ceremony. Requests the
|
|
1802
|
-
* `PublicKeyCredentialCreationOptions` the browser's `navigator.credentials
|
|
1803
|
-
* .create()` (or `@simplewebauthn/browser`'s `startRegistration`) needs.
|
|
1804
|
-
*
|
|
1805
|
-
* With a bearer token planted this adds a passkey to the signed-in account.
|
|
1806
|
-
* Without one, `username` is a prospective sign-up's handle, and
|
|
1807
|
-
* `recoveryTicket` a recovery's new passkey for the account it names. The
|
|
1808
|
-
* returned options are OPAQUE — Oxy does not own their shape (the browser /
|
|
1809
|
-
* `@simplewebauthn` does), so they pass through as `unknown` for the caller
|
|
1810
|
-
* to hand straight to the ceremony.
|
|
1811
|
-
*/
|
|
1812
|
-
async webauthnRegisterOptions(request: { username?: string; recoveryTicket?: string } = {}): Promise<unknown> {
|
|
1813
|
-
const signedOut = request.username !== undefined || request.recoveryTicket !== undefined;
|
|
1814
|
-
try {
|
|
1815
|
-
return await this.makeRequest<unknown>(
|
|
1816
|
-
'POST',
|
|
1817
|
-
'/auth/webauthn/register/options',
|
|
1818
|
-
{
|
|
1819
|
-
...(request.username !== undefined ? { username: request.username } : {}),
|
|
1820
|
-
...(request.recoveryTicket !== undefined ? { recoveryTicket: request.recoveryTicket } : {}),
|
|
1821
|
-
},
|
|
1822
|
-
{ cache: false, ...(signedOut ? { skipAuth: true } : {}) },
|
|
1823
|
-
);
|
|
1824
|
-
} catch (error) {
|
|
1825
|
-
throw this.handleError(error);
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
/**
|
|
1830
|
-
* Finish a WebAuthn / passkey REGISTRATION ceremony. Forwards the opaque
|
|
1831
|
-
* browser `RegistrationResponseJSON` (`response`) alongside the Oxy envelope
|
|
1832
|
-
* (desired `username` for signup + the device-session naming fields).
|
|
1833
|
-
*
|
|
1834
|
-
* Server branches, disambiguated by the response shape:
|
|
1835
|
-
* - **Sign-up** (no bearer; `username`, `email`, `emailTicket`) and
|
|
1836
|
-
* **recovery** (no bearer; `recoveryTicket`): the account is created, or
|
|
1837
|
-
* gains the passkey, and a session is minted — the response carries
|
|
1838
|
-
* `sessionId`, is the SAME {@link LoginResult} contract as
|
|
1839
|
-
* `POST /auth/verify`, and its access token is planted here.
|
|
1840
|
-
* - **Link** (bearer present): the passkey is attached to the signed-in
|
|
1841
|
-
* account and the server returns `{ success, message }` with no session,
|
|
1842
|
-
* which is returned verbatim (no token planting).
|
|
1843
|
-
*/
|
|
1844
|
-
async webauthnRegisterVerify(
|
|
1845
|
-
response: unknown,
|
|
1846
|
-
envelope: {
|
|
1847
|
-
username?: string;
|
|
1848
|
-
/** Sign-up: the recovery email, confirmed with `emailTicket`. */
|
|
1849
|
-
email?: string;
|
|
1850
|
-
emailTicket?: string;
|
|
1851
|
-
/** Recovery: the ticket {@link confirmEmailVerification} returned. */
|
|
1852
|
-
recoveryTicket?: string;
|
|
1853
|
-
deviceName?: string;
|
|
1854
|
-
deviceFingerprint?: string;
|
|
1855
|
-
} = {},
|
|
1856
|
-
): Promise<{ success: true; message: string } | LoginResult> {
|
|
1857
|
-
try {
|
|
1858
|
-
const res = await this.makeRequest<unknown>(
|
|
1859
|
-
'POST',
|
|
1860
|
-
'/auth/webauthn/register/verify',
|
|
1861
|
-
{ response, ...envelope },
|
|
1862
|
-
{
|
|
1863
|
-
cache: false,
|
|
1864
|
-
...(envelope.username !== undefined || envelope.recoveryTicket !== undefined ? { skipAuth: true } : {}),
|
|
1865
|
-
},
|
|
1866
|
-
);
|
|
1867
|
-
if (res && typeof res === 'object') {
|
|
1868
|
-
const record = res as Record<string, unknown>;
|
|
1869
|
-
// Signup branch: mints a session (LoginSessionResult, carries
|
|
1870
|
-
// `sessionId`). Parse against the login contract and plant the token.
|
|
1871
|
-
if ('sessionId' in record) {
|
|
1872
|
-
const parsed = safeParseContract(loginResultSchema, record);
|
|
1873
|
-
if (!parsed) {
|
|
1874
|
-
throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
|
|
1875
|
-
}
|
|
1876
|
-
if (parsed.accessToken) {
|
|
1877
|
-
this.setTokens(parsed.accessToken);
|
|
1878
|
-
}
|
|
1879
|
-
return parsed;
|
|
1880
|
-
}
|
|
1881
|
-
// Link branch: passkey attached to the signed-in account, no session.
|
|
1882
|
-
if (record.success === true && typeof record.message === 'string') {
|
|
1883
|
-
return { success: true, message: record.message };
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
|
|
1887
|
-
} catch (error) {
|
|
1888
|
-
throw this.handleError(error);
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
/**
|
|
1893
|
-
* Begin a WebAuthn / passkey AUTHENTICATION ceremony. Requests the
|
|
1894
|
-
* `PublicKeyCredentialRequestOptions` the browser's `navigator.credentials
|
|
1895
|
-
* .get()` (or `@simplewebauthn/browser`'s `startAuthentication`) needs.
|
|
1896
|
-
*
|
|
1897
|
-
* When `username` is present the server scopes `allowCredentials` to that
|
|
1898
|
-
* user's passkeys (username-first); when omitted it returns an empty
|
|
1899
|
-
* allow-list for the usernameless / discoverable-credential flow. The
|
|
1900
|
-
* returned options are OPAQUE and pass through as `unknown`.
|
|
1901
|
-
*/
|
|
1902
|
-
async webauthnLoginOptions(username?: string): Promise<unknown> {
|
|
1903
|
-
try {
|
|
1904
|
-
return await this.makeRequest<unknown>(
|
|
1905
|
-
'POST',
|
|
1906
|
-
'/auth/webauthn/login/options',
|
|
1907
|
-
{ ...(username !== undefined ? { username } : {}) },
|
|
1908
|
-
// Pre-session login ceremony — skip the bearer preflight.
|
|
1909
|
-
{ cache: false, skipAuth: true },
|
|
1910
|
-
);
|
|
1911
|
-
} catch (error) {
|
|
1912
|
-
throw this.handleError(error);
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
/**
|
|
1917
|
-
* Finish a WebAuthn / passkey AUTHENTICATION ceremony. Forwards the opaque
|
|
1918
|
-
* browser `AuthenticationResponseJSON` (`response`) alongside the
|
|
1919
|
-
* device-session envelope. Resolves to the SAME {@link LoginResult} contract
|
|
1920
|
-
* as `POST /auth/verify`; the access token is planted immediately, and the
|
|
1921
|
-
* response's `deviceId` + `deviceSecret` are the zero-cookie restore
|
|
1922
|
-
* credential.
|
|
1923
|
-
*/
|
|
1924
|
-
async webauthnLoginVerify(
|
|
1925
|
-
response: unknown,
|
|
1926
|
-
envelope: { deviceName?: string; deviceFingerprint?: string } = {},
|
|
1927
|
-
): Promise<LoginResult> {
|
|
1928
|
-
try {
|
|
1929
|
-
const res = await this.makeRequest<unknown>(
|
|
1930
|
-
'POST',
|
|
1931
|
-
'/auth/webauthn/login/verify',
|
|
1932
|
-
{ response, ...envelope },
|
|
1933
|
-
// Pre-session login ceremony — skip the bearer preflight.
|
|
1934
|
-
{ cache: false, skipAuth: true },
|
|
1935
|
-
);
|
|
1936
|
-
const parsed = safeParseContract(loginResultSchema, res);
|
|
1937
|
-
if (!parsed) {
|
|
1938
|
-
throw new Error('auth/webauthn/login/verify returned an unexpected response shape');
|
|
1939
|
-
}
|
|
1940
|
-
if (parsed.accessToken) {
|
|
1941
|
-
this.setTokens(parsed.accessToken);
|
|
1942
|
-
}
|
|
1943
|
-
return parsed;
|
|
1944
|
-
} catch (error) {
|
|
1945
|
-
throw this.handleError(error);
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
/**
|
|
1950
|
-
* Exchange an OAuth authorization code (returned to the RP redirect URI
|
|
1951
|
-
* after sign-in at auth.oxy.so) for a device-first session.
|
|
1952
|
-
* Public first-party clients use PKCE (`codeVerifier`); the access token is
|
|
1953
|
-
* planted immediately on success.
|
|
1954
|
-
*
|
|
1955
|
-
* Speaks the standard RFC 6749 §4.1.3 token request — a form-urlencoded
|
|
1956
|
-
* body with snake_case parameters and `grant_type=authorization_code` — and
|
|
1957
|
-
* reads the flat §5.1 response. The camelCase JSON request and `{ data }`
|
|
1958
|
-
* response this method used before were an Oxy invention no OAuth library
|
|
1959
|
-
* could interoperate with; the endpoint no longer accepts them. The method's
|
|
1960
|
-
* OWN signature is unchanged, so callers are unaffected.
|
|
1961
|
-
*
|
|
1962
|
-
* `deviceId` + `deviceSecret` are OPTIONAL and their absence is a valid
|
|
1963
|
-
* outcome, not an error. A third-party grant is meant to be isolated from the
|
|
1964
|
-
* browser's shared DeviceSession, so the token endpoint must be free to return
|
|
1965
|
-
* no device credential at all — the guard that used to require the pair made
|
|
1966
|
-
* that omission unshippable, since it turned every third-party sign-in through
|
|
1967
|
-
* the SDK into a silent `exchange-failed`.
|
|
1968
|
-
*
|
|
1969
|
-
* The cost is real and deliberate: a DEVICE-LESS session cannot use the
|
|
1970
|
-
* zero-cookie mint lane (`POST /session/device/token`), because that lane's
|
|
1971
|
-
* whole proof is possession of a `deviceSecret`. Its lifetime is therefore the
|
|
1972
|
-
* access token itself — nothing persists a restore credential, the cold boot's
|
|
1973
|
-
* `device-secret-mint` step reports `no-secret` and skips, and the refresh
|
|
1974
|
-
* scheduler has nothing to re-mint from. When the token expires the session
|
|
1975
|
-
* ends LOUDLY: the 401 lane clears the tokens and the provider resolves signed
|
|
1976
|
-
* out, so the app can run the OAuth flow again. It never degrades into a
|
|
1977
|
-
* session that looks alive and cannot refresh.
|
|
1978
|
-
*/
|
|
1979
|
-
async exchangeOAuthCode(params: {
|
|
1980
|
-
code: string;
|
|
1981
|
-
clientId: string;
|
|
1982
|
-
redirectUri: string;
|
|
1983
|
-
codeVerifier: string;
|
|
1984
|
-
}): Promise<OAuthTokenExchangeResult> {
|
|
1985
|
-
try {
|
|
1986
|
-
const form = new URLSearchParams({
|
|
1987
|
-
grant_type: 'authorization_code',
|
|
1988
|
-
code: params.code,
|
|
1989
|
-
redirect_uri: params.redirectUri,
|
|
1990
|
-
client_id: params.clientId,
|
|
1991
|
-
code_verifier: params.codeVerifier,
|
|
1992
|
-
});
|
|
1993
|
-
const res = await this.makeRequest<unknown>(
|
|
1994
|
-
'POST',
|
|
1995
|
-
'/auth/oauth/token',
|
|
1996
|
-
form,
|
|
1997
|
-
{ cache: false, skipAuth: true },
|
|
1998
|
-
);
|
|
1999
|
-
if (!res || typeof res !== 'object') {
|
|
2000
|
-
throw new Error('auth/oauth/token returned an unexpected response shape');
|
|
2001
|
-
}
|
|
2002
|
-
// RFC 6749 §5.1: every member sits at the TOP LEVEL of the document.
|
|
2003
|
-
const record = res as Record<string, unknown>;
|
|
2004
|
-
const accessToken = typeof record.access_token === 'string' ? record.access_token : undefined;
|
|
2005
|
-
const sessionId = typeof record.session_id === 'string' ? record.session_id : undefined;
|
|
2006
|
-
const deviceId = typeof record.deviceId === 'string' ? record.deviceId : undefined;
|
|
2007
|
-
const deviceSecret = typeof record.deviceSecret === 'string' ? record.deviceSecret : undefined;
|
|
2008
|
-
const userRaw = record.user;
|
|
2009
|
-
// The device pair is NOT part of this guard — see the note above. What is
|
|
2010
|
-
// still mandatory is what identifies the session at all.
|
|
2011
|
-
if (!sessionId || !userRaw || typeof userRaw !== 'object') {
|
|
2012
|
-
throw new Error('auth/oauth/token returned an incomplete session payload');
|
|
2013
|
-
}
|
|
2014
|
-
const userObj = userRaw as Record<string, unknown>;
|
|
2015
|
-
const userId = typeof userObj.id === 'string' ? userObj.id : undefined;
|
|
2016
|
-
if (!userId) {
|
|
2017
|
-
throw new Error('auth/oauth/token returned a session without user.id');
|
|
2018
|
-
}
|
|
2019
|
-
const expiresInSec =
|
|
2020
|
-
typeof record.expires_in === 'number' ? record.expires_in : DEFAULT_ACCESS_TOKEN_TTL_SECONDS;
|
|
2021
|
-
const expiresAt = new Date(Date.now() + expiresInSec * 1000).toISOString();
|
|
2022
|
-
if (accessToken) {
|
|
2023
|
-
this.setTokens(accessToken);
|
|
2024
|
-
}
|
|
2025
|
-
if (!deviceId || !deviceSecret) {
|
|
2026
|
-
logger.debug(
|
|
2027
|
-
'auth/oauth/token returned no device credential — this session lives only as long as its access token',
|
|
2028
|
-
{ component: 'oxy.auth', method: 'exchangeOAuthCode' },
|
|
2029
|
-
);
|
|
2030
|
-
}
|
|
2031
|
-
return {
|
|
2032
|
-
sessionId,
|
|
2033
|
-
expiresAt,
|
|
2034
|
-
accessToken,
|
|
2035
|
-
// Omitted rather than set to `undefined` when the server sent no device
|
|
2036
|
-
// credential, so a device-less grant serializes as the absence it is.
|
|
2037
|
-
...(deviceId ? { deviceId } : {}),
|
|
2038
|
-
...(deviceSecret ? { deviceSecret } : {}),
|
|
2039
|
-
user: {
|
|
2040
|
-
id: userId,
|
|
2041
|
-
username: typeof userObj.username === 'string' ? userObj.username : undefined,
|
|
2042
|
-
avatar: typeof userObj.avatar === 'string' ? userObj.avatar : undefined,
|
|
2043
|
-
},
|
|
2044
|
-
};
|
|
2045
|
-
} catch (error) {
|
|
2046
|
-
throw this.handleError(error);
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
/**
|
|
2051
|
-
* Fetch OpenID Connect userinfo for the current bearer (`GET /auth/oauth/userinfo`).
|
|
2052
|
-
* The response is a flat JSON document — no `{ data }` wrapper.
|
|
2053
|
-
*/
|
|
2054
|
-
async getOAuthUserInfo(): Promise<OAuthUserInfoResponse> {
|
|
2055
|
-
try {
|
|
2056
|
-
const res = await this.makeRequest<unknown>(
|
|
2057
|
-
'GET',
|
|
2058
|
-
'/auth/oauth/userinfo',
|
|
2059
|
-
undefined,
|
|
2060
|
-
{ cache: false },
|
|
2061
|
-
);
|
|
2062
|
-
if (!res || typeof res !== 'object') {
|
|
2063
|
-
throw new Error('auth/oauth/userinfo returned an unexpected response shape');
|
|
2064
|
-
}
|
|
2065
|
-
const record = res as Record<string, unknown>;
|
|
2066
|
-
const sub = typeof record.sub === 'string' ? record.sub : undefined;
|
|
2067
|
-
if (!sub) {
|
|
2068
|
-
throw new Error('auth/oauth/userinfo returned a response without sub');
|
|
2069
|
-
}
|
|
2070
|
-
return {
|
|
2071
|
-
sub,
|
|
2072
|
-
...(typeof record.preferred_username === 'string'
|
|
2073
|
-
? { preferred_username: record.preferred_username }
|
|
2074
|
-
: {}),
|
|
2075
|
-
...(typeof record.name === 'string' ? { name: record.name } : {}),
|
|
2076
|
-
...(typeof record.picture === 'string' ? { picture: record.picture } : {}),
|
|
2077
|
-
};
|
|
2078
|
-
} catch (error) {
|
|
2079
|
-
throw this.handleError(error);
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
};
|
|
2083
|
-
}
|