@oxy.so/core 4.9.0 → 4.9.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.
Files changed (40) hide show
  1. package/dist/cjs/HttpService.js +34 -15
  2. package/dist/cjs/api/accounts.js +2 -33
  3. package/dist/cjs/api/billing.js +0 -9
  4. package/dist/cjs/i18n/locales/en-US.js +0 -7
  5. package/dist/cjs/i18n/locales/es-ES.js +0 -7
  6. package/dist/cjs/server/OxyServer.js +43 -16
  7. package/dist/cjs/server/index.js +3 -2
  8. package/dist/cjs/server/workloadIdentity.js +6 -1
  9. package/dist/cjs/utils/corsPreflight.js +48 -0
  10. package/dist/esm/HttpService.js +34 -15
  11. package/dist/esm/api/accounts.js +1 -31
  12. package/dist/esm/api/billing.js +1 -10
  13. package/dist/esm/i18n/locales/en-US.js +0 -7
  14. package/dist/esm/i18n/locales/es-ES.js +0 -7
  15. package/dist/esm/server/OxyServer.js +42 -15
  16. package/dist/esm/server/index.js +1 -1
  17. package/dist/esm/server/workloadIdentity.js +6 -1
  18. package/dist/esm/utils/corsPreflight.js +45 -0
  19. package/dist/types/api/accounts.d.ts +1 -38
  20. package/dist/types/api/billing.d.ts +1 -4
  21. package/dist/types/i18n/locales/en-US.d.ts +0 -7
  22. package/dist/types/i18n/locales/es-ES.d.ts +0 -7
  23. package/dist/types/index.d.ts +0 -1
  24. package/dist/types/models/interfaces.d.ts +0 -2
  25. package/dist/types/server/OxyServer.d.ts +17 -1
  26. package/dist/types/server/index.d.ts +2 -2
  27. package/dist/types/server/workloadIdentity.d.ts +5 -0
  28. package/dist/types/utils/corsPreflight.d.ts +19 -0
  29. package/package.json +2 -2
  30. package/src/HttpService.ts +31 -12
  31. package/src/api/accounts.ts +1 -82
  32. package/src/api/billing.ts +0 -43
  33. package/src/i18n/locales/en-US.ts +0 -8
  34. package/src/i18n/locales/es-ES.ts +0 -8
  35. package/src/index.ts +0 -1
  36. package/src/models/interfaces.ts +0 -2
  37. package/src/server/OxyServer.ts +56 -15
  38. package/src/server/index.ts +2 -1
  39. package/src/server/workloadIdentity.ts +11 -1
  40. package/src/utils/corsPreflight.ts +49 -0
@@ -26,6 +26,7 @@ const redactUrl_1 = require("./utils/redactUrl");
26
26
  const OxyServices_errors_1 = require("./OxyServices.errors");
27
27
  const edgeRegion_1 = require("./utils/edgeRegion");
28
28
  const activityId_1 = require("./utils/activityId");
29
+ const corsPreflight_1 = require("./utils/corsPreflight");
29
30
  const slashes_1 = require("./utils/slashes");
30
31
  /** Comparison only fences local async work; it never validates token authority. */
31
32
  function sameTokenSession(left, right) {
@@ -508,12 +509,17 @@ class HttpService {
508
509
  const headers = {
509
510
  Accept: 'application/json',
510
511
  };
511
- // Only set Content-Type for non-FormData requests (FormData sets it automatically with boundary)
512
- if (isUrlEncoded) {
513
- headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
514
- }
515
- else if (!isFormData) {
516
- headers['Content-Type'] = 'application/json';
512
+ // Content-Type describes a body, so a GET (which never sends one) gets
513
+ // none: `application/json` is not a CORS-safelisted type, and on a GET it
514
+ // only made the browser preflight a request with nothing to describe.
515
+ // FormData sets its own multipart type with the boundary.
516
+ if (method !== 'GET') {
517
+ if (isUrlEncoded) {
518
+ headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
519
+ }
520
+ else if (!isFormData) {
521
+ headers['Content-Type'] = 'application/json';
522
+ }
517
523
  }
518
524
  // Add authorization header if available
519
525
  if (authHeader) {
@@ -533,9 +539,14 @@ class HttpService {
533
539
  }
534
540
  // This is Cloudflare's coarse serving PoP (for example `mad`), never an
535
541
  // IP or coordinate. Set it after caller headers so request code cannot
536
- // accidentally or deliberately substitute a different origin.
537
- Object.assign(headers, edgeRegionHeader);
538
- Object.assign(headers, activityIdHeader);
542
+ // accidentally or deliberately substitute a different origin. Both are
543
+ // advisory telemetry, so they ride only on a request the browser
544
+ // preflights anyway: on an anonymous GET they were the only thing that
545
+ // made it non-simple, and every distinct URL paid an OPTIONS round trip.
546
+ if ((0, corsPreflight_1.requiresCorsPreflight)(method, Object.entries(headers))) {
547
+ Object.assign(headers, edgeRegionHeader);
548
+ Object.assign(headers, activityIdHeader);
549
+ }
539
550
  // `URLSearchParams` is serialised explicitly rather than handed to
540
551
  // `fetch` as-is: RN's fetch does not consistently encode it, and doing
541
552
  // it here keeps the body identical across every platform.
@@ -1307,12 +1318,20 @@ class HttpService {
1307
1318
  headers.delete(name);
1308
1319
  if (authHeader)
1309
1320
  headers.set('Authorization', authHeader);
1310
- const edgeRegionHeader = (0, edgeRegion_1.peekBrowserEdgeRegionHeader)();
1311
- for (const [name, value] of Object.entries(edgeRegionHeader))
1312
- headers.set(name, value);
1313
- const activityIdHeader = (0, activityId_1.getBrowserActivityIdHeader)();
1314
- for (const [name, value] of Object.entries(activityIdHeader))
1315
- headers.set(name, value);
1321
+ // Telemetry rides only on a request the browser preflights anyway — see
1322
+ // `request()`.
1323
+ const sentHeaders = [];
1324
+ headers.forEach((value, name) => {
1325
+ sentHeaders.push([name, value]);
1326
+ });
1327
+ if ((0, corsPreflight_1.requiresCorsPreflight)(config.method, sentHeaders)) {
1328
+ const edgeRegionHeader = (0, edgeRegion_1.peekBrowserEdgeRegionHeader)();
1329
+ for (const [name, value] of Object.entries(edgeRegionHeader))
1330
+ headers.set(name, value);
1331
+ const activityIdHeader = (0, activityId_1.getBrowserActivityIdHeader)();
1332
+ for (const [name, value] of Object.entries(activityIdHeader))
1333
+ headers.set(name, value);
1334
+ }
1316
1335
  try {
1317
1336
  const body = config.method === 'GET' ? undefined : config.body;
1318
1337
  const init = {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AccountsApi = exports.AccountInvitationsApi = exports.AccountAgentKeysApi = exports.AccountMembersApi = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.isOperatorSwitchTargetKind = exports.isDelegatedActAsEligibleKind = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_KINDS = exports.ACCOUNT_CATEGORY_IDS = void 0;
3
+ exports.AccountsApi = exports.AccountAgentKeysApi = exports.AccountMembersApi = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.isOperatorSwitchTargetKind = exports.isDelegatedActAsEligibleKind = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_KINDS = exports.ACCOUNT_CATEGORY_IDS = void 0;
4
4
  /**
5
5
  * `oxy.accounts` — the unified account graph (`/accounts`).
6
6
  *
@@ -65,8 +65,7 @@ class AccountMembersApi {
65
65
  return res.members ?? [];
66
66
  }
67
67
  /**
68
- * @deprecated The API returns 410. Use accounts.invitations.create with an email.
69
- * Access is granted only after recipient confirmation.
68
+ * Add a member to an account.
70
69
  * @param accountId - The account's `_id`.
71
70
  * @param data - Target user's username or email and role (never `owner`).
72
71
  * The server resolves `usernameOrEmail` to a personal account; an unknown
@@ -120,40 +119,10 @@ class AccountAgentKeysApi {
120
119
  }
121
120
  }
122
121
  exports.AccountAgentKeysApi = AccountAgentKeysApi;
123
- /** Consent-based invitations; never cached or queued as offline mutations. */
124
- class AccountInvitationsApi {
125
- constructor(ctx) {
126
- this.ctx = ctx;
127
- }
128
- async list(accountId) {
129
- return (await this.ctx.request('GET', `/accounts/${enc(accountId)}/invitations`, undefined, { cache: false })).invitations;
130
- }
131
- async create(accountId, data) {
132
- return (await this.ctx.request('POST', `/accounts/${enc(accountId)}/invitations`, data, { cache: false })).invitation;
133
- }
134
- async resend(accountId, id) {
135
- return (await this.ctx.request('POST', `/accounts/${enc(accountId)}/invitations/${enc(id)}/resend`, undefined, { cache: false })).invitation;
136
- }
137
- async cancel(accountId, id) {
138
- return this.ctx.request('DELETE', `/accounts/${enc(accountId)}/invitations/${enc(id)}`, undefined, { cache: false });
139
- }
140
- async preview(token) {
141
- return this.ctx.request('POST', '/accounts/invitations/preview', { token }, { cache: false });
142
- }
143
- /** selectedUserId must equal the currently authenticated personal account. */
144
- async accept(token, selectedUserId) {
145
- const result = await this.ctx.request('POST', '/accounts/invitations/accept', { token, selectedUserId, confirmed: true }, { cache: false });
146
- invalidateAccountMembership(this.ctx, result.accountId);
147
- (0, accountCacheSweep_1.evictOxyAccountForestCache)(this.ctx.http);
148
- return result;
149
- }
150
- }
151
- exports.AccountInvitationsApi = AccountInvitationsApi;
152
122
  class AccountsApi {
153
123
  constructor(ctx) {
154
124
  this.ctx = ctx;
155
125
  this.members = new AccountMembersApi(ctx);
156
- this.invitations = new AccountInvitationsApi(ctx);
157
126
  this.agentKeys = new AccountAgentKeysApi(ctx);
158
127
  }
159
128
  /**
@@ -22,15 +22,6 @@ class BillingApi {
22
22
  constructor(ctx) {
23
23
  this.ctx = ctx;
24
24
  }
25
- async renewProductSubscription(request) {
26
- return contracts_1.personalPlanCheckoutResultSchema.parse(await this.ctx.request('POST', '/billing/product-subscriptions/renew', contracts_1.productSubscriptionRenewalRequestSchema.parse(request), { cache: false }));
27
- }
28
- async startProductPlanCheckout(request) {
29
- return contracts_1.personalPlanCheckoutResultSchema.parse(await this.ctx.request('POST', '/billing/checkout/product-plan', contracts_1.productPlanCheckoutRequestSchema.parse(request), { cache: false }));
30
- }
31
- async productPlans(productId) {
32
- return contracts_1.productPlanCatalogueSchema.parse(await this.ctx.request('GET', `/billing/product-plans/${encodeURIComponent(productId)}`, undefined, { cache: false }));
33
- }
34
25
  async startPersonalPlanCheckout(request) {
35
26
  return contracts_1.personalPlanCheckoutResultSchema.parse(await this.ctx.request('POST', '/billing/checkout/personal-plan', contracts_1.personalPlanCheckoutRequestSchema.parse(request), { cache: false }));
36
27
  }
@@ -1436,13 +1436,6 @@ const dict = {
1436
1436
  },
1437
1437
  },
1438
1438
  preferences: {
1439
- displayCurrency: {
1440
- label: 'Display currency',
1441
- saveFailed: 'Could not save display currency. Please try again.',
1442
- unavailable: 'Equivalent in {{currency}} unavailable',
1443
- approximate: 'Approx. {{amount}}',
1444
- estimate: 'Display estimate. The final payable amount is quoted in FAIR at checkout.',
1445
- },
1446
1439
  title: 'Preferences',
1447
1440
  subtitle: 'Theme, motion, and regional settings',
1448
1441
  sections: {
@@ -1436,13 +1436,6 @@ const dict = {
1436
1436
  },
1437
1437
  },
1438
1438
  preferences: {
1439
- displayCurrency: {
1440
- label: 'Moneda mostrada',
1441
- saveFailed: 'No se ha podido guardar la moneda. Inténtalo de nuevo.',
1442
- unavailable: 'Equivalencia en {{currency}} no disponible',
1443
- approximate: 'Aprox. {{amount}}',
1444
- estimate: 'Importe orientativo. El importe final se cotiza en FAIR al pagar.',
1445
- },
1446
1439
  title: 'Preferencias',
1447
1440
  subtitle: 'Tema, movimiento y configuración regional',
1448
1441
  sections: {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OxyServer = exports.ServiceCredentialMismatchError = exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS = void 0;
3
+ exports.OxyServer = exports.DEFAULT_SERVICE_TOKEN_AUDIENCE = exports.ServiceCredentialMismatchError = exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS = void 0;
4
4
  const contracts_1 = require("@oxy.so/contracts");
5
5
  /**
6
6
  * `OxyServer` — the Oxy client for a backend.
@@ -67,6 +67,12 @@ class ServiceCredentialMismatchError extends Error {
67
67
  }
68
68
  exports.ServiceCredentialMismatchError = ServiceCredentialMismatchError;
69
69
  const WORKLOAD_CACHE_KEY = 'workload-identity';
70
+ /** What `serviceToken()` mints, and what Oxy assumes when no audience is sent. */
71
+ exports.DEFAULT_SERVICE_TOKEN_AUDIENCE = 'oxy-api';
72
+ /** Tokens of different audiences never share a cache entry; the default keeps its old key. */
73
+ function audienceCacheKey(audience, key) {
74
+ return audience === exports.DEFAULT_SERVICE_TOKEN_AUDIENCE ? key : `${audience}\u0000${key}`;
75
+ }
70
76
  /** A token is reused until 60s before expiry (clock drift). */
71
77
  const TOKEN_REUSE_MARGIN_MS = 60000;
72
78
  /** Bound on remembered `appId:userId` delegation answers. */
@@ -202,19 +208,28 @@ class OxyServer extends OxyServices_1.OxyServices {
202
208
  * {@link ServiceCredentialMismatchError} rather than return the cached token.
203
209
  */
204
210
  async serviceToken(apiKey, apiSecret) {
211
+ return this.serviceTokenFor(exports.DEFAULT_SERVICE_TOKEN_AUDIENCE, apiKey, apiSecret);
212
+ }
213
+ /**
214
+ * A service token for `audience` — e.g. `kaana.ai` for https://kaana.ai/v1.
215
+ * Minted, cached and refreshed exactly as {@link serviceToken} (which is this
216
+ * with `oxy-api`), from the same credential or attested workload identity;
217
+ * each audience has its own cache entry, so one never answers for another.
218
+ */
219
+ async serviceTokenFor(audience, apiKey, apiSecret) {
205
220
  const key = apiKey || this.serviceApiKey;
206
221
  const secret = apiSecret || this.serviceApiSecret;
207
222
  if (!key || !secret) {
208
223
  // No credential — prove what this process IS instead. The fallback, not the
209
224
  // preference: a deployment that still has a credential keeps using it.
210
225
  if ((0, workloadIdentity_1.canAttestWorkloadIdentity)()) {
211
- return this.workloadServiceToken();
226
+ return this.workloadServiceToken(audience);
212
227
  }
213
228
  throw new Error('Service credentials not provided. Pass serviceAuth, call configureServiceAuth(), or pass apiKey and apiSecret.');
214
229
  }
215
230
  // The apiKey is the credential's PUBLIC id (`oxy_dk_…`, the OAuth
216
231
  // client_id), so it keys the in-memory cache as is; the secret never does.
217
- const cacheKey = key;
232
+ const cacheKey = audienceCacheKey(audience, key);
218
233
  const now = Date.now();
219
234
  const providedSecretBuf = Buffer.from(secret, 'utf8');
220
235
  let entry = this.serviceTokens.get(cacheKey);
@@ -247,7 +262,7 @@ class OxyServer extends OxyServices_1.OxyServices {
247
262
  entry = { token: '', expiresAt: 0, secretBuf: providedSecretBuf, pending: null, apiKey: key };
248
263
  this.serviceTokens.set(cacheKey, entry);
249
264
  }
250
- const pending = this.mintServiceToken(key, secret, cacheKey, providedSecretBuf);
265
+ const pending = this.mintServiceToken(key, secret, cacheKey, providedSecretBuf, audience);
251
266
  entry.pending = pending;
252
267
  try {
253
268
  return await pending;
@@ -280,7 +295,11 @@ class OxyServer extends OxyServices_1.OxyServices {
280
295
  this.serviceTokens.clear();
281
296
  return;
282
297
  }
283
- this.serviceTokens.delete(targetKey);
298
+ // Every audience minted from that credential goes with it.
299
+ for (const [cacheKey, entry] of this.serviceTokens) {
300
+ if (entry.apiKey === targetKey)
301
+ this.serviceTokens.delete(cacheKey);
302
+ }
284
303
  }
285
304
  /**
286
305
  * One request with this service's token. `actAs` sends `X-Oxy-User-Id`: the
@@ -406,8 +425,12 @@ class OxyServer extends OxyServices_1.OxyServices {
406
425
  canMintServiceToken() {
407
426
  return Boolean(this.serviceApiKey && this.serviceApiSecret) || (0, workloadIdentity_1.canAttestWorkloadIdentity)();
408
427
  }
409
- async mintServiceToken(key, secret, cacheKey, secretBuf) {
410
- const response = await this.request('POST', '/auth/service-token', { apiKey: key, apiSecret: secret }, { cache: false, retry: false, skipAuth: true });
428
+ async mintServiceToken(key, secret, cacheKey, secretBuf, audience) {
429
+ const response = await this.request('POST', '/auth/service-token', {
430
+ apiKey: key,
431
+ apiSecret: secret,
432
+ ...(audience === exports.DEFAULT_SERVICE_TOKEN_AUDIENCE ? {} : { audience }),
433
+ }, { cache: false, retry: false, skipAuth: true });
411
434
  const expiresAt = Date.now() + response.expiresIn * 1000;
412
435
  const entry = this.serviceTokens.get(cacheKey);
413
436
  if (entry) {
@@ -426,9 +449,10 @@ class OxyServer extends OxyServices_1.OxyServices {
426
449
  }
427
450
  return response.token;
428
451
  }
429
- /** A token obtained by attestation, cached like a credential's (one identity per process). */
430
- async workloadServiceToken() {
431
- const entry = this.serviceTokens.get(WORKLOAD_CACHE_KEY);
452
+ /** A token obtained by attestation, cached like a credential's (one identity per process, one entry per audience). */
453
+ async workloadServiceToken(audience) {
454
+ const cacheKey = audienceCacheKey(audience, WORKLOAD_CACHE_KEY);
455
+ const entry = this.serviceTokens.get(cacheKey);
432
456
  if (entry?.token &&
433
457
  reusableServiceToken(entry.token) &&
434
458
  entry.expiresAt > Date.now() + TOKEN_REUSE_MARGIN_MS)
@@ -442,10 +466,13 @@ class OxyServer extends OxyServices_1.OxyServices {
442
466
  pending: null,
443
467
  apiKey: WORKLOAD_CACHE_KEY,
444
468
  };
445
- this.serviceTokens.set(WORKLOAD_CACHE_KEY, seeded);
469
+ this.serviceTokens.set(cacheKey, seeded);
446
470
  const pending = (async () => {
447
- const granted = await (0, workloadIdentity_1.requestWorkloadServiceToken)({ baseUrl: this.baseURL });
448
- const current = this.serviceTokens.get(WORKLOAD_CACHE_KEY);
471
+ const granted = await (0, workloadIdentity_1.requestWorkloadServiceToken)({
472
+ baseUrl: this.baseURL,
473
+ ...(audience === exports.DEFAULT_SERVICE_TOKEN_AUDIENCE ? {} : { audience }),
474
+ });
475
+ const current = this.serviceTokens.get(cacheKey);
449
476
  if (current) {
450
477
  current.token = granted.token;
451
478
  current.expiresAt = Date.now() + granted.expiresIn * 1000;
@@ -457,13 +484,13 @@ class OxyServer extends OxyServices_1.OxyServices {
457
484
  return await pending;
458
485
  }
459
486
  catch (error) {
460
- const failed = this.serviceTokens.get(WORKLOAD_CACHE_KEY);
487
+ const failed = this.serviceTokens.get(cacheKey);
461
488
  if (failed?.pending === pending && !failed.token)
462
- this.serviceTokens.delete(WORKLOAD_CACHE_KEY);
489
+ this.serviceTokens.delete(cacheKey);
463
490
  throw error;
464
491
  }
465
492
  finally {
466
- const settled = this.serviceTokens.get(WORKLOAD_CACHE_KEY);
493
+ const settled = this.serviceTokens.get(cacheKey);
467
494
  if (settled?.pending === pending)
468
495
  settled.pending = null;
469
496
  }
@@ -16,14 +16,15 @@
16
16
  * ```
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.inlineScriptCspHash = exports.formatOxyCspPolicy = exports.extractInlineScripts = exports.cspSourcesFor = exports.createOxySecurityHeaders = exports.buildOxyPagesHeaders = exports.buildOxyCspDirectives = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.OXY_SERVICE_ENVIRONMENTS = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getRequiredOxyBillingPrincipal = exports.getOxyUserId = exports.getOxyRequestAttribution = exports.getOxyDelegatedUserId = exports.getOxyBillingPrincipal = exports.getOxyAuthRefusal = exports.getOxyActor = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = exports.OXY_ACCOUNT_DELETED_EVENT_URI = exports.OxyAccountEventError = exports.OXY_ALIA_MACHINE_SCOPES = exports.OXY_ALIA_RESOURCE_APPLICATION_ID = exports.isOxyAliaMachinePrincipal = exports.getOxyAliaMachineCredentialBearer = exports.createOxyAliaMachineCredentialAuth = exports.ServerReputationApi = exports.ServerNotificationsApi = exports.ServerLinkedAccountsApi = exports.ServerAssetsApi = exports.ServerAgencyApi = exports.ServerAppsApi = exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS = exports.ServiceCredentialMismatchError = exports.OxyServer = void 0;
20
- exports.signInAgentAccount = exports.hasBoundedServiceTokenLifetime = exports.OXY_SERVICE_TOKEN_MAX_LIFETIME_SECONDS = exports.requestWorkloadServiceToken = exports.canAttestWorkloadIdentity = exports.attestWorkloadIdentity = exports.withoutNodeHttpObservation = exports.observeNodeHttp = exports.createEcosystemTraffic = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.registrableApex = exports.OXY_IDENTITY_CACHE_PREFIXES = exports.oxyUserByIdCacheKey = exports.evictOxyIdentityCache = exports.publishOxyUserInvalidation = exports.createOxyUserInvalidationHandler = exports.createLiveCapabilityTicketVerifier = exports.verifySecret = exports.verifyOxyRequesterAssertion = exports.signOxyRequesterAssertion = exports.readOxyRequesterAssertionKeyId = exports.oxyRequesterAssertionClaimsSchema = exports.OxyRequesterAssertionError = exports.OXY_REQUESTER_ASSERTION_TYPE = exports.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS = exports.OXY_REQUESTER_ASSERTION_HEADER = exports.OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER = exports.createOxyRequesterAssertionAuth = exports.createOxyJwksKeyResolver = exports.verifyCapabilityTicket = exports.readCapabilityAuthorization = exports.issueCapabilityTicket = exports.inputSatisfiesCapabilityLimits = exports.createCapabilityTicketMiddleware = exports.CapabilityTicketError = exports.OXY_CSP_BASELINE = void 0;
19
+ exports.formatOxyCspPolicy = exports.extractInlineScripts = exports.cspSourcesFor = exports.createOxySecurityHeaders = exports.buildOxyPagesHeaders = exports.buildOxyCspDirectives = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.OXY_SERVICE_ENVIRONMENTS = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getRequiredOxyBillingPrincipal = exports.getOxyUserId = exports.getOxyRequestAttribution = exports.getOxyDelegatedUserId = exports.getOxyBillingPrincipal = exports.getOxyAuthRefusal = exports.getOxyActor = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = exports.OXY_ACCOUNT_DELETED_EVENT_URI = exports.OxyAccountEventError = exports.OXY_ALIA_MACHINE_SCOPES = exports.OXY_ALIA_RESOURCE_APPLICATION_ID = exports.isOxyAliaMachinePrincipal = exports.getOxyAliaMachineCredentialBearer = exports.createOxyAliaMachineCredentialAuth = exports.ServerReputationApi = exports.ServerNotificationsApi = exports.ServerLinkedAccountsApi = exports.ServerAssetsApi = exports.ServerAgencyApi = exports.ServerAppsApi = exports.DEFAULT_SERVICE_TOKEN_AUDIENCE = exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS = exports.ServiceCredentialMismatchError = exports.OxyServer = void 0;
20
+ exports.signInAgentAccount = exports.hasBoundedServiceTokenLifetime = exports.OXY_SERVICE_TOKEN_MAX_LIFETIME_SECONDS = exports.requestWorkloadServiceToken = exports.canAttestWorkloadIdentity = exports.attestWorkloadIdentity = exports.withoutNodeHttpObservation = exports.observeNodeHttp = exports.createEcosystemTraffic = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.registrableApex = exports.OXY_IDENTITY_CACHE_PREFIXES = exports.oxyUserByIdCacheKey = exports.evictOxyIdentityCache = exports.publishOxyUserInvalidation = exports.createOxyUserInvalidationHandler = exports.createLiveCapabilityTicketVerifier = exports.verifySecret = exports.verifyOxyRequesterAssertion = exports.signOxyRequesterAssertion = exports.readOxyRequesterAssertionKeyId = exports.oxyRequesterAssertionClaimsSchema = exports.OxyRequesterAssertionError = exports.OXY_REQUESTER_ASSERTION_TYPE = exports.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS = exports.OXY_REQUESTER_ASSERTION_HEADER = exports.OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER = exports.createOxyRequesterAssertionAuth = exports.createOxyJwksKeyResolver = exports.verifyCapabilityTicket = exports.readCapabilityAuthorization = exports.issueCapabilityTicket = exports.inputSatisfiesCapabilityLimits = exports.createCapabilityTicketMiddleware = exports.CapabilityTicketError = exports.OXY_CSP_BASELINE = exports.inlineScriptCspHash = void 0;
21
21
  // The backend client: OxyServices plus the service-token lane, middleware,
22
22
  // account events and the service-only namespace methods.
23
23
  var OxyServer_1 = require("./OxyServer");
24
24
  Object.defineProperty(exports, "OxyServer", { enumerable: true, get: function () { return OxyServer_1.OxyServer; } });
25
25
  Object.defineProperty(exports, "ServiceCredentialMismatchError", { enumerable: true, get: function () { return OxyServer_1.ServiceCredentialMismatchError; } });
26
26
  Object.defineProperty(exports, "ANONYMOUS_SERVICE_TOKEN_RETRY_MS", { enumerable: true, get: function () { return OxyServer_1.ANONYMOUS_SERVICE_TOKEN_RETRY_MS; } });
27
+ Object.defineProperty(exports, "DEFAULT_SERVICE_TOKEN_AUDIENCE", { enumerable: true, get: function () { return OxyServer_1.DEFAULT_SERVICE_TOKEN_AUDIENCE; } });
27
28
  var namespaces_1 = require("./namespaces");
28
29
  Object.defineProperty(exports, "ServerAppsApi", { enumerable: true, get: function () { return namespaces_1.ServerAppsApi; } });
29
30
  Object.defineProperty(exports, "ServerAgencyApi", { enumerable: true, get: function () { return namespaces_1.ServerAgencyApi; } });
@@ -176,7 +176,12 @@ async function requestWorkloadServiceToken(options) {
176
176
  const tokenResponse = await fetchImpl(`${base}/auth/service-token/workload`, {
177
177
  method: 'POST',
178
178
  headers: { 'content-type': 'application/json' },
179
- body: JSON.stringify({ provider: 'aws-iam', nonce: challenge.nonce, attestation }),
179
+ body: JSON.stringify({
180
+ provider: 'aws-iam',
181
+ nonce: challenge.nonce,
182
+ attestation,
183
+ ...(options.audience === undefined ? {} : { audience: options.audience }),
184
+ }),
180
185
  });
181
186
  if (!tokenResponse.ok) {
182
187
  // The body carries a bounded `reason`; the token never travels on a failure,
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ /**
3
+ * Whether a browser has to send a CORS preflight (`OPTIONS`) before this
4
+ * cross-origin request goes out, per the Fetch standard's CORS-safelisted
5
+ * methods and request headers.
6
+ *
7
+ * The SDK uses it for one invariant: its anonymous telemetry headers
8
+ * (`X-Oxy-Activity-Id`, `X-Oxy-Edge-Region`) ride only on requests that are
9
+ * preflighted anyway, so they never turn a request that would go straight out
10
+ * into one that first waits a round trip for an `OPTIONS`. A browser caches a
11
+ * preflight per URL, so without this every distinct anonymous read (a post's
12
+ * replies, a profile, a public app record) paid a round trip of its own.
13
+ *
14
+ * Deliberately conservative: a header it does not recognise as safelisted
15
+ * counts as needing a preflight, which only means the telemetry headers ride
16
+ * along exactly as they always did. Value-level rules (for example `Accept`'s
17
+ * 128-byte limit) are not modelled; a misjudgement in that direction drops two
18
+ * advisory headers from one request and nothing else.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.requiresCorsPreflight = requiresCorsPreflight;
22
+ const SAFELISTED_METHODS = new Set(['GET', 'HEAD', 'POST']);
23
+ const SAFELISTED_HEADERS = new Set([
24
+ 'accept',
25
+ 'accept-language',
26
+ 'content-language',
27
+ 'content-type',
28
+ ]);
29
+ const SAFELISTED_CONTENT_TYPES = new Set([
30
+ 'application/x-www-form-urlencoded',
31
+ 'multipart/form-data',
32
+ 'text/plain',
33
+ ]);
34
+ function requiresCorsPreflight(method, headers) {
35
+ if (!SAFELISTED_METHODS.has(method.toUpperCase()))
36
+ return true;
37
+ for (const [rawName, value] of headers) {
38
+ const name = rawName.toLowerCase();
39
+ if (!SAFELISTED_HEADERS.has(name))
40
+ return true;
41
+ if (name === 'content-type') {
42
+ const essence = value.split(';')[0]?.trim().toLowerCase() ?? '';
43
+ if (!SAFELISTED_CONTENT_TYPES.has(essence))
44
+ return true;
45
+ }
46
+ }
47
+ return false;
48
+ }
@@ -23,6 +23,7 @@ import { redactUrlQuery } from './utils/redactUrl.js';
23
23
  import { OxyAuthenticationError } from './OxyServices.errors.js';
24
24
  import { peekBrowserEdgeRegionHeader } from './utils/edgeRegion.js';
25
25
  import { getBrowserActivityIdHeader } from './utils/activityId.js';
26
+ import { requiresCorsPreflight } from './utils/corsPreflight.js';
26
27
  import { trimLeadingSlashes, trimTrailingSlashes } from './utils/slashes.js';
27
28
  /** Comparison only fences local async work; it never validates token authority. */
28
29
  function sameTokenSession(left, right) {
@@ -505,12 +506,17 @@ export class HttpService {
505
506
  const headers = {
506
507
  Accept: 'application/json',
507
508
  };
508
- // Only set Content-Type for non-FormData requests (FormData sets it automatically with boundary)
509
- if (isUrlEncoded) {
510
- headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
511
- }
512
- else if (!isFormData) {
513
- headers['Content-Type'] = 'application/json';
509
+ // Content-Type describes a body, so a GET (which never sends one) gets
510
+ // none: `application/json` is not a CORS-safelisted type, and on a GET it
511
+ // only made the browser preflight a request with nothing to describe.
512
+ // FormData sets its own multipart type with the boundary.
513
+ if (method !== 'GET') {
514
+ if (isUrlEncoded) {
515
+ headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
516
+ }
517
+ else if (!isFormData) {
518
+ headers['Content-Type'] = 'application/json';
519
+ }
514
520
  }
515
521
  // Add authorization header if available
516
522
  if (authHeader) {
@@ -530,9 +536,14 @@ export class HttpService {
530
536
  }
531
537
  // This is Cloudflare's coarse serving PoP (for example `mad`), never an
532
538
  // IP or coordinate. Set it after caller headers so request code cannot
533
- // accidentally or deliberately substitute a different origin.
534
- Object.assign(headers, edgeRegionHeader);
535
- Object.assign(headers, activityIdHeader);
539
+ // accidentally or deliberately substitute a different origin. Both are
540
+ // advisory telemetry, so they ride only on a request the browser
541
+ // preflights anyway: on an anonymous GET they were the only thing that
542
+ // made it non-simple, and every distinct URL paid an OPTIONS round trip.
543
+ if (requiresCorsPreflight(method, Object.entries(headers))) {
544
+ Object.assign(headers, edgeRegionHeader);
545
+ Object.assign(headers, activityIdHeader);
546
+ }
536
547
  // `URLSearchParams` is serialised explicitly rather than handed to
537
548
  // `fetch` as-is: RN's fetch does not consistently encode it, and doing
538
549
  // it here keeps the body identical across every platform.
@@ -1304,12 +1315,20 @@ export class HttpService {
1304
1315
  headers.delete(name);
1305
1316
  if (authHeader)
1306
1317
  headers.set('Authorization', authHeader);
1307
- const edgeRegionHeader = peekBrowserEdgeRegionHeader();
1308
- for (const [name, value] of Object.entries(edgeRegionHeader))
1309
- headers.set(name, value);
1310
- const activityIdHeader = getBrowserActivityIdHeader();
1311
- for (const [name, value] of Object.entries(activityIdHeader))
1312
- headers.set(name, value);
1318
+ // Telemetry rides only on a request the browser preflights anyway — see
1319
+ // `request()`.
1320
+ const sentHeaders = [];
1321
+ headers.forEach((value, name) => {
1322
+ sentHeaders.push([name, value]);
1323
+ });
1324
+ if (requiresCorsPreflight(config.method, sentHeaders)) {
1325
+ const edgeRegionHeader = peekBrowserEdgeRegionHeader();
1326
+ for (const [name, value] of Object.entries(edgeRegionHeader))
1327
+ headers.set(name, value);
1328
+ const activityIdHeader = getBrowserActivityIdHeader();
1329
+ for (const [name, value] of Object.entries(activityIdHeader))
1330
+ headers.set(name, value);
1331
+ }
1313
1332
  try {
1314
1333
  const body = config.method === 'GET' ? undefined : config.body;
1315
1334
  const init = {
@@ -54,8 +54,7 @@ export class AccountMembersApi {
54
54
  return res.members ?? [];
55
55
  }
56
56
  /**
57
- * @deprecated The API returns 410. Use accounts.invitations.create with an email.
58
- * Access is granted only after recipient confirmation.
57
+ * Add a member to an account.
59
58
  * @param accountId - The account's `_id`.
60
59
  * @param data - Target user's username or email and role (never `owner`).
61
60
  * The server resolves `usernameOrEmail` to a personal account; an unknown
@@ -107,39 +106,10 @@ export class AccountAgentKeysApi {
107
106
  return agentKeyOperationResultSchema.parse(response);
108
107
  }
109
108
  }
110
- /** Consent-based invitations; never cached or queued as offline mutations. */
111
- export class AccountInvitationsApi {
112
- constructor(ctx) {
113
- this.ctx = ctx;
114
- }
115
- async list(accountId) {
116
- return (await this.ctx.request('GET', `/accounts/${enc(accountId)}/invitations`, undefined, { cache: false })).invitations;
117
- }
118
- async create(accountId, data) {
119
- return (await this.ctx.request('POST', `/accounts/${enc(accountId)}/invitations`, data, { cache: false })).invitation;
120
- }
121
- async resend(accountId, id) {
122
- return (await this.ctx.request('POST', `/accounts/${enc(accountId)}/invitations/${enc(id)}/resend`, undefined, { cache: false })).invitation;
123
- }
124
- async cancel(accountId, id) {
125
- return this.ctx.request('DELETE', `/accounts/${enc(accountId)}/invitations/${enc(id)}`, undefined, { cache: false });
126
- }
127
- async preview(token) {
128
- return this.ctx.request('POST', '/accounts/invitations/preview', { token }, { cache: false });
129
- }
130
- /** selectedUserId must equal the currently authenticated personal account. */
131
- async accept(token, selectedUserId) {
132
- const result = await this.ctx.request('POST', '/accounts/invitations/accept', { token, selectedUserId, confirmed: true }, { cache: false });
133
- invalidateAccountMembership(this.ctx, result.accountId);
134
- evictOxyAccountForestCache(this.ctx.http);
135
- return result;
136
- }
137
- }
138
109
  export class AccountsApi {
139
110
  constructor(ctx) {
140
111
  this.ctx = ctx;
141
112
  this.members = new AccountMembersApi(ctx);
142
- this.invitations = new AccountInvitationsApi(ctx);
143
113
  this.agentKeys = new AccountAgentKeysApi(ctx);
144
114
  }
145
115
  /**
@@ -1,4 +1,4 @@
1
- import { personalPlanCheckoutRequestSchema, productPlanCheckoutRequestSchema, productSubscriptionRenewalRequestSchema, productPlanCatalogueSchema, personalPlanCheckoutResultSchema, } from '@oxy.so/contracts';
1
+ import { personalPlanCheckoutRequestSchema, personalPlanCheckoutResultSchema, } from '@oxy.so/contracts';
2
2
  /**
3
3
  * `oxy.billing` — the signed-in user's subscription, wallet and payment history.
4
4
  *
@@ -19,15 +19,6 @@ export class BillingApi {
19
19
  constructor(ctx) {
20
20
  this.ctx = ctx;
21
21
  }
22
- async renewProductSubscription(request) {
23
- return personalPlanCheckoutResultSchema.parse(await this.ctx.request('POST', '/billing/product-subscriptions/renew', productSubscriptionRenewalRequestSchema.parse(request), { cache: false }));
24
- }
25
- async startProductPlanCheckout(request) {
26
- return personalPlanCheckoutResultSchema.parse(await this.ctx.request('POST', '/billing/checkout/product-plan', productPlanCheckoutRequestSchema.parse(request), { cache: false }));
27
- }
28
- async productPlans(productId) {
29
- return productPlanCatalogueSchema.parse(await this.ctx.request('GET', `/billing/product-plans/${encodeURIComponent(productId)}`, undefined, { cache: false }));
30
- }
31
22
  async startPersonalPlanCheckout(request) {
32
23
  return personalPlanCheckoutResultSchema.parse(await this.ctx.request('POST', '/billing/checkout/personal-plan', personalPlanCheckoutRequestSchema.parse(request), { cache: false }));
33
24
  }
@@ -1434,13 +1434,6 @@ const dict = {
1434
1434
  },
1435
1435
  },
1436
1436
  preferences: {
1437
- displayCurrency: {
1438
- label: 'Display currency',
1439
- saveFailed: 'Could not save display currency. Please try again.',
1440
- unavailable: 'Equivalent in {{currency}} unavailable',
1441
- approximate: 'Approx. {{amount}}',
1442
- estimate: 'Display estimate. The final payable amount is quoted in FAIR at checkout.',
1443
- },
1444
1437
  title: 'Preferences',
1445
1438
  subtitle: 'Theme, motion, and regional settings',
1446
1439
  sections: {
@@ -1434,13 +1434,6 @@ const dict = {
1434
1434
  },
1435
1435
  },
1436
1436
  preferences: {
1437
- displayCurrency: {
1438
- label: 'Moneda mostrada',
1439
- saveFailed: 'No se ha podido guardar la moneda. Inténtalo de nuevo.',
1440
- unavailable: 'Equivalencia en {{currency}} no disponible',
1441
- approximate: 'Aprox. {{amount}}',
1442
- estimate: 'Importe orientativo. El importe final se cotiza en FAIR al pagar.',
1443
- },
1444
1437
  title: 'Preferencias',
1445
1438
  subtitle: 'Tema, movimiento y configuración regional',
1446
1439
  sections: {