@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.
@@ -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 null;
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