@oxy.so/core 4.5.0-oxy-one.1 → 4.6.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 (283) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/HttpService.js +63 -19
  3. package/dist/cjs/api/accounts.js +10 -3
  4. package/dist/cjs/api/agency.js +3 -1
  5. package/dist/cjs/api/apps.js +15 -4
  6. package/dist/cjs/api/assets.js +39 -11
  7. package/dist/cjs/api/auth.js +28 -8
  8. package/dist/cjs/api/billing.js +24 -7
  9. package/dist/cjs/api/civic.js +14 -4
  10. package/dist/cjs/api/follows.js +9 -2
  11. package/dist/cjs/api/identity.js +31 -9
  12. package/dist/cjs/api/nodes.js +3 -1
  13. package/dist/cjs/api/privacy.js +8 -2
  14. package/dist/cjs/api/session.js +15 -4
  15. package/dist/cjs/api/store.js +15 -5
  16. package/dist/cjs/api/topics.js +10 -3
  17. package/dist/cjs/api/users.js +16 -4
  18. package/dist/cjs/boot/sessionColdBoot.js +40 -13
  19. package/dist/cjs/constants/version.js +6 -6
  20. package/dist/cjs/crypto/identityMarker.js +3 -1
  21. package/dist/cjs/crypto/identityProof.js +6 -1
  22. package/dist/cjs/crypto/keyManager.js +87 -32
  23. package/dist/cjs/crypto/recoveryPhrase.js +2 -2
  24. package/dist/cjs/crypto/signatureService.js +3 -4
  25. package/dist/cjs/i18n/index.js +16 -3
  26. package/dist/cjs/i18n/locales/ar-SA.js +366 -366
  27. package/dist/cjs/i18n/locales/ca-ES.js +366 -366
  28. package/dist/cjs/i18n/locales/de-DE.js +366 -366
  29. package/dist/cjs/i18n/locales/en-US.js +1994 -1994
  30. package/dist/cjs/i18n/locales/es-ES.js +1994 -1994
  31. package/dist/cjs/i18n/locales/fr-FR.js +366 -366
  32. package/dist/cjs/i18n/locales/it-IT.js +366 -366
  33. package/dist/cjs/i18n/locales/ja-JP.js +366 -366
  34. package/dist/cjs/i18n/locales/ko-KR.js +366 -366
  35. package/dist/cjs/i18n/locales/pt-PT.js +366 -366
  36. package/dist/cjs/i18n/locales/zh-CN.js +366 -366
  37. package/dist/cjs/inference/OxyInferenceClient.js +12 -18
  38. package/dist/cjs/models/interfaces.js +11 -11
  39. package/dist/cjs/server/OxyServer.js +86 -28
  40. package/dist/cjs/server/agentAccount.js +6 -2
  41. package/dist/cjs/server/aliaMachineCredential.js +39 -14
  42. package/dist/cjs/server/auth.js +5 -4
  43. package/dist/cjs/server/capabilityTicket.js +5 -5
  44. package/dist/cjs/server/index.js +2 -1
  45. package/dist/cjs/server/liveCapabilityTicket.js +4 -1
  46. package/dist/cjs/server/middleware.js +61 -42
  47. package/dist/cjs/server/namespaces.js +28 -7
  48. package/dist/cjs/server/requesterAssertion.js +36 -30
  49. package/dist/cjs/server/serviceTokenLifetime.js +7 -4
  50. package/dist/cjs/server/traffic.js +53 -14
  51. package/dist/cjs/server/trafficNodeHttp.js +33 -8
  52. package/dist/cjs/server/workloadIdentity.client.js +4 -0
  53. package/dist/cjs/server/workloadIdentity.js +28 -1
  54. package/dist/cjs/session/SessionClient.js +54 -17
  55. package/dist/cjs/session/accountDelegationAccess.js +4 -6
  56. package/dist/cjs/session/accountDialogController.js +1 -1
  57. package/dist/cjs/session/authStateStore.js +18 -9
  58. package/dist/cjs/session/identityPin.js +6 -2
  59. package/dist/cjs/session/refresh.js +81 -23
  60. package/dist/cjs/session/sharedDeviceCredential.js +28 -15
  61. package/dist/cjs/shared/utils/errorUtils.js +1 -1
  62. package/dist/cjs/shared/utils/networkUtils.js +1 -1
  63. package/dist/cjs/shared/utils/themeUtils.js +1 -1
  64. package/dist/cjs/utils/accountCacheSweep.js +3 -1
  65. package/dist/cjs/utils/accountUtils.js +1 -3
  66. package/dist/cjs/utils/asyncUtils.js +1 -1
  67. package/dist/cjs/utils/authHelpers.js +2 -1
  68. package/dist/cjs/utils/avatarUtils.js +4 -2
  69. package/dist/cjs/utils/cache.js +1 -1
  70. package/dist/cjs/utils/cacheKey.js +3 -1
  71. package/dist/cjs/utils/deviceManager.js +4 -4
  72. package/dist/cjs/utils/errorUtils.js +6 -4
  73. package/dist/cjs/utils/languageUtils.js +404 -57
  74. package/dist/cjs/utils/profileLinks.js +1 -3
  75. package/dist/cjs/utils/requestUtils.js +7 -1
  76. package/dist/cjs/utils/userIdentity.js +1 -1
  77. package/dist/cjs/utils/validationUtils.js +5 -6
  78. package/dist/esm/HttpService.js +64 -20
  79. package/dist/esm/api/accounts.js +11 -4
  80. package/dist/esm/api/agency.js +3 -1
  81. package/dist/esm/api/apps.js +15 -4
  82. package/dist/esm/api/assets.js +39 -11
  83. package/dist/esm/api/auth.js +28 -8
  84. package/dist/esm/api/billing.js +25 -8
  85. package/dist/esm/api/civic.js +14 -4
  86. package/dist/esm/api/follows.js +9 -2
  87. package/dist/esm/api/identity.js +31 -9
  88. package/dist/esm/api/nodes.js +3 -1
  89. package/dist/esm/api/privacy.js +8 -2
  90. package/dist/esm/api/session.js +15 -4
  91. package/dist/esm/api/store.js +15 -5
  92. package/dist/esm/api/topics.js +10 -3
  93. package/dist/esm/api/users.js +16 -4
  94. package/dist/esm/boot/sessionColdBoot.js +41 -14
  95. package/dist/esm/constants/version.js +6 -6
  96. package/dist/esm/crypto/identityMarker.js +3 -1
  97. package/dist/esm/crypto/identityProof.js +6 -1
  98. package/dist/esm/crypto/internal.js +1 -1
  99. package/dist/esm/crypto/keyManager.js +87 -32
  100. package/dist/esm/crypto/recoveryPhrase.js +2 -2
  101. package/dist/esm/crypto/signatureService.js +3 -4
  102. package/dist/esm/i18n/index.js +16 -3
  103. package/dist/esm/i18n/locales/ar-SA.js +366 -366
  104. package/dist/esm/i18n/locales/ca-ES.js +366 -366
  105. package/dist/esm/i18n/locales/de-DE.js +366 -366
  106. package/dist/esm/i18n/locales/en-US.js +1994 -1994
  107. package/dist/esm/i18n/locales/es-ES.js +1994 -1994
  108. package/dist/esm/i18n/locales/fr-FR.js +366 -366
  109. package/dist/esm/i18n/locales/it-IT.js +366 -366
  110. package/dist/esm/i18n/locales/ja-JP.js +366 -366
  111. package/dist/esm/i18n/locales/ko-KR.js +366 -366
  112. package/dist/esm/i18n/locales/pt-PT.js +366 -366
  113. package/dist/esm/i18n/locales/zh-CN.js +366 -366
  114. package/dist/esm/index.js +1 -1
  115. package/dist/esm/inference/OxyInferenceClient.js +13 -19
  116. package/dist/esm/models/interfaces.js +11 -11
  117. package/dist/esm/server/OxyServer.js +87 -29
  118. package/dist/esm/server/agentAccount.js +6 -2
  119. package/dist/esm/server/aliaMachineCredential.js +39 -14
  120. package/dist/esm/server/auth.js +6 -5
  121. package/dist/esm/server/capabilityTicket.js +6 -6
  122. package/dist/esm/server/index.js +5 -5
  123. package/dist/esm/server/liveCapabilityTicket.js +6 -3
  124. package/dist/esm/server/middleware.js +62 -43
  125. package/dist/esm/server/namespaces.js +31 -10
  126. package/dist/esm/server/requesterAssertion.js +36 -30
  127. package/dist/esm/server/serviceTokenLifetime.js +7 -4
  128. package/dist/esm/server/traffic.js +55 -16
  129. package/dist/esm/server/trafficNodeHttp.js +34 -9
  130. package/dist/esm/server/userInvalidation.js +1 -1
  131. package/dist/esm/server/workloadIdentity.client.js +3 -0
  132. package/dist/esm/server/workloadIdentity.js +27 -1
  133. package/dist/esm/session/SessionClient.js +54 -17
  134. package/dist/esm/session/accountDelegationAccess.js +5 -7
  135. package/dist/esm/session/accountDialogController.js +1 -1
  136. package/dist/esm/session/authStateStore.js +18 -9
  137. package/dist/esm/session/identityPin.js +6 -2
  138. package/dist/esm/session/refresh.js +82 -24
  139. package/dist/esm/session/sharedDeviceCredential.js +28 -15
  140. package/dist/esm/shared/utils/errorUtils.js +1 -1
  141. package/dist/esm/shared/utils/networkUtils.js +1 -1
  142. package/dist/esm/shared/utils/themeUtils.js +1 -1
  143. package/dist/esm/utils/accountCacheSweep.js +3 -1
  144. package/dist/esm/utils/accountUtils.js +1 -3
  145. package/dist/esm/utils/asyncUtils.js +1 -1
  146. package/dist/esm/utils/authHelpers.js +2 -1
  147. package/dist/esm/utils/avatarUtils.js +4 -2
  148. package/dist/esm/utils/cache.js +1 -1
  149. package/dist/esm/utils/cacheKey.js +3 -1
  150. package/dist/esm/utils/deviceManager.js +4 -4
  151. package/dist/esm/utils/errorUtils.js +6 -4
  152. package/dist/esm/utils/languageUtils.js +404 -57
  153. package/dist/esm/utils/profileLinks.js +1 -3
  154. package/dist/esm/utils/requestUtils.js +7 -1
  155. package/dist/esm/utils/userIdentity.js +1 -1
  156. package/dist/esm/utils/validationUtils.js +5 -6
  157. package/dist/types/HttpService.d.ts +1 -1
  158. package/dist/types/api/accounts.d.ts +16 -3
  159. package/dist/types/api/apps.d.ts +4 -4
  160. package/dist/types/crypto/internal.d.ts +1 -1
  161. package/dist/types/index.d.ts +4 -4
  162. package/dist/types/models/interfaces.d.ts +9 -1
  163. package/dist/types/server/index.d.ts +6 -6
  164. package/dist/types/server/workloadIdentity.client.d.ts +5 -1
  165. package/dist/types/server/workloadIdentity.d.ts +26 -0
  166. package/dist/types/session/accountDelegationAccess.d.ts +1 -1
  167. package/dist/types/session/index.d.ts +1 -1
  168. package/dist/types/utils/userIdentity.d.ts +1 -1
  169. package/dist/types/utils/validationUtils.d.ts +4 -5
  170. package/package.json +3 -4
  171. package/src/HttpService.ts +183 -53
  172. package/src/OxyServices.errors.ts +7 -3
  173. package/src/OxyServices.ts +6 -2
  174. package/src/api/accounts.ts +96 -28
  175. package/src/api/agency.ts +24 -9
  176. package/src/api/appData.ts +12 -2
  177. package/src/api/apps.ts +81 -31
  178. package/src/api/assets.ts +113 -43
  179. package/src/api/auth.ts +145 -77
  180. package/src/api/billing.ts +112 -65
  181. package/src/api/civic.ts +61 -19
  182. package/src/api/contacts.ts +4 -1
  183. package/src/api/devices.ts +54 -21
  184. package/src/api/follows.ts +88 -31
  185. package/src/api/identity.ts +101 -33
  186. package/src/api/linkedAccounts.ts +26 -8
  187. package/src/api/nodes.ts +18 -6
  188. package/src/api/notifications.ts +26 -6
  189. package/src/api/privacy.ts +42 -8
  190. package/src/api/reputation.ts +18 -7
  191. package/src/api/session.ts +23 -5
  192. package/src/api/store.ts +81 -31
  193. package/src/api/topics.ts +18 -6
  194. package/src/api/users.ts +98 -35
  195. package/src/boot/sessionColdBoot.ts +60 -19
  196. package/src/civic/payloads.ts +1 -4
  197. package/src/client/context.ts +6 -1
  198. package/src/constants/version.ts +7 -6
  199. package/src/crypto/aead.ts +1 -5
  200. package/src/crypto/ecdh.ts +1 -4
  201. package/src/crypto/identityDerivations.ts +5 -1
  202. package/src/crypto/identityMarker.ts +3 -1
  203. package/src/crypto/identityProof.ts +6 -1
  204. package/src/crypto/index.ts +11 -11
  205. package/src/crypto/internal.ts +6 -1
  206. package/src/crypto/keyManager.ts +348 -98
  207. package/src/crypto/recoveryPhrase.ts +14 -9
  208. package/src/crypto/registrationPow.ts +1 -1
  209. package/src/crypto/signatureService.ts +14 -6
  210. package/src/i18n/accountCategoryLabels.ts +1 -4
  211. package/src/i18n/accountRoleLabels.ts +1 -4
  212. package/src/i18n/index.ts +25 -7
  213. package/src/i18n/locales/ar-SA.ts +375 -366
  214. package/src/i18n/locales/ca-ES.ts +382 -366
  215. package/src/i18n/locales/de-DE.ts +387 -366
  216. package/src/i18n/locales/en-US.ts +2034 -1994
  217. package/src/i18n/locales/es-ES.ts +2043 -1994
  218. package/src/i18n/locales/fr-FR.ts +387 -366
  219. package/src/i18n/locales/it-IT.ts +380 -366
  220. package/src/i18n/locales/ja-JP.ts +380 -366
  221. package/src/i18n/locales/ko-KR.ts +377 -366
  222. package/src/i18n/locales/pt-PT.ts +383 -366
  223. package/src/i18n/locales/zh-CN.ts +373 -366
  224. package/src/i18n/trustTierLabels.ts +1 -4
  225. package/src/index.ts +371 -349
  226. package/src/inference/OxyInferenceClient.ts +784 -780
  227. package/src/models/interfaces.ts +39 -27
  228. package/src/server/OxyServer.ts +227 -68
  229. package/src/server/agentAccount.ts +8 -2
  230. package/src/server/aliaMachineCredential.ts +57 -16
  231. package/src/server/auth.ts +9 -5
  232. package/src/server/capabilityTicket.ts +30 -21
  233. package/src/server/index.ts +29 -6
  234. package/src/server/liveCapabilityTicket.ts +31 -9
  235. package/src/server/middleware.ts +212 -121
  236. package/src/server/namespaces.ts +247 -94
  237. package/src/server/rateLimit.ts +2 -4
  238. package/src/server/requesterAssertion.ts +93 -54
  239. package/src/server/serviceTokenLifetime.ts +9 -4
  240. package/src/server/traffic.ts +117 -23
  241. package/src/server/trafficNodeHttp.ts +61 -14
  242. package/src/server/userInvalidation.ts +1 -4
  243. package/src/server/workloadIdentity.client.ts +12 -1
  244. package/src/server/workloadIdentity.ts +52 -5
  245. package/src/session/SessionClient.ts +170 -49
  246. package/src/session/accountDelegationAccess.ts +24 -31
  247. package/src/session/accountDialogController.ts +59 -14
  248. package/src/session/accountSwitchTargets.ts +10 -15
  249. package/src/session/authStateStore.ts +121 -81
  250. package/src/session/deviceDirectory.ts +1 -3
  251. package/src/session/identityPin.ts +10 -11
  252. package/src/session/identitySession.ts +3 -1
  253. package/src/session/index.ts +87 -82
  254. package/src/session/refresh.ts +138 -44
  255. package/src/session/sessionClientHost.ts +1 -3
  256. package/src/session/sharedDeviceCredential.ts +35 -14
  257. package/src/session/socketLoader.ts +9 -2
  258. package/src/shared/utils/errorUtils.ts +5 -2
  259. package/src/shared/utils/networkUtils.ts +5 -8
  260. package/src/shared/utils/themeUtils.ts +2 -6
  261. package/src/types/buffer.d.ts +66 -21
  262. package/src/types/expo-secure-store.d.ts +5 -1
  263. package/src/utils/accountCacheSweep.ts +4 -5
  264. package/src/utils/accountUtils.ts +107 -108
  265. package/src/utils/apiUtils.ts +3 -3
  266. package/src/utils/asyncUtils.ts +2 -2
  267. package/src/utils/authHelpers.ts +10 -5
  268. package/src/utils/avatarUtils.ts +11 -6
  269. package/src/utils/cache.ts +9 -10
  270. package/src/utils/cacheKey.ts +5 -1
  271. package/src/utils/coldBoot.ts +1 -3
  272. package/src/utils/commonsDelivery.ts +1 -4
  273. package/src/utils/deviceManager.ts +15 -14
  274. package/src/utils/errorUtils.ts +68 -86
  275. package/src/utils/languageUtils.ts +407 -58
  276. package/src/utils/oauthPkce.ts +1 -4
  277. package/src/utils/platform.ts +0 -1
  278. package/src/utils/profileLinks.ts +2 -5
  279. package/src/utils/requestUtils.ts +18 -14
  280. package/src/utils/sessionUtils.ts +34 -33
  281. package/src/utils/userHandle.ts +3 -1
  282. package/src/utils/userIdentity.ts +1 -1
  283. package/src/utils/validationUtils.ts +11 -9
@@ -67,7 +67,9 @@ interface ContainerCredentials {
67
67
  * checkout has no attestation to offer and must fall back to a credential.
68
68
  */
69
69
  export function canAttestWorkloadIdentity(env: NodeJS.ProcessEnv = process.env): boolean {
70
- return Boolean(env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || env.AWS_CONTAINER_CREDENTIALS_FULL_URI);
70
+ return Boolean(
71
+ env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || env.AWS_CONTAINER_CREDENTIALS_FULL_URI,
72
+ );
71
73
  }
72
74
 
73
75
  async function containerCredentials(fetchImpl: typeof fetch): Promise<ContainerCredentials> {
@@ -108,8 +110,15 @@ function sha256Hex(value: string): string {
108
110
  * attestation whose signature does not cover it, because a nonce the signature
109
111
  * does not cover can be swapped by whoever captured the attestation.
110
112
  */
111
- function signGetCallerIdentity(credentials: ContainerCredentials, nonce: string, now: Date): Record<string, string> {
112
- const amzDate = now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
113
+ function signGetCallerIdentity(
114
+ credentials: ContainerCredentials,
115
+ nonce: string,
116
+ now: Date,
117
+ ): Record<string, string> {
118
+ const amzDate = now
119
+ .toISOString()
120
+ .replace(/[-:]/g, '')
121
+ .replace(/\.\d{3}/, '');
113
122
  const dateStamp = amzDate.slice(0, 8);
114
123
 
115
124
  const headers: Record<string, string> = {
@@ -135,7 +144,10 @@ function signGetCallerIdentity(credentials: ContainerCredentials, nonce: string,
135
144
  const scope = `${dateStamp}/${STS_REGION}/sts/aws4_request`;
136
145
  const stringToSign = ['AWS4-HMAC-SHA256', amzDate, scope, sha256Hex(canonicalRequest)].join('\n');
137
146
 
138
- const signingKey = hmac(hmac(hmac(hmac(`AWS4${credentials.SecretAccessKey}`, dateStamp), STS_REGION), 'sts'), 'aws4_request');
147
+ const signingKey = hmac(
148
+ hmac(hmac(hmac(`AWS4${credentials.SecretAccessKey}`, dateStamp), STS_REGION), 'sts'),
149
+ 'aws4_request',
150
+ );
139
151
  const signature = createHmac('sha256', signingKey).update(stringToSign, 'utf8').digest('hex');
140
152
 
141
153
  return {
@@ -147,11 +159,46 @@ function signGetCallerIdentity(credentials: ContainerCredentials, nonce: string,
147
159
  }
148
160
 
149
161
  /** The AWS attestation: signed headers, and nothing else. */
150
- async function awsContainerAttestation(fetchImpl: typeof fetch, nonce: string): Promise<{ headers: Record<string, string> }> {
162
+ async function awsContainerAttestation(
163
+ fetchImpl: typeof fetch,
164
+ nonce: string,
165
+ ): Promise<{ headers: Record<string, string> }> {
151
166
  const credentials = await containerCredentials(fetchImpl);
152
167
  return { headers: signGetCallerIdentity(credentials, nonce, new Date()) };
153
168
  }
154
169
 
170
+ /** A proof of this process's identity, exactly as `POST /auth/service-token/workload` takes it. */
171
+ export interface WorkloadAttestation {
172
+ provider: 'aws-iam';
173
+ attestation: { headers: Record<string, string> };
174
+ }
175
+
176
+ /**
177
+ * Signs this process's identity proof over a nonce the CALLER chose, and sends
178
+ * it nowhere.
179
+ *
180
+ * {@link requestWorkloadServiceToken} is the ordinary consumer: Oxy issues the
181
+ * nonce and replays the proof. This exists for the one host that is its own
182
+ * verifier — Oxy's API acting as an application it hosts (Inbox) needs to learn
183
+ * which role it runs as, and the honest way to learn that is the way it learns
184
+ * anyone else's: sign `GetCallerIdentity` and let STS say whose signature it is.
185
+ * Reading a role name out of configuration would be the process's own word for
186
+ * what it is, which is what ADR 0026 refuses from every other caller.
187
+ *
188
+ * The same signer, so the two can never disagree about the shape the verifier
189
+ * accepts. Throws exactly as the token path does when there is no identity here.
190
+ */
191
+ export async function attestWorkloadIdentity(options: {
192
+ nonce: string;
193
+ fetch?: typeof fetch;
194
+ }): Promise<WorkloadAttestation> {
195
+ if (!options.nonce) throw new Error('A workload attestation needs a nonce to sign.');
196
+ return {
197
+ provider: 'aws-iam',
198
+ attestation: await awsContainerAttestation(options.fetch ?? fetch, options.nonce),
199
+ };
200
+ }
201
+
155
202
  /**
156
203
  * Exchanges this process's identity for an Oxy service token.
157
204
  *
@@ -45,7 +45,12 @@ export interface DeviceCredential {
45
45
  }
46
46
 
47
47
  export interface SessionClientHost {
48
- makeRequest<T>(method: 'GET' | 'POST', url: string, data?: unknown, options?: { cache?: boolean }): Promise<T>;
48
+ makeRequest<T>(
49
+ method: 'GET' | 'POST',
50
+ url: string,
51
+ data?: unknown,
52
+ options?: { cache?: boolean },
53
+ ): Promise<T>;
49
54
  getBaseURL(): string;
50
55
  getAccessToken(): string | null;
51
56
  /** Zero-cookie device credential for socket handshake when no bearer is planted yet. */
@@ -146,7 +151,8 @@ export class SessionClient {
146
151
  private tokenUnsub: (() => void) | null = null;
147
152
  private started = false;
148
153
  private lifecycleGeneration = 0;
149
- private fullSignOutPersistence: { state: DeviceSessionState; pending: Promise<void> } | null = null;
154
+ private fullSignOutPersistence: { state: DeviceSessionState; pending: Promise<void> } | null =
155
+ null;
150
156
  /** Same-origin cross-tab state-propagation channel; null on platforms without BroadcastChannel. */
151
157
  private channel: SessionBroadcastChannel | null = null;
152
158
  /** App-facing subscriptions to named server-pushed socket events. */
@@ -171,10 +177,16 @@ export class SessionClient {
171
177
  return pending;
172
178
  }
173
179
 
174
- private async requestDevice<T>(method: 'GET' | 'POST', url: string, data?: unknown, requestOptions?: { cache?: boolean }): Promise<T> {
180
+ private async requestDevice<T>(
181
+ method: 'GET' | 'POST',
182
+ url: string,
183
+ data?: unknown,
184
+ requestOptions?: { cache?: boolean },
185
+ ): Promise<T> {
175
186
  const generation = this.lifecycleGeneration;
176
187
  const response = await this.host.makeRequest<T>(method, url, data, requestOptions);
177
- if (generation !== this.lifecycleGeneration) throw new Error('Device session request was superseded');
188
+ if (generation !== this.lifecycleGeneration)
189
+ throw new Error('Device session request was superseded');
178
190
  return response;
179
191
  }
180
192
 
@@ -271,7 +283,11 @@ export class SessionClient {
271
283
  try {
272
284
  listener(payload);
273
285
  } catch (error) {
274
- logger.warn('[SessionClient] server-event listener threw', { component: 'SessionClient' }, error);
286
+ logger.warn(
287
+ '[SessionClient] server-event listener threw',
288
+ { component: 'SessionClient' },
289
+ error,
290
+ );
275
291
  }
276
292
  }
277
293
  });
@@ -312,7 +328,11 @@ export class SessionClient {
312
328
  * BEFORE `notify()`. This covers EVERY notify source (a switch push, a
313
329
  * cross-device push, a cold mint), not just the initial "no bearer yet" case.
314
330
  */
315
- protected applyState(raw: unknown, origin: SessionStateOrigin = 'push', activeToken?: string): boolean {
331
+ protected applyState(
332
+ raw: unknown,
333
+ origin: SessionStateOrigin = 'push',
334
+ activeToken?: string,
335
+ ): boolean {
316
336
  const generation = this.lifecycleGeneration;
317
337
  const next = safeParseContract(deviceSessionStateSchema, raw);
318
338
  if (!next) {
@@ -372,7 +392,8 @@ export class SessionClient {
372
392
  const needsMintBeforeNotify =
373
393
  transport != null &&
374
394
  next.accounts.length > 0 &&
375
- (activeAccountId === null || computeIdentityTag(this.host.getAccessToken()) !== activeAccountId);
395
+ (activeAccountId === null ||
396
+ computeIdentityTag(this.host.getAccessToken()) !== activeAccountId);
376
397
 
377
398
  const publish = (): void => {
378
399
  if (generation !== this.lifecycleGeneration || this.state !== next) return;
@@ -414,18 +435,26 @@ export class SessionClient {
414
435
  // Warm cold boot may fetch authoritative empty state before it ever had a
415
436
  // local projection. Its authenticated holder is history too. Decoding only
416
437
  // identifies local history; authority remains the validated REST response.
417
- const warmHolderEnded = previousState === null && origin === 'request' &&
418
- typeof bearerAccount === 'string' && bearerAccount.length > 0 &&
438
+ const warmHolderEnded =
439
+ previousState === null &&
440
+ origin === 'request' &&
441
+ typeof bearerAccount === 'string' &&
442
+ bearerAccount.length > 0 &&
419
443
  this.host.getDeviceCredential()?.deviceId === next.deviceId;
420
444
  if (
421
- ((previousState?.deviceId === next.deviceId && previousState.accounts.length > 0) || warmHolderEnded) &&
422
- next.accounts.length === 0 && pinnedAccountId === null && this.options.onFullExplicitSignOut
445
+ ((previousState?.deviceId === next.deviceId && previousState.accounts.length > 0) ||
446
+ warmHolderEnded) &&
447
+ next.accounts.length === 0 &&
448
+ pinnedAccountId === null &&
449
+ this.options.onFullExplicitSignOut
423
450
  ) {
424
451
  try {
425
- void this.persistFullSignOut().then(finishApply).catch((error) => {
426
- logger.error('[SessionClient] failed to persist received full sign-out', error);
427
- finishApply();
428
- });
452
+ void this.persistFullSignOut()
453
+ .then(finishApply)
454
+ .catch((error) => {
455
+ logger.error('[SessionClient] failed to persist received full sign-out', error);
456
+ finishApply();
457
+ });
429
458
  } catch (error) {
430
459
  logger.error('[SessionClient] failed to persist received full sign-out', error);
431
460
  finishApply();
@@ -434,17 +463,28 @@ export class SessionClient {
434
463
  }
435
464
 
436
465
  if (needsMintBeforeNotify) {
437
- void transport.ensureActiveToken(next).then(finishApply).catch((error) => {
438
- if (generation !== this.lifecycleGeneration) return;
439
- logger.warn('[SessionClient] ensureActiveToken failed — reverting session state', { component: 'SessionClient' }, error);
440
- // Do NOT notify under a mismatched bearer. Revert to the last applied
441
- // state so subscribers keep observing the account whose token is planted.
442
- this.state = previousState ?? null;
443
- });
466
+ void transport
467
+ .ensureActiveToken(next)
468
+ .then(finishApply)
469
+ .catch((error) => {
470
+ if (generation !== this.lifecycleGeneration) return;
471
+ logger.warn(
472
+ '[SessionClient] ensureActiveToken failed — reverting session state',
473
+ { component: 'SessionClient' },
474
+ error,
475
+ );
476
+ // Do NOT notify under a mismatched bearer. Revert to the last applied
477
+ // state so subscribers keep observing the account whose token is planted.
478
+ this.state = previousState ?? null;
479
+ });
444
480
  } else {
445
481
  if (transport) {
446
482
  void transport.ensureActiveToken(next).catch((error) => {
447
- logger.warn('[SessionClient] ensureActiveToken failed', { component: 'SessionClient' }, error);
483
+ logger.warn(
484
+ '[SessionClient] ensureActiveToken failed',
485
+ { component: 'SessionClient' },
486
+ error,
487
+ );
448
488
  });
449
489
  }
450
490
  finishApply();
@@ -470,15 +510,25 @@ export class SessionClient {
470
510
  ? []
471
511
  : parsed.error.issues.map((issue) =>
472
512
  issue.code === 'invalid_type'
473
- ? { path: issue.path.join('.'), code: issue.code, expected: issue.expected, received: issue.received }
513
+ ? {
514
+ path: issue.path.join('.'),
515
+ code: issue.code,
516
+ expected: issue.expected,
517
+ received: issue.received,
518
+ }
474
519
  : { path: issue.path.join('.'), code: issue.code },
475
520
  );
476
521
  const keys = raw && typeof raw === 'object' ? Object.keys(raw) : [];
477
- logger.warn('[SessionClient] discarded invalid session sync', { component: 'SessionClient', issues, keys });
522
+ logger.warn('[SessionClient] discarded invalid session sync', {
523
+ component: 'SessionClient',
524
+ issues,
525
+ keys,
526
+ });
478
527
  return;
479
528
  }
480
529
  this.commitSync(sync);
481
- if (this.fullSignOutPersistence?.state === this.state) await this.fullSignOutPersistence.pending;
530
+ if (this.fullSignOutPersistence?.state === this.state)
531
+ await this.fullSignOutPersistence.pending;
482
532
  }
483
533
 
484
534
  /**
@@ -563,7 +613,9 @@ export class SessionClient {
563
613
 
564
614
  /** `GET /session/device/directory` → {@link applyDirectory}. No notify. */
565
615
  private async fetchDirectory(): Promise<boolean> {
566
- const res = await this.requestDevice<unknown>('GET', '/session/device/directory', undefined, { cache: false });
616
+ const res = await this.requestDevice<unknown>('GET', '/session/device/directory', undefined, {
617
+ cache: false,
618
+ });
567
619
  return this.applyDirectory(res);
568
620
  }
569
621
 
@@ -593,7 +645,11 @@ export class SessionClient {
593
645
  return this.fetchDirectory().then(
594
646
  () => undefined,
595
647
  (error: unknown) => {
596
- logger.warn('[SessionClient] directory refresh failed', { component: 'SessionClient' }, error);
648
+ logger.warn(
649
+ '[SessionClient] directory refresh failed',
650
+ { component: 'SessionClient' },
651
+ error,
652
+ );
597
653
  },
598
654
  );
599
655
  }
@@ -625,7 +681,10 @@ export class SessionClient {
625
681
  * `activeToken: null` is not an error — it is an identity-pinned client, or a
626
682
  * caller whose application is not entitled to a bearer for the new context.
627
683
  */
628
- private plantActiveContextToken(directory: DeviceDirectory, accessToken: string | undefined): void {
684
+ private plantActiveContextToken(
685
+ directory: DeviceDirectory,
686
+ accessToken: string | undefined,
687
+ ): void {
629
688
  if (!accessToken) {
630
689
  return;
631
690
  }
@@ -674,12 +733,18 @@ export class SessionClient {
674
733
  try {
675
734
  await this.bootstrap();
676
735
  } catch (error) {
677
- logger.warn('[SessionClient] flat-state reconcile after activation failed', { component: 'SessionClient' }, error);
736
+ logger.warn(
737
+ '[SessionClient] flat-state reconcile after activation failed',
738
+ { component: 'SessionClient' },
739
+ error,
740
+ );
678
741
  }
679
742
  }
680
743
 
681
744
  async bootstrap(): Promise<void> {
682
- const res = await this.requestDevice<unknown>('GET', '/session/device/state', undefined, { cache: false });
745
+ const res = await this.requestDevice<unknown>('GET', '/session/device/state', undefined, {
746
+ cache: false,
747
+ });
683
748
  await this.applySync(res);
684
749
  }
685
750
 
@@ -726,7 +791,12 @@ export class SessionClient {
726
791
  * compatibility path for callers still keyed on account ids.
727
792
  */
728
793
  async activateContext(contextId: string): Promise<void> {
729
- const res = await this.requestDevice<unknown>('POST', '/session/device/activate', { contextId }, { cache: false });
794
+ const res = await this.requestDevice<unknown>(
795
+ 'POST',
796
+ '/session/device/activate',
797
+ { contextId },
798
+ { cache: false },
799
+ );
730
800
  const activation = safeParseContract(deviceActivateResponseSchema, res);
731
801
  if (!activation) {
732
802
  logger.warn('[SessionClient] discarded invalid activation response');
@@ -753,7 +823,12 @@ export class SessionClient {
753
823
  }
754
824
 
755
825
  async switchAccount(accountId: string): Promise<void> {
756
- const res = await this.requestDevice<unknown>('POST', '/session/device/switch', { accountId }, { cache: false });
826
+ const res = await this.requestDevice<unknown>(
827
+ 'POST',
828
+ '/session/device/switch',
829
+ { accountId },
830
+ { cache: false },
831
+ );
757
832
  await this.applySync(res);
758
833
  this.postCommitPing();
759
834
  }
@@ -770,7 +845,9 @@ export class SessionClient {
770
845
  * {@link signOutPrincipal} for the two that can tell those apart.
771
846
  */
772
847
  async signOut(target: { accountId: string } | { all: true }): Promise<void> {
773
- const res = await this.requestDevice<unknown>('POST', '/session/device/signout', target, { cache: false });
848
+ const res = await this.requestDevice<unknown>('POST', '/session/device/signout', target, {
849
+ cache: false,
850
+ });
774
851
  await this.applySync(res);
775
852
  this.postCommitPing();
776
853
  if (this.state?.accounts.length === 0) await this.persistFullSignOut();
@@ -814,16 +891,24 @@ export class SessionClient {
814
891
  * re-derived — including the equal-revision plant when a socket push already
815
892
  * applied this revision.
816
893
  */
817
- private async removeFromDevice(target: { contextId: string } | { principalId: string }): Promise<void> {
818
- const res = await this.requestDevice<unknown>('POST', '/session/device/signout', target, { cache: false });
894
+ private async removeFromDevice(
895
+ target: { contextId: string } | { principalId: string },
896
+ ): Promise<void> {
897
+ const res = await this.requestDevice<unknown>('POST', '/session/device/signout', target, {
898
+ cache: false,
899
+ });
819
900
  const removal = safeParseContract(deviceDirectorySyncSchema, res);
820
901
  if (!removal) {
821
902
  logger.warn('[SessionClient] discarded invalid device removal response');
822
903
  return;
823
904
  }
824
905
  const directoryApplied = this.applyDirectory(removal.directory);
825
- const stateApplied = this.commitSync({ state: removal.state, activeToken: removal.activeToken });
826
- if (this.fullSignOutPersistence?.state === this.state) await this.fullSignOutPersistence.pending;
906
+ const stateApplied = this.commitSync({
907
+ state: removal.state,
908
+ activeToken: removal.activeToken,
909
+ });
910
+ if (this.fullSignOutPersistence?.state === this.state)
911
+ await this.fullSignOutPersistence.pending;
827
912
  // `commitSync` publishes whenever the flat state moved. When only the
828
913
  // directory did — a socket push already applied this revision — the
829
914
  // directory half would otherwise never reach a subscriber.
@@ -835,7 +920,9 @@ export class SessionClient {
835
920
  }
836
921
 
837
922
  async addCurrentAccount(): Promise<void> {
838
- const res = await this.requestDevice<unknown>('POST', '/session/device/add', undefined, { cache: false });
923
+ const res = await this.requestDevice<unknown>('POST', '/session/device/add', undefined, {
924
+ cache: false,
925
+ });
839
926
  await this.applySync(res);
840
927
  this.postCommitPing();
841
928
  }
@@ -894,7 +981,11 @@ export class SessionClient {
894
981
  try {
895
982
  await this.bootstrap();
896
983
  } catch (error) {
897
- logger.warn('[SessionClient] bootstrap during start failed (non-fatal)', { component: 'SessionClient' }, error);
984
+ logger.warn(
985
+ '[SessionClient] bootstrap during start failed (non-fatal)',
986
+ { component: 'SessionClient' },
987
+ error,
988
+ );
898
989
  }
899
990
  }
900
991
  await this.connectSocket();
@@ -911,7 +1002,11 @@ export class SessionClient {
911
1002
  try {
912
1003
  this.channel.close();
913
1004
  } catch (error) {
914
- logger.debug('[SessionClient] BroadcastChannel close failed', { component: 'SessionClient' }, error);
1005
+ logger.debug(
1006
+ '[SessionClient] BroadcastChannel close failed',
1007
+ { component: 'SessionClient' },
1008
+ error,
1009
+ );
915
1010
  }
916
1011
  this.channel = null;
917
1012
  }
@@ -928,7 +1023,9 @@ export class SessionClient {
928
1023
  // if THAT yields nothing — only when no factory was injected.
929
1024
  const io = this.options.socketFactory ?? (await getSocketIO());
930
1025
  if (!io) {
931
- logger.warn('[SessionClient] no socket.io-client; running REST-only (no realtime sync)', { component: 'SessionClient' });
1026
+ logger.warn('[SessionClient] no socket.io-client; running REST-only (no realtime sync)', {
1027
+ component: 'SessionClient',
1028
+ });
932
1029
  return;
933
1030
  }
934
1031
  if (!this.started) return; // stopped while the dynamic import was in flight
@@ -977,7 +1074,11 @@ export class SessionClient {
977
1074
  const active = this.state?.activeAccountId ?? null;
978
1075
  if (active && active !== this.host.getCurrentAccountId() && this.host.getAccessToken()) {
979
1076
  void this.bootstrap().catch((error) => {
980
- logger.warn('[SessionClient] post-push token fetch failed', { component: 'SessionClient' }, error);
1077
+ logger.warn(
1078
+ '[SessionClient] post-push token fetch failed',
1079
+ { component: 'SessionClient' },
1080
+ error,
1081
+ );
981
1082
  });
982
1083
  }
983
1084
  });
@@ -1011,7 +1112,9 @@ export class SessionClient {
1011
1112
  private onSessionAccountsChanged(payload: unknown): void {
1012
1113
  const event = safeParseContract(sessionAccountsChangedEventSchema, payload);
1013
1114
  if (!event) {
1014
- logger.warn('[SessionClient] discarded invalid session_accounts_changed', { component: 'SessionClient' });
1115
+ logger.warn('[SessionClient] discarded invalid session_accounts_changed', {
1116
+ component: 'SessionClient',
1117
+ });
1015
1118
  return;
1016
1119
  }
1017
1120
  // The socket is in `user:<activeUserId>` for the planted bearer, so this should
@@ -1019,7 +1122,11 @@ export class SessionClient {
1019
1122
  if (event.userId !== this.host.getCurrentAccountId()) return;
1020
1123
  if (!this.host.getAccessToken()) return;
1021
1124
  void this.bootstrap().catch((error) => {
1022
- logger.warn('[SessionClient] session_accounts_changed refetch failed', { component: 'SessionClient' }, error);
1125
+ logger.warn(
1126
+ '[SessionClient] session_accounts_changed refetch failed',
1127
+ { component: 'SessionClient' },
1128
+ error,
1129
+ );
1023
1130
  });
1024
1131
  }
1025
1132
 
@@ -1032,13 +1139,19 @@ export class SessionClient {
1032
1139
  */
1033
1140
  private openBroadcastChannel(): void {
1034
1141
  if (this.channel) return;
1035
- const Ctor = (globalThis as { BroadcastChannel?: new (name: string) => SessionBroadcastChannel }).BroadcastChannel;
1142
+ const Ctor = (
1143
+ globalThis as { BroadcastChannel?: new (name: string) => SessionBroadcastChannel }
1144
+ ).BroadcastChannel;
1036
1145
  if (typeof Ctor !== 'function') return; // native RN / SSR — feature absent
1037
1146
  let channel: SessionBroadcastChannel;
1038
1147
  try {
1039
1148
  channel = new Ctor(SESSION_BROADCAST_CHANNEL);
1040
1149
  } catch (error) {
1041
- logger.debug('[SessionClient] BroadcastChannel unavailable', { component: 'SessionClient' }, error);
1150
+ logger.debug(
1151
+ '[SessionClient] BroadcastChannel unavailable',
1152
+ { component: 'SessionClient' },
1153
+ error,
1154
+ );
1042
1155
  return;
1043
1156
  }
1044
1157
  channel.onmessage = (event) => {
@@ -1049,7 +1162,11 @@ export class SessionClient {
1049
1162
  // re-sync does not re-post, so there is no cross-tab ping loop.
1050
1163
  if (this.host.getAccessToken()) {
1051
1164
  void this.bootstrap().catch((error) => {
1052
- logger.warn('[SessionClient] broadcast re-sync failed', { component: 'SessionClient' }, error);
1165
+ logger.warn(
1166
+ '[SessionClient] broadcast re-sync failed',
1167
+ { component: 'SessionClient' },
1168
+ error,
1169
+ );
1053
1170
  });
1054
1171
  }
1055
1172
  };
@@ -1068,7 +1185,11 @@ export class SessionClient {
1068
1185
  try {
1069
1186
  this.channel.postMessage({ type: 'commit', at: Date.now() });
1070
1187
  } catch (error) {
1071
- logger.debug('[SessionClient] BroadcastChannel post failed', { component: 'SessionClient' }, error);
1188
+ logger.debug(
1189
+ '[SessionClient] BroadcastChannel post failed',
1190
+ { component: 'SessionClient' },
1191
+ error,
1192
+ );
1072
1193
  }
1073
1194
  }
1074
1195
  }
@@ -14,24 +14,20 @@
14
14
  * API's already-resolved relationship and effective permissions identically.
15
15
  */
16
16
 
17
- import { isDelegatedActAsEligibleKind } from "@oxy.so/contracts";
18
- import type {
19
- AccountKind,
20
- AccountMember,
21
- AccountRelationship,
22
- } from "../api/accounts";
17
+ import { isDelegatedActAsEligibleKind } from '@oxy.so/contracts';
18
+ import type { AccountKind, AccountMember, AccountRelationship } from '../api/accounts';
23
19
 
24
20
  export interface AccountDelegationAccess {
25
- /** The caller owns the account or has a live membership in it. */
26
- readonly hasStanding: boolean;
27
- /** The account may be delegated and the caller holds effective `account:act_as`. */
28
- readonly canActAs: boolean;
21
+ /** The caller owns the account or has a live membership in it. */
22
+ readonly hasStanding: boolean;
23
+ /** The account may be delegated and the caller holds effective `account:act_as`. */
24
+ readonly canActAs: boolean;
29
25
  }
30
26
 
31
27
  export type AccountDelegationNode = {
32
- kind?: AccountKind | null;
33
- relationship?: AccountRelationship;
34
- callerMembership?: AccountMember | null;
28
+ kind?: AccountKind | null;
29
+ relationship?: AccountRelationship;
30
+ callerMembership?: AccountMember | null;
35
31
  };
36
32
 
37
33
  /**
@@ -47,26 +43,23 @@ export type AccountDelegationNode = {
47
43
  * authoritative: an explicit `account:act_as` revoke must remain a revoke.
48
44
  */
49
45
  export function resolveAccountDelegationAccess(
50
- node: AccountDelegationNode,
46
+ node: AccountDelegationNode,
51
47
  ): AccountDelegationAccess {
52
- const membership = node.callerMembership;
53
- const activeMembership = membership?.status === "active";
54
- const hasStanding =
55
- node.relationship === "self" ||
56
- node.relationship === "owner" ||
57
- activeMembership;
48
+ const membership = node.callerMembership;
49
+ const activeMembership = membership?.status === 'active';
50
+ const hasStanding =
51
+ node.relationship === 'self' || node.relationship === 'owner' || activeMembership;
58
52
 
59
- if (!hasStanding || !isDelegatedActAsEligibleKind(node.kind)) {
60
- return { hasStanding, canActAs: false };
61
- }
53
+ if (!hasStanding || !isDelegatedActAsEligibleKind(node.kind)) {
54
+ return { hasStanding, canActAs: false };
55
+ }
62
56
 
63
- if (membership !== null && membership !== undefined) {
64
- return {
65
- hasStanding,
66
- canActAs:
67
- activeMembership && membership.permissions.includes("account:act_as"),
68
- };
69
- }
57
+ if (membership !== null && membership !== undefined) {
58
+ return {
59
+ hasStanding,
60
+ canActAs: activeMembership && membership.permissions.includes('account:act_as'),
61
+ };
62
+ }
70
63
 
71
- return { hasStanding, canActAs: node.relationship === "owner" };
64
+ return { hasStanding, canActAs: node.relationship === 'owner' };
72
65
  }