@salesforce/lds-runtime-aura 1.442.0 → 1.443.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.
@@ -22,7 +22,7 @@ import { instrument, getRecordAvatarsAdapterFactory, getRecordAdapterFactory, co
22
22
  import { getInstrumentation } from 'o11y/client';
23
23
  import { findExecutableOperation, buildGraphQLInputExtension, addTypenameToDocument } from 'force/luvioGraphqlNormalization';
24
24
  import { print, resolveAndValidateGraphQLConfig, toGraphQLErrorResponse } from 'force/luvioOnestoreGraphqlParser';
25
- import { setServices } from 'force/luvioServiceProvisioner1';
25
+ import getServices, { setServices } from 'force/luvioServiceProvisioner1';
26
26
  import { assertIsValid, JsonSchemaViolationError, MissingRequiredPropertyError } from 'force/luvioJsonschemaValidate5';
27
27
  import { dispatchGlobalEvent, executeGlobalControllerRawResponse } from 'aura';
28
28
  import auraNetworkAdapter, { dispatchAuraAction, defaultActionConfig, instrument as instrument$1, forceRecordTransactionsDisabled as forceRecordTransactionsDisabled$1, ldsNetworkAdapterInstrument, CrudEventState, CrudEventType, UIAPI_RECORDS_PATH, UIAPI_RELATED_LIST_RECORDS_BATCH_PATH, UIAPI_RELATED_LIST_RECORDS_PATH } from 'force/ldsNetwork';
@@ -36,7 +36,6 @@ import { instrument as instrument$5 } from '@lwc/state';
36
36
  import { withRegistration, register, setDefaultLuvio } from 'force/ldsEngine';
37
37
  import applyPredictionRequestLimit from '@salesforce/gate/lds.pdl.applyRequestLimit';
38
38
  import { pageScopedCache } from 'instrumentation/utility';
39
- import { createStorage, clearStorages } from 'force/ldsDurableStorage';
40
39
  import lightningConnectEnabled from '@salesforce/gate/ui.services.LightningConnect.enabled';
41
40
  import bypassAppRestrictionEnabled from '@salesforce/gate/ui.services.LightningConnect.BypassAppRestriction.enabled';
42
41
  import csrfValidationEnabled from '@salesforce/gate/ui.services.LightningConnect.CsrfValidation.enabled';
@@ -47,6 +46,7 @@ import useHttpUiapiOneRuntimePublic from '@salesforce/gate/lds.useHttpUiapiOneRu
47
46
  import useHttpUiapiOneRuntimePrivate from '@salesforce/gate/lds.useHttpUiapiOneRuntimePrivate';
48
47
  import disableCreateContentDocumentAndVersionHTTPLexRuntime from '@salesforce/gate/lds.lex.http.disableCreateContentDocumentAndVersion';
49
48
  import useHotspotLimit from '@salesforce/gate/lds.pdl.useHotspotLimit';
49
+ import { createStorage, clearStorages } from 'force/ldsDurableStorage';
50
50
  import { registerSubRequestNetworkAdapter } from 'force/ldsNetwork';
51
51
 
52
52
  const { create: create$1, freeze, keys: keys$2, entries: entries$1 } = Object;
@@ -2644,7 +2644,7 @@ function buildServiceDescriptor$d(luvio) {
2644
2644
  },
2645
2645
  };
2646
2646
  }
2647
- // version: 1.442.0-e47893165a
2647
+ // version: 1.443.0-be70f6bb6e
2648
2648
 
2649
2649
  class AuraGraphQLNormalizedCacheControlCommand extends AuraNormalizedCacheControlCommand {
2650
2650
  constructor(config, documentRootType, services) {
@@ -2982,7 +2982,7 @@ function buildServiceDescriptor$9(notifyRecordUpdateAvailable, getNormalizedLuvi
2982
2982
  },
2983
2983
  };
2984
2984
  }
2985
- // version: 1.442.0-e47893165a
2985
+ // version: 1.443.0-be70f6bb6e
2986
2986
 
2987
2987
  class RetryService {
2988
2988
  constructor(defaultRetryPolicy) {
@@ -3134,6 +3134,50 @@ function buildServiceDescriptor$8(defaultRetryPolicy) {
3134
3134
  };
3135
3135
  }
3136
3136
 
3137
+ class BasicRenewableResourceManager {
3138
+ constructor(config) {
3139
+ this.config = config;
3140
+ }
3141
+ get() {
3142
+ return this.config.storage.get().then((cached) => cached !== void 0 ? cached : this.fetchAndStore());
3143
+ }
3144
+ refresh(staleValue) {
3145
+ if (staleValue === void 0) {
3146
+ return this.fetchAndStore();
3147
+ }
3148
+ return this.config.storage.get().then(
3149
+ (current) => current !== void 0 && current !== staleValue ? current : this.fetchAndStore()
3150
+ );
3151
+ }
3152
+ clear() {
3153
+ return this.config.storage.clear();
3154
+ }
3155
+ /**
3156
+ * Single in-flight fetch shared by all concurrent `get`/`refresh` callers.
3157
+ * Overwrites storage on a successful fetch of a defined value only; a
3158
+ * rejected fetch or a resolved-`undefined` leaves the cached value intact.
3159
+ */
3160
+ fetchAndStore() {
3161
+ if (this.inFlightFetch) return this.inFlightFetch;
3162
+ const pending = this.config.fetch().then(
3163
+ (fetched) => fetched === void 0 ? fetched : this.config.storage.set(fetched).then(() => fetched)
3164
+ );
3165
+ this.inFlightFetch = pending;
3166
+ const releaseSlot = () => {
3167
+ if (this.inFlightFetch === pending) this.inFlightFetch = void 0;
3168
+ };
3169
+ pending.then(releaseSlot, releaseSlot);
3170
+ return pending;
3171
+ }
3172
+ }
3173
+ function buildRenewableResourceManagerDescriptor(type, service) {
3174
+ return {
3175
+ version: "1.0",
3176
+ service,
3177
+ type
3178
+ };
3179
+ }
3180
+
3137
3181
  function isUserVisibleError(error) {
3138
3182
  return error instanceof Error && "type" in error && error.type === "user-visible";
3139
3183
  }
@@ -5673,9 +5717,28 @@ function getEnvironmentSetting(name) {
5673
5717
  }
5674
5718
  return undefined;
5675
5719
  }
5676
- // version: 1.442.0-d73ebfc396
5720
+ // version: 1.443.0-3de9a44799
5721
+
5722
+ /**
5723
+ * Helpers for reaching the Aura framework from the LDS Aura runtime.
5724
+ *
5725
+ * `window.$A` is only present when the runtime is hosted inside LEX; in tests
5726
+ * and non-Aura runtimes it is absent. Centralizing the guarded access keeps the
5727
+ * `$A` lookup in one place instead of duplicating the `environmentHasAura`
5728
+ * check across modules.
5729
+ */
5730
+ /**
5731
+ * Returns `$A.clientService` when running inside an Aura environment, or
5732
+ * `undefined` otherwise. Defensive: never throws.
5733
+ */
5734
+ function getAuraClientService() {
5735
+ if (typeof window === 'undefined' || typeof window.$A === 'undefined') {
5736
+ return undefined;
5737
+ }
5738
+ return window.$A.clientService;
5739
+ }
5677
5740
 
5678
- const environmentHasAura = typeof window !== 'undefined' && typeof window.$A !== 'undefined';
5741
+ const auraClientService = getAuraClientService();
5679
5742
  const defaultConfig = {
5680
5743
  maxAllowedParallelXHRCounts: 9,
5681
5744
  forceRecordTransactionsDisabled: false,
@@ -5686,10 +5749,11 @@ const configServiceDescriptor = buildServiceDescriptor$1({
5686
5749
  default: defaultConfig,
5687
5750
  });
5688
5751
  const configService = configServiceDescriptor.service;
5689
- if (environmentHasAura) {
5752
+ if (auraClientService?.maxAllowedParallelXHRCounts) {
5690
5753
  configService.set('lex', (config) => {
5691
5754
  config.maxAllowedParallelXHRCounts =
5692
- window['$A'].clientService.maxAllowedParallelXHRCounts();
5755
+ auraClientService.maxAllowedParallelXHRCounts?.() ??
5756
+ defaultConfig.maxAllowedParallelXHRCounts;
5693
5757
  config.forceRecordTransactionsDisabled = getEnvironmentSetting(EnvironmentSettings.ForceRecordTransactionsDisabled);
5694
5758
  });
5695
5759
  }
@@ -6119,146 +6183,91 @@ function buildLexRuntimeLuvio5xxStatusResponseInterceptor() {
6119
6183
  };
6120
6184
  }
6121
6185
 
6122
- const CSRF_TOKEN_KEY = 'salesforce_csrf_token';
6123
- const CSRF_STORAGE_NAME = 'ldsCSRFToken';
6124
- const BASE_URI = '/services/data/v68.0';
6125
- const UI_API_BASE_URI = `${BASE_URI}/ui-api`;
6126
- const CSRF_TOKEN_ENDPOINT = `${UI_API_BASE_URI}/session/csrf`;
6127
- const CSRF_STORAGE_CONFIG = {
6128
- name: CSRF_STORAGE_NAME,
6129
- persistent: true,
6130
- secure: true,
6131
- maxSize: 1024,
6132
- expiration: 24 * 60 * 60,
6133
- clearOnInit: false,
6134
- debugLogging: false,
6135
- };
6136
6186
  /**
6137
- * Manages CSRF token fetching and caching for secure requests.
6138
- * Implements a singleton pattern to ensure consistent token management across the application.
6187
+ * Normalizes Connect REST error envelopes into the Aura Shape A
6188
+ * (ConnectInJava) shape, so consumers can read `response.body.errorCode`
6189
+ * regardless of transport.
6190
+ *
6191
+ * - HTTP error envelope (this path): `[{ errorCode, message }, ...]`
6192
+ * - Aura Shape A: `{ errorCode, message, statusCode, ... }`
6193
+ *
6194
+ * The full array is preserved at `body.enhancedErrorInfo.allErrors` since
6195
+ * Connect can return multiple errors per response.
6196
+ *
6197
+ * Aura Shape B (`{ error: "..." }`) is an Aura-only fallback that never
6198
+ * appears on the HTTP path, so no normalization is needed for it here.
6199
+ *
6200
+ * Ordering: must run AFTER any other interceptor that inspects the raw
6201
+ * error body shape (e.g. the 5xx interceptor's ErrorId extraction reads
6202
+ * `body[0].message` from the array form).
6139
6203
  */
6140
- class CsrfTokenManager {
6141
- constructor() {
6142
- this.tokenPromise = null;
6143
- this.refreshInFlight = null;
6144
- // Initialize AuraStorage
6145
- this.storage = createStorage(CSRF_STORAGE_CONFIG);
6146
- }
6147
- static getInstance() {
6148
- if (!CsrfTokenManager.instance) {
6149
- CsrfTokenManager.instance = new CsrfTokenManager();
6150
- }
6151
- return CsrfTokenManager.instance;
6152
- }
6153
- /**
6154
- * Obtain a CSRF token, either from AuraStorage or by fetching a fresh one.
6155
- *
6156
- * @private
6157
- */
6158
- async loadOrFetchToken() {
6159
- // First try to get token from AuraStorage
6160
- if (this.storage) {
6161
- try {
6162
- const cachedToken = await this.storage.get(CSRF_TOKEN_KEY);
6163
- if (typeof cachedToken === 'string' && cachedToken) {
6164
- return cachedToken;
6165
- }
6166
- }
6167
- catch {
6168
- // If storage read fails, continue to fetch
6169
- }
6170
- }
6171
- // No cached token, fetch from server
6172
- return this.fetchFreshToken();
6173
- }
6174
- /**
6175
- * Call API endpoint to acquire a CSRF token and cache it.
6176
- *
6177
- * @private
6178
- */
6179
- async fetchFreshToken() {
6180
- try {
6181
- const response = await fetch(CSRF_TOKEN_ENDPOINT, {
6182
- method: 'GET',
6183
- credentials: 'same-origin',
6184
- });
6185
- if (!response.ok) {
6186
- return undefined;
6187
- }
6188
- const data = await response.json();
6189
- const token = data.csrfToken;
6190
- if (token && this.storage) {
6191
- // Cache the token in AuraStorage
6192
- try {
6193
- await this.storage.set(CSRF_TOKEN_KEY, token);
6194
- }
6195
- catch {
6196
- // Non-fatal: token is still available even if caching fails
6197
- }
6198
- }
6199
- return token;
6200
- }
6201
- catch {
6202
- return undefined;
6203
- }
6204
- }
6205
- /**
6206
- * Returns the current token value as a Promise.
6207
- * Lazy-loads the token on first call (from cache or by fetching).
6208
- */
6209
- async getToken() {
6210
- // Lazy initialization: only fetch token when actually needed
6211
- if (!this.tokenPromise) {
6212
- this.tokenPromise = this.loadOrFetchToken();
6213
- }
6214
- return this.tokenPromise;
6215
- }
6216
- /**
6217
- * Obtains and returns a new token value as a promise.
6218
- * This will clear the cached token and fetch a fresh one.
6219
- *
6220
- * Concurrent calls coalesce onto a single in-flight refresh — important when
6221
- * multiple requests fail with INVALID_ACCESS_TOKEN simultaneously and each
6222
- * retry policy independently calls refreshToken(). Without this, every caller
6223
- * triggers its own /session/csrf round-trip.
6224
- */
6225
- refreshToken() {
6226
- if (this.refreshInFlight) {
6227
- return this.refreshInFlight;
6204
+ function buildLuvioErrorBodyNormalizationInterceptor() {
6205
+ return async (response) => {
6206
+ if (!response.ok && Array.isArray(response.body)) {
6207
+ const allErrors = response.body;
6208
+ response.body = {
6209
+ ...allErrors[0],
6210
+ statusCode: response.status,
6211
+ enhancedErrorInfo: {
6212
+ allErrors,
6213
+ },
6214
+ };
6228
6215
  }
6229
- const refresh = (async () => {
6230
- if (this.storage) {
6231
- try {
6232
- await this.storage.remove(CSRF_TOKEN_KEY);
6233
- }
6234
- catch {
6235
- // Non-fatal: continue with refresh even if clear fails
6236
- }
6237
- }
6238
- return this.fetchFreshToken();
6239
- })();
6240
- this.refreshInFlight = refresh;
6241
- this.tokenPromise = refresh;
6242
- // Clear the in-flight reference once settled (success or failure) so
6243
- // subsequent token expirations can trigger a fresh refresh.
6244
- refresh.finally(() => {
6245
- if (this.refreshInFlight === refresh) {
6246
- this.refreshInFlight = null;
6247
- }
6216
+ return response;
6217
+ };
6218
+ }
6219
+
6220
+ /**
6221
+ * Service name the CSRF token manager is registered under by
6222
+ * `initializeOneStore` (see `buildRenewableResourceManagerDescriptor`).
6223
+ */
6224
+ const CSRF_TOKEN_MANAGER_SERVICE = 'csrfTokenManager';
6225
+ const CSRF_TOKEN_MANAGER_REQUEST = {
6226
+ [CSRF_TOKEN_MANAGER_SERVICE]: {
6227
+ type: CSRF_TOKEN_MANAGER_SERVICE,
6228
+ version: '1.0',
6229
+ },
6230
+ };
6231
+ let cached;
6232
+ /**
6233
+ * Resolves the CSRF token manager from the service provisioner.
6234
+ *
6235
+ * The manager is registered during `initializeOneStore`, which runs before any
6236
+ * request flows; the interceptors and retry policies that call this resolve
6237
+ * lazily (per request / per retry), so the service is always available by then.
6238
+ *
6239
+ * The resolved promise is memoized so resolution happens once rather than per
6240
+ * request. A rejection is NOT memoized — `getServices` rejects when the service
6241
+ * is unavailable, and caching that would permanently wedge CSRF handling — so a
6242
+ * later call retries the resolution.
6243
+ */
6244
+ function getCsrfTokenManager() {
6245
+ if (!cached) {
6246
+ cached = Promise.resolve(getServices(CSRF_TOKEN_MANAGER_REQUEST))
6247
+ .then((services) => services[CSRF_TOKEN_MANAGER_SERVICE])
6248
+ .catch((error) => {
6249
+ cached = undefined;
6250
+ throw error;
6248
6251
  });
6249
- return refresh;
6250
- }
6251
- /**
6252
- * Reset the singleton instance (useful for testing).
6253
- * @internal
6254
- */
6255
- static resetInstance() {
6256
- CsrfTokenManager.instance = null;
6257
6252
  }
6253
+ return cached;
6258
6254
  }
6259
- CsrfTokenManager.instance = null;
6260
6255
 
6261
6256
  const CSRF_TOKEN_HEADER = 'X-CSRF-Token';
6257
+ /**
6258
+ * Resolves the CSRF token manager and returns the current token. Returns
6259
+ * `undefined` if the manager cannot be resolved (service not yet provisioned)
6260
+ * or has no token, so a request is never blocked by token resolution.
6261
+ */
6262
+ async function getCsrfToken() {
6263
+ try {
6264
+ const manager = await getCsrfTokenManager();
6265
+ return await manager.get();
6266
+ }
6267
+ catch {
6268
+ return undefined;
6269
+ }
6270
+ }
6262
6271
  /**
6263
6272
  * Checks if all required gates are enabled for CSRF token interceptor.
6264
6273
  *
@@ -6301,7 +6310,6 @@ function isCsrfMethod(method) {
6301
6310
  * @returns A RequestInterceptor function for FetchParameters
6302
6311
  */
6303
6312
  function buildCsrfTokenInterceptor() {
6304
- const csrfTokenManager = CsrfTokenManager.getInstance();
6305
6313
  return async (fetchArgs) => {
6306
6314
  // Check if all required gates are enabled before running
6307
6315
  if (!areCsrfGatesEnabled()) {
@@ -6318,7 +6326,7 @@ function buildCsrfTokenInterceptor() {
6318
6326
  }
6319
6327
  // Only add CSRF token for mutating operations
6320
6328
  if (isCsrfMethod(method)) {
6321
- const token = await csrfTokenManager.getToken();
6329
+ const token = await getCsrfToken();
6322
6330
  if (token) {
6323
6331
  // eslint-disable-next-line no-param-reassign
6324
6332
  fetchArgs = setHeader(CSRF_TOKEN_HEADER, token, fetchArgs);
@@ -6335,7 +6343,6 @@ function buildCsrfTokenInterceptor() {
6335
6343
  * @returns A request interceptor function for Luvio ResourceRequest objects
6336
6344
  */
6337
6345
  function buildLuvioCsrfTokenInterceptor() {
6338
- const csrfTokenManager = CsrfTokenManager.getInstance();
6339
6346
  return async (resourceRequest) => {
6340
6347
  // Check if all required gates are enabled before running
6341
6348
  if (!areCsrfGatesEnabled()) {
@@ -6349,7 +6356,7 @@ function buildLuvioCsrfTokenInterceptor() {
6349
6356
  if (isCsrfMethod(resourceRequest.method)) {
6350
6357
  // Don't overwrite existing CSRF token header if it already exists
6351
6358
  if (!resourceRequest.headers[CSRF_TOKEN_HEADER]) {
6352
- const token = await csrfTokenManager.getToken();
6359
+ const token = await getCsrfToken();
6353
6360
  if (token) {
6354
6361
  resourceRequest.headers[CSRF_TOKEN_HEADER] = token;
6355
6362
  }
@@ -6389,6 +6396,39 @@ function buildLuvioEntityEncodingInterceptor() {
6389
6396
  };
6390
6397
  }
6391
6398
 
6399
+ const FIRST_PARTY_HEADER = 'X-Salesforce-First-Party';
6400
+ const FIRST_PARTY_VALUE = 'platform-ui';
6401
+ /**
6402
+ * Builds a request interceptor that adds the LDS first party header to every
6403
+ * outbound request.
6404
+ *
6405
+ * @returns A RequestInterceptor function for FetchParameters
6406
+ */
6407
+ function buildFirstPartyHeaderInterceptor() {
6408
+ return async (fetchArgs) => {
6409
+ const returnedFetchArgs = setHeader(FIRST_PARTY_HEADER, FIRST_PARTY_VALUE, fetchArgs);
6410
+ return resolvedPromiseLike$2(returnedFetchArgs);
6411
+ };
6412
+ }
6413
+ /**
6414
+ * Builds a Luvio request interceptor that adds the LDS first party header to
6415
+ * every outbound `ResourceRequest`. See {@link buildFirstPartyHeaderInterceptor} for
6416
+ * the header semantics.
6417
+ *
6418
+ * @returns A request interceptor for Luvio ResourceRequest objects
6419
+ */
6420
+ function buildLuvioFirstPartyHeaderInterceptor() {
6421
+ return async (resourceRequest) => {
6422
+ if (!resourceRequest.headers) {
6423
+ resourceRequest.headers = {};
6424
+ }
6425
+ if (!resourceRequest.headers[FIRST_PARTY_HEADER]) {
6426
+ resourceRequest.headers[FIRST_PARTY_HEADER] = FIRST_PARTY_VALUE;
6427
+ }
6428
+ return resolvedPromiseLike$2(resourceRequest);
6429
+ };
6430
+ }
6431
+
6392
6432
  function createInstrumentationIdContext() {
6393
6433
  return () => ({
6394
6434
  instrumentationId: generateRequestId(),
@@ -6618,7 +6658,6 @@ class LuvioCsrfTokenRetryPolicy extends RetryPolicy {
6618
6658
  constructor(config = DEFAULT_CONFIG$3) {
6619
6659
  super();
6620
6660
  this.config = config;
6621
- this.csrfTokenManager = CsrfTokenManager.getInstance();
6622
6661
  }
6623
6662
  setRequestContext(context) {
6624
6663
  this.requestContext = context;
@@ -6640,11 +6679,20 @@ class LuvioCsrfTokenRetryPolicy extends RetryPolicy {
6640
6679
  if (!this.requestContext) {
6641
6680
  return;
6642
6681
  }
6643
- const newToken = await this.csrfTokenManager.refreshToken();
6682
+ let newToken;
6683
+ try {
6684
+ const manager = await getCsrfTokenManager();
6685
+ newToken = await manager.refresh();
6686
+ }
6687
+ catch {
6688
+ // Manager unavailable — drop the stale token below so the request
6689
+ // interceptor refetches on the retry.
6690
+ newToken = undefined;
6691
+ }
6644
6692
  const req = this.requestContext.request;
6645
6693
  const { [CSRF_TOKEN_HEADER]: _stale, ...remainingHeaders } = req.headers ?? {};
6646
6694
  // If refresh failed, drop the stale token entirely so the request interceptor
6647
- // will fetch a fresh one via getToken() on the retry.
6695
+ // will fetch a fresh one via get() on the retry.
6648
6696
  const headers = newToken
6649
6697
  ? { ...remainingHeaders, [CSRF_TOKEN_HEADER]: newToken }
6650
6698
  : remainingHeaders;
@@ -7149,12 +7197,14 @@ const composedFetchNetworkAdapter = {
7149
7197
  buildLuvioTransportMarksSendInterceptor(),
7150
7198
  buildLuvioPageScopedCacheRequestInterceptor(),
7151
7199
  buildLuvioCsrfTokenInterceptor(),
7200
+ buildLuvioFirstPartyHeaderInterceptor(),
7152
7201
  buildLuvioEntityEncodingInterceptor(),
7153
7202
  ],
7154
7203
  retry: buildLuvioFetchRetryInterceptor(),
7155
7204
  response: [
7156
7205
  buildLexRuntimeLuvio5xxStatusResponseInterceptor(),
7157
7206
  buildLexRuntimeLuvioAuthExpirationRedirectResponseInterceptor(),
7207
+ buildLuvioErrorBodyNormalizationInterceptor(),
7158
7208
  buildLuvioTransportMarksReceiveInterceptor(),
7159
7209
  buildLuvioActionMarksReceiveInterceptor(),
7160
7210
  ],
@@ -7199,7 +7249,6 @@ class CsrfTokenRetryPolicy extends RetryPolicy {
7199
7249
  constructor(config = DEFAULT_CONFIG$1) {
7200
7250
  super();
7201
7251
  this.config = config;
7202
- this.csrfTokenManager = CsrfTokenManager.getInstance();
7203
7252
  }
7204
7253
  /**
7205
7254
  * Allows the fetch service to pass mutable request context.
@@ -7251,7 +7300,15 @@ class CsrfTokenRetryPolicy extends RetryPolicy {
7251
7300
  */
7252
7301
  async prepareRetry(_result, _context) {
7253
7302
  // Refresh the CSRF token (we already know this is a CSRF error from shouldRetry)
7254
- const newToken = await this.csrfTokenManager.refreshToken();
7303
+ let newToken;
7304
+ try {
7305
+ const manager = await getCsrfTokenManager();
7306
+ newToken = await manager.refresh();
7307
+ }
7308
+ catch {
7309
+ // Manager unavailable — the retry will fail again, which is expected.
7310
+ newToken = undefined;
7311
+ }
7255
7312
  if (!newToken || !this.requestContext) {
7256
7313
  // If we can't get a new token or don't have request context,
7257
7314
  // the retry will fail again but that's expected
@@ -10084,6 +10141,7 @@ function getLexRuntimeDefaultInterceptorConfig(logger) {
10084
10141
  buildTransportMarksSendInterceptor(),
10085
10142
  buildCsrfTokenInterceptor(),
10086
10143
  buildEntityEncodingInterceptor(),
10144
+ buildFirstPartyHeaderInterceptor(),
10087
10145
  ],
10088
10146
  retry: buildCsrfRetryInterceptor(),
10089
10147
  response: [
@@ -10195,6 +10253,149 @@ class FetchThrottlingRetryPolicy extends RetryPolicy {
10195
10253
  }
10196
10254
  }
10197
10255
 
10256
+ const CSRF_TOKEN_KEY = 'salesforce_csrf_token';
10257
+ const CSRF_STORAGE_NAME = 'ldsCSRFToken';
10258
+ const BASE_URI = '/services/data/v68.0';
10259
+ const UI_API_BASE_URI = `${BASE_URI}/ui-api`;
10260
+ const CSRF_TOKEN_ENDPOINT = `${UI_API_BASE_URI}/session/csrf`;
10261
+ const CSRF_STORAGE_CONFIG = {
10262
+ name: CSRF_STORAGE_NAME,
10263
+ persistent: true,
10264
+ secure: true,
10265
+ maxSize: 1024,
10266
+ expiration: 24 * 60 * 60,
10267
+ clearOnInit: false,
10268
+ debugLogging: false,
10269
+ };
10270
+ /**
10271
+ * Reads the CSRF token Aura preloads onto the bootstrap payload. When the
10272
+ * Connect Framework CSRF token is minted at template-render time it is exposed
10273
+ * via `$A.clientService.getConnectCsrfToken()`. Reading it here lets the first
10274
+ * state-changing request skip the cold `/ui-api/session/csrf` round trip.
10275
+ *
10276
+ * Defensive against a missing getter or server killswitch: returns `undefined`,
10277
+ * never throws.
10278
+ */
10279
+ function readPreloadedToken() {
10280
+ const clientService = getAuraClientService();
10281
+ if (typeof clientService?.getConnectCsrfToken !== 'function') {
10282
+ return undefined;
10283
+ }
10284
+ try {
10285
+ const token = clientService.getConnectCsrfToken();
10286
+ return typeof token === 'string' && token ? token : undefined;
10287
+ }
10288
+ catch {
10289
+ return undefined;
10290
+ }
10291
+ }
10292
+ /**
10293
+ * Fetches a fresh CSRF token from the server. Resolves `undefined` (not a
10294
+ * rejection) on any non-ok response, missing token, or network/parse error so
10295
+ * the manager treats it as "no value" and leaves any cached token intact. The
10296
+ * CSRF retry policy — not this fetch — owns retry semantics.
10297
+ */
10298
+ async function fetchFreshToken() {
10299
+ try {
10300
+ const response = await fetch(CSRF_TOKEN_ENDPOINT, {
10301
+ method: 'GET',
10302
+ credentials: 'same-origin',
10303
+ });
10304
+ if (!response.ok) {
10305
+ return undefined;
10306
+ }
10307
+ const data = await response.json();
10308
+ const token = data.csrfToken;
10309
+ return typeof token === 'string' && token ? token : undefined;
10310
+ }
10311
+ catch {
10312
+ return undefined;
10313
+ }
10314
+ }
10315
+ /**
10316
+ * Adapts the durable AuraStorage backing store to the {@link ResourceStorage}
10317
+ * contract the renewable-resource manager expects.
10318
+ *
10319
+ * The Aura-preloaded token is written into the store **once at creation** (only
10320
+ * when the store is otherwise empty), so the first `get()` finds it and the
10321
+ * first mutation skips the network. Seeding the store — rather than having
10322
+ * `get()` fall back to the preload on every empty read — means a later
10323
+ * `clear()` (logout / org switch) is not silently resurrected by the preload.
10324
+ *
10325
+ * Every operation awaits the one-time seed so reads and writes observe a
10326
+ * consistent store. Storage read/write failures are non-fatal.
10327
+ */
10328
+ function buildCsrfResourceStorage() {
10329
+ const storage = createStorage(CSRF_STORAGE_CONFIG);
10330
+ // Pre-seed the preloaded token into the store, once, if the store is empty.
10331
+ const seeded = (async () => {
10332
+ const preloadedToken = readPreloadedToken();
10333
+ if (!storage || !preloadedToken) {
10334
+ return;
10335
+ }
10336
+ try {
10337
+ const existing = await storage.get(CSRF_TOKEN_KEY);
10338
+ if (typeof existing !== 'string' || !existing) {
10339
+ await storage.set(CSRF_TOKEN_KEY, preloadedToken);
10340
+ }
10341
+ }
10342
+ catch {
10343
+ // Non-fatal: fall back to fetching a fresh token on first use.
10344
+ }
10345
+ })();
10346
+ return {
10347
+ get: async () => {
10348
+ await seeded;
10349
+ if (storage) {
10350
+ try {
10351
+ const cached = await storage.get(CSRF_TOKEN_KEY);
10352
+ if (typeof cached === 'string' && cached) {
10353
+ return cached;
10354
+ }
10355
+ }
10356
+ catch {
10357
+ // Non-fatal: treat as a cache miss so the manager fetches.
10358
+ }
10359
+ }
10360
+ return undefined;
10361
+ },
10362
+ set: async (value) => {
10363
+ await seeded;
10364
+ if (storage) {
10365
+ try {
10366
+ await storage.set(CSRF_TOKEN_KEY, value);
10367
+ }
10368
+ catch {
10369
+ // Non-fatal: token is still usable even if caching fails.
10370
+ }
10371
+ }
10372
+ },
10373
+ clear: async () => {
10374
+ await seeded;
10375
+ if (storage) {
10376
+ try {
10377
+ await storage.remove(CSRF_TOKEN_KEY);
10378
+ }
10379
+ catch {
10380
+ // Non-fatal: continue even if the clear fails.
10381
+ }
10382
+ }
10383
+ },
10384
+ };
10385
+ }
10386
+ /**
10387
+ * Builds the CSRF token manager: a {@link BasicRenewableResourceManager} that
10388
+ * lazily caches the token in durable storage, coalesces concurrent fetches onto
10389
+ * a single in-flight request, and dedups refresh storms. Registered as a
10390
+ * provisioner service (`csrfTokenManager`) by `initializeOneStore`.
10391
+ */
10392
+ function buildCsrfTokenManager() {
10393
+ return new BasicRenewableResourceManager({
10394
+ fetch: fetchFreshToken,
10395
+ storage: buildCsrfResourceStorage(),
10396
+ });
10397
+ }
10398
+
10198
10399
  /* eslint-disable no-console */
10199
10400
  /**
10200
10401
  * Default storage configuration for the durable cache
@@ -10789,6 +10990,7 @@ function initializeOneStore(luvio) {
10789
10990
  const retryPolicy = new ComposedRetryPolicy([throttlingPolicy, csrfPolicy]);
10790
10991
  const retryServiceDescriptor = buildServiceDescriptor$8(retryPolicy);
10791
10992
  const retryService = retryServiceDescriptor.service;
10993
+ const csrfTokenManagerServiceDescriptor = buildRenewableResourceManagerDescriptor(CSRF_TOKEN_MANAGER_SERVICE, buildCsrfTokenManager());
10792
10994
  const prefetchSfapJwtServiceDescriptor = {
10793
10995
  type: 'prefetchSfapJwt',
10794
10996
  version: '1.0',
@@ -10836,6 +11038,7 @@ function initializeOneStore(luvio) {
10836
11038
  buildLWCGraphQLWireBindingsServiceDescriptor(),
10837
11039
  configServiceDescriptor,
10838
11040
  prefetchSfapJwtServiceDescriptor,
11041
+ csrfTokenManagerServiceDescriptor,
10839
11042
  ];
10840
11043
  setServices(services);
10841
11044
  }
@@ -10855,4 +11058,4 @@ function ldsEngineCreator() {
10855
11058
  }
10856
11059
 
10857
11060
  export { LexRequestStrategy, PdlPrefetcherEventType, PdlRequestPriority, buildPredictorForContext, configService, ldsEngineCreator as default, initializeLDS, initializeOneStore, notifyUpdateAvailableFactory, registerRequestStrategy, saveRequestAsPrediction, subscribeToPrefetcherEvents, unregisterRequestStrategy, whenPredictionsReady };
10858
- // version: 1.442.0-e47893165a
11061
+ // version: 1.443.0-be70f6bb6e
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Helpers for reaching the Aura framework from the LDS Aura runtime.
3
+ *
4
+ * `window.$A` is only present when the runtime is hosted inside LEX; in tests
5
+ * and non-Aura runtimes it is absent. Centralizing the guarded access keeps the
6
+ * `$A` lookup in one place instead of duplicating the `environmentHasAura`
7
+ * check across modules.
8
+ */
9
+ /**
10
+ * The subset of the Aura client service LDS reaches for. Methods are optional
11
+ * because availability depends on the host's Aura version and server gates.
12
+ */
13
+ export interface AuraClientService {
14
+ maxAllowedParallelXHRCounts?: () => number;
15
+ getConnectCsrfToken?: () => unknown;
16
+ }
17
+ /**
18
+ * Returns `$A.clientService` when running inside an Aura environment, or
19
+ * `undefined` otherwise. Defensive: never throws.
20
+ */
21
+ export declare function getAuraClientService(): AuraClientService | undefined;
@@ -1,45 +1,8 @@
1
+ import { type RenewableResourceManager } from '@conduit-client/service-renewable-resource-manager/v1';
1
2
  /**
2
- * Manages CSRF token fetching and caching for secure requests.
3
- * Implements a singleton pattern to ensure consistent token management across the application.
3
+ * Builds the CSRF token manager: a {@link BasicRenewableResourceManager} that
4
+ * lazily caches the token in durable storage, coalesces concurrent fetches onto
5
+ * a single in-flight request, and dedups refresh storms. Registered as a
6
+ * provisioner service (`csrfTokenManager`) by `initializeOneStore`.
4
7
  */
5
- declare class CsrfTokenManager {
6
- private static instance;
7
- private tokenPromise;
8
- private refreshInFlight;
9
- private storage;
10
- private constructor();
11
- static getInstance(): CsrfTokenManager;
12
- /**
13
- * Obtain a CSRF token, either from AuraStorage or by fetching a fresh one.
14
- *
15
- * @private
16
- */
17
- private loadOrFetchToken;
18
- /**
19
- * Call API endpoint to acquire a CSRF token and cache it.
20
- *
21
- * @private
22
- */
23
- private fetchFreshToken;
24
- /**
25
- * Returns the current token value as a Promise.
26
- * Lazy-loads the token on first call (from cache or by fetching).
27
- */
28
- getToken(): Promise<string | undefined>;
29
- /**
30
- * Obtains and returns a new token value as a promise.
31
- * This will clear the cached token and fetch a fresh one.
32
- *
33
- * Concurrent calls coalesce onto a single in-flight refresh — important when
34
- * multiple requests fail with INVALID_ACCESS_TOKEN simultaneously and each
35
- * retry policy independently calls refreshToken(). Without this, every caller
36
- * triggers its own /session/csrf round-trip.
37
- */
38
- refreshToken(): Promise<string | undefined>;
39
- /**
40
- * Reset the singleton instance (useful for testing).
41
- * @internal
42
- */
43
- static resetInstance(): void;
44
- }
45
- export { CsrfTokenManager };
8
+ export declare function buildCsrfTokenManager(): RenewableResourceManager<string>;
@@ -0,0 +1,19 @@
1
+ import type { RenewableResourceManager } from '@conduit-client/service-renewable-resource-manager/v1';
2
+ /**
3
+ * Service name the CSRF token manager is registered under by
4
+ * `initializeOneStore` (see `buildRenewableResourceManagerDescriptor`).
5
+ */
6
+ export declare const CSRF_TOKEN_MANAGER_SERVICE = "csrfTokenManager";
7
+ /**
8
+ * Resolves the CSRF token manager from the service provisioner.
9
+ *
10
+ * The manager is registered during `initializeOneStore`, which runs before any
11
+ * request flows; the interceptors and retry policies that call this resolve
12
+ * lazily (per request / per retry), so the service is always available by then.
13
+ *
14
+ * The resolved promise is memoized so resolution happens once rather than per
15
+ * request. A rejection is NOT memoized — `getServices` rejects when the service
16
+ * is unavailable, and caching that would permanently wedge CSRF handling — so a
17
+ * later call retries the resolution.
18
+ */
19
+ export declare function getCsrfTokenManager(): Promise<RenewableResourceManager<string>>;
@@ -0,0 +1,17 @@
1
+ import { type RequestInterceptor } from '@conduit-client/service-fetch-network/v1';
2
+ import type { ResourceRequest } from '@luvio/engine';
3
+ /**
4
+ * Builds a request interceptor that adds the LDS first party header to every
5
+ * outbound request.
6
+ *
7
+ * @returns A RequestInterceptor function for FetchParameters
8
+ */
9
+ export declare function buildFirstPartyHeaderInterceptor(): RequestInterceptor;
10
+ /**
11
+ * Builds a Luvio request interceptor that adds the LDS first party header to
12
+ * every outbound `ResourceRequest`. See {@link buildFirstPartyHeaderInterceptor} for
13
+ * the header semantics.
14
+ *
15
+ * @returns A request interceptor for Luvio ResourceRequest objects
16
+ */
17
+ export declare function buildLuvioFirstPartyHeaderInterceptor(): (resourceRequest: ResourceRequest) => Promise<ResourceRequest>;
@@ -0,0 +1,20 @@
1
+ import type { ResponseInterceptor as LuvioResponseInterceptor } from '@salesforce/lds-network-fetch';
2
+ /**
3
+ * Normalizes Connect REST error envelopes into the Aura Shape A
4
+ * (ConnectInJava) shape, so consumers can read `response.body.errorCode`
5
+ * regardless of transport.
6
+ *
7
+ * - HTTP error envelope (this path): `[{ errorCode, message }, ...]`
8
+ * - Aura Shape A: `{ errorCode, message, statusCode, ... }`
9
+ *
10
+ * The full array is preserved at `body.enhancedErrorInfo.allErrors` since
11
+ * Connect can return multiple errors per response.
12
+ *
13
+ * Aura Shape B (`{ error: "..." }`) is an Aura-only fallback that never
14
+ * appears on the HTTP path, so no normalization is needed for it here.
15
+ *
16
+ * Ordering: must run AFTER any other interceptor that inspects the raw
17
+ * error body shape (e.g. the 5xx interceptor's ErrorId extraction reads
18
+ * `body[0].message` from the array form).
19
+ */
20
+ export declare function buildLuvioErrorBodyNormalizationInterceptor(): LuvioResponseInterceptor;
@@ -26,7 +26,6 @@ export interface MutableFetchRequest {
26
26
  */
27
27
  export declare class CsrfTokenRetryPolicy extends RetryPolicy<Response> {
28
28
  private config;
29
- private csrfTokenManager;
30
29
  private requestContext?;
31
30
  constructor(config?: CsrfTokenRetryPolicyConfig);
32
31
  /**
@@ -12,7 +12,6 @@ export interface MutableLuvioRequest {
12
12
  }
13
13
  export declare class LuvioCsrfTokenRetryPolicy extends RetryPolicy<FetchResponse<any>> {
14
14
  private config;
15
- private csrfTokenManager;
16
15
  private requestContext?;
17
16
  constructor(config?: LuvioCsrfTokenRetryPolicyConfig);
18
17
  setRequestContext(context: MutableLuvioRequest): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/lds-runtime-aura",
3
- "version": "1.442.0",
3
+ "version": "1.443.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "LDS engine for Aura runtime.",
6
6
  "main": "dist/ldsEngineCreator.js",
@@ -34,60 +34,61 @@
34
34
  "release:corejar": "yarn build && ../core-build/scripts/core.js --name=lds-runtime-aura"
35
35
  },
36
36
  "devDependencies": {
37
- "@conduit-client/service-provisioner": "3.21.0",
38
- "@conduit-client/tools-core": "3.21.0",
39
- "@salesforce/lds-adapters-apex": "^1.442.0",
40
- "@salesforce/lds-adapters-uiapi": "^1.442.0",
41
- "@salesforce/lds-ads-bridge": "^1.442.0",
42
- "@salesforce/lds-aura-storage": "^1.442.0",
43
- "@salesforce/lds-bindings": "^1.442.0",
44
- "@salesforce/lds-instrumentation": "^1.442.0",
45
- "@salesforce/lds-network-adapter": "^1.442.0",
46
- "@salesforce/lds-network-aura": "^1.442.0",
47
- "@salesforce/lds-network-fetch": "^1.442.0",
37
+ "@conduit-client/service-provisioner": "3.22.0",
38
+ "@conduit-client/tools-core": "3.22.0",
39
+ "@salesforce/lds-adapters-apex": "^1.443.0",
40
+ "@salesforce/lds-adapters-uiapi": "^1.443.0",
41
+ "@salesforce/lds-ads-bridge": "^1.443.0",
42
+ "@salesforce/lds-aura-storage": "^1.443.0",
43
+ "@salesforce/lds-bindings": "^1.443.0",
44
+ "@salesforce/lds-instrumentation": "^1.443.0",
45
+ "@salesforce/lds-network-adapter": "^1.443.0",
46
+ "@salesforce/lds-network-aura": "^1.443.0",
47
+ "@salesforce/lds-network-fetch": "^1.443.0",
48
48
  "jwt-encode": "1.0.1"
49
49
  },
50
50
  "dependencies": {
51
- "@conduit-client/command-aura-graphql-normalized-cache-control": "3.21.0",
52
- "@conduit-client/command-aura-network": "3.21.0",
53
- "@conduit-client/command-aura-normalized-cache-control": "3.21.0",
54
- "@conduit-client/command-aura-resource-cache-control": "3.21.0",
55
- "@conduit-client/command-fetch-network": "3.21.0",
56
- "@conduit-client/command-http-graphql-normalized-cache-control": "3.21.0",
57
- "@conduit-client/command-http-normalized-cache-control": "3.21.0",
58
- "@conduit-client/command-ndjson": "3.21.0",
59
- "@conduit-client/command-network": "3.21.0",
60
- "@conduit-client/command-sse": "3.21.0",
61
- "@conduit-client/command-streaming": "3.21.0",
62
- "@conduit-client/service-aura-network": "3.21.0",
63
- "@conduit-client/service-bindings-imperative": "3.21.0",
64
- "@conduit-client/service-bindings-lwc": "3.21.0",
65
- "@conduit-client/service-cache": "3.21.0",
66
- "@conduit-client/service-cache-control": "3.21.0",
67
- "@conduit-client/service-cache-inclusion-policy": "3.21.0",
68
- "@conduit-client/service-config": "3.21.0",
69
- "@conduit-client/service-feature-flags": "3.21.0",
70
- "@conduit-client/service-fetch-network": "3.21.0",
71
- "@conduit-client/service-instrument-command": "3.21.0",
72
- "@conduit-client/service-pubsub": "3.21.0",
73
- "@conduit-client/service-store": "3.21.0",
74
- "@conduit-client/utils": "3.21.0",
51
+ "@conduit-client/command-aura-graphql-normalized-cache-control": "3.22.0",
52
+ "@conduit-client/command-aura-network": "3.22.0",
53
+ "@conduit-client/command-aura-normalized-cache-control": "3.22.0",
54
+ "@conduit-client/command-aura-resource-cache-control": "3.22.0",
55
+ "@conduit-client/command-fetch-network": "3.22.0",
56
+ "@conduit-client/command-http-graphql-normalized-cache-control": "3.22.0",
57
+ "@conduit-client/command-http-normalized-cache-control": "3.22.0",
58
+ "@conduit-client/command-ndjson": "3.22.0",
59
+ "@conduit-client/command-network": "3.22.0",
60
+ "@conduit-client/command-sse": "3.22.0",
61
+ "@conduit-client/command-streaming": "3.22.0",
62
+ "@conduit-client/service-aura-network": "3.22.0",
63
+ "@conduit-client/service-bindings-imperative": "3.22.0",
64
+ "@conduit-client/service-bindings-lwc": "3.22.0",
65
+ "@conduit-client/service-cache": "3.22.0",
66
+ "@conduit-client/service-cache-control": "3.22.0",
67
+ "@conduit-client/service-cache-inclusion-policy": "3.22.0",
68
+ "@conduit-client/service-config": "3.22.0",
69
+ "@conduit-client/service-feature-flags": "3.22.0",
70
+ "@conduit-client/service-fetch-network": "3.22.0",
71
+ "@conduit-client/service-instrument-command": "3.22.0",
72
+ "@conduit-client/service-pubsub": "3.22.0",
73
+ "@conduit-client/service-renewable-resource-manager": "3.22.0",
74
+ "@conduit-client/service-store": "3.22.0",
75
+ "@conduit-client/utils": "3.22.0",
75
76
  "@luvio/network-adapter-composable": "0.160.5",
76
77
  "@luvio/network-adapter-fetch": "0.160.5",
77
78
  "@lwc/state": "^0.29.0",
78
- "@salesforce/lds-adapters-onestore-graphql": "^1.442.0",
79
+ "@salesforce/lds-adapters-onestore-graphql": "^1.443.0",
79
80
  "@salesforce/lds-adapters-uiapi-lex": "^1.415.0",
80
- "@salesforce/lds-durable-storage": "^1.442.0",
81
- "@salesforce/lds-luvio-service": "^1.442.0",
82
- "@salesforce/lds-luvio-uiapi-records-service": "^1.442.0"
81
+ "@salesforce/lds-durable-storage": "^1.443.0",
82
+ "@salesforce/lds-luvio-service": "^1.443.0",
83
+ "@salesforce/lds-luvio-uiapi-records-service": "^1.443.0"
83
84
  },
84
85
  "luvioBundlesize": [
85
86
  {
86
87
  "path": "./dist/ldsEngineCreator.js",
87
88
  "maxSize": {
88
- "none": "383 kB",
89
+ "none": "390 kB",
89
90
  "min": "190 kB",
90
- "compressed": "64.5 kB"
91
+ "compressed": "65.5 kB"
91
92
  }
92
93
  }
93
94
  ],