@oxy.so/core 7.11.1 → 7.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/HttpService.js +70 -1
- package/dist/cjs/OxyServices.js +1 -1
- package/dist/cjs/api/apps.js +1 -1
- package/dist/cjs/api/auth.js +1 -1
- package/dist/cjs/api/billing.js +4 -3
- package/dist/cjs/api/devices.js +5 -4
- package/dist/cjs/api/follows.js +11 -10
- package/dist/cjs/api/holderEvidence.js +12 -0
- package/dist/cjs/api/privacy.js +8 -7
- package/dist/cjs/api/session.js +128 -7
- package/dist/cjs/api/topics.js +4 -3
- package/dist/cjs/api/users.js +5 -4
- package/dist/cjs/i18n/locales/compact/es-ES.generated.js +1 -1
- package/dist/cjs/i18n/locales/en-US.js +40 -15
- package/dist/cjs/i18n/locales/es-ES.js +40 -15
- package/dist/cjs/index.js +6 -4
- package/dist/esm/HttpService.js +70 -1
- package/dist/esm/OxyServices.js +1 -1
- package/dist/esm/api/apps.js +1 -1
- package/dist/esm/api/auth.js +1 -1
- package/dist/esm/api/billing.js +4 -3
- package/dist/esm/api/devices.js +5 -4
- package/dist/esm/api/follows.js +11 -10
- package/dist/esm/api/holderEvidence.js +9 -0
- package/dist/esm/api/privacy.js +8 -7
- package/dist/esm/api/session.js +128 -7
- package/dist/esm/api/topics.js +4 -3
- package/dist/esm/api/users.js +5 -4
- package/dist/esm/i18n/locales/compact/es-ES.generated.js +1 -1
- package/dist/esm/i18n/locales/en-US.js +40 -15
- package/dist/esm/i18n/locales/es-ES.js +40 -15
- package/dist/esm/index.js +1 -0
- package/dist/types/HttpService.d.ts +25 -1
- package/dist/types/api/holderEvidence.d.ts +45 -0
- package/dist/types/api/session.d.ts +21 -0
- package/dist/types/i18n/locales/en-US.d.ts +37 -12
- package/dist/types/i18n/locales/es-ES.d.ts +37 -12
- package/dist/types/index.d.ts +3 -1
- package/dist/types/server/safety.d.ts +9 -9
- package/package.json +2 -2
- package/src/HttpService.ts +108 -1
- package/src/OxyServices.ts +4 -1
- package/src/api/apps.ts +1 -1
- package/src/api/auth.ts +9 -4
- package/src/api/billing.ts +12 -5
- package/src/api/devices.ts +6 -4
- package/src/api/follows.ts +12 -10
- package/src/api/holderEvidence.ts +66 -0
- package/src/api/privacy.ts +9 -7
- package/src/api/session.ts +163 -9
- package/src/api/topics.ts +5 -3
- package/src/api/users.ts +6 -4
- package/src/i18n/locales/compact/es-ES.generated.ts +1 -1
- package/src/i18n/locales/en-US.ts +41 -15
- package/src/i18n/locales/es-ES.ts +42 -15
- package/src/index.ts +8 -0
|
@@ -161,6 +161,9 @@ declare const dict: {
|
|
|
161
161
|
requestExpired: string;
|
|
162
162
|
secondFactorInvalid: string;
|
|
163
163
|
originNotAllowed: string;
|
|
164
|
+
network: string;
|
|
165
|
+
invalidInput: string;
|
|
166
|
+
server: string;
|
|
164
167
|
generic: string;
|
|
165
168
|
};
|
|
166
169
|
subtitleToApp: string;
|
|
@@ -223,6 +226,31 @@ declare const dict: {
|
|
|
223
226
|
invalidDescription: string;
|
|
224
227
|
};
|
|
225
228
|
};
|
|
229
|
+
actionErrors: {
|
|
230
|
+
updateProfile: string;
|
|
231
|
+
uploadAvatar: string;
|
|
232
|
+
updateSettings: string;
|
|
233
|
+
updatePrivacy: string;
|
|
234
|
+
updateNotifications: string;
|
|
235
|
+
updatePreferences: string;
|
|
236
|
+
revokeConnectedApp: string;
|
|
237
|
+
switchSession: string;
|
|
238
|
+
logout: string;
|
|
239
|
+
logoutAll: string;
|
|
240
|
+
updateDeviceName: string;
|
|
241
|
+
removeDevice: string;
|
|
242
|
+
};
|
|
243
|
+
usernamePolicy: {
|
|
244
|
+
hint: string;
|
|
245
|
+
required: string;
|
|
246
|
+
tooShort: string;
|
|
247
|
+
tooLong: string;
|
|
248
|
+
noSpaces: string;
|
|
249
|
+
characters: string;
|
|
250
|
+
onlyNumbers: string;
|
|
251
|
+
reserved: string;
|
|
252
|
+
mustEndInBot: string;
|
|
253
|
+
};
|
|
226
254
|
signup: {
|
|
227
255
|
title: string;
|
|
228
256
|
subtitle: string;
|
|
@@ -231,7 +259,6 @@ declare const dict: {
|
|
|
231
259
|
username: {
|
|
232
260
|
label: string;
|
|
233
261
|
placeholder: string;
|
|
234
|
-
required: string;
|
|
235
262
|
taken: string;
|
|
236
263
|
};
|
|
237
264
|
email: {
|
|
@@ -303,8 +330,6 @@ declare const dict: {
|
|
|
303
330
|
label: string;
|
|
304
331
|
placeholder: string;
|
|
305
332
|
choose: string;
|
|
306
|
-
required: string;
|
|
307
|
-
tooShort: string;
|
|
308
333
|
};
|
|
309
334
|
email: {
|
|
310
335
|
title: string;
|
|
@@ -893,7 +918,6 @@ declare const dict: {
|
|
|
893
918
|
firstPlaceholder: string;
|
|
894
919
|
lastLabel: string;
|
|
895
920
|
lastPlaceholder: string;
|
|
896
|
-
saveFailed: string;
|
|
897
921
|
};
|
|
898
922
|
avatar: {
|
|
899
923
|
title: string;
|
|
@@ -1828,9 +1852,6 @@ declare const dict: {
|
|
|
1828
1852
|
label: string;
|
|
1829
1853
|
available: string;
|
|
1830
1854
|
taken: string;
|
|
1831
|
-
tooShort: string;
|
|
1832
|
-
invalidChars: string;
|
|
1833
|
-
mustEndInBot: string;
|
|
1834
1855
|
checkFailed: string;
|
|
1835
1856
|
};
|
|
1836
1857
|
accountCategory: {
|
|
@@ -1865,14 +1886,14 @@ declare const dict: {
|
|
|
1865
1886
|
transfer: string;
|
|
1866
1887
|
};
|
|
1867
1888
|
errors: {
|
|
1868
|
-
|
|
1889
|
+
emailRequired: string;
|
|
1890
|
+
emailInvalid: string;
|
|
1869
1891
|
missingAccount: string;
|
|
1870
1892
|
noPermission: string;
|
|
1871
|
-
userNotFound: string;
|
|
1872
1893
|
};
|
|
1873
1894
|
invite: {
|
|
1874
1895
|
title: string;
|
|
1875
|
-
|
|
1896
|
+
emailLabel: string;
|
|
1876
1897
|
roleLabel: string;
|
|
1877
1898
|
submit: string;
|
|
1878
1899
|
};
|
|
@@ -1885,8 +1906,9 @@ declare const dict: {
|
|
|
1885
1906
|
description: string;
|
|
1886
1907
|
};
|
|
1887
1908
|
toasts: {
|
|
1888
|
-
|
|
1889
|
-
|
|
1909
|
+
invited: string;
|
|
1910
|
+
inviteFailed: string;
|
|
1911
|
+
invitationUpdateFailed: string;
|
|
1890
1912
|
removed: string;
|
|
1891
1913
|
removeFailed: string;
|
|
1892
1914
|
roleUpdated: string;
|
|
@@ -1948,6 +1970,7 @@ declare const dict: {
|
|
|
1948
1970
|
title: string;
|
|
1949
1971
|
warning: string;
|
|
1950
1972
|
confirmLabel: string;
|
|
1973
|
+
confirmMismatch: string;
|
|
1951
1974
|
confirm: string;
|
|
1952
1975
|
error: string;
|
|
1953
1976
|
handoff: {
|
|
@@ -2300,6 +2323,7 @@ declare const dict: {
|
|
|
2300
2323
|
newLabel: string;
|
|
2301
2324
|
repeatLabel: string;
|
|
2302
2325
|
tooShort: string;
|
|
2326
|
+
tooLong: string;
|
|
2303
2327
|
mismatch: string;
|
|
2304
2328
|
signOutOthers: string;
|
|
2305
2329
|
save: string;
|
|
@@ -2362,6 +2386,7 @@ declare const dict: {
|
|
|
2362
2386
|
request: string;
|
|
2363
2387
|
notYet: string;
|
|
2364
2388
|
remove: string;
|
|
2389
|
+
invalid: string;
|
|
2365
2390
|
};
|
|
2366
2391
|
remove: {
|
|
2367
2392
|
title: string;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -24,7 +24,9 @@ export { OxyServices, OXY_API_URL, OXY_CLOUD_URL } from './OxyServices';
|
|
|
24
24
|
export type { OxyConfig, LinkedHttpClient } from './OxyServices';
|
|
25
25
|
export { OxyApiError, OxyAuthenticationError, OxyAuthenticationTimeoutError, AssetUrlResolutionError, ServiceAssetMetadataError, } from './OxyServices.errors';
|
|
26
26
|
export type { HttpMethod } from './client/context';
|
|
27
|
-
export type { RequestOptions, ResponsePayload, ResponseRequest, ResponseTransport, AuthenticatedResponseRequest, AuthRefreshReason, AuthRefreshHandler, } from './HttpService';
|
|
27
|
+
export type { RequestOptions, ResponsePayload, ResponseRequest, ResponseTransport, AuthenticatedResponseRequest, AuthRefreshReason, AuthRefreshHandler, CredentialGeneration, } from './HttpService';
|
|
28
|
+
export { sameHolderIdentity } from './api/holderEvidence';
|
|
29
|
+
export type { HolderEvidence, HolderEvidenceIdentity, HolderEvidenceResult, HolderEvidenceUnavailableReason, } from './api/holderEvidence';
|
|
28
30
|
export type { SessionApi, DeviceCredentialProvider, SessionValidation } from './api/session';
|
|
29
31
|
export type { AuthApi, ChallengeResponse, RegistrationRequest, ChallengeVerifyRequest, PublicKeyCheckResponse, OAuthUserInfoResponse, OAuthTokenExchangeResult, CommonsSignInHandle, CommonsSignInStatus, CommonsSignInPurpose, CommonsOAuthContext, CommonsApprovalInfo, CommonsApprovalSubjectAccount, CommonsSignInActionResult, CommonsOAuthFinalizeResult, CommonsDeliveryResult, SignInDeviceOptions, ClaimedSession, } from './api/auth';
|
|
30
32
|
export type { UsersApi, ResolveExternalUserInput, DeleteAccountProof } from './api/users';
|
|
@@ -17,7 +17,7 @@ export declare class ServerSafetyApi extends SafetyApi {
|
|
|
17
17
|
/** The original personal bearer, a verified MCP connection proof, or explicit anonymous access. */
|
|
18
18
|
profileAccess(input: ProfileAccessInput, options?: SafetyRequester): Promise<{
|
|
19
19
|
allowed: boolean;
|
|
20
|
-
reason: "blocked" | "private" | "public" | "
|
|
20
|
+
reason: "blocked" | "private" | "public" | "unavailable" | "owner";
|
|
21
21
|
policyVersion: "1";
|
|
22
22
|
}>;
|
|
23
23
|
profileAccessBatch(input: ProfileAccessBatchInput, options: SafetyRequester): Promise<{
|
|
@@ -25,14 +25,14 @@ export declare class ServerSafetyApi extends SafetyApi {
|
|
|
25
25
|
subjectAccountId: string;
|
|
26
26
|
decision: {
|
|
27
27
|
allowed: boolean;
|
|
28
|
-
reason: "blocked" | "private" | "public" | "
|
|
28
|
+
reason: "blocked" | "private" | "public" | "unavailable" | "owner" | "allowed" | "unsupported_consumer" | "contact_not_approved" | "mature_content" | "unrated_content" | "approval_required";
|
|
29
29
|
policyVersion: "2";
|
|
30
30
|
};
|
|
31
31
|
}[];
|
|
32
32
|
}>;
|
|
33
33
|
messageAccess(input: MessageAccessInput, options: SafetyRequester): Promise<{
|
|
34
34
|
allowed: boolean;
|
|
35
|
-
reason: "blocked" | "private" | "public" | "
|
|
35
|
+
reason: "blocked" | "private" | "public" | "unavailable" | "owner" | "allowed" | "unsupported_consumer" | "contact_not_approved" | "mature_content" | "unrated_content" | "approval_required";
|
|
36
36
|
policyVersion: "2";
|
|
37
37
|
}>;
|
|
38
38
|
messageAccessBatch(input: MessageAccessBatchInput, options: SafetyRequester): Promise<{
|
|
@@ -40,7 +40,7 @@ export declare class ServerSafetyApi extends SafetyApi {
|
|
|
40
40
|
resourceId: string;
|
|
41
41
|
decision: {
|
|
42
42
|
allowed: boolean;
|
|
43
|
-
reason: "blocked" | "private" | "public" | "
|
|
43
|
+
reason: "blocked" | "private" | "public" | "unavailable" | "owner" | "allowed" | "unsupported_consumer" | "contact_not_approved" | "mature_content" | "unrated_content" | "approval_required";
|
|
44
44
|
policyVersion: "2";
|
|
45
45
|
};
|
|
46
46
|
}[];
|
|
@@ -60,7 +60,7 @@ export declare class ServerSafetyApi extends SafetyApi {
|
|
|
60
60
|
resourceId: string;
|
|
61
61
|
decision: {
|
|
62
62
|
allowed: boolean;
|
|
63
|
-
reason: "blocked" | "private" | "public" | "
|
|
63
|
+
reason: "blocked" | "private" | "public" | "unavailable" | "owner" | "allowed" | "unsupported_consumer" | "contact_not_approved" | "mature_content" | "unrated_content" | "approval_required";
|
|
64
64
|
policyVersion: "2";
|
|
65
65
|
};
|
|
66
66
|
}[];
|
|
@@ -70,7 +70,7 @@ export declare class ServerSafetyApi extends SafetyApi {
|
|
|
70
70
|
targetAccountId: string;
|
|
71
71
|
decision: {
|
|
72
72
|
allowed: boolean;
|
|
73
|
-
reason: "blocked" | "private" | "public" | "
|
|
73
|
+
reason: "blocked" | "private" | "public" | "unavailable" | "owner" | "allowed" | "unsupported_consumer" | "contact_not_approved" | "mature_content" | "unrated_content" | "approval_required";
|
|
74
74
|
policyVersion: "2";
|
|
75
75
|
};
|
|
76
76
|
}[];
|
|
@@ -96,7 +96,7 @@ export declare class ServerSafetyApi extends SafetyApi {
|
|
|
96
96
|
}>;
|
|
97
97
|
contentAccess(input: ContentAccessInput, options: SafetyRequester): Promise<{
|
|
98
98
|
allowed: boolean;
|
|
99
|
-
reason: "blocked" | "private" | "public" | "
|
|
99
|
+
reason: "blocked" | "private" | "public" | "unavailable" | "owner" | "allowed" | "unsupported_consumer" | "contact_not_approved" | "mature_content" | "unrated_content" | "approval_required";
|
|
100
100
|
policyVersion: "2";
|
|
101
101
|
}>;
|
|
102
102
|
contentAccessBatch(input: ContentAccessBatchInput, options: SafetyRequester): Promise<{
|
|
@@ -104,14 +104,14 @@ export declare class ServerSafetyApi extends SafetyApi {
|
|
|
104
104
|
resourceId: string;
|
|
105
105
|
decision: {
|
|
106
106
|
allowed: boolean;
|
|
107
|
-
reason: "blocked" | "private" | "public" | "
|
|
107
|
+
reason: "blocked" | "private" | "public" | "unavailable" | "owner" | "allowed" | "unsupported_consumer" | "contact_not_approved" | "mature_content" | "unrated_content" | "approval_required";
|
|
108
108
|
policyVersion: "2";
|
|
109
109
|
};
|
|
110
110
|
}[];
|
|
111
111
|
}>;
|
|
112
112
|
deliveryAccess(input: DeliveryAccessInput): Promise<{
|
|
113
113
|
allowed: boolean;
|
|
114
|
-
reason: "blocked" | "private" | "public" | "
|
|
114
|
+
reason: "blocked" | "private" | "public" | "unavailable" | "owner" | "allowed" | "unsupported_consumer" | "contact_not_approved" | "mature_content" | "unrated_content" | "approval_required";
|
|
115
115
|
policyVersion: "2";
|
|
116
116
|
}>;
|
|
117
117
|
requestPurchaseApproval(input: PurchaseEffects, options: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.13.0",
|
|
4
4
|
"description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"dependencies": {
|
|
182
182
|
"@noble/ciphers": "^1.3.0",
|
|
183
183
|
"@noble/hashes": "^1.8.0",
|
|
184
|
-
"@oxy.so/contracts": "6.
|
|
184
|
+
"@oxy.so/contracts": "6.5.0",
|
|
185
185
|
"@oxy.so/protocol": "^1.5.1",
|
|
186
186
|
"@oxy.so/telemetry": "^2.1.0",
|
|
187
187
|
"@scure/bip39": "^1.6.0",
|
package/src/HttpService.ts
CHANGED
|
@@ -56,6 +56,21 @@ export type AccessTokenProvider = () => string | null;
|
|
|
56
56
|
*/
|
|
57
57
|
export type AnonymousAuthProvider = () => Promise<string | null>;
|
|
58
58
|
|
|
59
|
+
declare const credentialGenerationBrand: unique symbol;
|
|
60
|
+
/**
|
|
61
|
+
* An opaque, immutable token for ONE installation of a bearer in a client.
|
|
62
|
+
*
|
|
63
|
+
* `TokenStore` swaps it on every write path (setter, refresh, account or actor
|
|
64
|
+
* switch, cross-tab or restored token, clear), so two installations of the same
|
|
65
|
+
* bytes (A, B, A) are different generations. It names a local sequence only: it
|
|
66
|
+
* is not evidence of an account, a scope or authorization.
|
|
67
|
+
*/
|
|
68
|
+
export type CredentialGeneration = { readonly [credentialGenerationBrand]: true };
|
|
69
|
+
|
|
70
|
+
function newCredentialGeneration(): CredentialGeneration {
|
|
71
|
+
return Object.freeze({}) as CredentialGeneration;
|
|
72
|
+
}
|
|
73
|
+
|
|
59
74
|
/** Comparison only fences local async work; it never validates token authority. */
|
|
60
75
|
function sameTokenSession(left: string | null, right: string | null): boolean {
|
|
61
76
|
if (left === right) return true;
|
|
@@ -190,6 +205,13 @@ interface FormDataLike {
|
|
|
190
205
|
}
|
|
191
206
|
|
|
192
207
|
export interface RequestOptions {
|
|
208
|
+
/**
|
|
209
|
+
* Bind a write to ONE installation of the source's bearer. The transport
|
|
210
|
+
* rejects (`AUTH_SESSION_CHANGED`) when the generation was replaced before
|
|
211
|
+
* dispatch, after queueing, or before the result is returned. Implies no cache,
|
|
212
|
+
* deduplication or retry, and a 401 neither refreshes nor clears the session.
|
|
213
|
+
*/
|
|
214
|
+
expectedCredentialGeneration?: CredentialGeneration;
|
|
193
215
|
cache?: boolean;
|
|
194
216
|
cacheTTL?: number;
|
|
195
217
|
deduplicate?: boolean;
|
|
@@ -434,16 +456,29 @@ const CACHE_SIZE_WARNING_THROTTLE_MS = 60000;
|
|
|
434
456
|
*/
|
|
435
457
|
class TokenStore {
|
|
436
458
|
private accessToken: string | null = null;
|
|
459
|
+
private generation: CredentialGeneration = newCredentialGeneration();
|
|
437
460
|
|
|
438
461
|
setTokens(accessToken: string): void {
|
|
462
|
+
this.generation = newCredentialGeneration();
|
|
439
463
|
this.accessToken = accessToken;
|
|
440
464
|
}
|
|
441
465
|
|
|
466
|
+
/** The identity of the bearer's current installation, never of its bytes. */
|
|
467
|
+
getGeneration(): CredentialGeneration {
|
|
468
|
+
return this.generation;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Retire the current generation without touching the bearer (provider swaps). */
|
|
472
|
+
invalidateGeneration(): void {
|
|
473
|
+
this.generation = newCredentialGeneration();
|
|
474
|
+
}
|
|
475
|
+
|
|
442
476
|
getAccessToken(): string | null {
|
|
443
477
|
return this.accessToken;
|
|
444
478
|
}
|
|
445
479
|
|
|
446
480
|
clearTokens(): void {
|
|
481
|
+
this.generation = newCredentialGeneration();
|
|
447
482
|
this.accessToken = null;
|
|
448
483
|
}
|
|
449
484
|
|
|
@@ -576,6 +611,7 @@ export class HttpService {
|
|
|
576
611
|
* bearer — and through the token listeners, the account — back.
|
|
577
612
|
*/
|
|
578
613
|
private sessionEpoch = 0;
|
|
614
|
+
private readonly credentialSource: HttpService | null;
|
|
579
615
|
/** Preserve a known device across legacy rotations which omit the claim. */
|
|
580
616
|
private sessionDeviceId: string | null = null;
|
|
581
617
|
/** Preserve known actor and context hints across rotations without those claims. */
|
|
@@ -618,8 +654,9 @@ export class HttpService {
|
|
|
618
654
|
averageResponseTime: 0,
|
|
619
655
|
};
|
|
620
656
|
|
|
621
|
-
constructor(config: OxyConfig) {
|
|
657
|
+
constructor(config: OxyConfig, credentialSource: HttpService | null = null) {
|
|
622
658
|
this.config = config;
|
|
659
|
+
this.credentialSource = credentialSource;
|
|
623
660
|
this.baseURL = config.baseURL;
|
|
624
661
|
this.tokenStore = new TokenStore();
|
|
625
662
|
|
|
@@ -760,6 +797,21 @@ export class HttpService {
|
|
|
760
797
|
*/
|
|
761
798
|
async request<T = unknown>(config: RequestConfig): Promise<ResponsePayload<T>> {
|
|
762
799
|
if (config.signal?.aborted) throw createCancelledError('Request cancelled before it was sent');
|
|
800
|
+
if (config.expectedCredentialGeneration) {
|
|
801
|
+
// The prototype is deliberately limited to writes; it does not
|
|
802
|
+
// yet define leases for cached/deduplicated reads or raw response paths.
|
|
803
|
+
if (
|
|
804
|
+
!['POST', 'PUT', 'PATCH', 'DELETE'].includes(config.method) ||
|
|
805
|
+
config.skipAuth ||
|
|
806
|
+
Object.keys(config.headers ?? {}).some((key) => key.toLowerCase() === 'authorization')
|
|
807
|
+
) {
|
|
808
|
+
throw new Error('A credential generation requires an implicitly authenticated write');
|
|
809
|
+
}
|
|
810
|
+
(this.credentialSource ?? this).assertCredentialGeneration(
|
|
811
|
+
config.expectedCredentialGeneration,
|
|
812
|
+
);
|
|
813
|
+
config = { ...config, cache: false, deduplicate: false, retry: false };
|
|
814
|
+
}
|
|
763
815
|
// The provider can change independently of the last request. Synchronize
|
|
764
816
|
// before deriving the cache and deduplication identity, including hits.
|
|
765
817
|
const explicitAuthorization = Object.keys(config.headers ?? {}).some(
|
|
@@ -878,6 +930,15 @@ export class HttpService {
|
|
|
878
930
|
const authHeader = usesSessionAuth
|
|
879
931
|
? await awaitRequestSignal(this.getAuthHeader(), signal)
|
|
880
932
|
: null;
|
|
933
|
+
const assertExpectedCredential = () => {
|
|
934
|
+
if (config.expectedCredentialGeneration) {
|
|
935
|
+
(this.credentialSource ?? this).assertCredentialGeneration(
|
|
936
|
+
config.expectedCredentialGeneration,
|
|
937
|
+
authHeader,
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
assertExpectedCredential();
|
|
881
942
|
const isOriginalSession = () => {
|
|
882
943
|
if (this.sessionEpoch !== contextEpoch) return false;
|
|
883
944
|
const currentToken = this.syncAccessTokenFromProvider();
|
|
@@ -929,6 +990,7 @@ export class HttpService {
|
|
|
929
990
|
if (usesSessionAuth && !isOriginalSession()) {
|
|
930
991
|
throw new OxyAuthenticationError('The request session changed', 'AUTH_SESSION_CHANGED');
|
|
931
992
|
}
|
|
993
|
+
assertExpectedCredential();
|
|
932
994
|
|
|
933
995
|
/**
|
|
934
996
|
* Whether OUR timer fired, so a timeout can be told apart from a
|
|
@@ -1042,6 +1104,9 @@ export class HttpService {
|
|
|
1042
1104
|
throw new Error('Requester proofs require a transport that rejects redirects');
|
|
1043
1105
|
}
|
|
1044
1106
|
|
|
1107
|
+
// Header and source generation are checked together in this synchronous
|
|
1108
|
+
// dispatch segment, after the preflight and any queue wait.
|
|
1109
|
+
assertExpectedCredential();
|
|
1045
1110
|
const response = useXhrForUpload
|
|
1046
1111
|
? await this.uploadViaXHR(
|
|
1047
1112
|
fullUrl,
|
|
@@ -1065,6 +1130,13 @@ export class HttpService {
|
|
|
1065
1130
|
// it, including on every throw between here and there.
|
|
1066
1131
|
if (timeoutId) clearTimeout(timeoutId);
|
|
1067
1132
|
|
|
1133
|
+
try {
|
|
1134
|
+
assertExpectedCredential();
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
void discardResponseBody(response);
|
|
1137
|
+
throw error;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1068
1140
|
if (usesSessionAuth && !isOriginalSession()) {
|
|
1069
1141
|
void discardResponseBody(response);
|
|
1070
1142
|
throw new OxyAuthenticationError('The request session changed', 'AUTH_SESSION_CHANGED');
|
|
@@ -1077,6 +1149,7 @@ export class HttpService {
|
|
|
1077
1149
|
// session routes; the refresh handler owns session rotation.
|
|
1078
1150
|
if (
|
|
1079
1151
|
response.status === 401 &&
|
|
1152
|
+
!config.expectedCredentialGeneration &&
|
|
1080
1153
|
!config._isAuthRetry &&
|
|
1081
1154
|
usesSessionAuth &&
|
|
1082
1155
|
isOriginalSession()
|
|
@@ -1218,12 +1291,14 @@ export class HttpService {
|
|
|
1218
1291
|
if (usesSessionAuth && !isOriginalSession()) {
|
|
1219
1292
|
throw new OxyAuthenticationError('The request session changed', 'AUTH_SESSION_CHANGED');
|
|
1220
1293
|
}
|
|
1294
|
+
assertExpectedCredential();
|
|
1221
1295
|
const duration = Date.now() - startTime;
|
|
1222
1296
|
this.updateMetrics(true, duration);
|
|
1223
1297
|
this.config.onRequestEnd?.(url, method, duration, true);
|
|
1224
1298
|
|
|
1225
1299
|
return responseData as T;
|
|
1226
1300
|
} catch (error: unknown) {
|
|
1301
|
+
assertExpectedCredential();
|
|
1227
1302
|
const duration = Date.now() - startTime;
|
|
1228
1303
|
this.updateMetrics(false, duration);
|
|
1229
1304
|
// A body reader can reject or finish parsing an error after the viewer
|
|
@@ -1348,6 +1423,7 @@ export class HttpService {
|
|
|
1348
1423
|
if (usesSessionAuth && !isOriginalSession()) {
|
|
1349
1424
|
throw new OxyAuthenticationError('The request session changed', 'AUTH_SESSION_CHANGED');
|
|
1350
1425
|
}
|
|
1426
|
+
assertExpectedCredential();
|
|
1351
1427
|
|
|
1352
1428
|
// Cache the result if caching is enabled
|
|
1353
1429
|
if (cache && cacheKey && lease.valid && result) {
|
|
@@ -2192,6 +2268,36 @@ export class HttpService {
|
|
|
2192
2268
|
return this.sessionEpoch;
|
|
2193
2269
|
}
|
|
2194
2270
|
|
|
2271
|
+
/** Capture only the local generation; the caller must separately bind authority. */
|
|
2272
|
+
captureCredentialGeneration(): CredentialGeneration | null {
|
|
2273
|
+
// A pull-only provider can change A→B→A between reads without notifying us.
|
|
2274
|
+
if (this.accessTokenProvider) return null;
|
|
2275
|
+
return this.tokenStore.getAccessToken() ? this.tokenStore.getGeneration() : null;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
isCredentialGenerationCurrent(generation: CredentialGeneration): boolean {
|
|
2279
|
+
return (
|
|
2280
|
+
!this.accessTokenProvider &&
|
|
2281
|
+
this.tokenStore.getAccessToken() !== null &&
|
|
2282
|
+
generation === this.tokenStore.getGeneration()
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
private assertCredentialGeneration(
|
|
2287
|
+
generation: CredentialGeneration,
|
|
2288
|
+
selectedHeader?: string | null,
|
|
2289
|
+
): void {
|
|
2290
|
+
const token = this.tokenStore.getAccessToken();
|
|
2291
|
+
if (
|
|
2292
|
+
this.accessTokenProvider ||
|
|
2293
|
+
!token ||
|
|
2294
|
+
generation !== this.tokenStore.getGeneration() ||
|
|
2295
|
+
(selectedHeader !== undefined && selectedHeader !== `Bearer ${token}`)
|
|
2296
|
+
) {
|
|
2297
|
+
throw new OxyAuthenticationError('The request credential changed', 'AUTH_SESSION_CHANGED');
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2195
2301
|
/** Whether {@link endSession} ran and no token has been planted since. */
|
|
2196
2302
|
hasSessionEnded(): boolean {
|
|
2197
2303
|
return this.sessionEnded;
|
|
@@ -2202,6 +2308,7 @@ export class HttpService {
|
|
|
2202
2308
|
}
|
|
2203
2309
|
|
|
2204
2310
|
setAccessTokenProvider(provider: AccessTokenProvider | null): void {
|
|
2311
|
+
this.tokenStore.invalidateGeneration();
|
|
2205
2312
|
this.accessTokenProvider = provider;
|
|
2206
2313
|
}
|
|
2207
2314
|
|
package/src/OxyServices.ts
CHANGED
|
@@ -159,7 +159,10 @@ export class OxyServices {
|
|
|
159
159
|
* Query). Pass `enableCache: true` to opt in.
|
|
160
160
|
*/
|
|
161
161
|
createLinkedClient(config: OxyConfig): LinkedHttpClient {
|
|
162
|
-
const client = new HttpService(
|
|
162
|
+
const client = new HttpService(
|
|
163
|
+
{ ...config, enableCache: config.enableCache ?? false },
|
|
164
|
+
this.http,
|
|
165
|
+
);
|
|
163
166
|
|
|
164
167
|
const syncToken = (accessToken: string | null): void => {
|
|
165
168
|
const current = client.getAccessToken();
|
package/src/api/apps.ts
CHANGED
|
@@ -494,7 +494,7 @@ export class ConnectedAppsApi {
|
|
|
494
494
|
async revoke(applicationId: string): Promise<void> {
|
|
495
495
|
await this.ctx.request<{ revoked: boolean }>(
|
|
496
496
|
'DELETE',
|
|
497
|
-
`/auth/grants/${applicationId}`,
|
|
497
|
+
`/auth/grants/${enc(applicationId)}`,
|
|
498
498
|
undefined,
|
|
499
499
|
{ cache: false },
|
|
500
500
|
);
|
package/src/api/auth.ts
CHANGED
|
@@ -1622,10 +1622,15 @@ export class AuthApi {
|
|
|
1622
1622
|
*/
|
|
1623
1623
|
async checkUsername(username: string): Promise<{ available: boolean; message: string }> {
|
|
1624
1624
|
// Public availability lookup (pre-session) — skip the bearer preflight.
|
|
1625
|
-
return this.ctx.request(
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1625
|
+
return this.ctx.request(
|
|
1626
|
+
'GET',
|
|
1627
|
+
`/auth/check-username/${encodeURIComponent(username)}`,
|
|
1628
|
+
undefined,
|
|
1629
|
+
{
|
|
1630
|
+
cache: false,
|
|
1631
|
+
skipAuth: true,
|
|
1632
|
+
},
|
|
1633
|
+
);
|
|
1629
1634
|
}
|
|
1630
1635
|
|
|
1631
1636
|
/**
|
package/src/api/billing.ts
CHANGED
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
} from '@oxy.so/contracts/client';
|
|
41
41
|
import type { OxyContext } from '../client/context';
|
|
42
42
|
|
|
43
|
+
const enc = encodeURIComponent;
|
|
44
|
+
|
|
43
45
|
const SUBSCRIPTION_TTL = 2 * 60 * 1000;
|
|
44
46
|
const WALLET_TTL = 60 * 1000;
|
|
45
47
|
|
|
@@ -282,7 +284,7 @@ export class BillingApi {
|
|
|
282
284
|
/** A user's subscription (default: the signed-in user's). */
|
|
283
285
|
async subscription(userId?: string): Promise<Subscription> {
|
|
284
286
|
const id = this.resolveUserId(userId);
|
|
285
|
-
return this.ctx.request<Subscription>('GET', `/subscription/${id}`, undefined, {
|
|
287
|
+
return this.ctx.request<Subscription>('GET', `/subscription/${enc(id)}`, undefined, {
|
|
286
288
|
cache: true,
|
|
287
289
|
cacheTTL: SUBSCRIPTION_TTL,
|
|
288
290
|
});
|
|
@@ -291,7 +293,7 @@ export class BillingApi {
|
|
|
291
293
|
/** A user's wallet (default: the signed-in user's). Cached briefly: the balance moves. */
|
|
292
294
|
async wallet(userId?: string): Promise<Wallet> {
|
|
293
295
|
const id = this.resolveUserId(userId);
|
|
294
|
-
return this.ctx.request<Wallet>('GET', `/wallet/${id}`, undefined, {
|
|
296
|
+
return this.ctx.request<Wallet>('GET', `/wallet/${enc(id)}`, undefined, {
|
|
295
297
|
cache: true,
|
|
296
298
|
cacheTTL: WALLET_TTL,
|
|
297
299
|
});
|
|
@@ -305,9 +307,14 @@ export class BillingApi {
|
|
|
305
307
|
const params: Record<string, number> = {};
|
|
306
308
|
if (options.limit) params.limit = options.limit;
|
|
307
309
|
if (options.offset) params.offset = options.offset;
|
|
308
|
-
return this.ctx.request<WalletTransactionsPage>(
|
|
309
|
-
|
|
310
|
-
|
|
310
|
+
return this.ctx.request<WalletTransactionsPage>(
|
|
311
|
+
'GET',
|
|
312
|
+
`/wallet/transactions/${enc(id)}`,
|
|
313
|
+
params,
|
|
314
|
+
{
|
|
315
|
+
cache: false,
|
|
316
|
+
},
|
|
317
|
+
);
|
|
311
318
|
}
|
|
312
319
|
|
|
313
320
|
private resolveUserId(userId?: string): string {
|
package/src/api/devices.ts
CHANGED
|
@@ -40,6 +40,8 @@ import type {
|
|
|
40
40
|
import { logger } from '../logger';
|
|
41
41
|
import { extractErrorStatus } from '../utils/errorUtils';
|
|
42
42
|
|
|
43
|
+
const enc = encodeURIComponent;
|
|
44
|
+
|
|
43
45
|
/** One of the signed-in user's devices (`GET /devices`). */
|
|
44
46
|
export interface UserDevice {
|
|
45
47
|
id: string;
|
|
@@ -139,7 +141,7 @@ export class DevicesApi {
|
|
|
139
141
|
* @param deviceId - The device ID to remove
|
|
140
142
|
*/
|
|
141
143
|
async remove(deviceId: string): Promise<void> {
|
|
142
|
-
await this.ctx.request('DELETE', `/devices/${deviceId}`, undefined, { cache: false });
|
|
144
|
+
await this.ctx.request('DELETE', `/devices/${enc(deviceId)}`, undefined, { cache: false });
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
/**
|
|
@@ -152,7 +154,7 @@ export class DevicesApi {
|
|
|
152
154
|
// Cache disabled by default to ensure fresh session data
|
|
153
155
|
return this.ctx.request<DeviceLinkedSession[]>(
|
|
154
156
|
'GET',
|
|
155
|
-
`/session/device/sessions/${sessionId}`,
|
|
157
|
+
`/session/device/sessions/${enc(sessionId)}`,
|
|
156
158
|
undefined,
|
|
157
159
|
{
|
|
158
160
|
cache: false, // Don't cache sessions - always get fresh data
|
|
@@ -178,7 +180,7 @@ export class DevicesApi {
|
|
|
178
180
|
if (excludeCurrent) urlParams.excludeCurrent = 'true';
|
|
179
181
|
return this.ctx.request<DeviceLinkedSessionLogoutResponse>(
|
|
180
182
|
'POST',
|
|
181
|
-
`/session/device/logout-all/${sessionId}`,
|
|
183
|
+
`/session/device/logout-all/${enc(sessionId)}`,
|
|
182
184
|
urlParams,
|
|
183
185
|
{ cache: false },
|
|
184
186
|
);
|
|
@@ -196,7 +198,7 @@ export class DevicesApi {
|
|
|
196
198
|
): Promise<{ success: boolean; message: string; deviceName: string }> {
|
|
197
199
|
return this.ctx.request(
|
|
198
200
|
'PUT',
|
|
199
|
-
`/session/device/name/${sessionId}`,
|
|
201
|
+
`/session/device/name/${enc(sessionId)}`,
|
|
200
202
|
{ deviceName },
|
|
201
203
|
{ cache: false },
|
|
202
204
|
);
|