@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/cjs/HttpService.js
CHANGED
|
@@ -236,6 +236,7 @@ class HttpService {
|
|
|
236
236
|
this.noFresherTokenThan = null;
|
|
237
237
|
this.authRefreshHandler = null;
|
|
238
238
|
this.accessTokenProvider = null;
|
|
239
|
+
this.anonymousAuthProvider = null;
|
|
239
240
|
this.deviceSecretMintInFlight = null;
|
|
240
241
|
/**
|
|
241
242
|
* Bumped by every {@link endSession}. A re-mint captures it before its first
|
|
@@ -921,7 +922,7 @@ class HttpService {
|
|
|
921
922
|
async getAuthHeader() {
|
|
922
923
|
const accessToken = this.syncAccessTokenFromProvider();
|
|
923
924
|
if (!accessToken) {
|
|
924
|
-
return
|
|
925
|
+
return this.getAnonymousAuthHeader();
|
|
925
926
|
}
|
|
926
927
|
try {
|
|
927
928
|
const decoded = (0, jwt_decode_1.jwtDecode)(accessToken);
|
|
@@ -950,6 +951,24 @@ class HttpService {
|
|
|
950
951
|
return null;
|
|
951
952
|
}
|
|
952
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* The bearer for a request that has no user session behind it, from the
|
|
956
|
+
* {@link AnonymousAuthProvider} if one is installed. Never throws: a provider
|
|
957
|
+
* that fails sends the request anonymous, exactly as it went before any
|
|
958
|
+
* provider existed.
|
|
959
|
+
*/
|
|
960
|
+
async getAnonymousAuthHeader() {
|
|
961
|
+
if (!this.anonymousAuthProvider)
|
|
962
|
+
return null;
|
|
963
|
+
try {
|
|
964
|
+
const token = await this.anonymousAuthProvider();
|
|
965
|
+
return token ? `Bearer ${token}` : null;
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
this.logger.debug('Anonymous auth provider failed; sending the request anonymous:', error);
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
953
972
|
async refreshAccessToken(reason) {
|
|
954
973
|
if (!this.authRefreshHandler) {
|
|
955
974
|
return null;
|
|
@@ -1258,6 +1277,16 @@ class HttpService {
|
|
|
1258
1277
|
setAccessTokenProvider(provider) {
|
|
1259
1278
|
this.accessTokenProvider = provider;
|
|
1260
1279
|
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Authenticate requests that carry no user session with the bearer this
|
|
1282
|
+
* provider returns — in practice a backend's own service token (see
|
|
1283
|
+
* `OxyConfig.serviceIdentity`). A user session always wins: the provider is
|
|
1284
|
+
* consulted only when there is no access token, and never for `skipAuth`
|
|
1285
|
+
* requests. `null` removes it.
|
|
1286
|
+
*/
|
|
1287
|
+
setAnonymousAuthProvider(provider) {
|
|
1288
|
+
this.anonymousAuthProvider = provider;
|
|
1289
|
+
}
|
|
1261
1290
|
clearTokens() {
|
|
1262
1291
|
this.tokenStore.clearTokens();
|
|
1263
1292
|
// Drop the response cache on logout. The cache is identity-scoped, so a
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.ServiceCredentialMismatchError = exports.parseCommonsApprovalExpiresAt = exports.getCommonsApprovalBlockingReason = void 0;
|
|
36
|
+
exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS = exports.ServiceCredentialMismatchError = exports.parseCommonsApprovalExpiresAt = exports.getCommonsApprovalBlockingReason = void 0;
|
|
37
37
|
exports.OxyServicesAuthMixin = OxyServicesAuthMixin;
|
|
38
38
|
const contracts_1 = require("@oxy.so/contracts");
|
|
39
39
|
var commonsApproval_1 = require("../utils/commonsApproval");
|
|
@@ -125,6 +125,11 @@ class ServiceCredentialMismatchError extends Error {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
exports.ServiceCredentialMismatchError = ServiceCredentialMismatchError;
|
|
128
|
+
/**
|
|
129
|
+
* How long a failed service-token mint sends session-less requests anonymous
|
|
130
|
+
* before the next attempt, under `serviceIdentity: 'when-anonymous'`.
|
|
131
|
+
*/
|
|
132
|
+
exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS = 30000;
|
|
128
133
|
function OxyServicesAuthMixin(Base) {
|
|
129
134
|
return class extends Base {
|
|
130
135
|
constructor(...args) {
|
|
@@ -151,6 +156,56 @@ function OxyServicesAuthMixin(Base) {
|
|
|
151
156
|
this._serviceApiKey = null;
|
|
152
157
|
/** @internal Raw apiSecret stored by configureServiceAuth() for use by getServiceToken() */
|
|
153
158
|
this._serviceApiSecret = null;
|
|
159
|
+
/**
|
|
160
|
+
* Before this instant, a request without a user session does not try to
|
|
161
|
+
* mint a service token and goes out anonymous. Set after a failed mint; see
|
|
162
|
+
* {@link _serviceTokenForAnonymousRequest}.
|
|
163
|
+
*
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
this._anonymousServiceTokenRetryAt = 0;
|
|
167
|
+
if (this.config.serviceIdentity === 'when-anonymous') {
|
|
168
|
+
this.httpService.setAnonymousAuthProvider(() => this._serviceTokenForAnonymousRequest());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The bearer for a request with no user session, under `serviceIdentity:
|
|
173
|
+
* 'when-anonymous'`: this process's service token, or `null` to send the
|
|
174
|
+
* request anonymous as before.
|
|
175
|
+
*
|
|
176
|
+
* `null`, never a throw, whenever there is no token to offer:
|
|
177
|
+
* - no key pair and no attestable workload (a local checkout) — asking
|
|
178
|
+
* costs nothing and there is nothing to ask for;
|
|
179
|
+
* - the mint failed. The request still goes out, anonymous, and so does
|
|
180
|
+
* every request for the next {@link ANONYMOUS_SERVICE_TOKEN_RETRY_MS}, so
|
|
181
|
+
* an Oxy that is refusing attestation (seen for ~2.5 minutes after a
|
|
182
|
+
* deploy on 2026-09-25) is asked once per window rather than once per
|
|
183
|
+
* read, and no read waits on a mint that just failed.
|
|
184
|
+
*
|
|
185
|
+
* The token itself comes from {@link getServiceToken}: cached for its
|
|
186
|
+
* lifetime and single-flight, so concurrent reads share one mint.
|
|
187
|
+
*
|
|
188
|
+
* @internal
|
|
189
|
+
*/
|
|
190
|
+
async _serviceTokenForAnonymousRequest() {
|
|
191
|
+
if (Date.now() < this._anonymousServiceTokenRetryAt)
|
|
192
|
+
return null;
|
|
193
|
+
const hasKeyPair = Boolean(this._serviceApiKey && this._serviceApiSecret);
|
|
194
|
+
if (!hasKeyPair && !(await this._canUseWorkloadIdentity()))
|
|
195
|
+
return null;
|
|
196
|
+
try {
|
|
197
|
+
return await this.getServiceToken();
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
this._anonymousServiceTokenRetryAt = Date.now() + exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS;
|
|
201
|
+
logger_1.logger.warn('[oxy.auth] No service token for a request without a session; sending it anonymous', {
|
|
202
|
+
component: 'auth',
|
|
203
|
+
method: '_serviceTokenForAnonymousRequest',
|
|
204
|
+
retryInMs: exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS,
|
|
205
|
+
error: error instanceof Error ? error.message : String(error),
|
|
206
|
+
});
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
154
209
|
}
|
|
155
210
|
/**
|
|
156
211
|
* Hash an apiKey into a stable Map cache key. Uses Node's SHA-256 — service
|