@salesforce/lds-runtime-one-app 1.462.0 → 1.464.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.
- package/dist/oneAppLdsEngineCreator.js +294 -82
- package/dist/types/fetch-service-descriptors/jwt-authorized-fetch-service.d.ts +2 -1
- package/dist/types/request-interceptors/idempotency-key.d.ts +24 -0
- package/dist/types/request-interceptors/jwt-parameterization.d.ts +22 -14
- package/dist/types/retry-interceptors/luvio-fetch-retry.d.ts +6 -3
- package/dist/types/retry-policies/luvio-csrf-token-retry-policy.d.ts +1 -8
- package/dist/types/retry-policies/luvio-idempotency-retry-policy.d.ts +29 -0
- package/package.json +41 -41
|
@@ -16,6 +16,7 @@ import { HttpStatusCode as HttpStatusCode$2, InMemoryStore, Environment, Luvio,
|
|
|
16
16
|
import usePredictiveLoading from '@salesforce/gate/lds.usePredictiveLoading';
|
|
17
17
|
import useLocalStorage from '@salesforce/gate/lds.pdl.useLocalStorage';
|
|
18
18
|
import useOneStoreGraphql from '@salesforce/gate/lds.oneStoreGraphqlEnabled.ltng';
|
|
19
|
+
import usePersistedQueries from '@salesforce/gate/lds.graphqlusepersistedqueries';
|
|
19
20
|
import canCombineRecordLayoutRequests from '@salesforce/gate/lds.pdl.canCombineRecordLayoutRequests';
|
|
20
21
|
import { GetApexWireAdapterFactory, registerPrefetcher as registerPrefetcher$1 } from 'force/ldsAdaptersApex';
|
|
21
22
|
import { instrument, getRecordAvatarsAdapterFactory, getRecordAdapterFactory, coerceFieldIdArray, coerceLayoutTypeArray, coerceLayoutModeArray, getRecordsAdapterFactory, getRecordActionsAdapterFactory, getObjectInfosAdapterFactory, coerceObjectIdArray, getObjectInfoAdapterFactory, coerceObjectId, getRelatedListsActionsAdapterFactory, getRelatedListInfoBatchAdapterFactory, getRelatedListInfoAdapterFactory, getRelatedListRecordsBatchAdapterFactory, getRelatedListRecordsAdapterFactory, getListInfoByNameAdapterFactory, getListInfosByObjectNameAdapterFactory, getListRecordsByNameAdapterFactory, getListObjectInfoAdapterFactory, getRelatedListsInfoAdapterFactory, getRelatedListActionsAdapterFactory, getRecordId18Array, buildRecordRepKeyFromId, configuration, InMemoryRecordRepresentationQueryEvaluator, UiApiNamespace, RecordRepresentationRepresentationType, registerPrefetcher, RecordRepresentationVersion } from 'force/ldsAdaptersUiapi';
|
|
@@ -28,7 +29,7 @@ import { dispatchGlobalEvent as dispatchGlobalEvent$1, executeGlobalControllerRa
|
|
|
28
29
|
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';
|
|
29
30
|
import { ThirdPartyTracker } from 'instrumentation:thirdPartyTracker';
|
|
30
31
|
import { markStart, markEnd, counter, registerCacheStats, perfStart, perfEnd, registerPeriodicLogger, interaction, timer, mark } from 'instrumentation/service';
|
|
31
|
-
import { instrument as instrument$2,
|
|
32
|
+
import { instrument as instrument$2, isMutatingMethod, getFetchMethod, isIdempotentWrite, readUiApiErrorCode, isRetryableMethod, isRetryableReadPost, parseRetryAfterMs, compilePathMatchers, setupLexNetworkAdapter, UIAPI_PUBLIC_PATHS, UIAPI_PRIVATE_PATHS, isRetryableOperationType } from 'force/ldsNetworkFetch';
|
|
32
33
|
import { REFRESH_ADAPTER_EVENT, ADAPTER_UNFULFILLED_ERROR, instrument as instrument$3 } from 'force/ldsBindings';
|
|
33
34
|
import { stateManagerInstrumentation, OperationTypeValue, LRUCache, instrumentAdapter, instrumentLuvio, logMessage as logMessage$1, setupInstrumentation as setupInstrumentation$1, logObjectInfoChanged as logObjectInfoChanged$1, updatePercentileHistogramMetric, incrementCounterMetric, incrementGetRecordNotifyChangeAllowCount, incrementGetRecordNotifyChangeDropCount, incrementNotifyRecordUpdateAvailableAllowCount, incrementNotifyRecordUpdateAvailableDropCount, setLdsAdaptersUiapiInstrumentation, logError as logError$2, setLdsNetworkAdapterInstrumentation, executeAsyncActivity, METRIC_KEYS, onIdleDetected } from 'force/ldsInstrumentation';
|
|
34
35
|
import { instrument as instrument$4 } from 'force/adsBridge';
|
|
@@ -388,6 +389,15 @@ function buildServiceDescriptor$q() {
|
|
|
388
389
|
};
|
|
389
390
|
}
|
|
390
391
|
|
|
392
|
+
function applyOperationRequestOptions$1(init, operationType) {
|
|
393
|
+
if (operationType === void 0) {
|
|
394
|
+
return init;
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
...init,
|
|
398
|
+
onestore: { ...init?.onestore, retrySafe: operationType === "query" }
|
|
399
|
+
};
|
|
400
|
+
}
|
|
391
401
|
class AuraNetworkCommand extends NetworkCommand$1 {
|
|
392
402
|
constructor(services) {
|
|
393
403
|
super(services);
|
|
@@ -406,6 +416,18 @@ class AuraNetworkCommand extends NetworkCommand$1 {
|
|
|
406
416
|
"Fetch parameters must be specified when using HTTP transport on an Aura adapter"
|
|
407
417
|
);
|
|
408
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* The command's operation type. Generated command subclasses override this
|
|
421
|
+
* getter with their concrete operation (see the generator's
|
|
422
|
+
* `generateOperationType`); the base returns `undefined` for commands that
|
|
423
|
+
* don't declare one. On the HTTP fallback path (`fetch`) it derives the
|
|
424
|
+
* request's `onestore.retrySafe` — reads (`query`) are retry-safe, writes
|
|
425
|
+
* (`mutation`) are not. Mirrors `FetchNetworkCommand.operationType`, which
|
|
426
|
+
* this class does not inherit (it extends `NetworkCommand` directly).
|
|
427
|
+
*/
|
|
428
|
+
get operationType() {
|
|
429
|
+
return void 0;
|
|
430
|
+
}
|
|
409
431
|
coerceAuraErrors(auraErrors) {
|
|
410
432
|
return new UserVisibleError(getFetchResponseFromAuraError(auraErrors[0]));
|
|
411
433
|
}
|
|
@@ -507,9 +529,11 @@ class AuraNetworkCommand extends NetworkCommand$1 {
|
|
|
507
529
|
this.coerceAuraErrors
|
|
508
530
|
);
|
|
509
531
|
} else if (this.shouldUseFetch()) {
|
|
510
|
-
const
|
|
532
|
+
const [input, init] = this.fetchParams;
|
|
533
|
+
const initWithOptions = applyOperationRequestOptions$1(init, this.operationType);
|
|
511
534
|
try {
|
|
512
|
-
|
|
535
|
+
const fetchCall = initWithOptions === void 0 ? this.services.fetch(input) : this.services.fetch(input, initWithOptions);
|
|
536
|
+
return this.convertFetchResponseToData(fetchCall);
|
|
513
537
|
} catch (reason) {
|
|
514
538
|
return resolvedPromiseLike$2(err$1(toError(reason)));
|
|
515
539
|
}
|
|
@@ -1363,6 +1387,15 @@ class NetworkCommand extends BaseCommand {
|
|
|
1363
1387
|
async afterRequestHooks(_options) {
|
|
1364
1388
|
}
|
|
1365
1389
|
}
|
|
1390
|
+
function applyOperationRequestOptions(init, operationType) {
|
|
1391
|
+
if (operationType === void 0) {
|
|
1392
|
+
return init;
|
|
1393
|
+
}
|
|
1394
|
+
return {
|
|
1395
|
+
...init,
|
|
1396
|
+
onestore: { ...init?.onestore, retrySafe: operationType === "query" }
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1366
1399
|
function isAbortableCommand(command) {
|
|
1367
1400
|
return !!command && typeof command === "object" && "isAbortableCommand" in command && command.isAbortableCommand === true;
|
|
1368
1401
|
}
|
|
@@ -1452,7 +1485,8 @@ const _FetchNetworkCommand = class _FetchNetworkCommand extends NetworkCommand {
|
|
|
1452
1485
|
* `generateOperationType`); the base returns `undefined` for commands that
|
|
1453
1486
|
* don't declare one (e.g. hand-written commands). Transport-layer
|
|
1454
1487
|
* interceptors use it to tell a read (`query`) apart from a write — see
|
|
1455
|
-
* `fetch`, which folds it into the context seed
|
|
1488
|
+
* `fetch`, which folds it into the context seed and derives
|
|
1489
|
+
* `onestore.retrySafe` from it.
|
|
1456
1490
|
*/
|
|
1457
1491
|
get operationType() {
|
|
1458
1492
|
return void 0;
|
|
@@ -1461,8 +1495,9 @@ const _FetchNetworkCommand = class _FetchNetworkCommand extends NetworkCommand {
|
|
|
1461
1495
|
try {
|
|
1462
1496
|
const [input, init] = this.fetchParams;
|
|
1463
1497
|
const operationType = this.operationType;
|
|
1498
|
+
const initWithOptions = applyOperationRequestOptions(init, operationType);
|
|
1464
1499
|
const seed = operationType === void 0 ? contextSeed : { operationType, ...contextSeed };
|
|
1465
|
-
const initWithSeed = seed === void 0 ?
|
|
1500
|
+
const initWithSeed = seed === void 0 ? initWithOptions : { ...initWithOptions, __contextSeed: seed };
|
|
1466
1501
|
const fetchCall = initWithSeed === void 0 ? this.services.fetch(input) : this.services.fetch(input, initWithSeed);
|
|
1467
1502
|
return this.convertFetchResponseToData(fetchCall);
|
|
1468
1503
|
} catch (reason) {
|
|
@@ -2601,7 +2636,7 @@ function buildServiceDescriptor$d(luvio) {
|
|
|
2601
2636
|
},
|
|
2602
2637
|
};
|
|
2603
2638
|
}
|
|
2604
|
-
// version: 1.
|
|
2639
|
+
// version: 1.464.0-e54c238397
|
|
2605
2640
|
|
|
2606
2641
|
class AuraGraphQLNormalizedCacheControlCommand extends AuraNormalizedCacheControlCommand {
|
|
2607
2642
|
constructor(config, documentRootType, services) {
|
|
@@ -2940,7 +2975,7 @@ function buildServiceDescriptor$9(notifyRecordUpdateAvailable, getNormalizedLuvi
|
|
|
2940
2975
|
},
|
|
2941
2976
|
};
|
|
2942
2977
|
}
|
|
2943
|
-
// version: 1.
|
|
2978
|
+
// version: 1.464.0-e54c238397
|
|
2944
2979
|
|
|
2945
2980
|
class RetryService {
|
|
2946
2981
|
constructor(defaultRetryPolicy) {
|
|
@@ -4412,7 +4447,12 @@ function buildServiceDescriptor$2(interceptors = {
|
|
|
4412
4447
|
}
|
|
4413
4448
|
const fetchArgs = cleanInit === void 0 ? [input] : [input, cleanInit];
|
|
4414
4449
|
const baseContext = interceptors.createContext?.();
|
|
4415
|
-
const
|
|
4450
|
+
const onestore = cleanInit?.onestore;
|
|
4451
|
+
const context = contextSeed === void 0 && onestore === void 0 ? baseContext : {
|
|
4452
|
+
...baseContext,
|
|
4453
|
+
...contextSeed,
|
|
4454
|
+
...onestore !== void 0 && { onestore }
|
|
4455
|
+
};
|
|
4416
4456
|
const {
|
|
4417
4457
|
request: requestInterceptors = [],
|
|
4418
4458
|
retry: retryInterceptor = void 0,
|
|
@@ -4460,10 +4500,14 @@ function getTextEncoder() {
|
|
|
4460
4500
|
}
|
|
4461
4501
|
return textEncoder;
|
|
4462
4502
|
}
|
|
4463
|
-
function buildCompressionInterceptor(
|
|
4464
|
-
const threshold = config.threshold ?? 1024;
|
|
4503
|
+
function buildCompressionInterceptor(defaultConfig) {
|
|
4465
4504
|
return async (args) => {
|
|
4466
4505
|
const [resource, options = {}] = args;
|
|
4506
|
+
const config = options.onestore?.compression ?? defaultConfig;
|
|
4507
|
+
if (config === void 0) {
|
|
4508
|
+
return resolvedPromiseLike$2(args);
|
|
4509
|
+
}
|
|
4510
|
+
const threshold = config.threshold ?? 1024;
|
|
4467
4511
|
if (!options.body || typeof options.body !== "string") {
|
|
4468
4512
|
return resolvedPromiseLike$2(args);
|
|
4469
4513
|
}
|
|
@@ -5862,7 +5906,7 @@ function getEnvironmentSetting(name) {
|
|
|
5862
5906
|
}
|
|
5863
5907
|
return undefined;
|
|
5864
5908
|
}
|
|
5865
|
-
// version: 1.
|
|
5909
|
+
// version: 1.464.0-b35c0e7723
|
|
5866
5910
|
|
|
5867
5911
|
/**
|
|
5868
5912
|
* Helpers for reaching the Aura framework from the LDS Aura runtime.
|
|
@@ -6277,7 +6321,13 @@ function getOrGenerateErrorId(responseBody, status, statusText) {
|
|
|
6277
6321
|
return generateErrorId(context);
|
|
6278
6322
|
}
|
|
6279
6323
|
function buildLexRuntime5xxStatusResponseInterceptor(logger) {
|
|
6280
|
-
return async (response) => {
|
|
6324
|
+
return async (response, context) => {
|
|
6325
|
+
// The caller opted in (`onestore.handleServerError`, exposed by the fetch service as
|
|
6326
|
+
// `context.onestore`) to receive the 5xx itself, so skip the gack and the throw and
|
|
6327
|
+
// let the response flow to the command's error handling.
|
|
6328
|
+
if (context?.onestore?.handleServerError === true) {
|
|
6329
|
+
return response;
|
|
6330
|
+
}
|
|
6281
6331
|
if (response.status >= 500 && response.status < 600) {
|
|
6282
6332
|
const internalLogMessage = `LEX runtime 5xx: ${response.status} ${response.statusText || ''}`.trim();
|
|
6283
6333
|
logger.warn(internalLogMessage);
|
|
@@ -6481,7 +6531,7 @@ function buildCsrfTokenInterceptor() {
|
|
|
6481
6531
|
return resolvedPromiseLike$2(fetchArgs);
|
|
6482
6532
|
}
|
|
6483
6533
|
// Only add CSRF token for mutating operations
|
|
6484
|
-
if (
|
|
6534
|
+
if (isMutatingMethod(getFetchMethod(fetchArgs))) {
|
|
6485
6535
|
const token = await getCsrfToken();
|
|
6486
6536
|
if (token) {
|
|
6487
6537
|
// eslint-disable-next-line no-param-reassign
|
|
@@ -6509,7 +6559,7 @@ function buildLuvioCsrfTokenInterceptor() {
|
|
|
6509
6559
|
resourceRequest.headers = {};
|
|
6510
6560
|
}
|
|
6511
6561
|
// Only add CSRF token for mutating operations
|
|
6512
|
-
if (
|
|
6562
|
+
if (isMutatingMethod(resourceRequest.method)) {
|
|
6513
6563
|
// Don't overwrite existing CSRF token header if it already exists
|
|
6514
6564
|
if (!resourceRequest.headers[CSRF_TOKEN_HEADER]) {
|
|
6515
6565
|
const token = await getCsrfToken();
|
|
@@ -6522,6 +6572,53 @@ function buildLuvioCsrfTokenInterceptor() {
|
|
|
6522
6572
|
};
|
|
6523
6573
|
}
|
|
6524
6574
|
|
|
6575
|
+
const IDEMPOTENCY_KEY_HEADER = 'Idempotency-Key';
|
|
6576
|
+
// Server-side idempotency error codes (UI SDK / UI-API `ApiExceptionCode`); the
|
|
6577
|
+
// mobile draft-queue handler is the reference consumer.
|
|
6578
|
+
const ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED = 'IDEMPOTENCY_FEATURE_NOT_ENABLED';
|
|
6579
|
+
const ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED = 'IDEMPOTENCY_NOT_SUPPORTED';
|
|
6580
|
+
const ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR = 'IDEMPOTENCY_BACKEND_OPERATION_ERROR';
|
|
6581
|
+
const ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER = 'IDEMPOTENCY_KEY_USED_DIFFERENT_USER';
|
|
6582
|
+
const ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST = 'IDEMPOTENCY_CONCURRENT_REQUEST';
|
|
6583
|
+
const ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED = 'IDEMPOTENCY_KEY_ALREADY_USED';
|
|
6584
|
+
/**
|
|
6585
|
+
* Generates an RFC4122 v4 UUID for use as an idempotency key, preferring
|
|
6586
|
+
* `crypto.randomUUID()` and falling back to a `Math.random()` builder if it is
|
|
6587
|
+
* unavailable (matches the SFAP request-id approach in `network-sfap`).
|
|
6588
|
+
*/
|
|
6589
|
+
function mintIdempotencyKey() {
|
|
6590
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
6591
|
+
return crypto.randomUUID();
|
|
6592
|
+
}
|
|
6593
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
6594
|
+
const r = (Math.random() * 16) | 0;
|
|
6595
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
6596
|
+
return v.toString(16);
|
|
6597
|
+
});
|
|
6598
|
+
}
|
|
6599
|
+
/**
|
|
6600
|
+
* Builds a Luvio request interceptor that mints and attaches an `Idempotency-Key`
|
|
6601
|
+
* on mutations to onboarded endpoints, once per logical write, so the retry
|
|
6602
|
+
* service re-sends the same key on every replay and the server can dedupe.
|
|
6603
|
+
*
|
|
6604
|
+
* Minting is gated here; retry *eligibility* is decided separately by
|
|
6605
|
+
* `LuvioIdempotencyRetryPolicy` from the header's presence, so a stale
|
|
6606
|
+
* registry flag can never cause an unsafe replay.
|
|
6607
|
+
*/
|
|
6608
|
+
function buildLuvioIdempotencyKeyInterceptor() {
|
|
6609
|
+
return async (resourceRequest) => {
|
|
6610
|
+
if (!resourceRequest.headers) {
|
|
6611
|
+
resourceRequest.headers = {};
|
|
6612
|
+
}
|
|
6613
|
+
if (isMutatingMethod(resourceRequest.method) &&
|
|
6614
|
+
isIdempotentWrite(resourceRequest.basePath) &&
|
|
6615
|
+
!resourceRequest.headers[IDEMPOTENCY_KEY_HEADER]) {
|
|
6616
|
+
resourceRequest.headers[IDEMPOTENCY_KEY_HEADER] = mintIdempotencyKey();
|
|
6617
|
+
}
|
|
6618
|
+
return resolvedPromiseLike$2(resourceRequest);
|
|
6619
|
+
};
|
|
6620
|
+
}
|
|
6621
|
+
|
|
6525
6622
|
const ENTITY_ENCODING_HEADER = 'X-Chatter-Entity-Encoding';
|
|
6526
6623
|
const ENTITY_ENCODING_DISABLED_VALUE = 'false';
|
|
6527
6624
|
/**
|
|
@@ -6818,11 +6915,11 @@ function buildLuvioThirdPartyTrackerFinishInterceptor() {
|
|
|
6818
6915
|
};
|
|
6819
6916
|
}
|
|
6820
6917
|
|
|
6821
|
-
const DEFAULT_CONFIG$
|
|
6918
|
+
const DEFAULT_CONFIG$4 = {
|
|
6822
6919
|
maxRetries: 1,
|
|
6823
6920
|
};
|
|
6824
6921
|
class LuvioCsrfTokenRetryPolicy extends RetryPolicy {
|
|
6825
|
-
constructor(config = DEFAULT_CONFIG$
|
|
6922
|
+
constructor(config = DEFAULT_CONFIG$4) {
|
|
6826
6923
|
super();
|
|
6827
6924
|
this.config = config;
|
|
6828
6925
|
}
|
|
@@ -6866,21 +6963,12 @@ class LuvioCsrfTokenRetryPolicy extends RetryPolicy {
|
|
|
6866
6963
|
this.requestContext.request = { ...req, headers };
|
|
6867
6964
|
}
|
|
6868
6965
|
}
|
|
6869
|
-
/**
|
|
6870
|
-
* Returns true when a Luvio FetchResponse indicates a CSRF token error.
|
|
6871
|
-
* Body is already parsed JSON on FetchResponse, so no clone/json() needed.
|
|
6872
|
-
*
|
|
6873
|
-
* UIAPI error bodies come in both shapes: an object `{ errorCode, message }`
|
|
6874
|
-
* for single errors, or an array `[{ errorCode, message }]` for endpoints that
|
|
6875
|
-
* return collections of errors. Handle both.
|
|
6876
|
-
*/
|
|
6966
|
+
/** True when a Luvio FetchResponse indicates a CSRF token error. */
|
|
6877
6967
|
function isCsrfError$1(response) {
|
|
6878
|
-
|
|
6879
|
-
const errorCode = Array.isArray(body) ? body[0]?.errorCode : body?.errorCode;
|
|
6880
|
-
return errorCode === 'INVALID_ACCESS_TOKEN';
|
|
6968
|
+
return readUiApiErrorCode(response) === 'INVALID_ACCESS_TOKEN';
|
|
6881
6969
|
}
|
|
6882
6970
|
|
|
6883
|
-
const DEFAULT_CONFIG$
|
|
6971
|
+
const DEFAULT_CONFIG$3 = {
|
|
6884
6972
|
maxRetries: 3,
|
|
6885
6973
|
maxTimeToRetry: 10000,
|
|
6886
6974
|
baseDelay: 250,
|
|
@@ -6894,7 +6982,7 @@ class LuvioFetchThrottlingRetryPolicy extends RetryPolicy {
|
|
|
6894
6982
|
// is why we take the plain `ResourceRequest` rather than the mutable-container
|
|
6895
6983
|
// pattern the CSRF policy uses — that policy rewrites the request (new token)
|
|
6896
6984
|
// between attempts; this one never mutates it.
|
|
6897
|
-
constructor(config = DEFAULT_CONFIG$
|
|
6985
|
+
constructor(config = DEFAULT_CONFIG$3, request) {
|
|
6898
6986
|
super();
|
|
6899
6987
|
this.config = config;
|
|
6900
6988
|
this.request = request;
|
|
@@ -6922,57 +7010,157 @@ class LuvioFetchThrottlingRetryPolicy extends RetryPolicy {
|
|
|
6922
7010
|
return Math.max(0, delay + jitter);
|
|
6923
7011
|
}
|
|
6924
7012
|
parseRetryAfterHeader(result) {
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
7013
|
+
return parseRetryAfterMs(result.headers);
|
|
7014
|
+
}
|
|
7015
|
+
}
|
|
7016
|
+
|
|
7017
|
+
const DEFAULT_CONFIG$2 = {
|
|
7018
|
+
maxRetries: 3,
|
|
7019
|
+
};
|
|
7020
|
+
// The idempotency handler never emits 429 (that's throttling's domain). Its 503
|
|
7021
|
+
// is a keyed-write lock-acquire failure (scenario 7), not throttling's keyless
|
|
7022
|
+
// GET/read-POST 503 — the two never contend (see classifyResponse).
|
|
7023
|
+
const HTTP_STATUS_BAD_REQUEST = 400;
|
|
7024
|
+
const HTTP_STATUS_REQUEST_TIMEOUT = 408;
|
|
7025
|
+
const HTTP_STATUS_CONFLICT = 409;
|
|
7026
|
+
const HTTP_STATUS_UNPROCESSABLE_ENTITY = 422;
|
|
7027
|
+
const HTTP_STATUS_SERVER_ERROR = 500;
|
|
7028
|
+
const HTTP_STATUS_SERVICE_UNAVAILABLE = 503;
|
|
7029
|
+
// Terminal cases (IDEMPOTENCY_AUTHENTICATION_FAILED on 401, INVALID_IDEMPOTENCY_KEY
|
|
7030
|
+
// on 400) are deliberately absent so they fall through to 'none'.
|
|
7031
|
+
const STRIP_ERROR_CODES = [
|
|
7032
|
+
ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED,
|
|
7033
|
+
ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED,
|
|
7034
|
+
ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR,
|
|
7035
|
+
];
|
|
7036
|
+
const ROTATE_ERROR_CODES = [
|
|
7037
|
+
ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER,
|
|
7038
|
+
ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED,
|
|
7039
|
+
];
|
|
7040
|
+
/**
|
|
7041
|
+
* Maps an idempotent-write response to its disposition. Status alone is
|
|
7042
|
+
* ambiguous (e.g. 409 is same-key for a concurrent request but rotate for a
|
|
7043
|
+
* different user), so pair it with the body's errorCode.
|
|
7044
|
+
*/
|
|
7045
|
+
function classifyResponse(response) {
|
|
7046
|
+
const errorCode = readUiApiErrorCode(response);
|
|
7047
|
+
// FetchResponse.status is typed as HttpStatusCode, which omits 408/409/422
|
|
7048
|
+
// even though the idempotency handler emits them; widen to match.
|
|
7049
|
+
const status = response.status;
|
|
7050
|
+
switch (status) {
|
|
7051
|
+
case HTTP_STATUS_SERVER_ERROR:
|
|
7052
|
+
return errorCode === ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR ? 'strip' : 'none';
|
|
7053
|
+
case HTTP_STATUS_BAD_REQUEST:
|
|
7054
|
+
// FEATURE_NOT_ENABLED / NOT_SUPPORTED → strip. Any other 400 (incl.
|
|
7055
|
+
// INVALID_IDEMPOTENCY_KEY and CSRF's INVALID_ACCESS_TOKEN) is terminal.
|
|
7056
|
+
return errorCode !== undefined && STRIP_ERROR_CODES.includes(errorCode)
|
|
7057
|
+
? 'strip'
|
|
7058
|
+
: 'none';
|
|
7059
|
+
case HTTP_STATUS_REQUEST_TIMEOUT:
|
|
7060
|
+
// Backend connection timeout — the write may have already committed;
|
|
7061
|
+
// replay same-key so the server can dedupe.
|
|
7062
|
+
return 'same-key';
|
|
7063
|
+
case HTTP_STATUS_SERVICE_UNAVAILABLE:
|
|
7064
|
+
// Lock-acquire failure (scenario 7): the write never committed. Only a
|
|
7065
|
+
// keyed write reaches here (shouldRetry gates on key presence) — a
|
|
7066
|
+
// keyless 503 is the throttling policy's GET/read-POST case.
|
|
7067
|
+
return 'same-key';
|
|
7068
|
+
case HTTP_STATUS_CONFLICT:
|
|
7069
|
+
// KEY_USED_DIFFERENT_USER → rotate; CONCURRENT_REQUEST → same key.
|
|
7070
|
+
if (errorCode !== undefined && ROTATE_ERROR_CODES.includes(errorCode)) {
|
|
7071
|
+
return 'rotate';
|
|
6936
7072
|
}
|
|
7073
|
+
return errorCode === ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST ? 'same-key' : 'none';
|
|
7074
|
+
case HTTP_STATUS_UNPROCESSABLE_ENTITY:
|
|
7075
|
+
// Same key reused for a different request body → needs a new key.
|
|
7076
|
+
return errorCode === ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED ? 'rotate' : 'none';
|
|
7077
|
+
default:
|
|
7078
|
+
return 'none';
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
/**
|
|
7082
|
+
* Retry policy for writes carrying an `Idempotency-Key`. A peer of the CSRF and
|
|
7083
|
+
* throttling policies in the composed retry chain, not a modification of
|
|
7084
|
+
* either: its 503 (keyed-write lock-acquire) and throttling's 503 (keyless
|
|
7085
|
+
* GET/read-POST) never contend because they key off different requests, and it
|
|
7086
|
+
* only claims a 400/409 when the body carries an idempotency-specific
|
|
7087
|
+
* errorCode, so it never steals CSRF's INVALID_ACCESS_TOKEN.
|
|
7088
|
+
*
|
|
7089
|
+
* Eligibility follows from the header's *presence* on the request, not from
|
|
7090
|
+
* the mint interceptor's registry flag — so a stale flag can never cause an
|
|
7091
|
+
* unsafe replay.
|
|
7092
|
+
*/
|
|
7093
|
+
class LuvioIdempotencyRetryPolicy extends RetryPolicy {
|
|
7094
|
+
constructor(config = DEFAULT_CONFIG$2) {
|
|
7095
|
+
super();
|
|
7096
|
+
this.config = config;
|
|
7097
|
+
}
|
|
7098
|
+
setRequestContext(context) {
|
|
7099
|
+
this.requestContext = context;
|
|
7100
|
+
}
|
|
7101
|
+
async shouldRetry(result, context) {
|
|
7102
|
+
if (context.attempt >= this.config.maxRetries) {
|
|
7103
|
+
return false;
|
|
6937
7104
|
}
|
|
6938
|
-
|
|
6939
|
-
|
|
7105
|
+
const headers = this.requestContext?.request.headers;
|
|
7106
|
+
if (!headers || !headers[IDEMPOTENCY_KEY_HEADER]) {
|
|
7107
|
+
return false;
|
|
6940
7108
|
}
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
return
|
|
7109
|
+
return classifyResponse(result) !== 'none';
|
|
7110
|
+
}
|
|
7111
|
+
async calculateDelay(result, _context) {
|
|
7112
|
+
// Only same-key replays (408/409/503) carry a Retry-After to honor;
|
|
7113
|
+
// strip/rotate retry immediately.
|
|
7114
|
+
if (classifyResponse(result) !== 'same-key') {
|
|
7115
|
+
return 0;
|
|
6948
7116
|
}
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
7117
|
+
// Honor the server's Retry-After in full, like the mobile handler — it's
|
|
7118
|
+
// a lock-hold hint, not a client-owned backoff (unlike the throttling
|
|
7119
|
+
// sibling's capped delay). maxRetries bounds replays instead.
|
|
7120
|
+
return parseRetryAfterMs(result.headers) ?? 0;
|
|
7121
|
+
}
|
|
7122
|
+
async prepareRetry(result, _context) {
|
|
7123
|
+
if (!this.requestContext) {
|
|
7124
|
+
return;
|
|
6952
7125
|
}
|
|
6953
|
-
const
|
|
6954
|
-
if (
|
|
6955
|
-
return
|
|
7126
|
+
const disposition = classifyResponse(result);
|
|
7127
|
+
if (disposition === 'same-key' || disposition === 'none') {
|
|
7128
|
+
return;
|
|
6956
7129
|
}
|
|
6957
|
-
|
|
7130
|
+
const req = this.requestContext.request;
|
|
7131
|
+
// Drop the existing key via immutable spread (matches the CSRF policy).
|
|
7132
|
+
const { [IDEMPOTENCY_KEY_HEADER]: _stale, ...remainingHeaders } = req.headers ?? {};
|
|
7133
|
+
const headers = disposition === 'rotate'
|
|
7134
|
+
? { ...remainingHeaders, [IDEMPOTENCY_KEY_HEADER]: mintIdempotencyKey() }
|
|
7135
|
+
: // 'strip': retry plain — the feature is unavailable for this request.
|
|
7136
|
+
remainingHeaders;
|
|
7137
|
+
this.requestContext.request = { ...req, headers };
|
|
6958
7138
|
}
|
|
6959
7139
|
}
|
|
6960
7140
|
|
|
6961
7141
|
/**
|
|
6962
|
-
* Builds the LuvioRetryInterceptor used by the UIAPI fetch adapter.
|
|
7142
|
+
* Builds the LuvioRetryInterceptor used by the UIAPI fetch adapter. A fresh
|
|
7143
|
+
* RetryService and policies are created per request to keep the
|
|
7144
|
+
* mutableRequest context isolated across concurrent requests.
|
|
6963
7145
|
*
|
|
6964
|
-
*
|
|
6965
|
-
*
|
|
7146
|
+
* Policy order matters: ComposedRetryPolicy uses the first matching policy's
|
|
7147
|
+
* shouldRetry/delay. A keyed-write 503 fails throttling's method gate, so it
|
|
7148
|
+
* falls to the idempotency policy to replay same-key.
|
|
6966
7149
|
*/
|
|
6967
7150
|
function buildLuvioFetchRetryInterceptor() {
|
|
6968
7151
|
return (request, doFetch) => {
|
|
7152
|
+
const idempotencyPolicy = new LuvioIdempotencyRetryPolicy();
|
|
6969
7153
|
const csrfPolicy = new LuvioCsrfTokenRetryPolicy();
|
|
6970
7154
|
const composedPolicy = new ComposedRetryPolicy([
|
|
7155
|
+
idempotencyPolicy,
|
|
6971
7156
|
new LuvioFetchThrottlingRetryPolicy(undefined, request),
|
|
6972
7157
|
csrfPolicy,
|
|
6973
7158
|
]);
|
|
6974
7159
|
const retryService = new RetryService(composedPolicy);
|
|
6975
7160
|
const mutableRequest = { request };
|
|
7161
|
+
// Both policies rewrite headers on the shared request between attempts
|
|
7162
|
+
// (idempotency strips/rotates the key; CSRF refreshes the token).
|
|
7163
|
+
idempotencyPolicy.setRequestContext(mutableRequest);
|
|
6976
7164
|
csrfPolicy.setRequestContext(mutableRequest);
|
|
6977
7165
|
return retryService.applyRetry(() => doFetch(mutableRequest.request));
|
|
6978
7166
|
};
|
|
@@ -7184,6 +7372,7 @@ const composedFetchNetworkAdapter = {
|
|
|
7184
7372
|
buildLuvioTransportMarksSendInterceptor(),
|
|
7185
7373
|
buildLuvioPageScopedCacheRequestInterceptor(),
|
|
7186
7374
|
buildLuvioCsrfTokenInterceptor(),
|
|
7375
|
+
buildLuvioIdempotencyKeyInterceptor(),
|
|
7187
7376
|
buildLuvioFirstPartyHeaderInterceptor(),
|
|
7188
7377
|
buildLuvioEntityEncodingInterceptor(),
|
|
7189
7378
|
],
|
|
@@ -7453,15 +7642,21 @@ function buildFetchRetryInterceptor() {
|
|
|
7453
7642
|
}
|
|
7454
7643
|
|
|
7455
7644
|
/**
|
|
7456
|
-
*
|
|
7645
|
+
* Legacy context-seed key under which a custom command forwards its JWT mint
|
|
7457
7646
|
* parameters. The framework's `buildServiceDescriptor` merges the request init's
|
|
7458
7647
|
* `__contextSeed` onto the per-request interceptor context, so this interceptor
|
|
7459
|
-
*
|
|
7648
|
+
* can read the params off `context[JWT_MINT_PARAMS_SEED_KEY]`.
|
|
7649
|
+
*
|
|
7650
|
+
* **Transitional.** The first-class channel is now the typed
|
|
7651
|
+
* `init.onestore.mintParams` field (onestore 3.29.0), which this interceptor
|
|
7652
|
+
* prefers. This seed key remains only as a fallback while the core
|
|
7653
|
+
* `onestore-custom-adapters` producers still write `__contextSeed`; it — together
|
|
7654
|
+
* with the `context` fallback and the interceptor's `context` parameter — is
|
|
7655
|
+
* removed once those producers emit `onestore.mintParams`. That removal is the
|
|
7656
|
+
* follow-up PR #8314 (tracked under W-24472492).
|
|
7460
7657
|
*
|
|
7461
|
-
*
|
|
7462
|
-
*
|
|
7463
|
-
* define it and never inspects the param shape; the typed shape lives in the
|
|
7464
|
-
* resolver.
|
|
7658
|
+
* It is intentionally an opaque key — OneStore does not define it and never
|
|
7659
|
+
* inspects the param shape; the typed shape lives in the resolver.
|
|
7465
7660
|
*/
|
|
7466
7661
|
const JWT_MINT_PARAMS_SEED_KEY = 'jwtMintParams';
|
|
7467
7662
|
/**
|
|
@@ -7492,15 +7687,17 @@ function hasAuthorizationHeader([resource, options]) {
|
|
|
7492
7687
|
* Builds the request interceptor that bridges the per-request context seed to the
|
|
7493
7688
|
* dispatching SFAP JwtManager for the **parameterized** mint path.
|
|
7494
7689
|
*
|
|
7495
|
-
* For a parameterized SFAP command, the
|
|
7496
|
-
*
|
|
7497
|
-
*
|
|
7498
|
-
*
|
|
7499
|
-
*
|
|
7500
|
-
*
|
|
7690
|
+
* For a parameterized SFAP command, the mint params arrive on the typed
|
|
7691
|
+
* `init.onestore.mintParams` field (preferred) or, transitionally, under the
|
|
7692
|
+
* legacy `context[jwtMintParams]` seed. This interceptor reads them (onestore
|
|
7693
|
+
* first), calls `jwtManager.getJwt(params)` (which the dispatching resolver routes
|
|
7694
|
+
* to the parameterized resolver), attaches the `Authorization` header, and
|
|
7695
|
+
* rewrites the request URL via the minted `baseUri`. The presence of that
|
|
7696
|
+
* `Authorization` header is the signal the descriptor's guarded legacy interceptor
|
|
7697
|
+
* uses to skip — so the legacy path does not mint a second time.
|
|
7501
7698
|
*
|
|
7502
|
-
* For a legacy parameterless command
|
|
7503
|
-
*
|
|
7699
|
+
* For a legacy parameterless command neither channel carries mint params, so this
|
|
7700
|
+
* interceptor **early-returns on its first statement** and the request flows
|
|
7504
7701
|
* unchanged to the legacy `buildJwtRequestHeaderInterceptor` — guaranteeing zero
|
|
7505
7702
|
* behavior change for non-opted-in adapters.
|
|
7506
7703
|
*
|
|
@@ -7514,8 +7711,11 @@ function hasAuthorizationHeader([resource, options]) {
|
|
|
7514
7711
|
*/
|
|
7515
7712
|
function buildJwtParameterizationInterceptor(jwtManager, jwtRequestModifier = (_extraInfo, fetchArgs) => fetchArgs) {
|
|
7516
7713
|
return (fetchArgs, context) => {
|
|
7517
|
-
|
|
7518
|
-
|
|
7714
|
+
// Prefer the typed `init.onestore.mintParams`; fall back to the legacy
|
|
7715
|
+
// `context[JWT_MINT_PARAMS_SEED_KEY]` seed while the core producers still
|
|
7716
|
+
// write it (transitional dual-read — see JWT_MINT_PARAMS_SEED_KEY).
|
|
7717
|
+
const mintParams = fetchArgs[1]?.onestore?.mintParams ??
|
|
7718
|
+
context?.[JWT_MINT_PARAMS_SEED_KEY];
|
|
7519
7719
|
// No mint params → not a parameterized request. Do nothing; the legacy
|
|
7520
7720
|
// interceptor handles it. This MUST be the first statement so non-opted-in
|
|
7521
7721
|
// SFAP commands observe no work at all.
|
|
@@ -7610,7 +7810,8 @@ function buildJwtAuthorizedSfapFetchServiceDescriptor(logger) {
|
|
|
7610
7810
|
*
|
|
7611
7811
|
* It reuses the module's `sfapJwtManager`: `cdp_url` is emitted on the same
|
|
7612
7812
|
* `SFAP_API`-scope JWT, so the Data360 command mints the same kind of token — it
|
|
7613
|
-
* just forwards the data-cloud scopes via
|
|
7813
|
+
* just forwards the data-cloud scopes via `onestore.mintParams` (or, transitionally,
|
|
7814
|
+
* the legacy context-seed `jwtMintParams`).
|
|
7614
7815
|
*
|
|
7615
7816
|
* Gated behind its own `data_360_cdp_url_rewrite` auth-scope tag so it binds ONLY to
|
|
7616
7817
|
* the Data360 command — the SFAP fleet never flows past this interceptor.
|
|
@@ -7741,10 +7942,11 @@ function buildJwtRequestInterceptor(logger) {
|
|
|
7741
7942
|
* (`cdp_url`), and modifiers only receive `extraInfo`. So — like the copilot
|
|
7742
7943
|
* interceptor that reads `decodedInfo.iss` — it holds the token directly:
|
|
7743
7944
|
*
|
|
7744
|
-
* 1. Read the command's
|
|
7745
|
-
*
|
|
7746
|
-
* resolver inside `sfapJwtManager`).
|
|
7747
|
-
* to authorize with, so leave the
|
|
7945
|
+
* 1. Read the command's mint params from `init.onestore.mintParams` (preferred)
|
|
7946
|
+
* or the legacy per-request context seed, and mint (via the parameterized
|
|
7947
|
+
* SFAP resolver, routed by the dispatching resolver inside `sfapJwtManager`).
|
|
7948
|
+
* Without mint params there is no token to authorize with, so leave the
|
|
7949
|
+
* request untouched.
|
|
7748
7950
|
* 2. Decode `cdp_url` from the JWT and resolve the tenant-specific endpoint
|
|
7749
7951
|
* (TSE). `cdp_url` is a bare host, so a scheme is prepended before parsing
|
|
7750
7952
|
* and the protocol is forced to `https:`.
|
|
@@ -7763,7 +7965,11 @@ function buildJwtRequestInterceptor(logger) {
|
|
|
7763
7965
|
*/
|
|
7764
7966
|
function buildData360HostRewriteInterceptor(logger) {
|
|
7765
7967
|
return (fetchArgs, context) => {
|
|
7766
|
-
|
|
7968
|
+
// Prefer the typed `init.onestore.mintParams`; fall back to the legacy
|
|
7969
|
+
// `context[JWT_MINT_PARAMS_SEED_KEY]` seed while the core producers still
|
|
7970
|
+
// write it (transitional dual-read — see JWT_MINT_PARAMS_SEED_KEY).
|
|
7971
|
+
const mintParams = fetchArgs[1]?.onestore?.mintParams ??
|
|
7972
|
+
context?.[JWT_MINT_PARAMS_SEED_KEY];
|
|
7767
7973
|
// No mint params → not a parameterized Data360 request. Nothing to do.
|
|
7768
7974
|
if (mintParams === undefined) {
|
|
7769
7975
|
return resolvedPromiseLike$2(fetchArgs);
|
|
@@ -10478,6 +10684,10 @@ function getLexRuntimeDefaultInterceptorConfig(logger) {
|
|
|
10478
10684
|
buildCsrfTokenInterceptor(),
|
|
10479
10685
|
buildEntityEncodingInterceptor(),
|
|
10480
10686
|
buildFirstPartyHeaderInterceptor(),
|
|
10687
|
+
// Per-request gzip: compresses only operations that opt in via
|
|
10688
|
+
// `init.onestore.compression`; a pass-through no-op otherwise. Last in
|
|
10689
|
+
// the chain so it compresses the final body, after body-modifying interceptors.
|
|
10690
|
+
buildCompressionInterceptor(),
|
|
10481
10691
|
],
|
|
10482
10692
|
retry: buildFetchRetryInterceptor(),
|
|
10483
10693
|
response: [
|
|
@@ -11319,6 +11529,8 @@ function initializeOneStore(luvio) {
|
|
|
11319
11529
|
// across LEX and sites. Paired with the HTTP UIAPI fetch path being
|
|
11320
11530
|
// disabled on sites (see network-fetch.ts).
|
|
11321
11531
|
featureFlagsService.set('graphQLUseAura', true);
|
|
11532
|
+
// Persisted (named) GraphQL queries; no consumers yet (W-23971639).
|
|
11533
|
+
featureFlagsService.set('graphQLUsePersistedQueries', usePersistedQueries.isOpen({ fallback: false }));
|
|
11322
11534
|
const services = [
|
|
11323
11535
|
instrumentationServiceDescriptor,
|
|
11324
11536
|
buildLexRuntimeDefaultFetchServiceDescriptor(loggerService, retryService),
|
|
@@ -11379,4 +11591,4 @@ function ldsEngineCreator() {
|
|
|
11379
11591
|
}
|
|
11380
11592
|
|
|
11381
11593
|
export { LexRequestStrategy, PDL_ENGINE_REGISTRATION_ID, PdlPrefetcherEventType, PdlRequestPriority, buildPredictorForContext, configService, ldsEngineCreator as default, initializeLDS, initializeOneStore, notifyUpdateAvailableFactory, registerRequestStrategy, saveRequestAsPrediction, subscribeToPrefetcherEvents, unregisterRequestStrategy, whenPredictionsReady };
|
|
11382
|
-
// version: 1.
|
|
11594
|
+
// version: 1.464.0-e54c238397
|
|
@@ -20,7 +20,8 @@ export declare function buildJwtAuthorizedSfapFetchServiceDescriptor(logger: Log
|
|
|
20
20
|
*
|
|
21
21
|
* It reuses the module's `sfapJwtManager`: `cdp_url` is emitted on the same
|
|
22
22
|
* `SFAP_API`-scope JWT, so the Data360 command mints the same kind of token — it
|
|
23
|
-
* just forwards the data-cloud scopes via
|
|
23
|
+
* just forwards the data-cloud scopes via `onestore.mintParams` (or, transitionally,
|
|
24
|
+
* the legacy context-seed `jwtMintParams`).
|
|
24
25
|
*
|
|
25
26
|
* Gated behind its own `data_360_cdp_url_rewrite` auth-scope tag so it binds ONLY to
|
|
26
27
|
* the Data360 command — the SFAP fleet never flows past this interceptor.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ResourceRequest } from '@luvio/engine';
|
|
2
|
+
export declare const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
|
|
3
|
+
export declare const ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED = "IDEMPOTENCY_FEATURE_NOT_ENABLED";
|
|
4
|
+
export declare const ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED = "IDEMPOTENCY_NOT_SUPPORTED";
|
|
5
|
+
export declare const ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR = "IDEMPOTENCY_BACKEND_OPERATION_ERROR";
|
|
6
|
+
export declare const ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER = "IDEMPOTENCY_KEY_USED_DIFFERENT_USER";
|
|
7
|
+
export declare const ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST = "IDEMPOTENCY_CONCURRENT_REQUEST";
|
|
8
|
+
export declare const ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED = "IDEMPOTENCY_KEY_ALREADY_USED";
|
|
9
|
+
/**
|
|
10
|
+
* Generates an RFC4122 v4 UUID for use as an idempotency key, preferring
|
|
11
|
+
* `crypto.randomUUID()` and falling back to a `Math.random()` builder if it is
|
|
12
|
+
* unavailable (matches the SFAP request-id approach in `network-sfap`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function mintIdempotencyKey(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Builds a Luvio request interceptor that mints and attaches an `Idempotency-Key`
|
|
17
|
+
* on mutations to onboarded endpoints, once per logical write, so the retry
|
|
18
|
+
* service re-sends the same key on every replay and the server can dedupe.
|
|
19
|
+
*
|
|
20
|
+
* Minting is gated here; retry *eligibility* is decided separately by
|
|
21
|
+
* `LuvioIdempotencyRetryPolicy` from the header's presence, so a stale
|
|
22
|
+
* registry flag can never cause an unsafe replay.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildLuvioIdempotencyKeyInterceptor(): (resourceRequest: ResourceRequest) => Promise<ResourceRequest>;
|
|
@@ -3,15 +3,21 @@ import { type JwtRequestModifier } from '@conduit-client/service-fetch-network/v
|
|
|
3
3
|
import type { JwtManager } from '@conduit-client/jwt-manager';
|
|
4
4
|
import type { ExtraInfo } from '../network-sfap';
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Legacy context-seed key under which a custom command forwards its JWT mint
|
|
7
7
|
* parameters. The framework's `buildServiceDescriptor` merges the request init's
|
|
8
8
|
* `__contextSeed` onto the per-request interceptor context, so this interceptor
|
|
9
|
-
*
|
|
9
|
+
* can read the params off `context[JWT_MINT_PARAMS_SEED_KEY]`.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* **Transitional.** The first-class channel is now the typed
|
|
12
|
+
* `init.onestore.mintParams` field (onestore 3.29.0), which this interceptor
|
|
13
|
+
* prefers. This seed key remains only as a fallback while the core
|
|
14
|
+
* `onestore-custom-adapters` producers still write `__contextSeed`; it — together
|
|
15
|
+
* with the `context` fallback and the interceptor's `context` parameter — is
|
|
16
|
+
* removed once those producers emit `onestore.mintParams`. That removal is the
|
|
17
|
+
* follow-up PR #8314 (tracked under W-24472492).
|
|
18
|
+
*
|
|
19
|
+
* It is intentionally an opaque key — OneStore does not define it and never
|
|
20
|
+
* inspects the param shape; the typed shape lives in the resolver.
|
|
15
21
|
*/
|
|
16
22
|
export declare const JWT_MINT_PARAMS_SEED_KEY = "jwtMintParams";
|
|
17
23
|
/**
|
|
@@ -27,15 +33,17 @@ export declare function hasAuthorizationHeader([resource, options]: FetchParamet
|
|
|
27
33
|
* Builds the request interceptor that bridges the per-request context seed to the
|
|
28
34
|
* dispatching SFAP JwtManager for the **parameterized** mint path.
|
|
29
35
|
*
|
|
30
|
-
* For a parameterized SFAP command, the
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
+
* For a parameterized SFAP command, the mint params arrive on the typed
|
|
37
|
+
* `init.onestore.mintParams` field (preferred) or, transitionally, under the
|
|
38
|
+
* legacy `context[jwtMintParams]` seed. This interceptor reads them (onestore
|
|
39
|
+
* first), calls `jwtManager.getJwt(params)` (which the dispatching resolver routes
|
|
40
|
+
* to the parameterized resolver), attaches the `Authorization` header, and
|
|
41
|
+
* rewrites the request URL via the minted `baseUri`. The presence of that
|
|
42
|
+
* `Authorization` header is the signal the descriptor's guarded legacy interceptor
|
|
43
|
+
* uses to skip — so the legacy path does not mint a second time.
|
|
36
44
|
*
|
|
37
|
-
* For a legacy parameterless command
|
|
38
|
-
*
|
|
45
|
+
* For a legacy parameterless command neither channel carries mint params, so this
|
|
46
|
+
* interceptor **early-returns on its first statement** and the request flows
|
|
39
47
|
* unchanged to the legacy `buildJwtRequestHeaderInterceptor` — guaranteeing zero
|
|
40
48
|
* behavior change for non-opted-in adapters.
|
|
41
49
|
*
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { LuvioRetryInterceptor } from '@salesforce/lds-network-fetch';
|
|
2
2
|
/**
|
|
3
|
-
* Builds the LuvioRetryInterceptor used by the UIAPI fetch adapter.
|
|
3
|
+
* Builds the LuvioRetryInterceptor used by the UIAPI fetch adapter. A fresh
|
|
4
|
+
* RetryService and policies are created per request to keep the
|
|
5
|
+
* mutableRequest context isolated across concurrent requests.
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
+
* Policy order matters: ComposedRetryPolicy uses the first matching policy's
|
|
8
|
+
* shouldRetry/delay. A keyed-write 503 fails throttling's method gate, so it
|
|
9
|
+
* falls to the idempotency policy to replay same-key.
|
|
7
10
|
*/
|
|
8
11
|
export declare function buildLuvioFetchRetryInterceptor(): LuvioRetryInterceptor;
|
|
@@ -19,13 +19,6 @@ export declare class LuvioCsrfTokenRetryPolicy extends RetryPolicy<FetchResponse
|
|
|
19
19
|
calculateDelay(_result: FetchResponse<any>, _context: RetryContext<FetchResponse<any>>): Promise<number>;
|
|
20
20
|
prepareRetry(_result: FetchResponse<any>, _context: RetryContext<FetchResponse<any>>): Promise<void>;
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Returns true when a Luvio FetchResponse indicates a CSRF token error.
|
|
24
|
-
* Body is already parsed JSON on FetchResponse, so no clone/json() needed.
|
|
25
|
-
*
|
|
26
|
-
* UIAPI error bodies come in both shapes: an object `{ errorCode, message }`
|
|
27
|
-
* for single errors, or an array `[{ errorCode, message }]` for endpoints that
|
|
28
|
-
* return collections of errors. Handle both.
|
|
29
|
-
*/
|
|
22
|
+
/** True when a Luvio FetchResponse indicates a CSRF token error. */
|
|
30
23
|
export declare function isCsrfError(response: FetchResponse<any>): boolean;
|
|
31
24
|
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RetryContext } from '@conduit-client/service-retry/v1';
|
|
2
|
+
import { RetryPolicy } from '@conduit-client/service-retry/v1';
|
|
3
|
+
import type { FetchResponse } from '@luvio/engine';
|
|
4
|
+
import type { MutableLuvioRequest } from './luvio-csrf-token-retry-policy';
|
|
5
|
+
type LuvioIdempotencyRetryPolicyConfig = {
|
|
6
|
+
maxRetries: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Retry policy for writes carrying an `Idempotency-Key`. A peer of the CSRF and
|
|
10
|
+
* throttling policies in the composed retry chain, not a modification of
|
|
11
|
+
* either: its 503 (keyed-write lock-acquire) and throttling's 503 (keyless
|
|
12
|
+
* GET/read-POST) never contend because they key off different requests, and it
|
|
13
|
+
* only claims a 400/409 when the body carries an idempotency-specific
|
|
14
|
+
* errorCode, so it never steals CSRF's INVALID_ACCESS_TOKEN.
|
|
15
|
+
*
|
|
16
|
+
* Eligibility follows from the header's *presence* on the request, not from
|
|
17
|
+
* the mint interceptor's registry flag — so a stale flag can never cause an
|
|
18
|
+
* unsafe replay.
|
|
19
|
+
*/
|
|
20
|
+
export declare class LuvioIdempotencyRetryPolicy extends RetryPolicy<FetchResponse<any>> {
|
|
21
|
+
private config;
|
|
22
|
+
private requestContext?;
|
|
23
|
+
constructor(config?: LuvioIdempotencyRetryPolicyConfig);
|
|
24
|
+
setRequestContext(context: MutableLuvioRequest): void;
|
|
25
|
+
shouldRetry(result: FetchResponse<any>, context: RetryContext<FetchResponse<any>>): Promise<boolean>;
|
|
26
|
+
calculateDelay(result: FetchResponse<any>, _context: RetryContext<FetchResponse<any>>): Promise<number>;
|
|
27
|
+
prepareRetry(result: FetchResponse<any>, _context: RetryContext<FetchResponse<any>>): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/lds-runtime-one-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.464.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
5
5
|
"description": "LDS engine for the one.app (LEX) runtime.",
|
|
6
6
|
"main": "dist/oneAppLdsEngineCreator.js",
|
|
@@ -34,53 +34,53 @@
|
|
|
34
34
|
"release:corejar": "yarn build && ../core-build/scripts/core.js --name=lds-runtime-one-app"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@conduit-client/service-provisioner": "3.
|
|
38
|
-
"@conduit-client/tools-core": "3.
|
|
39
|
-
"@salesforce/lds-adapters-apex": "^1.
|
|
40
|
-
"@salesforce/lds-adapters-uiapi": "^1.
|
|
41
|
-
"@salesforce/lds-ads-bridge": "^1.
|
|
42
|
-
"@salesforce/lds-aura-storage": "^1.
|
|
43
|
-
"@salesforce/lds-bindings": "^1.
|
|
44
|
-
"@salesforce/lds-instrumentation": "^1.
|
|
45
|
-
"@salesforce/lds-network-adapter": "^1.
|
|
46
|
-
"@salesforce/lds-network-aura": "^1.
|
|
47
|
-
"@salesforce/lds-network-fetch": "^1.
|
|
37
|
+
"@conduit-client/service-provisioner": "3.32.0",
|
|
38
|
+
"@conduit-client/tools-core": "3.32.0",
|
|
39
|
+
"@salesforce/lds-adapters-apex": "^1.464.0",
|
|
40
|
+
"@salesforce/lds-adapters-uiapi": "^1.464.0",
|
|
41
|
+
"@salesforce/lds-ads-bridge": "^1.464.0",
|
|
42
|
+
"@salesforce/lds-aura-storage": "^1.464.0",
|
|
43
|
+
"@salesforce/lds-bindings": "^1.464.0",
|
|
44
|
+
"@salesforce/lds-instrumentation": "^1.464.0",
|
|
45
|
+
"@salesforce/lds-network-adapter": "^1.464.0",
|
|
46
|
+
"@salesforce/lds-network-aura": "^1.464.0",
|
|
47
|
+
"@salesforce/lds-network-fetch": "^1.464.0",
|
|
48
48
|
"jwt-encode": "1.0.1"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@conduit-client/command-aura-graphql-normalized-cache-control": "3.
|
|
52
|
-
"@conduit-client/command-aura-network": "3.
|
|
53
|
-
"@conduit-client/command-aura-normalized-cache-control": "3.
|
|
54
|
-
"@conduit-client/command-fetch-network": "3.
|
|
55
|
-
"@conduit-client/command-http-graphql-normalized-cache-control": "3.
|
|
56
|
-
"@conduit-client/command-http-normalized-cache-control": "3.
|
|
57
|
-
"@conduit-client/command-ndjson": "3.
|
|
58
|
-
"@conduit-client/command-network": "3.
|
|
59
|
-
"@conduit-client/command-sse": "3.
|
|
60
|
-
"@conduit-client/command-streaming": "3.
|
|
61
|
-
"@conduit-client/jwt-manager": "3.
|
|
62
|
-
"@conduit-client/service-aura-network": "3.
|
|
63
|
-
"@conduit-client/service-bindings-imperative": "3.
|
|
64
|
-
"@conduit-client/service-bindings-lwc": "3.
|
|
65
|
-
"@conduit-client/service-cache": "3.
|
|
66
|
-
"@conduit-client/service-cache-control": "3.
|
|
67
|
-
"@conduit-client/service-cache-inclusion-policy": "3.
|
|
68
|
-
"@conduit-client/service-config": "3.
|
|
69
|
-
"@conduit-client/service-feature-flags": "3.
|
|
70
|
-
"@conduit-client/service-fetch-network": "3.
|
|
71
|
-
"@conduit-client/service-instrument-command": "3.
|
|
72
|
-
"@conduit-client/service-pubsub": "3.
|
|
73
|
-
"@conduit-client/service-renewable-resource-manager": "3.
|
|
74
|
-
"@conduit-client/service-store": "3.
|
|
75
|
-
"@conduit-client/utils": "3.
|
|
51
|
+
"@conduit-client/command-aura-graphql-normalized-cache-control": "3.32.0",
|
|
52
|
+
"@conduit-client/command-aura-network": "3.32.0",
|
|
53
|
+
"@conduit-client/command-aura-normalized-cache-control": "3.32.0",
|
|
54
|
+
"@conduit-client/command-fetch-network": "3.32.0",
|
|
55
|
+
"@conduit-client/command-http-graphql-normalized-cache-control": "3.32.0",
|
|
56
|
+
"@conduit-client/command-http-normalized-cache-control": "3.32.0",
|
|
57
|
+
"@conduit-client/command-ndjson": "3.32.0",
|
|
58
|
+
"@conduit-client/command-network": "3.32.0",
|
|
59
|
+
"@conduit-client/command-sse": "3.32.0",
|
|
60
|
+
"@conduit-client/command-streaming": "3.32.0",
|
|
61
|
+
"@conduit-client/jwt-manager": "3.32.0",
|
|
62
|
+
"@conduit-client/service-aura-network": "3.32.0",
|
|
63
|
+
"@conduit-client/service-bindings-imperative": "3.32.0",
|
|
64
|
+
"@conduit-client/service-bindings-lwc": "3.32.0",
|
|
65
|
+
"@conduit-client/service-cache": "3.32.0",
|
|
66
|
+
"@conduit-client/service-cache-control": "3.32.0",
|
|
67
|
+
"@conduit-client/service-cache-inclusion-policy": "3.32.0",
|
|
68
|
+
"@conduit-client/service-config": "3.32.0",
|
|
69
|
+
"@conduit-client/service-feature-flags": "3.32.0",
|
|
70
|
+
"@conduit-client/service-fetch-network": "3.32.0",
|
|
71
|
+
"@conduit-client/service-instrument-command": "3.32.0",
|
|
72
|
+
"@conduit-client/service-pubsub": "3.32.0",
|
|
73
|
+
"@conduit-client/service-renewable-resource-manager": "3.32.0",
|
|
74
|
+
"@conduit-client/service-store": "3.32.0",
|
|
75
|
+
"@conduit-client/utils": "3.32.0",
|
|
76
76
|
"@luvio/network-adapter-composable": "0.161.2",
|
|
77
77
|
"@luvio/network-adapter-fetch": "0.161.2",
|
|
78
78
|
"@lwc/state": "^0.29.0",
|
|
79
|
-
"@salesforce/lds-adapters-onestore-graphql": "^1.
|
|
79
|
+
"@salesforce/lds-adapters-onestore-graphql": "^1.464.0",
|
|
80
80
|
"@salesforce/lds-adapters-uiapi-lex": "^1.415.0",
|
|
81
|
-
"@salesforce/lds-durable-storage": "^1.
|
|
82
|
-
"@salesforce/lds-luvio-service": "^1.
|
|
83
|
-
"@salesforce/lds-luvio-uiapi-records-service": "^1.
|
|
81
|
+
"@salesforce/lds-durable-storage": "^1.464.0",
|
|
82
|
+
"@salesforce/lds-luvio-service": "^1.464.0",
|
|
83
|
+
"@salesforce/lds-luvio-uiapi-records-service": "^1.464.0"
|
|
84
84
|
},
|
|
85
85
|
"luvioBundlesize": [
|
|
86
86
|
{
|