@oxy.so/core 1.18.0 → 1.18.1
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/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +30 -1
- package/dist/cjs/mixins/OxyServices.auth.js +56 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +30 -1
- package/dist/esm/mixins/OxyServices.auth.js +55 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +21 -0
- package/dist/types/mixins/OxyServices.auth.d.ts +33 -0
- package/dist/types/models/interfaces.d.ts +21 -0
- package/package.json +1 -1
- package/src/HttpService.ts +35 -1
- package/src/__tests__/serviceIdentityWhenAnonymous.test.ts +159 -0
- package/src/mixins/OxyServices.auth.ts +55 -0
- package/src/models/interfaces.ts +21 -0
package/dist/esm/HttpService.js
CHANGED
|
@@ -233,6 +233,7 @@ export class HttpService {
|
|
|
233
233
|
this.noFresherTokenThan = null;
|
|
234
234
|
this.authRefreshHandler = null;
|
|
235
235
|
this.accessTokenProvider = null;
|
|
236
|
+
this.anonymousAuthProvider = null;
|
|
236
237
|
this.deviceSecretMintInFlight = null;
|
|
237
238
|
/**
|
|
238
239
|
* Bumped by every {@link endSession}. A re-mint captures it before its first
|
|
@@ -918,7 +919,7 @@ export class HttpService {
|
|
|
918
919
|
async getAuthHeader() {
|
|
919
920
|
const accessToken = this.syncAccessTokenFromProvider();
|
|
920
921
|
if (!accessToken) {
|
|
921
|
-
return
|
|
922
|
+
return this.getAnonymousAuthHeader();
|
|
922
923
|
}
|
|
923
924
|
try {
|
|
924
925
|
const decoded = jwtDecode(accessToken);
|
|
@@ -947,6 +948,24 @@ export class HttpService {
|
|
|
947
948
|
return null;
|
|
948
949
|
}
|
|
949
950
|
}
|
|
951
|
+
/**
|
|
952
|
+
* The bearer for a request that has no user session behind it, from the
|
|
953
|
+
* {@link AnonymousAuthProvider} if one is installed. Never throws: a provider
|
|
954
|
+
* that fails sends the request anonymous, exactly as it went before any
|
|
955
|
+
* provider existed.
|
|
956
|
+
*/
|
|
957
|
+
async getAnonymousAuthHeader() {
|
|
958
|
+
if (!this.anonymousAuthProvider)
|
|
959
|
+
return null;
|
|
960
|
+
try {
|
|
961
|
+
const token = await this.anonymousAuthProvider();
|
|
962
|
+
return token ? `Bearer ${token}` : null;
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
this.logger.debug('Anonymous auth provider failed; sending the request anonymous:', error);
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
950
969
|
async refreshAccessToken(reason) {
|
|
951
970
|
if (!this.authRefreshHandler) {
|
|
952
971
|
return null;
|
|
@@ -1255,6 +1274,16 @@ export class HttpService {
|
|
|
1255
1274
|
setAccessTokenProvider(provider) {
|
|
1256
1275
|
this.accessTokenProvider = provider;
|
|
1257
1276
|
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Authenticate requests that carry no user session with the bearer this
|
|
1279
|
+
* provider returns — in practice a backend's own service token (see
|
|
1280
|
+
* `OxyConfig.serviceIdentity`). A user session always wins: the provider is
|
|
1281
|
+
* consulted only when there is no access token, and never for `skipAuth`
|
|
1282
|
+
* requests. `null` removes it.
|
|
1283
|
+
*/
|
|
1284
|
+
setAnonymousAuthProvider(provider) {
|
|
1285
|
+
this.anonymousAuthProvider = provider;
|
|
1286
|
+
}
|
|
1258
1287
|
clearTokens() {
|
|
1259
1288
|
this.tokenStore.clearTokens();
|
|
1260
1289
|
// Drop the response cache on logout. The cache is identity-scoped, so a
|
|
@@ -85,6 +85,11 @@ export class ServiceCredentialMismatchError extends Error {
|
|
|
85
85
|
this.name = 'ServiceCredentialMismatchError';
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* How long a failed service-token mint sends session-less requests anonymous
|
|
90
|
+
* before the next attempt, under `serviceIdentity: 'when-anonymous'`.
|
|
91
|
+
*/
|
|
92
|
+
export const ANONYMOUS_SERVICE_TOKEN_RETRY_MS = 30000;
|
|
88
93
|
export function OxyServicesAuthMixin(Base) {
|
|
89
94
|
return class extends Base {
|
|
90
95
|
constructor(...args) {
|
|
@@ -111,6 +116,56 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
111
116
|
this._serviceApiKey = null;
|
|
112
117
|
/** @internal Raw apiSecret stored by configureServiceAuth() for use by getServiceToken() */
|
|
113
118
|
this._serviceApiSecret = null;
|
|
119
|
+
/**
|
|
120
|
+
* Before this instant, a request without a user session does not try to
|
|
121
|
+
* mint a service token and goes out anonymous. Set after a failed mint; see
|
|
122
|
+
* {@link _serviceTokenForAnonymousRequest}.
|
|
123
|
+
*
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
this._anonymousServiceTokenRetryAt = 0;
|
|
127
|
+
if (this.config.serviceIdentity === 'when-anonymous') {
|
|
128
|
+
this.httpService.setAnonymousAuthProvider(() => this._serviceTokenForAnonymousRequest());
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The bearer for a request with no user session, under `serviceIdentity:
|
|
133
|
+
* 'when-anonymous'`: this process's service token, or `null` to send the
|
|
134
|
+
* request anonymous as before.
|
|
135
|
+
*
|
|
136
|
+
* `null`, never a throw, whenever there is no token to offer:
|
|
137
|
+
* - no key pair and no attestable workload (a local checkout) — asking
|
|
138
|
+
* costs nothing and there is nothing to ask for;
|
|
139
|
+
* - the mint failed. The request still goes out, anonymous, and so does
|
|
140
|
+
* every request for the next {@link ANONYMOUS_SERVICE_TOKEN_RETRY_MS}, so
|
|
141
|
+
* an Oxy that is refusing attestation (seen for ~2.5 minutes after a
|
|
142
|
+
* deploy on 2026-09-25) is asked once per window rather than once per
|
|
143
|
+
* read, and no read waits on a mint that just failed.
|
|
144
|
+
*
|
|
145
|
+
* The token itself comes from {@link getServiceToken}: cached for its
|
|
146
|
+
* lifetime and single-flight, so concurrent reads share one mint.
|
|
147
|
+
*
|
|
148
|
+
* @internal
|
|
149
|
+
*/
|
|
150
|
+
async _serviceTokenForAnonymousRequest() {
|
|
151
|
+
if (Date.now() < this._anonymousServiceTokenRetryAt)
|
|
152
|
+
return null;
|
|
153
|
+
const hasKeyPair = Boolean(this._serviceApiKey && this._serviceApiSecret);
|
|
154
|
+
if (!hasKeyPair && !(await this._canUseWorkloadIdentity()))
|
|
155
|
+
return null;
|
|
156
|
+
try {
|
|
157
|
+
return await this.getServiceToken();
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
this._anonymousServiceTokenRetryAt = Date.now() + ANONYMOUS_SERVICE_TOKEN_RETRY_MS;
|
|
161
|
+
logger.warn('[oxy.auth] No service token for a request without a session; sending it anonymous', {
|
|
162
|
+
component: 'auth',
|
|
163
|
+
method: '_serviceTokenForAnonymousRequest',
|
|
164
|
+
retryInMs: ANONYMOUS_SERVICE_TOKEN_RETRY_MS,
|
|
165
|
+
error: error instanceof Error ? error.message : String(error),
|
|
166
|
+
});
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
114
169
|
}
|
|
115
170
|
/**
|
|
116
171
|
* Hash an apiKey into a stable Map cache key. Uses Node's SHA-256 — service
|