@oxy.so/core 1.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.
Files changed (673) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +16 -0
  3. package/README.md +221 -0
  4. package/dist/cjs/.tsbuildinfo +1 -0
  5. package/dist/cjs/HttpService.js +1270 -0
  6. package/dist/cjs/OxyServices.base.js +390 -0
  7. package/dist/cjs/OxyServices.errors.js +95 -0
  8. package/dist/cjs/OxyServices.js +72 -0
  9. package/dist/cjs/boot/sessionColdBoot.js +395 -0
  10. package/dist/cjs/constants/version.js +16 -0
  11. package/dist/cjs/crypto/aead.js +79 -0
  12. package/dist/cjs/crypto/ecdh.js +35 -0
  13. package/dist/cjs/crypto/identityMarker.js +255 -0
  14. package/dist/cjs/crypto/kdf.js +39 -0
  15. package/dist/cjs/crypto/keyManager.js +2234 -0
  16. package/dist/cjs/crypto/polyfill.js +131 -0
  17. package/dist/cjs/crypto/recoveryPhrase.js +242 -0
  18. package/dist/cjs/crypto/signatureService.js +273 -0
  19. package/dist/cjs/i18n/accountCategoryLabels.js +44 -0
  20. package/dist/cjs/i18n/accountRoleLabels.js +27 -0
  21. package/dist/cjs/i18n/index.js +90 -0
  22. package/dist/cjs/i18n/locales/ar-SA.json +282 -0
  23. package/dist/cjs/i18n/locales/ca-ES.json +282 -0
  24. package/dist/cjs/i18n/locales/de-DE.json +282 -0
  25. package/dist/cjs/i18n/locales/en-US.json +2161 -0
  26. package/dist/cjs/i18n/locales/es-ES.json +2161 -0
  27. package/dist/cjs/i18n/locales/fr-FR.json +282 -0
  28. package/dist/cjs/i18n/locales/it-IT.json +282 -0
  29. package/dist/cjs/i18n/locales/ja-JP.json +282 -0
  30. package/dist/cjs/i18n/locales/ko-KR.json +282 -0
  31. package/dist/cjs/i18n/locales/locales/ar-SA.json +282 -0
  32. package/dist/cjs/i18n/locales/locales/ca-ES.json +282 -0
  33. package/dist/cjs/i18n/locales/locales/de-DE.json +282 -0
  34. package/dist/cjs/i18n/locales/locales/en-US.json +2161 -0
  35. package/dist/cjs/i18n/locales/locales/es-ES.json +2161 -0
  36. package/dist/cjs/i18n/locales/locales/fr-FR.json +282 -0
  37. package/dist/cjs/i18n/locales/locales/it-IT.json +282 -0
  38. package/dist/cjs/i18n/locales/locales/ja-JP.json +282 -0
  39. package/dist/cjs/i18n/locales/locales/ko-KR.json +282 -0
  40. package/dist/cjs/i18n/locales/locales/pt-PT.json +282 -0
  41. package/dist/cjs/i18n/locales/locales/zh-CN.json +282 -0
  42. package/dist/cjs/i18n/locales/pt-PT.json +282 -0
  43. package/dist/cjs/i18n/locales/zh-CN.json +282 -0
  44. package/dist/cjs/i18n/reputationCategoryLabels.js +20 -0
  45. package/dist/cjs/i18n/trustTierLabels.js +19 -0
  46. package/dist/cjs/index.js +498 -0
  47. package/dist/cjs/inference/OxyInferenceClient.js +544 -0
  48. package/dist/cjs/logger/index.js +149 -0
  49. package/dist/cjs/mixins/OxyServices.accounts.js +555 -0
  50. package/dist/cjs/mixins/OxyServices.analytics.js +49 -0
  51. package/dist/cjs/mixins/OxyServices.appData.js +108 -0
  52. package/dist/cjs/mixins/OxyServices.assets.js +764 -0
  53. package/dist/cjs/mixins/OxyServices.auth.js +1254 -0
  54. package/dist/cjs/mixins/OxyServices.chains.js +73 -0
  55. package/dist/cjs/mixins/OxyServices.civic.js +611 -0
  56. package/dist/cjs/mixins/OxyServices.connectedApps.js +78 -0
  57. package/dist/cjs/mixins/OxyServices.contacts.js +50 -0
  58. package/dist/cjs/mixins/OxyServices.deviceBoot.js +191 -0
  59. package/dist/cjs/mixins/OxyServices.deviceTransfer.js +314 -0
  60. package/dist/cjs/mixins/OxyServices.devices.js +116 -0
  61. package/dist/cjs/mixins/OxyServices.features.js +335 -0
  62. package/dist/cjs/mixins/OxyServices.followGraph.js +221 -0
  63. package/dist/cjs/mixins/OxyServices.identity.js +461 -0
  64. package/dist/cjs/mixins/OxyServices.identityBackup.js +172 -0
  65. package/dist/cjs/mixins/OxyServices.inference.js +59 -0
  66. package/dist/cjs/mixins/OxyServices.language.js +125 -0
  67. package/dist/cjs/mixins/OxyServices.links.js +72 -0
  68. package/dist/cjs/mixins/OxyServices.location.js +43 -0
  69. package/dist/cjs/mixins/OxyServices.nodes.js +175 -0
  70. package/dist/cjs/mixins/OxyServices.notifications.js +63 -0
  71. package/dist/cjs/mixins/OxyServices.payment.js +158 -0
  72. package/dist/cjs/mixins/OxyServices.privacy.js +199 -0
  73. package/dist/cjs/mixins/OxyServices.reputation.js +289 -0
  74. package/dist/cjs/mixins/OxyServices.security.js +89 -0
  75. package/dist/cjs/mixins/OxyServices.store.js +266 -0
  76. package/dist/cjs/mixins/OxyServices.topics.js +137 -0
  77. package/dist/cjs/mixins/OxyServices.user.js +902 -0
  78. package/dist/cjs/mixins/OxyServices.utility.js +1098 -0
  79. package/dist/cjs/mixins/index.js +135 -0
  80. package/dist/cjs/mixins/mixinHelpers.js +53 -0
  81. package/dist/cjs/models/Topic.js +16 -0
  82. package/dist/cjs/models/interfaces.js +20 -0
  83. package/dist/cjs/models/session.js +2 -0
  84. package/dist/cjs/server/auth.js +167 -0
  85. package/dist/cjs/server/capabilityTicket.js +192 -0
  86. package/dist/cjs/server/cors.js +225 -0
  87. package/dist/cjs/server/index.js +97 -0
  88. package/dist/cjs/server/rateLimit.js +207 -0
  89. package/dist/cjs/server/safeFetch.js +458 -0
  90. package/dist/cjs/server/securityHeaders.js +377 -0
  91. package/dist/cjs/server/userInvalidation.js +156 -0
  92. package/dist/cjs/server/verifySecret.js +50 -0
  93. package/dist/cjs/session/SessionClient.js +865 -0
  94. package/dist/cjs/session/accountDelegationAccess.js +48 -0
  95. package/dist/cjs/session/accountDialogController.js +1131 -0
  96. package/dist/cjs/session/accountSwitchTargets.js +81 -0
  97. package/dist/cjs/session/authStateStore.js +404 -0
  98. package/dist/cjs/session/createSessionClient.js +32 -0
  99. package/dist/cjs/session/deviceDirectory.js +143 -0
  100. package/dist/cjs/session/deviceSwitcherRows.js +76 -0
  101. package/dist/cjs/session/identityPin.js +256 -0
  102. package/dist/cjs/session/identitySession.js +135 -0
  103. package/dist/cjs/session/projectSessionState.js +121 -0
  104. package/dist/cjs/session/refresh.js +398 -0
  105. package/dist/cjs/session/sessionClientHost.js +34 -0
  106. package/dist/cjs/session/sharedDeviceCredential.js +247 -0
  107. package/dist/cjs/session/socketLoader.js +55 -0
  108. package/dist/cjs/shared/utils/colorUtils.js +153 -0
  109. package/dist/cjs/shared/utils/errorUtils.js +183 -0
  110. package/dist/cjs/shared/utils/index.js +49 -0
  111. package/dist/cjs/shared/utils/networkUtils.js +183 -0
  112. package/dist/cjs/shared/utils/themeUtils.js +106 -0
  113. package/dist/cjs/utils/accountCacheSweep.js +80 -0
  114. package/dist/cjs/utils/accountUtils.js +140 -0
  115. package/dist/cjs/utils/activityId.js +6 -0
  116. package/dist/cjs/utils/apiUtils.js +91 -0
  117. package/dist/cjs/utils/asyncUtils.js +222 -0
  118. package/dist/cjs/utils/authHelpers.js +120 -0
  119. package/dist/cjs/utils/authWebUrl.js +19 -0
  120. package/dist/cjs/utils/avatarUtils.js +29 -0
  121. package/dist/cjs/utils/cache.js +235 -0
  122. package/dist/cjs/utils/cacheKey.js +85 -0
  123. package/dist/cjs/utils/coldBoot.js +122 -0
  124. package/dist/cjs/utils/commonsApproval.js +31 -0
  125. package/dist/cjs/utils/commonsDelivery.js +59 -0
  126. package/dist/cjs/utils/deviceManager.js +174 -0
  127. package/dist/cjs/utils/displayNamePolicyRanges.generated.js +60 -0
  128. package/dist/cjs/utils/edgeRegion.js +6 -0
  129. package/dist/cjs/utils/errorUtils.js +243 -0
  130. package/dist/cjs/utils/identityCacheSweep.js +97 -0
  131. package/dist/cjs/utils/languageUtils.js +224 -0
  132. package/dist/cjs/utils/oauthPkce.js +311 -0
  133. package/dist/cjs/utils/officialOrigins.js +64 -0
  134. package/dist/cjs/utils/oxyServiceEnvironment.js +19 -0
  135. package/dist/cjs/utils/platform.js +119 -0
  136. package/dist/cjs/utils/profileLinks.js +63 -0
  137. package/dist/cjs/utils/redactUrl.js +29 -0
  138. package/dist/cjs/utils/registrableApex.js +45 -0
  139. package/dist/cjs/utils/requestUtils.js +209 -0
  140. package/dist/cjs/utils/sessionUtils.js +181 -0
  141. package/dist/cjs/utils/textNormalization.js +158 -0
  142. package/dist/cjs/utils/userHandle.js +37 -0
  143. package/dist/cjs/utils/userIdentity.js +50 -0
  144. package/dist/cjs/utils/validationUtils.js +370 -0
  145. package/dist/cjs/utils/webauthnOrigin.js +51 -0
  146. package/dist/esm/.tsbuildinfo +1 -0
  147. package/dist/esm/HttpService.js +1266 -0
  148. package/dist/esm/OxyServices.base.js +386 -0
  149. package/dist/esm/OxyServices.errors.js +88 -0
  150. package/dist/esm/OxyServices.js +66 -0
  151. package/dist/esm/boot/sessionColdBoot.js +392 -0
  152. package/dist/esm/constants/version.js +13 -0
  153. package/dist/esm/crypto/aead.js +74 -0
  154. package/dist/esm/crypto/ecdh.js +32 -0
  155. package/dist/esm/crypto/identityMarker.js +248 -0
  156. package/dist/esm/crypto/kdf.js +36 -0
  157. package/dist/esm/crypto/keyManager.js +2227 -0
  158. package/dist/esm/crypto/polyfill.js +129 -0
  159. package/dist/esm/crypto/recoveryPhrase.js +238 -0
  160. package/dist/esm/crypto/signatureService.js +269 -0
  161. package/dist/esm/i18n/accountCategoryLabels.js +37 -0
  162. package/dist/esm/i18n/accountRoleLabels.js +20 -0
  163. package/dist/esm/i18n/index.js +83 -0
  164. package/dist/esm/i18n/locales/ar-SA.json +282 -0
  165. package/dist/esm/i18n/locales/ca-ES.json +282 -0
  166. package/dist/esm/i18n/locales/de-DE.json +282 -0
  167. package/dist/esm/i18n/locales/en-US.json +2161 -0
  168. package/dist/esm/i18n/locales/es-ES.json +2161 -0
  169. package/dist/esm/i18n/locales/fr-FR.json +282 -0
  170. package/dist/esm/i18n/locales/it-IT.json +282 -0
  171. package/dist/esm/i18n/locales/ja-JP.json +282 -0
  172. package/dist/esm/i18n/locales/ko-KR.json +282 -0
  173. package/dist/esm/i18n/locales/locales/ar-SA.json +282 -0
  174. package/dist/esm/i18n/locales/locales/ca-ES.json +282 -0
  175. package/dist/esm/i18n/locales/locales/de-DE.json +282 -0
  176. package/dist/esm/i18n/locales/locales/en-US.json +2161 -0
  177. package/dist/esm/i18n/locales/locales/es-ES.json +2161 -0
  178. package/dist/esm/i18n/locales/locales/fr-FR.json +282 -0
  179. package/dist/esm/i18n/locales/locales/it-IT.json +282 -0
  180. package/dist/esm/i18n/locales/locales/ja-JP.json +282 -0
  181. package/dist/esm/i18n/locales/locales/ko-KR.json +282 -0
  182. package/dist/esm/i18n/locales/locales/pt-PT.json +282 -0
  183. package/dist/esm/i18n/locales/locales/zh-CN.json +282 -0
  184. package/dist/esm/i18n/locales/pt-PT.json +282 -0
  185. package/dist/esm/i18n/locales/zh-CN.json +282 -0
  186. package/dist/esm/i18n/reputationCategoryLabels.js +13 -0
  187. package/dist/esm/i18n/trustTierLabels.js +12 -0
  188. package/dist/esm/index.js +254 -0
  189. package/dist/esm/inference/OxyInferenceClient.js +538 -0
  190. package/dist/esm/logger/index.js +140 -0
  191. package/dist/esm/mixins/OxyServices.accounts.js +543 -0
  192. package/dist/esm/mixins/OxyServices.analytics.js +46 -0
  193. package/dist/esm/mixins/OxyServices.appData.js +103 -0
  194. package/dist/esm/mixins/OxyServices.assets.js +761 -0
  195. package/dist/esm/mixins/OxyServices.auth.js +1247 -0
  196. package/dist/esm/mixins/OxyServices.chains.js +70 -0
  197. package/dist/esm/mixins/OxyServices.civic.js +605 -0
  198. package/dist/esm/mixins/OxyServices.connectedApps.js +75 -0
  199. package/dist/esm/mixins/OxyServices.contacts.js +47 -0
  200. package/dist/esm/mixins/OxyServices.deviceBoot.js +186 -0
  201. package/dist/esm/mixins/OxyServices.deviceTransfer.js +311 -0
  202. package/dist/esm/mixins/OxyServices.devices.js +113 -0
  203. package/dist/esm/mixins/OxyServices.features.js +332 -0
  204. package/dist/esm/mixins/OxyServices.followGraph.js +218 -0
  205. package/dist/esm/mixins/OxyServices.identity.js +457 -0
  206. package/dist/esm/mixins/OxyServices.identityBackup.js +169 -0
  207. package/dist/esm/mixins/OxyServices.inference.js +56 -0
  208. package/dist/esm/mixins/OxyServices.language.js +122 -0
  209. package/dist/esm/mixins/OxyServices.links.js +69 -0
  210. package/dist/esm/mixins/OxyServices.location.js +40 -0
  211. package/dist/esm/mixins/OxyServices.nodes.js +172 -0
  212. package/dist/esm/mixins/OxyServices.notifications.js +60 -0
  213. package/dist/esm/mixins/OxyServices.payment.js +155 -0
  214. package/dist/esm/mixins/OxyServices.privacy.js +196 -0
  215. package/dist/esm/mixins/OxyServices.reputation.js +286 -0
  216. package/dist/esm/mixins/OxyServices.security.js +86 -0
  217. package/dist/esm/mixins/OxyServices.store.js +263 -0
  218. package/dist/esm/mixins/OxyServices.topics.js +134 -0
  219. package/dist/esm/mixins/OxyServices.user.js +899 -0
  220. package/dist/esm/mixins/OxyServices.utility.js +1095 -0
  221. package/dist/esm/mixins/index.js +132 -0
  222. package/dist/esm/mixins/mixinHelpers.js +48 -0
  223. package/dist/esm/models/Topic.js +13 -0
  224. package/dist/esm/models/interfaces.js +17 -0
  225. package/dist/esm/models/session.js +1 -0
  226. package/dist/esm/server/auth.js +154 -0
  227. package/dist/esm/server/capabilityTicket.js +183 -0
  228. package/dist/esm/server/cors.js +220 -0
  229. package/dist/esm/server/index.js +47 -0
  230. package/dist/esm/server/rateLimit.js +201 -0
  231. package/dist/esm/server/safeFetch.js +447 -0
  232. package/dist/esm/server/securityHeaders.js +364 -0
  233. package/dist/esm/server/userInvalidation.js +152 -0
  234. package/dist/esm/server/verifySecret.js +47 -0
  235. package/dist/esm/session/SessionClient.js +861 -0
  236. package/dist/esm/session/accountDelegationAccess.js +45 -0
  237. package/dist/esm/session/accountDialogController.js +1126 -0
  238. package/dist/esm/session/accountSwitchTargets.js +77 -0
  239. package/dist/esm/session/authStateStore.js +398 -0
  240. package/dist/esm/session/createSessionClient.js +29 -0
  241. package/dist/esm/session/deviceDirectory.js +135 -0
  242. package/dist/esm/session/deviceSwitcherRows.js +72 -0
  243. package/dist/esm/session/identityPin.js +249 -0
  244. package/dist/esm/session/identitySession.js +131 -0
  245. package/dist/esm/session/projectSessionState.js +114 -0
  246. package/dist/esm/session/refresh.js +390 -0
  247. package/dist/esm/session/sessionClientHost.js +31 -0
  248. package/dist/esm/session/sharedDeviceCredential.js +239 -0
  249. package/dist/esm/session/socketLoader.js +19 -0
  250. package/dist/esm/shared/utils/colorUtils.js +143 -0
  251. package/dist/esm/shared/utils/errorUtils.js +170 -0
  252. package/dist/esm/shared/utils/index.js +15 -0
  253. package/dist/esm/shared/utils/networkUtils.js +173 -0
  254. package/dist/esm/shared/utils/themeUtils.js +98 -0
  255. package/dist/esm/utils/accountCacheSweep.js +75 -0
  256. package/dist/esm/utils/accountUtils.js +131 -0
  257. package/dist/esm/utils/activityId.js +2 -0
  258. package/dist/esm/utils/apiUtils.js +84 -0
  259. package/dist/esm/utils/asyncUtils.js +207 -0
  260. package/dist/esm/utils/authHelpers.js +111 -0
  261. package/dist/esm/utils/authWebUrl.js +16 -0
  262. package/dist/esm/utils/avatarUtils.js +26 -0
  263. package/dist/esm/utils/cache.js +227 -0
  264. package/dist/esm/utils/cacheKey.js +80 -0
  265. package/dist/esm/utils/coldBoot.js +119 -0
  266. package/dist/esm/utils/commonsApproval.js +27 -0
  267. package/dist/esm/utils/commonsDelivery.js +54 -0
  268. package/dist/esm/utils/deviceManager.js +170 -0
  269. package/dist/esm/utils/displayNamePolicyRanges.generated.js +57 -0
  270. package/dist/esm/utils/edgeRegion.js +2 -0
  271. package/dist/esm/utils/errorUtils.js +232 -0
  272. package/dist/esm/utils/identityCacheSweep.js +92 -0
  273. package/dist/esm/utils/languageUtils.js +212 -0
  274. package/dist/esm/utils/oauthPkce.js +297 -0
  275. package/dist/esm/utils/officialOrigins.js +59 -0
  276. package/dist/esm/utils/oxyServiceEnvironment.js +16 -0
  277. package/dist/esm/utils/platform.js +110 -0
  278. package/dist/esm/utils/profileLinks.js +60 -0
  279. package/dist/esm/utils/redactUrl.js +26 -0
  280. package/dist/esm/utils/registrableApex.js +42 -0
  281. package/dist/esm/utils/requestUtils.js +203 -0
  282. package/dist/esm/utils/sessionUtils.js +171 -0
  283. package/dist/esm/utils/textNormalization.js +154 -0
  284. package/dist/esm/utils/userHandle.js +33 -0
  285. package/dist/esm/utils/userIdentity.js +45 -0
  286. package/dist/esm/utils/validationUtils.js +350 -0
  287. package/dist/esm/utils/webauthnOrigin.js +48 -0
  288. package/dist/types/.tsbuildinfo +1 -0
  289. package/dist/types/HttpService.d.ts +413 -0
  290. package/dist/types/OxyServices.base.d.ts +187 -0
  291. package/dist/types/OxyServices.d.ts +139 -0
  292. package/dist/types/OxyServices.errors.d.ts +84 -0
  293. package/dist/types/boot/sessionColdBoot.d.ts +140 -0
  294. package/dist/types/constants/version.d.ts +13 -0
  295. package/dist/types/crypto/aead.d.ts +56 -0
  296. package/dist/types/crypto/ecdh.d.ts +29 -0
  297. package/dist/types/crypto/identityMarker.d.ts +94 -0
  298. package/dist/types/crypto/kdf.d.ts +25 -0
  299. package/dist/types/crypto/keyManager.d.ts +582 -0
  300. package/dist/types/crypto/polyfill.d.ts +25 -0
  301. package/dist/types/crypto/recoveryPhrase.d.ts +173 -0
  302. package/dist/types/crypto/signatureService.d.ts +136 -0
  303. package/dist/types/i18n/accountCategoryLabels.d.ts +34 -0
  304. package/dist/types/i18n/accountRoleLabels.d.ts +10 -0
  305. package/dist/types/i18n/index.d.ts +3 -0
  306. package/dist/types/i18n/reputationCategoryLabels.d.ts +10 -0
  307. package/dist/types/i18n/trustTierLabels.d.ts +9 -0
  308. package/dist/types/index.d.ts +145 -0
  309. package/dist/types/inference/OxyInferenceClient.d.ts +342 -0
  310. package/dist/types/logger/index.d.ts +104 -0
  311. package/dist/types/mixins/OxyServices.accounts.d.ts +868 -0
  312. package/dist/types/mixins/OxyServices.analytics.d.ts +72 -0
  313. package/dist/types/mixins/OxyServices.appData.d.ts +108 -0
  314. package/dist/types/mixins/OxyServices.assets.d.ts +329 -0
  315. package/dist/types/mixins/OxyServices.auth.d.ts +908 -0
  316. package/dist/types/mixins/OxyServices.chains.d.ts +156 -0
  317. package/dist/types/mixins/OxyServices.civic.d.ts +510 -0
  318. package/dist/types/mixins/OxyServices.connectedApps.d.ts +169 -0
  319. package/dist/types/mixins/OxyServices.contacts.d.ts +100 -0
  320. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +185 -0
  321. package/dist/types/mixins/OxyServices.deviceTransfer.d.ts +149 -0
  322. package/dist/types/mixins/OxyServices.devices.d.ts +103 -0
  323. package/dist/types/mixins/OxyServices.features.d.ts +250 -0
  324. package/dist/types/mixins/OxyServices.followGraph.d.ts +223 -0
  325. package/dist/types/mixins/OxyServices.identity.d.ts +351 -0
  326. package/dist/types/mixins/OxyServices.identityBackup.d.ts +129 -0
  327. package/dist/types/mixins/OxyServices.inference.d.ts +95 -0
  328. package/dist/types/mixins/OxyServices.language.d.ts +87 -0
  329. package/dist/types/mixins/OxyServices.links.d.ts +104 -0
  330. package/dist/types/mixins/OxyServices.location.d.ts +70 -0
  331. package/dist/types/mixins/OxyServices.nodes.d.ts +198 -0
  332. package/dist/types/mixins/OxyServices.notifications.d.ts +143 -0
  333. package/dist/types/mixins/OxyServices.payment.d.ts +117 -0
  334. package/dist/types/mixins/OxyServices.privacy.d.ts +146 -0
  335. package/dist/types/mixins/OxyServices.reputation.d.ts +202 -0
  336. package/dist/types/mixins/OxyServices.security.d.ts +84 -0
  337. package/dist/types/mixins/OxyServices.store.d.ts +334 -0
  338. package/dist/types/mixins/OxyServices.topics.d.ts +110 -0
  339. package/dist/types/mixins/OxyServices.user.d.ts +558 -0
  340. package/dist/types/mixins/OxyServices.utility.d.ts +393 -0
  341. package/dist/types/mixins/index.d.ts +87 -0
  342. package/dist/types/mixins/mixinHelpers.d.ts +31 -0
  343. package/dist/types/models/Topic.d.ts +42 -0
  344. package/dist/types/models/interfaces.d.ts +675 -0
  345. package/dist/types/models/session.d.ts +51 -0
  346. package/dist/types/server/auth.d.ts +130 -0
  347. package/dist/types/server/capabilityTicket.d.ts +41 -0
  348. package/dist/types/server/cors.d.ts +98 -0
  349. package/dist/types/server/index.d.ts +35 -0
  350. package/dist/types/server/rateLimit.d.ts +40 -0
  351. package/dist/types/server/safeFetch.d.ts +137 -0
  352. package/dist/types/server/securityHeaders.d.ts +221 -0
  353. package/dist/types/server/userInvalidation.d.ts +114 -0
  354. package/dist/types/server/verifySecret.d.ts +29 -0
  355. package/dist/types/session/SessionClient.d.ts +418 -0
  356. package/dist/types/session/accountDelegationAccess.d.ts +40 -0
  357. package/dist/types/session/accountDialogController.d.ts +648 -0
  358. package/dist/types/session/accountSwitchTargets.d.ts +70 -0
  359. package/dist/types/session/authStateStore.d.ts +153 -0
  360. package/dist/types/session/createSessionClient.d.ts +30 -0
  361. package/dist/types/session/deviceDirectory.d.ts +182 -0
  362. package/dist/types/session/deviceSwitcherRows.d.ts +92 -0
  363. package/dist/types/session/identityPin.d.ts +87 -0
  364. package/dist/types/session/identitySession.d.ts +105 -0
  365. package/dist/types/session/projectSessionState.d.ts +74 -0
  366. package/dist/types/session/refresh.d.ts +179 -0
  367. package/dist/types/session/sessionClientHost.d.ts +18 -0
  368. package/dist/types/session/sharedDeviceCredential.d.ts +202 -0
  369. package/dist/types/session/socketLoader.d.ts +11 -0
  370. package/dist/types/shared/utils/colorUtils.d.ts +104 -0
  371. package/dist/types/shared/utils/errorUtils.d.ts +97 -0
  372. package/dist/types/shared/utils/index.d.ts +13 -0
  373. package/dist/types/shared/utils/networkUtils.d.ts +139 -0
  374. package/dist/types/shared/utils/themeUtils.d.ts +90 -0
  375. package/dist/types/utils/accountCacheSweep.d.ts +75 -0
  376. package/dist/types/utils/accountUtils.d.ts +100 -0
  377. package/dist/types/utils/activityId.d.ts +2 -0
  378. package/dist/types/utils/apiUtils.d.ts +95 -0
  379. package/dist/types/utils/asyncUtils.d.ts +62 -0
  380. package/dist/types/utils/authHelpers.d.ts +58 -0
  381. package/dist/types/utils/authWebUrl.d.ts +16 -0
  382. package/dist/types/utils/avatarUtils.d.ts +16 -0
  383. package/dist/types/utils/cache.d.ts +130 -0
  384. package/dist/types/utils/cacheKey.d.ts +66 -0
  385. package/dist/types/utils/coldBoot.d.ts +131 -0
  386. package/dist/types/utils/commonsApproval.d.ts +13 -0
  387. package/dist/types/utils/commonsDelivery.d.ts +105 -0
  388. package/dist/types/utils/deviceManager.d.ts +65 -0
  389. package/dist/types/utils/displayNamePolicyRanges.generated.d.ts +57 -0
  390. package/dist/types/utils/edgeRegion.d.ts +2 -0
  391. package/dist/types/utils/errorUtils.d.ts +125 -0
  392. package/dist/types/utils/identityCacheSweep.d.ts +80 -0
  393. package/dist/types/utils/languageUtils.d.ts +100 -0
  394. package/dist/types/utils/oauthPkce.d.ts +157 -0
  395. package/dist/types/utils/officialOrigins.d.ts +13 -0
  396. package/dist/types/utils/oxyServiceEnvironment.d.ts +17 -0
  397. package/dist/types/utils/platform.d.ts +49 -0
  398. package/dist/types/utils/profileLinks.d.ts +44 -0
  399. package/dist/types/utils/redactUrl.d.ts +17 -0
  400. package/dist/types/utils/registrableApex.d.ts +27 -0
  401. package/dist/types/utils/requestUtils.d.ts +123 -0
  402. package/dist/types/utils/sessionUtils.d.ts +54 -0
  403. package/dist/types/utils/textNormalization.d.ts +103 -0
  404. package/dist/types/utils/userHandle.d.ts +26 -0
  405. package/dist/types/utils/userIdentity.d.ts +21 -0
  406. package/dist/types/utils/validationUtils.d.ts +268 -0
  407. package/dist/types/utils/webauthnOrigin.d.ts +32 -0
  408. package/package.json +168 -0
  409. package/src/HttpService.ts +1553 -0
  410. package/src/OxyServices.base.ts +478 -0
  411. package/src/OxyServices.errors.ts +122 -0
  412. package/src/OxyServices.ts +172 -0
  413. package/src/__tests__/authSocket.test.ts +96 -0
  414. package/src/__tests__/httpServiceActivityId.test.ts +49 -0
  415. package/src/__tests__/httpServiceAuthSelfAwait.test.ts +108 -0
  416. package/src/__tests__/httpServiceCache.test.ts +318 -0
  417. package/src/__tests__/httpServiceCsrf.test.ts +235 -0
  418. package/src/__tests__/httpServiceFormEncoded.test.ts +142 -0
  419. package/src/__tests__/httpServiceQueueDeadlock.test.ts +128 -0
  420. package/src/__tests__/httpServiceUnwrapEnvelope.test.ts +115 -0
  421. package/src/__tests__/inSessionRefresh.test.ts +260 -0
  422. package/src/__tests__/linkedClient.test.ts +370 -0
  423. package/src/__tests__/parseHttpErrorBody.test.ts +116 -0
  424. package/src/__tests__/serverValueImportsDeclared.test.ts +120 -0
  425. package/src/__tests__/userIdentity.test.ts +143 -0
  426. package/src/boot/__tests__/sessionColdBoot.identity.test.ts +393 -0
  427. package/src/boot/__tests__/sessionColdBoot.sharedDevice.test.ts +325 -0
  428. package/src/boot/__tests__/sessionColdBoot.test.ts +570 -0
  429. package/src/boot/sessionColdBoot.ts +529 -0
  430. package/src/constants/version.ts +15 -0
  431. package/src/crypto/__tests__/backupMaterial.test.ts +86 -0
  432. package/src/crypto/__tests__/cryptoPrimitives.test.ts +226 -0
  433. package/src/crypto/__tests__/identityMocks.ts +125 -0
  434. package/src/crypto/__tests__/keyManager.atomicity.test.ts +272 -0
  435. package/src/crypto/__tests__/keyManager.cacheSafety.test.ts +175 -0
  436. package/src/crypto/__tests__/keyManager.identityStatus.test.ts +217 -0
  437. package/src/crypto/__tests__/keyManager.recoveryLadder.test.ts +179 -0
  438. package/src/crypto/__tests__/keyManager.recoveryMnemonic.test.ts +138 -0
  439. package/src/crypto/__tests__/keyManager.storageMigration.test.ts +227 -0
  440. package/src/crypto/__tests__/keyManager.test.ts +329 -0
  441. package/src/crypto/__tests__/polyfill.test.ts +109 -0
  442. package/src/crypto/__tests__/recoveryPhrase.test.ts +61 -0
  443. package/src/crypto/__tests__/scopedSeed.test.ts +126 -0
  444. package/src/crypto/__tests__/signChallengeShared.test.ts +63 -0
  445. package/src/crypto/__tests__/signedRecord.test.ts +230 -0
  446. package/src/crypto/aead.ts +97 -0
  447. package/src/crypto/ecdh.ts +37 -0
  448. package/src/crypto/identityMarker.ts +291 -0
  449. package/src/crypto/kdf.ts +43 -0
  450. package/src/crypto/keyManager.ts +2607 -0
  451. package/src/crypto/polyfill.ts +143 -0
  452. package/src/crypto/recoveryPhrase.ts +337 -0
  453. package/src/crypto/signatureService.ts +350 -0
  454. package/src/i18n/__tests__/accountCategoryLabels.test.ts +62 -0
  455. package/src/i18n/__tests__/accountRoleLabels.test.ts +54 -0
  456. package/src/i18n/__tests__/reputationCategoryLabels.test.ts +56 -0
  457. package/src/i18n/__tests__/trustTierLabels.test.ts +47 -0
  458. package/src/i18n/accountCategoryLabels.ts +44 -0
  459. package/src/i18n/accountRoleLabels.ts +26 -0
  460. package/src/i18n/index.ts +88 -0
  461. package/src/i18n/locales/ar-SA.json +282 -0
  462. package/src/i18n/locales/ca-ES.json +282 -0
  463. package/src/i18n/locales/de-DE.json +282 -0
  464. package/src/i18n/locales/en-US.json +2161 -0
  465. package/src/i18n/locales/es-ES.json +2161 -0
  466. package/src/i18n/locales/fr-FR.json +282 -0
  467. package/src/i18n/locales/it-IT.json +282 -0
  468. package/src/i18n/locales/ja-JP.json +282 -0
  469. package/src/i18n/locales/ko-KR.json +282 -0
  470. package/src/i18n/locales/pt-PT.json +282 -0
  471. package/src/i18n/locales/zh-CN.json +282 -0
  472. package/src/i18n/reputationCategoryLabels.ts +20 -0
  473. package/src/i18n/trustTierLabels.ts +18 -0
  474. package/src/index.ts +881 -0
  475. package/src/inference/OxyInferenceClient.ts +888 -0
  476. package/src/inference/__tests__/OxyInferenceClient.test.ts +834 -0
  477. package/src/logger/__tests__/logger.test.ts +207 -0
  478. package/src/logger/index.ts +217 -0
  479. package/src/mixins/OxyServices.accounts.ts +1330 -0
  480. package/src/mixins/OxyServices.analytics.ts +53 -0
  481. package/src/mixins/OxyServices.appData.ts +158 -0
  482. package/src/mixins/OxyServices.assets.ts +861 -0
  483. package/src/mixins/OxyServices.auth.ts +1858 -0
  484. package/src/mixins/OxyServices.chains.ts +134 -0
  485. package/src/mixins/OxyServices.civic.ts +945 -0
  486. package/src/mixins/OxyServices.connectedApps.ts +174 -0
  487. package/src/mixins/OxyServices.contacts.ts +73 -0
  488. package/src/mixins/OxyServices.deviceBoot.ts +210 -0
  489. package/src/mixins/OxyServices.deviceTransfer.ts +393 -0
  490. package/src/mixins/OxyServices.devices.ts +120 -0
  491. package/src/mixins/OxyServices.features.ts +454 -0
  492. package/src/mixins/OxyServices.followGraph.ts +293 -0
  493. package/src/mixins/OxyServices.identity.ts +705 -0
  494. package/src/mixins/OxyServices.identityBackup.ts +250 -0
  495. package/src/mixins/OxyServices.inference.ts +57 -0
  496. package/src/mixins/OxyServices.language.ts +144 -0
  497. package/src/mixins/OxyServices.links.ts +107 -0
  498. package/src/mixins/OxyServices.location.ts +46 -0
  499. package/src/mixins/OxyServices.nodes.ts +337 -0
  500. package/src/mixins/OxyServices.notifications.ts +142 -0
  501. package/src/mixins/OxyServices.payment.ts +163 -0
  502. package/src/mixins/OxyServices.privacy.ts +219 -0
  503. package/src/mixins/OxyServices.reputation.ts +436 -0
  504. package/src/mixins/OxyServices.security.ts +110 -0
  505. package/src/mixins/OxyServices.store.ts +585 -0
  506. package/src/mixins/OxyServices.topics.ts +169 -0
  507. package/src/mixins/OxyServices.user.ts +1137 -0
  508. package/src/mixins/OxyServices.utility.ts +1370 -0
  509. package/src/mixins/__tests__/OxyServices.civic.test.ts +1095 -0
  510. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +124 -0
  511. package/src/mixins/__tests__/OxyServices.deviceTransfer.test.ts +271 -0
  512. package/src/mixins/__tests__/OxyServices.identity.test.ts +376 -0
  513. package/src/mixins/__tests__/OxyServices.links.test.ts +154 -0
  514. package/src/mixins/__tests__/OxyServices.nodes.test.ts +341 -0
  515. package/src/mixins/__tests__/OxyServices.rotateKey.test.ts +276 -0
  516. package/src/mixins/__tests__/OxyServices.serviceAssetMetadata.test.ts +192 -0
  517. package/src/mixins/__tests__/OxyServices.serviceAssetMetadataBySha.test.ts +135 -0
  518. package/src/mixins/__tests__/accounts.test.ts +727 -0
  519. package/src/mixins/__tests__/appData.test.ts +203 -0
  520. package/src/mixins/__tests__/assetCredentials.test.ts +47 -0
  521. package/src/mixins/__tests__/assetUpload.test.ts +191 -0
  522. package/src/mixins/__tests__/backgroundCredential.httpIntegration.test.ts +135 -0
  523. package/src/mixins/__tests__/chains.test.ts +113 -0
  524. package/src/mixins/__tests__/commonsSignIn.test.ts +1144 -0
  525. package/src/mixins/__tests__/connectedApps.test.ts +123 -0
  526. package/src/mixins/__tests__/deviceTokenMintPinned.test.ts +113 -0
  527. package/src/mixins/__tests__/discoveryErrorHandling.test.ts +266 -0
  528. package/src/mixins/__tests__/followCacheInvalidation.test.ts +173 -0
  529. package/src/mixins/__tests__/followGraph.test.ts +147 -0
  530. package/src/mixins/__tests__/followGraphPagination.test.ts +271 -0
  531. package/src/mixins/__tests__/getFileDownloadUrl.test.ts +346 -0
  532. package/src/mixins/__tests__/getFollowStatuses.test.ts +95 -0
  533. package/src/mixins/__tests__/getUsersByIds.test.ts +149 -0
  534. package/src/mixins/__tests__/identityBackup.test.ts +315 -0
  535. package/src/mixins/__tests__/identityWriteCacheInvalidation.test.ts +407 -0
  536. package/src/mixins/__tests__/inferenceFactory.test.ts +58 -0
  537. package/src/mixins/__tests__/onTokensChanged.test.ts +129 -0
  538. package/src/mixins/__tests__/preSessionSkipAuth.test.ts +284 -0
  539. package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +155 -0
  540. package/src/mixins/__tests__/reputation.test.ts +522 -0
  541. package/src/mixins/__tests__/serviceAuth.test.ts +832 -0
  542. package/src/mixins/__tests__/serviceTokenJwks.test.ts +208 -0
  543. package/src/mixins/__tests__/store.test.ts +304 -0
  544. package/src/mixins/__tests__/topics.test.ts +156 -0
  545. package/src/mixins/__tests__/userReadCacheBypass.test.ts +129 -0
  546. package/src/mixins/__tests__/userTokenAuth.test.ts +746 -0
  547. package/src/mixins/__tests__/verifyChallenge.test.ts +130 -0
  548. package/src/mixins/__tests__/verifyServiceActingAs.test.ts +167 -0
  549. package/src/mixins/__tests__/webauthnAuth.test.ts +205 -0
  550. package/src/mixins/index.ts +201 -0
  551. package/src/mixins/mixinHelpers.ts +69 -0
  552. package/src/models/Topic.ts +46 -0
  553. package/src/models/interfaces.ts +786 -0
  554. package/src/models/session.ts +55 -0
  555. package/src/server/__tests__/auth.test.ts +78 -0
  556. package/src/server/__tests__/capabilityTicket.test.ts +111 -0
  557. package/src/server/__tests__/cors.socket.test.ts +225 -0
  558. package/src/server/__tests__/cors.test.ts +158 -0
  559. package/src/server/__tests__/rateLimit.test.ts +257 -0
  560. package/src/server/__tests__/safeFetch.test.ts +232 -0
  561. package/src/server/__tests__/securityHeaders.test.ts +348 -0
  562. package/src/server/__tests__/serviceTokenAttribution.test.ts +396 -0
  563. package/src/server/__tests__/userInvalidation.test.ts +203 -0
  564. package/src/server/__tests__/verifySecret.test.ts +40 -0
  565. package/src/server/auth.ts +271 -0
  566. package/src/server/capabilityTicket.ts +258 -0
  567. package/src/server/cors.ts +277 -0
  568. package/src/server/index.ts +136 -0
  569. package/src/server/rateLimit.ts +313 -0
  570. package/src/server/safeFetch.ts +586 -0
  571. package/src/server/securityHeaders.ts +467 -0
  572. package/src/server/userInvalidation.ts +199 -0
  573. package/src/server/verifySecret.ts +52 -0
  574. package/src/session/SessionClient.ts +1001 -0
  575. package/src/session/__tests__/SessionClient.additive.test.ts +153 -0
  576. package/src/session/__tests__/SessionClient.broadcastChannel.test.ts +114 -0
  577. package/src/session/__tests__/SessionClient.diagnostics.test.ts +91 -0
  578. package/src/session/__tests__/SessionClient.directory.test.ts +688 -0
  579. package/src/session/__tests__/SessionClient.httpIntegration.test.ts +65 -0
  580. package/src/session/__tests__/SessionClient.identityPin.test.ts +175 -0
  581. package/src/session/__tests__/SessionClient.rest.test.ts +119 -0
  582. package/src/session/__tests__/SessionClient.serverEvents.test.ts +72 -0
  583. package/src/session/__tests__/SessionClient.socket.test.ts +202 -0
  584. package/src/session/__tests__/SessionClient.socketFactory.test.ts +81 -0
  585. package/src/session/__tests__/SessionClient.state.test.ts +78 -0
  586. package/src/session/__tests__/SessionClient.switchTokenOrder.test.ts +170 -0
  587. package/src/session/__tests__/accountDelegationAccess.test.ts +104 -0
  588. package/src/session/__tests__/accountDialogController.test.ts +1969 -0
  589. package/src/session/__tests__/accountDialogShape.test.ts +118 -0
  590. package/src/session/__tests__/accountSwitchTargets.test.ts +137 -0
  591. package/src/session/__tests__/authStateStore.test.ts +451 -0
  592. package/src/session/__tests__/deviceDirectory.test.ts +422 -0
  593. package/src/session/__tests__/deviceSwitcherRows.test.ts +223 -0
  594. package/src/session/__tests__/identityPin.test.ts +131 -0
  595. package/src/session/__tests__/identitySession.test.ts +194 -0
  596. package/src/session/__tests__/projectSessionState.test.ts +105 -0
  597. package/src/session/__tests__/refresh.identityPin.test.ts +307 -0
  598. package/src/session/__tests__/refresh.test.ts +443 -0
  599. package/src/session/__tests__/sessionIntegration.test.ts +209 -0
  600. package/src/session/__tests__/sharedDeviceCredential.test.ts +300 -0
  601. package/src/session/accountDelegationAccess.ts +72 -0
  602. package/src/session/accountDialogController.ts +1494 -0
  603. package/src/session/accountSwitchTargets.ts +93 -0
  604. package/src/session/authStateStore.ts +502 -0
  605. package/src/session/createSessionClient.ts +39 -0
  606. package/src/session/deviceDirectory.ts +269 -0
  607. package/src/session/deviceSwitcherRows.ts +145 -0
  608. package/src/session/identityPin.ts +310 -0
  609. package/src/session/identitySession.ts +206 -0
  610. package/src/session/projectSessionState.ts +137 -0
  611. package/src/session/refresh.ts +517 -0
  612. package/src/session/sessionClientHost.ts +39 -0
  613. package/src/session/sharedDeviceCredential.ts +349 -0
  614. package/src/session/socketLoader.ts +30 -0
  615. package/src/shared/utils/colorUtils.ts +155 -0
  616. package/src/shared/utils/errorUtils.ts +181 -0
  617. package/src/shared/utils/index.ts +59 -0
  618. package/src/shared/utils/networkUtils.ts +248 -0
  619. package/src/shared/utils/themeUtils.ts +115 -0
  620. package/src/types/buffer.d.ts +97 -0
  621. package/src/types/expo-crypto.d.ts +16 -0
  622. package/src/types/expo-secure-store.d.ts +17 -0
  623. package/src/utils/__tests__/accountUtils.test.ts +151 -0
  624. package/src/utils/__tests__/activityId.test.ts +45 -0
  625. package/src/utils/__tests__/asyncUtils.test.ts +187 -0
  626. package/src/utils/__tests__/authWebUrl.test.ts +15 -0
  627. package/src/utils/__tests__/cacheKey.test.ts +0 -0
  628. package/src/utils/__tests__/coldBoot.test.ts +496 -0
  629. package/src/utils/__tests__/commonsApproval.test.ts +60 -0
  630. package/src/utils/__tests__/identityCacheSweep.test.ts +151 -0
  631. package/src/utils/__tests__/languageUtils.test.ts +219 -0
  632. package/src/utils/__tests__/oauthPkce.test.ts +272 -0
  633. package/src/utils/__tests__/officialOrigins.test.ts +31 -0
  634. package/src/utils/__tests__/oxyServiceEnvironment.test.ts +7 -0
  635. package/src/utils/__tests__/profileLinks.test.ts +180 -0
  636. package/src/utils/__tests__/redactUrl.test.ts +33 -0
  637. package/src/utils/__tests__/registrableApex.test.ts +62 -0
  638. package/src/utils/__tests__/textNormalization.test.ts +205 -0
  639. package/src/utils/__tests__/validationUtils.test.ts +703 -0
  640. package/src/utils/__tests__/webauthnOrigin.test.ts +83 -0
  641. package/src/utils/accountCacheSweep.ts +93 -0
  642. package/src/utils/accountUtils.ts +194 -0
  643. package/src/utils/activityId.ts +2 -0
  644. package/src/utils/apiUtils.ts +137 -0
  645. package/src/utils/asyncUtils.ts +280 -0
  646. package/src/utils/authHelpers.ts +148 -0
  647. package/src/utils/authWebUrl.ts +17 -0
  648. package/src/utils/avatarUtils.ts +39 -0
  649. package/src/utils/cache.ts +273 -0
  650. package/src/utils/cacheKey.ts +93 -0
  651. package/src/utils/coldBoot.ts +218 -0
  652. package/src/utils/commonsApproval.ts +39 -0
  653. package/src/utils/commonsDelivery.ts +132 -0
  654. package/src/utils/deviceManager.ts +203 -0
  655. package/src/utils/displayNamePolicyRanges.generated.ts +69 -0
  656. package/src/utils/edgeRegion.ts +2 -0
  657. package/src/utils/errorUtils.ts +352 -0
  658. package/src/utils/identityCacheSweep.ts +104 -0
  659. package/src/utils/languageUtils.ts +256 -0
  660. package/src/utils/oauthPkce.ts +392 -0
  661. package/src/utils/officialOrigins.ts +62 -0
  662. package/src/utils/oxyServiceEnvironment.ts +17 -0
  663. package/src/utils/platform.ts +139 -0
  664. package/src/utils/profileLinks.ts +91 -0
  665. package/src/utils/redactUrl.ts +28 -0
  666. package/src/utils/registrableApex.ts +41 -0
  667. package/src/utils/requestUtils.ts +237 -0
  668. package/src/utils/sessionUtils.ts +206 -0
  669. package/src/utils/textNormalization.ts +162 -0
  670. package/src/utils/userHandle.ts +49 -0
  671. package/src/utils/userIdentity.ts +60 -0
  672. package/src/utils/validationUtils.ts +390 -0
  673. package/src/utils/webauthnOrigin.ts +52 -0
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Connected-apps (OAuth grants) SDK tests.
3
+ *
4
+ * `listConnectedApps()` reads the user's authorized applications from
5
+ * `GET /auth/grants` and caches the response (identity-scoped). `revokeAppGrant`
6
+ * deletes a grant via `DELETE /auth/grants/:applicationId` and MUST invalidate
7
+ * the cached `GET:/auth/grants` so a re-read observes the removal instead of the
8
+ * STALE pre-revoke list (mirrors the privacy/follow invalidation contract).
9
+ */
10
+
11
+ import { OxyServices } from '../../OxyServices';
12
+ import type { ConnectedApp } from '../OxyServices.connectedApps';
13
+
14
+ /** Build a non-verified JWT whose payload decodes to the given claims. */
15
+ function makeJwt(payload: Record<string, unknown>): string {
16
+ const b64url = (obj: Record<string, unknown>): string =>
17
+ Buffer.from(JSON.stringify(obj)).toString('base64url');
18
+ const fullPayload = { exp: Math.floor(Date.now() / 1000) + 3600, ...payload };
19
+ return `${b64url({ alg: 'none', typ: 'JWT' })}.${b64url(fullPayload)}.sig`;
20
+ }
21
+
22
+ /** A JSON `Response` mimicking the API's `{ data: ... }` success envelope. */
23
+ function jsonResponse(data: unknown): Response {
24
+ return new Response(JSON.stringify({ data }), {
25
+ status: 200,
26
+ headers: { 'content-type': 'application/json' },
27
+ });
28
+ }
29
+
30
+ const APP_A: ConnectedApp = {
31
+ applicationId: 'app-a',
32
+ name: 'App A',
33
+ logoUrl: 'https://cdn.example/a.png',
34
+ scopes: ['profile', 'email'],
35
+ firstGrantedAt: '2026-01-01T00:00:00.000Z',
36
+ lastUsedAt: '2026-06-01T00:00:00.000Z',
37
+ };
38
+
39
+ const APP_B: ConnectedApp = {
40
+ applicationId: 'app-b',
41
+ name: 'App B',
42
+ scopes: ['profile'],
43
+ firstGrantedAt: '2026-02-01T00:00:00.000Z',
44
+ lastUsedAt: '2026-06-02T00:00:00.000Z',
45
+ };
46
+
47
+ describe('connected apps (OAuth grants)', () => {
48
+ let originalFetch: typeof globalThis.fetch;
49
+ let fetchMock: jest.Mock<Promise<Response>, [RequestInfo | URL, RequestInit?]>;
50
+ let oxy: OxyServices;
51
+
52
+ beforeEach(() => {
53
+ originalFetch = globalThis.fetch;
54
+ fetchMock = jest.fn();
55
+ globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
56
+ oxy = new OxyServices({ baseURL: 'http://test.invalid' });
57
+ oxy.httpService.setTokens(makeJwt({ userId: 'me' }));
58
+ });
59
+
60
+ afterEach(() => {
61
+ globalThis.fetch = originalFetch;
62
+ jest.clearAllMocks();
63
+ });
64
+
65
+ it('lists connected apps, unwrapping the { data } envelope', async () => {
66
+ fetchMock.mockResolvedValueOnce(jsonResponse([APP_A, APP_B]));
67
+ const apps = await oxy.listConnectedApps();
68
+ expect(apps).toEqual([APP_A, APP_B]);
69
+ expect(fetchMock).toHaveBeenCalledTimes(1);
70
+ const [url, init] = fetchMock.mock.calls[0];
71
+ expect(String(url)).toBe('http://test.invalid/auth/grants');
72
+ expect(init?.method).toBe('GET');
73
+ });
74
+
75
+ it('caches the list within the TTL (second read is a cache hit)', async () => {
76
+ fetchMock.mockResolvedValueOnce(jsonResponse([APP_A]));
77
+ expect(await oxy.listConnectedApps()).toEqual([APP_A]);
78
+ expect(fetchMock).toHaveBeenCalledTimes(1);
79
+
80
+ // Second read within the TTL must not hit the network.
81
+ expect(await oxy.listConnectedApps()).toEqual([APP_A]);
82
+ expect(fetchMock).toHaveBeenCalledTimes(1);
83
+ });
84
+
85
+ it('revokes a grant via DELETE /auth/grants/:applicationId', async () => {
86
+ fetchMock.mockResolvedValueOnce(jsonResponse({ revoked: true }));
87
+ await expect(oxy.revokeAppGrant('app-a')).resolves.toBeUndefined();
88
+ expect(fetchMock).toHaveBeenCalledTimes(1);
89
+ const [url, init] = fetchMock.mock.calls[0];
90
+ expect(String(url)).toBe('http://test.invalid/auth/grants/app-a');
91
+ expect(init?.method).toBe('DELETE');
92
+ });
93
+
94
+ it('busts the cached list after revokeAppGrant', async () => {
95
+ // 1) Warm the cache.
96
+ fetchMock.mockResolvedValueOnce(jsonResponse([APP_A, APP_B]));
97
+ expect(await oxy.listConnectedApps()).toEqual([APP_A, APP_B]);
98
+ expect(fetchMock).toHaveBeenCalledTimes(1);
99
+
100
+ // A second read is a cache hit (no extra network call).
101
+ await oxy.listConnectedApps();
102
+ expect(fetchMock).toHaveBeenCalledTimes(1);
103
+
104
+ // 2) Revoke — must invalidate the cached list.
105
+ fetchMock.mockResolvedValueOnce(jsonResponse({ revoked: true }));
106
+ await oxy.revokeAppGrant('app-a');
107
+ expect(fetchMock).toHaveBeenCalledTimes(2);
108
+
109
+ // 3) Re-read MUST re-fetch and observe the revoked app gone.
110
+ fetchMock.mockResolvedValueOnce(jsonResponse([APP_B]));
111
+ const after = await oxy.listConnectedApps();
112
+ expect(fetchMock).toHaveBeenCalledTimes(3);
113
+ expect(after).toEqual([APP_B]);
114
+ });
115
+
116
+ it('invalidates the exact GET:/auth/grants key on revoke', async () => {
117
+ const clearSpy = jest.spyOn(oxy, 'clearCacheEntry');
118
+ fetchMock.mockResolvedValueOnce(jsonResponse({ revoked: true }));
119
+ await oxy.revokeAppGrant('app-a');
120
+ expect(clearSpy).toHaveBeenCalledWith('GET:/auth/grants');
121
+ clearSpy.mockRestore();
122
+ });
123
+ });
@@ -0,0 +1,113 @@
1
+ /**
2
+ * `mintFromDeviceSecret` — the pinned (identity-bound) mint.
3
+ *
4
+ * The pinned form adds `accountId` to the zero-cookie mint body so an identity
5
+ * vault gets ITS account's token without the device's `activeAccountId` being
6
+ * touched. Its one new failure mode — `401 account_not_on_device` — must be
7
+ * distinguishable from a bad device secret, because the remedies are opposite:
8
+ * a bad secret is dropped, a stale identity binding is re-established while the
9
+ * (healthy) secret is kept.
10
+ */
11
+ import { OxyServices } from '../../OxyServices';
12
+ import { AccountNotOnDeviceError } from '../OxyServices.deviceBoot';
13
+
14
+ const MINT_RESPONSE = {
15
+ accessToken: 'access-1',
16
+ expiresAt: '2030-01-01T00:00:00.000Z',
17
+ nextDeviceSecret: 'ds-next',
18
+ state: {
19
+ deviceId: 'dev-1',
20
+ accounts: [{ accountId: 'vault-user', sessionId: 'sess-vault', authuser: 0 }],
21
+ activeAccountId: 'other-user',
22
+ revision: 3,
23
+ updatedAt: 1_700_000_000_000,
24
+ },
25
+ };
26
+
27
+ describe('OxyServices.mintFromDeviceSecret', () => {
28
+ let oxy: OxyServices;
29
+ let makeRequestSpy: jest.SpyInstance;
30
+
31
+ beforeEach(() => {
32
+ oxy = new OxyServices({ baseURL: 'http://test.invalid' });
33
+ makeRequestSpy = jest.spyOn(oxy, 'makeRequest');
34
+ });
35
+
36
+ afterEach(() => {
37
+ jest.restoreAllMocks();
38
+ });
39
+
40
+ it('omits accountId from the body when unpinned', async () => {
41
+ makeRequestSpy.mockResolvedValue(MINT_RESPONSE);
42
+
43
+ await oxy.mintFromDeviceSecret('dev-1', 'ds-1');
44
+
45
+ expect(makeRequestSpy).toHaveBeenCalledWith(
46
+ 'POST',
47
+ '/session/device/token',
48
+ { deviceId: 'dev-1', deviceSecret: 'ds-1' },
49
+ expect.objectContaining({ cache: false, skipAuth: true, retry: false, bypassQueue: true }),
50
+ );
51
+ });
52
+
53
+ it('sends accountId when pinned', async () => {
54
+ makeRequestSpy.mockResolvedValue(MINT_RESPONSE);
55
+
56
+ const mint = await oxy.mintFromDeviceSecret('dev-1', 'ds-1', { accountId: 'vault-user' });
57
+
58
+ expect(makeRequestSpy).toHaveBeenCalledWith(
59
+ 'POST',
60
+ '/session/device/token',
61
+ { deviceId: 'dev-1', deviceSecret: 'ds-1', accountId: 'vault-user' },
62
+ expect.anything(),
63
+ );
64
+ // The response still reports the device's TRUE active account — a pinned
65
+ // mint never mutates it.
66
+ expect(mint.state.activeAccountId).toBe('other-user');
67
+ });
68
+
69
+ it('maps a 401 account_not_on_device to the typed AccountNotOnDeviceError', async () => {
70
+ makeRequestSpy.mockRejectedValue(
71
+ Object.assign(new Error('account_not_on_device'), { status: 401, code: 'UNAUTHORIZED' }),
72
+ );
73
+
74
+ const error = await oxy
75
+ .mintFromDeviceSecret('dev-1', 'ds-1', { accountId: 'vault-user' })
76
+ .then(
77
+ () => null,
78
+ (thrown: unknown) => thrown,
79
+ );
80
+
81
+ expect(error).toBeInstanceOf(AccountNotOnDeviceError);
82
+ expect(error).toMatchObject({ name: 'AccountNotOnDeviceError', accountId: 'vault-user', status: 401 });
83
+ });
84
+
85
+ it('leaves every OTHER 401 as an ordinary error (a bad secret is not an identity problem)', async () => {
86
+ makeRequestSpy.mockRejectedValue(
87
+ Object.assign(new Error('invalid_device_secret'), { status: 401, code: 'UNAUTHORIZED' }),
88
+ );
89
+
90
+ const error = await oxy
91
+ .mintFromDeviceSecret('dev-1', 'ds-1', { accountId: 'vault-user' })
92
+ .then(
93
+ () => null,
94
+ (thrown: unknown) => thrown,
95
+ );
96
+
97
+ expect(error).not.toBeInstanceOf(AccountNotOnDeviceError);
98
+ expect(error).toMatchObject({ message: 'invalid_device_secret', status: 401 });
99
+ });
100
+
101
+ it('does not classify an UNPINNED mint failure as account_not_on_device', async () => {
102
+ makeRequestSpy.mockRejectedValue(
103
+ Object.assign(new Error('account_not_on_device'), { status: 401, code: 'UNAUTHORIZED' }),
104
+ );
105
+
106
+ const error = await oxy.mintFromDeviceSecret('dev-1', 'ds-1').then(
107
+ () => null,
108
+ (thrown: unknown) => thrown,
109
+ );
110
+
111
+ expect(error).not.toBeInstanceOf(AccountNotOnDeviceError);
112
+ });
113
+ });
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Discovery error-handling tests (C3).
3
+ *
4
+ * `resolveProfile` and `getProfileRecommendations` are best-effort discovery
5
+ * reads. Previously they swallowed every failure identically (`catch { return
6
+ * null }`), making a 404 "handle not found" indistinguishable from a 5xx /
7
+ * network failure in the field. These tests pin down the hardened behavior:
8
+ *
9
+ * - `resolveProfile` STILL returns `null` on any failure (contract unchanged),
10
+ * but emits a `debug` log that flags whether it was a not-found vs a failure.
11
+ * - `getProfileRecommendations` STILL rethrows (contract unchanged), but emits
12
+ * a `debug` log first for observability.
13
+ */
14
+
15
+ import { OxyServices } from '../../OxyServices';
16
+ import { logger } from '../../logger';
17
+
18
+ /** A JSON success `Response` mimicking the API's `{ data: ... }` envelope. */
19
+ function jsonResponse(data: unknown): Response {
20
+ return new Response(JSON.stringify({ data }), {
21
+ status: 200,
22
+ headers: { 'content-type': 'application/json' },
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Build a non-verified JWT whose payload decodes to the given claims.
28
+ * `jwtDecode` only base64url-decodes the middle segment (no signature check).
29
+ * Used to put the SDK in an authenticated state so a state-changing POST
30
+ * carries a bearer header and skips the CSRF-token pre-fetch.
31
+ */
32
+ function makeJwt(payload: Record<string, unknown>): string {
33
+ const b64url = (obj: Record<string, unknown>): string =>
34
+ Buffer.from(JSON.stringify(obj)).toString('base64url');
35
+ const fullPayload = { exp: Math.floor(Date.now() / 1000) + 3600, ...payload };
36
+ return `${b64url({ alg: 'none', typ: 'JWT' })}.${b64url(fullPayload)}.sig`;
37
+ }
38
+
39
+ /** A JSON error `Response` with the given HTTP status. */
40
+ function errorResponse(status: number, message: string): Response {
41
+ return new Response(JSON.stringify({ message }), {
42
+ status,
43
+ headers: { 'content-type': 'application/json' },
44
+ });
45
+ }
46
+
47
+ describe('discovery error handling', () => {
48
+ let originalFetch: typeof globalThis.fetch;
49
+ let fetchMock: jest.Mock<Promise<Response>, [RequestInfo | URL, RequestInit?]>;
50
+ let debugSpy: jest.SpyInstance;
51
+ let oxy: OxyServices;
52
+
53
+ beforeEach(() => {
54
+ originalFetch = globalThis.fetch;
55
+ fetchMock = jest.fn();
56
+ globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
57
+ debugSpy = jest.spyOn(logger, 'debug').mockImplementation(() => {});
58
+ // Disable retry so a single error response surfaces immediately.
59
+ oxy = new OxyServices({ baseURL: 'http://test.invalid', enableRetry: false });
60
+ });
61
+
62
+ afterEach(() => {
63
+ globalThis.fetch = originalFetch;
64
+ debugSpy.mockRestore();
65
+ jest.clearAllMocks();
66
+ });
67
+
68
+ describe('resolveProfile', () => {
69
+ it('returns the normalized user when discovery succeeds', async () => {
70
+ fetchMock.mockResolvedValueOnce(jsonResponse({ id: 'u1', username: 'remote' }));
71
+ const result = await oxy.resolveProfile('remote@mastodon.social');
72
+ expect(result?.id).toBe('u1');
73
+ expect(debugSpy).not.toHaveBeenCalled();
74
+ });
75
+
76
+ it('returns null AND logs a not-found debug entry on 404', async () => {
77
+ fetchMock.mockResolvedValueOnce(errorResponse(404, 'not found'));
78
+ const result = await oxy.resolveProfile('ghost@nowhere.example');
79
+ expect(result).toBeNull();
80
+ expect(debugSpy).toHaveBeenCalledTimes(1);
81
+ const [message, context] = debugSpy.mock.calls[0];
82
+ expect(message).toContain('not found');
83
+ expect(context).toMatchObject({
84
+ method: 'resolveProfile',
85
+ handle: 'ghost@nowhere.example',
86
+ status: 404,
87
+ notFound: true,
88
+ });
89
+ });
90
+
91
+ it('returns null AND logs a failure (not not-found) debug entry on 5xx', async () => {
92
+ fetchMock.mockResolvedValueOnce(errorResponse(503, 'upstream down'));
93
+ const result = await oxy.resolveProfile('remote@down.example');
94
+ expect(result).toBeNull();
95
+ expect(debugSpy).toHaveBeenCalledTimes(1);
96
+ const [message, context] = debugSpy.mock.calls[0];
97
+ expect(message).toContain('discovery failed');
98
+ expect(context).toMatchObject({
99
+ method: 'resolveProfile',
100
+ status: 503,
101
+ notFound: false,
102
+ });
103
+ });
104
+
105
+ it('returns null on a network failure (no status)', async () => {
106
+ fetchMock.mockRejectedValueOnce(new TypeError('Failed to fetch'));
107
+ const result = await oxy.resolveProfile('remote@offline.example');
108
+ expect(result).toBeNull();
109
+ expect(debugSpy).toHaveBeenCalledTimes(1);
110
+ const [, context] = debugSpy.mock.calls[0];
111
+ expect(context).toMatchObject({ notFound: false });
112
+ });
113
+ });
114
+
115
+ describe('getProfileRecommendations', () => {
116
+ it('returns the list on success without logging', async () => {
117
+ fetchMock.mockResolvedValueOnce(jsonResponse([{ id: 'r1', username: 'rec' }]));
118
+ const result = await oxy.getProfileRecommendations();
119
+ expect(result).toHaveLength(1);
120
+ expect(debugSpy).not.toHaveBeenCalled();
121
+ });
122
+
123
+ it('rethrows on failure AND logs a debug entry first (contract unchanged)', async () => {
124
+ fetchMock.mockResolvedValueOnce(errorResponse(500, 'boom'));
125
+ await expect(oxy.getProfileRecommendations()).rejects.toThrow();
126
+ expect(debugSpy).toHaveBeenCalledTimes(1);
127
+ const [message, context] = debugSpy.mock.calls[0];
128
+ expect(message).toContain('discovery read failed');
129
+ expect(context).toMatchObject({
130
+ method: 'getProfileRecommendations',
131
+ status: 500,
132
+ });
133
+ });
134
+ });
135
+
136
+ /**
137
+ * GET-vs-POST routing for the recommendation contract (Wave 2).
138
+ *
139
+ * The simple/back-compat case (no options, or only `excludeTypes`/`limit`)
140
+ * keeps using the cached `GET /profiles/recommendations`. The scored (v2)
141
+ * fields (`clientId`/`offset`/`excludeIds`/`boosts`/`signalWeights`) switch to
142
+ * `POST /profiles/recommendations` with the validated request body.
143
+ */
144
+ describe('getProfileRecommendations routing (GET vs POST)', () => {
145
+ // Authenticate so the state-changing POST carries a bearer header and the
146
+ // SDK skips the CSRF-token pre-fetch (which would otherwise consume a mock).
147
+ // A bearer token does not affect GET routing or the request body, so the
148
+ // GET-path assertions below are unaffected.
149
+ beforeEach(() => {
150
+ oxy.httpService.setTokens(makeJwt({ userId: 'me' }));
151
+ });
152
+
153
+ /** The `(url, init)` pair of the single recorded fetch call. */
154
+ function lastCall(): { url: string; init: RequestInit | undefined } {
155
+ const [input, init] = fetchMock.mock.calls[fetchMock.mock.calls.length - 1];
156
+ return { url: String(input), init };
157
+ }
158
+
159
+ it('uses GET (no body) when called with no options', async () => {
160
+ fetchMock.mockResolvedValueOnce(jsonResponse([{ id: 'r1' }]));
161
+ await oxy.getProfileRecommendations();
162
+ const { url, init } = lastCall();
163
+ expect(init?.method).toBe('GET');
164
+ expect(init?.body).toBeUndefined();
165
+ expect(url).toContain('/profiles/recommendations');
166
+ expect(url).not.toContain('excludeTypes');
167
+ });
168
+
169
+ it('uses GET with excludeTypes as a comma-joined query param (legacy shape)', async () => {
170
+ fetchMock.mockResolvedValueOnce(jsonResponse([{ id: 'r1' }]));
171
+ await oxy.getProfileRecommendations({ excludeTypes: ['federated', 'agent'] });
172
+ const { url, init } = lastCall();
173
+ expect(init?.method).toBe('GET');
174
+ expect(init?.body).toBeUndefined();
175
+ // Comma is URL-encoded as %2C in the query string.
176
+ expect(decodeURIComponent(url)).toContain('excludeTypes=federated,agent');
177
+ });
178
+
179
+ it('uses GET with limit as a query param', async () => {
180
+ fetchMock.mockResolvedValueOnce(jsonResponse([{ id: 'r1' }]));
181
+ await oxy.getProfileRecommendations({ limit: 5 });
182
+ const { url, init } = lastCall();
183
+ expect(init?.method).toBe('GET');
184
+ expect(url).toContain('limit=5');
185
+ });
186
+
187
+ it('uses POST with the validated body when clientId is present', async () => {
188
+ fetchMock.mockResolvedValueOnce(jsonResponse([{ id: 'r1' }]));
189
+ await oxy.getProfileRecommendations({ clientId: 'app-123', limit: 10 });
190
+ const { url, init } = lastCall();
191
+ expect(init?.method).toBe('POST');
192
+ expect(url).toContain('/profiles/recommendations');
193
+ expect(url).not.toContain('?');
194
+ expect(JSON.parse(String(init?.body))).toMatchObject({ clientId: 'app-123', limit: 10 });
195
+ });
196
+
197
+ it('uses POST when boosts are present', async () => {
198
+ fetchMock.mockResolvedValueOnce(jsonResponse([{ id: 'r1' }]));
199
+ await oxy.getProfileRecommendations({
200
+ boosts: [{ userIds: ['u1', 'u2'], weight: 2, reason: 'editorial' }],
201
+ });
202
+ const { init } = lastCall();
203
+ expect(init?.method).toBe('POST');
204
+ expect(JSON.parse(String(init?.body)).boosts[0]).toMatchObject({
205
+ userIds: ['u1', 'u2'],
206
+ weight: 2,
207
+ });
208
+ });
209
+
210
+ it('uses POST when excludeIds are present', async () => {
211
+ fetchMock.mockResolvedValueOnce(jsonResponse([{ id: 'r1' }]));
212
+ await oxy.getProfileRecommendations({ excludeIds: ['x1', 'x2'] });
213
+ const { init } = lastCall();
214
+ expect(init?.method).toBe('POST');
215
+ expect(JSON.parse(String(init?.body)).excludeIds).toEqual(['x1', 'x2']);
216
+ });
217
+
218
+ it('uses POST when signalWeights are present', async () => {
219
+ fetchMock.mockResolvedValueOnce(jsonResponse([{ id: 'r1' }]));
220
+ await oxy.getProfileRecommendations({ signalWeights: { graph: 3 } });
221
+ const { init } = lastCall();
222
+ expect(init?.method).toBe('POST');
223
+ expect(JSON.parse(String(init?.body)).signalWeights).toEqual({ graph: 3 });
224
+ });
225
+
226
+ it('uses POST when offset is present', async () => {
227
+ fetchMock.mockResolvedValueOnce(jsonResponse([{ id: 'r1' }]));
228
+ await oxy.getProfileRecommendations({ offset: 20, limit: 10 });
229
+ const { init } = lastCall();
230
+ expect(init?.method).toBe('POST');
231
+ expect(JSON.parse(String(init?.body))).toMatchObject({ offset: 20, limit: 10 });
232
+ });
233
+
234
+ it('validates the POST body against the contract and rejects bad input', async () => {
235
+ // weight is clamped to [-5, 5] by the schema; 99 must be rejected
236
+ // client-side BEFORE any network call.
237
+ await expect(
238
+ oxy.getProfileRecommendations({ boosts: [{ userIds: ['u1'], weight: 99 }] }),
239
+ ).rejects.toThrow();
240
+ expect(fetchMock).not.toHaveBeenCalled();
241
+ });
242
+
243
+ it('returns the scored items (score/matchedSignals) from the POST path', async () => {
244
+ fetchMock.mockResolvedValueOnce(
245
+ jsonResponse([
246
+ {
247
+ id: 'r1',
248
+ name: { displayName: 'Rec One' },
249
+ mutualCount: 3,
250
+ score: 0.87,
251
+ matchedSignals: ['graph', 'verified'],
252
+ _count: { followers: 10, following: 5 },
253
+ },
254
+ ]),
255
+ );
256
+ const result = await oxy.getProfileRecommendations({ clientId: 'app-1' });
257
+ expect(result).toHaveLength(1);
258
+ expect(result[0]).toMatchObject({
259
+ id: 'r1',
260
+ score: 0.87,
261
+ matchedSignals: ['graph', 'verified'],
262
+ mutualCount: 3,
263
+ });
264
+ });
265
+ });
266
+ });
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Follow-status cache invalidation tests.
3
+ *
4
+ * Regression coverage for the "follow resets after navigating away and back"
5
+ * bug. `getFollowStatus(userId)` caches `GET /users/<id>/follow-status` for
6
+ * ~1 minute (identity-scoped). A follow/unfollow write must INVALIDATE that
7
+ * cached entry, otherwise a `FollowButton` that remounts within the TTL window
8
+ * re-reads the STALE pre-write status and reverts the optimistic UI.
9
+ *
10
+ * These tests pin down that:
11
+ * - a cached `follow-status` is NOT served after `followUser` /
12
+ * `unfollowUser` / `followUsers` / `unfollowUsers` — the next read
13
+ * re-fetches and observes the new server truth,
14
+ * - each mutation invalidates the exact logical key(s) for the affected ids.
15
+ */
16
+
17
+ import { OxyServices } from '../../OxyServices';
18
+
19
+ /**
20
+ * Build a non-verified JWT whose payload decodes to the given claims.
21
+ * `jwtDecode` only base64url-decodes the middle segment (no signature check).
22
+ */
23
+ function makeJwt(payload: Record<string, unknown>): string {
24
+ const b64url = (obj: Record<string, unknown>): string =>
25
+ Buffer.from(JSON.stringify(obj)).toString('base64url');
26
+ const fullPayload = { exp: Math.floor(Date.now() / 1000) + 3600, ...payload };
27
+ return `${b64url({ alg: 'none', typ: 'JWT' })}.${b64url(fullPayload)}.sig`;
28
+ }
29
+
30
+ /** A JSON `Response` mimicking the API's `{ data: ... }` success envelope. */
31
+ function jsonResponse(data: unknown): Response {
32
+ return new Response(JSON.stringify({ data }), {
33
+ status: 200,
34
+ headers: { 'content-type': 'application/json' },
35
+ });
36
+ }
37
+
38
+ describe('follow-status cache invalidation', () => {
39
+ let originalFetch: typeof globalThis.fetch;
40
+ let fetchMock: jest.Mock<Promise<Response>, [RequestInfo | URL, RequestInit?]>;
41
+ let oxy: OxyServices;
42
+
43
+ beforeEach(() => {
44
+ originalFetch = globalThis.fetch;
45
+ fetchMock = jest.fn();
46
+ globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
47
+ oxy = new OxyServices({ baseURL: 'http://test.invalid' });
48
+ oxy.httpService.setTokens(makeJwt({ userId: 'me' }));
49
+ });
50
+
51
+ afterEach(() => {
52
+ globalThis.fetch = originalFetch;
53
+ jest.clearAllMocks();
54
+ });
55
+
56
+ it('does NOT serve a stale cached follow-status after followUser', async () => {
57
+ // 1) Warm the cache: not following yet.
58
+ fetchMock.mockResolvedValueOnce(jsonResponse({ isFollowing: false }));
59
+ const before = await oxy.getFollowStatus('target-1');
60
+ expect(before.isFollowing).toBe(false);
61
+ expect(fetchMock).toHaveBeenCalledTimes(1);
62
+
63
+ // A second read within the TTL is a cache hit (no extra network call).
64
+ await oxy.getFollowStatus('target-1');
65
+ expect(fetchMock).toHaveBeenCalledTimes(1);
66
+
67
+ // 2) Follow — write succeeds and must bust the cached follow-status.
68
+ fetchMock.mockResolvedValueOnce(jsonResponse({ success: true, message: 'ok' }));
69
+ await oxy.followUser('target-1');
70
+ expect(fetchMock).toHaveBeenCalledTimes(2);
71
+
72
+ // 3) Remount-style re-read MUST re-fetch and observe the fresh truth.
73
+ fetchMock.mockResolvedValueOnce(jsonResponse({ isFollowing: true }));
74
+ const after = await oxy.getFollowStatus('target-1');
75
+ expect(fetchMock).toHaveBeenCalledTimes(3);
76
+ expect(after.isFollowing).toBe(true);
77
+ });
78
+
79
+ it('does NOT serve a stale cached follow-status after unfollowUser', async () => {
80
+ fetchMock.mockResolvedValueOnce(jsonResponse({ isFollowing: true }));
81
+ const before = await oxy.getFollowStatus('target-2');
82
+ expect(before.isFollowing).toBe(true);
83
+ expect(fetchMock).toHaveBeenCalledTimes(1);
84
+
85
+ fetchMock.mockResolvedValueOnce(jsonResponse({ success: true, message: 'ok' }));
86
+ await oxy.unfollowUser('target-2');
87
+ expect(fetchMock).toHaveBeenCalledTimes(2);
88
+
89
+ fetchMock.mockResolvedValueOnce(jsonResponse({ isFollowing: false }));
90
+ const after = await oxy.getFollowStatus('target-2');
91
+ expect(fetchMock).toHaveBeenCalledTimes(3);
92
+ expect(after.isFollowing).toBe(false);
93
+ });
94
+
95
+ it('busts cached follow-status for every id after followUsers (bulk)', async () => {
96
+ fetchMock.mockResolvedValueOnce(jsonResponse({ isFollowing: false }));
97
+ fetchMock.mockResolvedValueOnce(jsonResponse({ isFollowing: false }));
98
+ await oxy.getFollowStatus('a');
99
+ await oxy.getFollowStatus('b');
100
+ expect(fetchMock).toHaveBeenCalledTimes(2);
101
+
102
+ fetchMock.mockResolvedValueOnce(
103
+ jsonResponse({
104
+ results: [
105
+ { userId: 'a', success: true, alreadyFollowing: false },
106
+ { userId: 'b', success: true, alreadyFollowing: false },
107
+ ],
108
+ followedCount: 2,
109
+ }),
110
+ );
111
+ await oxy.followUsers(['a', 'b']);
112
+ expect(fetchMock).toHaveBeenCalledTimes(3);
113
+
114
+ // Both ids must re-fetch.
115
+ fetchMock.mockResolvedValueOnce(jsonResponse({ isFollowing: true }));
116
+ fetchMock.mockResolvedValueOnce(jsonResponse({ isFollowing: true }));
117
+ expect((await oxy.getFollowStatus('a')).isFollowing).toBe(true);
118
+ expect((await oxy.getFollowStatus('b')).isFollowing).toBe(true);
119
+ expect(fetchMock).toHaveBeenCalledTimes(5);
120
+ });
121
+
122
+ it('busts cached follow-status for every id after unfollowUsers (bulk)', async () => {
123
+ fetchMock.mockResolvedValueOnce(jsonResponse({ isFollowing: true }));
124
+ fetchMock.mockResolvedValueOnce(jsonResponse({ isFollowing: true }));
125
+ await oxy.getFollowStatus('a');
126
+ await oxy.getFollowStatus('b');
127
+ expect(fetchMock).toHaveBeenCalledTimes(2);
128
+
129
+ fetchMock.mockResolvedValueOnce(
130
+ jsonResponse({
131
+ results: [
132
+ { userId: 'a', success: true, wasFollowing: true },
133
+ { userId: 'b', success: true, wasFollowing: true },
134
+ ],
135
+ unfollowedCount: 2,
136
+ }),
137
+ );
138
+ await oxy.unfollowUsers(['a', 'b']);
139
+ expect(fetchMock).toHaveBeenCalledTimes(3);
140
+
141
+ fetchMock.mockResolvedValueOnce(jsonResponse({ isFollowing: false }));
142
+ fetchMock.mockResolvedValueOnce(jsonResponse({ isFollowing: false }));
143
+ expect((await oxy.getFollowStatus('a')).isFollowing).toBe(false);
144
+ expect((await oxy.getFollowStatus('b')).isFollowing).toBe(false);
145
+ expect(fetchMock).toHaveBeenCalledTimes(5);
146
+ });
147
+
148
+ it('invalidates the exact logical follow-status key on a single follow', async () => {
149
+ const clearSpy = jest.spyOn(oxy, 'clearCacheEntry');
150
+ const clearPrefixSpy = jest.spyOn(oxy, 'clearCacheByPrefix');
151
+ fetchMock.mockResolvedValueOnce(jsonResponse({ success: true, message: 'ok' }));
152
+
153
+ await oxy.followUser('target-3');
154
+
155
+ expect(clearSpy).toHaveBeenCalledWith('GET:/users/target-3/follow-status');
156
+ expect(clearSpy).toHaveBeenCalledWith('GET:/users/target-3');
157
+ expect(clearPrefixSpy).toHaveBeenCalledWith('GET:/profiles/username/');
158
+ expect(clearPrefixSpy).toHaveBeenCalledWith('GET:/profiles/resolve');
159
+ clearSpy.mockRestore();
160
+ clearPrefixSpy.mockRestore();
161
+ });
162
+
163
+ it('does not invalidate or call the network when bulk follow gets an empty list', async () => {
164
+ const clearSpy = jest.spyOn(oxy, 'clearCacheEntry');
165
+
166
+ const result = await oxy.followUsers([]);
167
+
168
+ expect(result).toEqual({ results: [], followedCount: 0 });
169
+ expect(fetchMock).not.toHaveBeenCalled();
170
+ expect(clearSpy).not.toHaveBeenCalled();
171
+ clearSpy.mockRestore();
172
+ });
173
+ });