@oxy.so/core 4.5.0-oxy-one.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cjs/HttpService.js +63 -19
- package/dist/cjs/api/accounts.js +10 -3
- package/dist/cjs/api/agency.js +3 -1
- package/dist/cjs/api/apps.js +15 -4
- package/dist/cjs/api/assets.js +49 -12
- package/dist/cjs/api/auth.js +37 -11
- package/dist/cjs/api/billing.js +24 -7
- package/dist/cjs/api/civic.js +14 -4
- package/dist/cjs/api/follows.js +9 -2
- package/dist/cjs/api/identity.js +31 -9
- package/dist/cjs/api/nodes.js +3 -1
- package/dist/cjs/api/privacy.js +8 -2
- package/dist/cjs/api/session.js +15 -4
- package/dist/cjs/api/store.js +15 -5
- package/dist/cjs/api/topics.js +10 -3
- package/dist/cjs/api/users.js +16 -4
- package/dist/cjs/boot/sessionColdBoot.js +40 -13
- package/dist/cjs/constants/version.js +6 -6
- package/dist/cjs/crypto/identityMarker.js +3 -1
- package/dist/cjs/crypto/identityProof.js +6 -1
- package/dist/cjs/crypto/keyManager.js +87 -32
- package/dist/cjs/crypto/recoveryPhrase.js +2 -2
- package/dist/cjs/crypto/signatureService.js +3 -4
- package/dist/cjs/i18n/index.js +16 -3
- package/dist/cjs/i18n/locales/ar-SA.js +366 -366
- package/dist/cjs/i18n/locales/ca-ES.js +366 -366
- package/dist/cjs/i18n/locales/de-DE.js +366 -366
- package/dist/cjs/i18n/locales/en-US.js +1994 -1994
- package/dist/cjs/i18n/locales/es-ES.js +1994 -1994
- package/dist/cjs/i18n/locales/fr-FR.js +366 -366
- package/dist/cjs/i18n/locales/it-IT.js +366 -366
- package/dist/cjs/i18n/locales/ja-JP.js +366 -366
- package/dist/cjs/i18n/locales/ko-KR.js +366 -366
- package/dist/cjs/i18n/locales/pt-PT.js +366 -366
- package/dist/cjs/i18n/locales/zh-CN.js +366 -366
- package/dist/cjs/inference/OxyInferenceClient.js +12 -18
- package/dist/cjs/models/interfaces.js +11 -11
- package/dist/cjs/server/OxyServer.js +86 -28
- package/dist/cjs/server/agentAccount.js +6 -2
- package/dist/cjs/server/aliaMachineCredential.js +39 -14
- package/dist/cjs/server/auth.js +5 -4
- package/dist/cjs/server/capabilityTicket.js +5 -5
- package/dist/cjs/server/index.js +2 -1
- package/dist/cjs/server/liveCapabilityTicket.js +4 -1
- package/dist/cjs/server/middleware.js +61 -42
- package/dist/cjs/server/namespaces.js +28 -7
- package/dist/cjs/server/requesterAssertion.js +36 -30
- package/dist/cjs/server/serviceTokenLifetime.js +7 -4
- package/dist/cjs/server/traffic.js +53 -14
- package/dist/cjs/server/trafficNodeHttp.js +33 -8
- package/dist/cjs/server/workloadIdentity.client.js +4 -0
- package/dist/cjs/server/workloadIdentity.js +28 -1
- package/dist/cjs/session/SessionClient.js +54 -17
- package/dist/cjs/session/accountDelegationAccess.js +4 -6
- package/dist/cjs/session/accountDialogController.js +1 -1
- package/dist/cjs/session/authStateStore.js +18 -9
- package/dist/cjs/session/identityPin.js +6 -2
- package/dist/cjs/session/refresh.js +81 -23
- package/dist/cjs/session/sharedDeviceCredential.js +28 -15
- package/dist/cjs/shared/utils/errorUtils.js +1 -1
- package/dist/cjs/shared/utils/networkUtils.js +1 -1
- package/dist/cjs/shared/utils/themeUtils.js +1 -1
- package/dist/cjs/utils/accountCacheSweep.js +3 -1
- package/dist/cjs/utils/accountUtils.js +1 -3
- package/dist/cjs/utils/asyncUtils.js +1 -1
- package/dist/cjs/utils/authHelpers.js +2 -1
- package/dist/cjs/utils/avatarUtils.js +4 -2
- package/dist/cjs/utils/cache.js +1 -1
- package/dist/cjs/utils/cacheKey.js +3 -1
- package/dist/cjs/utils/deviceManager.js +4 -4
- package/dist/cjs/utils/errorUtils.js +6 -4
- package/dist/cjs/utils/languageUtils.js +404 -57
- package/dist/cjs/utils/profileLinks.js +1 -3
- package/dist/cjs/utils/requestUtils.js +7 -1
- package/dist/cjs/utils/userIdentity.js +1 -1
- package/dist/cjs/utils/validationUtils.js +5 -6
- package/dist/esm/HttpService.js +64 -20
- package/dist/esm/api/accounts.js +11 -4
- package/dist/esm/api/agency.js +3 -1
- package/dist/esm/api/apps.js +15 -4
- package/dist/esm/api/assets.js +49 -12
- package/dist/esm/api/auth.js +37 -11
- package/dist/esm/api/billing.js +25 -8
- package/dist/esm/api/civic.js +14 -4
- package/dist/esm/api/follows.js +9 -2
- package/dist/esm/api/identity.js +31 -9
- package/dist/esm/api/nodes.js +3 -1
- package/dist/esm/api/privacy.js +8 -2
- package/dist/esm/api/session.js +15 -4
- package/dist/esm/api/store.js +15 -5
- package/dist/esm/api/topics.js +10 -3
- package/dist/esm/api/users.js +16 -4
- package/dist/esm/boot/sessionColdBoot.js +41 -14
- package/dist/esm/constants/version.js +6 -6
- package/dist/esm/crypto/identityMarker.js +3 -1
- package/dist/esm/crypto/identityProof.js +6 -1
- package/dist/esm/crypto/internal.js +1 -1
- package/dist/esm/crypto/keyManager.js +87 -32
- package/dist/esm/crypto/recoveryPhrase.js +2 -2
- package/dist/esm/crypto/signatureService.js +3 -4
- package/dist/esm/i18n/index.js +16 -3
- package/dist/esm/i18n/locales/ar-SA.js +366 -366
- package/dist/esm/i18n/locales/ca-ES.js +366 -366
- package/dist/esm/i18n/locales/de-DE.js +366 -366
- package/dist/esm/i18n/locales/en-US.js +1994 -1994
- package/dist/esm/i18n/locales/es-ES.js +1994 -1994
- package/dist/esm/i18n/locales/fr-FR.js +366 -366
- package/dist/esm/i18n/locales/it-IT.js +366 -366
- package/dist/esm/i18n/locales/ja-JP.js +366 -366
- package/dist/esm/i18n/locales/ko-KR.js +366 -366
- package/dist/esm/i18n/locales/pt-PT.js +366 -366
- package/dist/esm/i18n/locales/zh-CN.js +366 -366
- package/dist/esm/index.js +1 -1
- package/dist/esm/inference/OxyInferenceClient.js +13 -19
- package/dist/esm/models/interfaces.js +11 -11
- package/dist/esm/server/OxyServer.js +87 -29
- package/dist/esm/server/agentAccount.js +6 -2
- package/dist/esm/server/aliaMachineCredential.js +39 -14
- package/dist/esm/server/auth.js +6 -5
- package/dist/esm/server/capabilityTicket.js +6 -6
- package/dist/esm/server/index.js +5 -5
- package/dist/esm/server/liveCapabilityTicket.js +6 -3
- package/dist/esm/server/middleware.js +62 -43
- package/dist/esm/server/namespaces.js +31 -10
- package/dist/esm/server/requesterAssertion.js +36 -30
- package/dist/esm/server/serviceTokenLifetime.js +7 -4
- package/dist/esm/server/traffic.js +55 -16
- package/dist/esm/server/trafficNodeHttp.js +34 -9
- package/dist/esm/server/userInvalidation.js +1 -1
- package/dist/esm/server/workloadIdentity.client.js +3 -0
- package/dist/esm/server/workloadIdentity.js +27 -1
- package/dist/esm/session/SessionClient.js +54 -17
- package/dist/esm/session/accountDelegationAccess.js +5 -7
- package/dist/esm/session/accountDialogController.js +1 -1
- package/dist/esm/session/authStateStore.js +18 -9
- package/dist/esm/session/identityPin.js +6 -2
- package/dist/esm/session/refresh.js +82 -24
- package/dist/esm/session/sharedDeviceCredential.js +28 -15
- package/dist/esm/shared/utils/errorUtils.js +1 -1
- package/dist/esm/shared/utils/networkUtils.js +1 -1
- package/dist/esm/shared/utils/themeUtils.js +1 -1
- package/dist/esm/utils/accountCacheSweep.js +3 -1
- package/dist/esm/utils/accountUtils.js +1 -3
- package/dist/esm/utils/asyncUtils.js +1 -1
- package/dist/esm/utils/authHelpers.js +2 -1
- package/dist/esm/utils/avatarUtils.js +4 -2
- package/dist/esm/utils/cache.js +1 -1
- package/dist/esm/utils/cacheKey.js +3 -1
- package/dist/esm/utils/deviceManager.js +4 -4
- package/dist/esm/utils/errorUtils.js +6 -4
- package/dist/esm/utils/languageUtils.js +404 -57
- package/dist/esm/utils/profileLinks.js +1 -3
- package/dist/esm/utils/requestUtils.js +7 -1
- package/dist/esm/utils/userIdentity.js +1 -1
- package/dist/esm/utils/validationUtils.js +5 -6
- package/dist/types/HttpService.d.ts +1 -1
- package/dist/types/api/accounts.d.ts +1 -1
- package/dist/types/api/apps.d.ts +4 -4
- package/dist/types/api/assets.d.ts +2 -0
- package/dist/types/api/auth.d.ts +8 -3
- package/dist/types/crypto/internal.d.ts +1 -1
- package/dist/types/index.d.ts +4 -4
- package/dist/types/server/index.d.ts +6 -6
- package/dist/types/server/requesterAssertion.d.ts +2 -2
- package/dist/types/server/workloadIdentity.client.d.ts +5 -1
- package/dist/types/server/workloadIdentity.d.ts +26 -0
- package/dist/types/session/accountDelegationAccess.d.ts +1 -1
- package/dist/types/session/index.d.ts +1 -1
- package/dist/types/utils/userIdentity.d.ts +1 -1
- package/dist/types/utils/validationUtils.d.ts +4 -5
- package/package.json +2 -3
- package/src/HttpService.ts +183 -53
- package/src/OxyServices.errors.ts +7 -3
- package/src/OxyServices.ts +6 -2
- package/src/api/accounts.ts +76 -26
- package/src/api/agency.ts +24 -9
- package/src/api/appData.ts +12 -2
- package/src/api/apps.ts +81 -31
- package/src/api/assets.ts +122 -40
- package/src/api/auth.ts +154 -79
- package/src/api/billing.ts +112 -65
- package/src/api/civic.ts +61 -19
- package/src/api/contacts.ts +4 -1
- package/src/api/devices.ts +54 -21
- package/src/api/follows.ts +88 -31
- package/src/api/identity.ts +101 -33
- package/src/api/linkedAccounts.ts +26 -8
- package/src/api/nodes.ts +18 -6
- package/src/api/notifications.ts +26 -6
- package/src/api/privacy.ts +42 -8
- package/src/api/reputation.ts +18 -7
- package/src/api/session.ts +23 -5
- package/src/api/store.ts +81 -31
- package/src/api/topics.ts +18 -6
- package/src/api/users.ts +98 -35
- package/src/boot/sessionColdBoot.ts +60 -19
- package/src/civic/payloads.ts +1 -4
- package/src/client/context.ts +6 -1
- package/src/constants/version.ts +7 -6
- package/src/crypto/aead.ts +1 -5
- package/src/crypto/ecdh.ts +1 -4
- package/src/crypto/identityDerivations.ts +5 -1
- package/src/crypto/identityMarker.ts +3 -1
- package/src/crypto/identityProof.ts +6 -1
- package/src/crypto/index.ts +11 -11
- package/src/crypto/internal.ts +6 -1
- package/src/crypto/keyManager.ts +348 -98
- package/src/crypto/recoveryPhrase.ts +14 -9
- package/src/crypto/registrationPow.ts +1 -1
- package/src/crypto/signatureService.ts +14 -6
- package/src/i18n/accountCategoryLabels.ts +1 -4
- package/src/i18n/accountRoleLabels.ts +1 -4
- package/src/i18n/index.ts +25 -7
- package/src/i18n/locales/ar-SA.ts +375 -366
- package/src/i18n/locales/ca-ES.ts +382 -366
- package/src/i18n/locales/de-DE.ts +387 -366
- package/src/i18n/locales/en-US.ts +2034 -1994
- package/src/i18n/locales/es-ES.ts +2043 -1994
- package/src/i18n/locales/fr-FR.ts +387 -366
- package/src/i18n/locales/it-IT.ts +380 -366
- package/src/i18n/locales/ja-JP.ts +380 -366
- package/src/i18n/locales/ko-KR.ts +377 -366
- package/src/i18n/locales/pt-PT.ts +383 -366
- package/src/i18n/locales/zh-CN.ts +373 -366
- package/src/i18n/trustTierLabels.ts +1 -4
- package/src/index.ts +371 -349
- package/src/inference/OxyInferenceClient.ts +784 -780
- package/src/models/interfaces.ts +30 -27
- package/src/server/OxyServer.ts +227 -68
- package/src/server/agentAccount.ts +8 -2
- package/src/server/aliaMachineCredential.ts +57 -16
- package/src/server/auth.ts +9 -5
- package/src/server/capabilityTicket.ts +30 -21
- package/src/server/index.ts +29 -6
- package/src/server/liveCapabilityTicket.ts +31 -9
- package/src/server/middleware.ts +212 -121
- package/src/server/namespaces.ts +247 -94
- package/src/server/rateLimit.ts +2 -4
- package/src/server/requesterAssertion.ts +93 -54
- package/src/server/serviceTokenLifetime.ts +9 -4
- package/src/server/traffic.ts +117 -23
- package/src/server/trafficNodeHttp.ts +61 -14
- package/src/server/userInvalidation.ts +1 -4
- package/src/server/workloadIdentity.client.ts +12 -1
- package/src/server/workloadIdentity.ts +52 -5
- package/src/session/SessionClient.ts +170 -49
- package/src/session/accountDelegationAccess.ts +24 -31
- package/src/session/accountDialogController.ts +59 -14
- package/src/session/accountSwitchTargets.ts +10 -15
- package/src/session/authStateStore.ts +121 -81
- package/src/session/deviceDirectory.ts +1 -3
- package/src/session/identityPin.ts +10 -11
- package/src/session/identitySession.ts +3 -1
- package/src/session/index.ts +87 -82
- package/src/session/refresh.ts +138 -44
- package/src/session/sessionClientHost.ts +1 -3
- package/src/session/sharedDeviceCredential.ts +35 -14
- package/src/session/socketLoader.ts +9 -2
- package/src/shared/utils/errorUtils.ts +5 -2
- package/src/shared/utils/networkUtils.ts +5 -8
- package/src/shared/utils/themeUtils.ts +2 -6
- package/src/types/buffer.d.ts +66 -21
- package/src/types/expo-secure-store.d.ts +5 -1
- package/src/utils/accountCacheSweep.ts +4 -5
- package/src/utils/accountUtils.ts +107 -108
- package/src/utils/apiUtils.ts +3 -3
- package/src/utils/asyncUtils.ts +2 -2
- package/src/utils/authHelpers.ts +10 -5
- package/src/utils/avatarUtils.ts +11 -6
- package/src/utils/cache.ts +9 -10
- package/src/utils/cacheKey.ts +5 -1
- package/src/utils/coldBoot.ts +1 -3
- package/src/utils/commonsDelivery.ts +1 -4
- package/src/utils/deviceManager.ts +15 -14
- package/src/utils/errorUtils.ts +68 -86
- package/src/utils/languageUtils.ts +407 -58
- package/src/utils/oauthPkce.ts +1 -4
- package/src/utils/platform.ts +0 -1
- package/src/utils/profileLinks.ts +2 -5
- package/src/utils/requestUtils.ts +18 -14
- package/src/utils/sessionUtils.ts +34 -33
- package/src/utils/userHandle.ts +3 -1
- package/src/utils/userIdentity.ts +1 -1
- 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(
|
|
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(
|
|
112
|
-
|
|
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(
|
|
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(
|
|
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>(
|
|
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 =
|
|
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>(
|
|
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)
|
|
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(
|
|
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(
|
|
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 ||
|
|
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 =
|
|
418
|
-
|
|
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) ||
|
|
422
|
-
|
|
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()
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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(
|
|
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
|
-
? {
|
|
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', {
|
|
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)
|
|
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, {
|
|
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(
|
|
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(
|
|
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(
|
|
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, {
|
|
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>(
|
|
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>(
|
|
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, {
|
|
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(
|
|
818
|
-
|
|
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({
|
|
826
|
-
|
|
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, {
|
|
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(
|
|
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(
|
|
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)', {
|
|
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(
|
|
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', {
|
|
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(
|
|
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 = (
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
46
|
+
node: AccountDelegationNode,
|
|
51
47
|
): AccountDelegationAccess {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
if (!hasStanding || !isDelegatedActAsEligibleKind(node.kind)) {
|
|
54
|
+
return { hasStanding, canActAs: false };
|
|
55
|
+
}
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
64
|
+
return { hasStanding, canActAs: node.relationship === 'owner' };
|
|
72
65
|
}
|