@bleedingdev/modern-js-create-request 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21

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.
@@ -1,151 +1,54 @@
1
1
  import "node:module";
2
2
  import { compile } from "path-to-regexp";
3
3
  import { stringify } from "qs";
4
- import { IdentityBindingViolationError, ProducerDomainNotConfiguredError, TRACEPARENT_HEADER, attachOperationContextHeaders, buildEnvelopeHeaderValue, deleteHeader, extractPathParamNames, firstHeaderValue, isEmptyDomain, isSecuredRequestId, parseTraceparentValue, readHeader, resolveConfiguredRequest, toOrigin, writeHeader } from "./policyCore.mjs";
5
- import { executeWithResilience } from "./transport.mjs";
6
- import { BFF_DEFAULT_PROTECTED_IDENTITY_HEADERS, BFF_ENVELOPE_HEADER, BFF_OPERATION_CONTEXT_DETAIL_HEADER, BFF_OPERATION_CONTEXT_HEADER } from "./types.mjs";
4
+ import { readHeader, writeHeader } from "./headers.mjs";
7
5
  import { getUploadPayload } from "./utiles.mjs";
8
- const OPERATION_CONTEXT_DETAIL_HEADER = BFF_OPERATION_CONTEXT_DETAIL_HEADER;
6
+ const extractPathParamNames = (path)=>Array.from(path.matchAll(/:([A-Za-z0-9_]+)/g)).flatMap(([, key])=>key ? [
7
+ key
8
+ ] : []);
9
9
  const createRequestFactory = (environment)=>{
10
10
  const isServerTarget = 'server' === environment.target;
11
11
  const realRequest = new Map();
12
12
  const realAllowedHeaders = new Map();
13
- const realResolveHeaders = new Map();
14
- const realRequireEnvelope = new Map();
15
- const realAllowCrossOriginEnvelope = new Map();
16
- const realTransportResilience = new Map();
17
- const realIdentityBinding = new Map();
18
- const realOperationContract = new Map();
19
13
  const domainMap = new Map();
20
- const attachEnvelopeHeaderIfRequired = (headers, requestId, url, incomingHeaders)=>{
21
- const shouldRequireEnvelope = realRequireEnvelope.get(requestId) ?? isSecuredRequestId(requestId);
22
- if (!shouldRequireEnvelope) return;
23
- headers[BFF_ENVELOPE_HEADER] = buildEnvelopeHeaderValue({
14
+ const startRequest = (requestId, hooks)=>{
15
+ const context = {
24
16
  requestId,
25
17
  target: environment.target,
26
- sourceOrigin: environment.resolveSourceOrigin(incomingHeaders),
27
- targetOrigin: toOrigin(url),
28
- traceContext: parseTraceparentValue(readHeader(headers, TRACEPARENT_HEADER)),
29
- allowCrossOriginEnvelope: realAllowCrossOriginEnvelope.get(requestId)
30
- });
31
- };
32
- const attachSecuredOperationHeaders = (headers, requestId, method, path, operationContext)=>{
33
- if (!isSecuredRequestId(requestId)) return;
34
- attachOperationContextHeaders({
35
- headers,
36
- requestId,
37
- target: environment.target,
38
- method,
39
- path,
40
- operationContext,
41
- operationContract: realOperationContract.get(requestId),
42
- operationContextHeader: BFF_OPERATION_CONTEXT_HEADER,
43
- operationContextDetailHeader: OPERATION_CONTEXT_DETAIL_HEADER
44
- });
18
+ incomingHeaders: environment.readIncomingHeaders(),
19
+ configured: realRequest.has(requestId),
20
+ configuredDomain: domainMap.get(requestId)
21
+ };
22
+ hooks?.onStart?.(context);
23
+ return context;
45
24
  };
46
- const applyIdentityAndForwardedHeaders = (headers, requestId, incomingHeaders)=>{
47
- const identityBinding = realIdentityBinding.get(requestId);
48
- const identityBindingEnabled = identityBinding?.enabled ?? isSecuredRequestId(requestId);
49
- const identityBindingStrict = identityBinding?.strict ?? isSecuredRequestId(requestId);
50
- const protectedIdentityHeaders = (identityBinding?.protectedHeaders || BFF_DEFAULT_PROTECTED_IDENTITY_HEADERS).map((header)=>header.toLowerCase());
51
- const targetAllowedHeaders = realAllowedHeaders.get(requestId) || [];
25
+ const prepareHeaders = (headers, context, hooks)=>{
26
+ const allowedHeaders = realAllowedHeaders.get(context.requestId) || [];
52
27
  const forwardedHeaders = {};
53
- if (isServerTarget) for (const key of targetAllowedHeaders){
54
- const incomingValue = readHeader(incomingHeaders, key);
55
- if (void 0 !== incomingValue) writeHeader(forwardedHeaders, key, incomingValue);
56
- }
57
- if (identityBindingEnabled) {
58
- const derivedIdentityHeaders = {};
59
- if (isServerTarget) for (const header of protectedIdentityHeaders){
60
- const incomingHeaderValue = readHeader(incomingHeaders, header);
61
- if (void 0 !== incomingHeaderValue) writeHeader(derivedIdentityHeaders, header, incomingHeaderValue);
62
- }
63
- const customDerivedHeaders = identityBinding?.deriveHeaders?.({
64
- requestId,
65
- target: environment.target,
66
- incomingHeaders: isServerTarget ? {
67
- ...incomingHeaders
68
- } : {},
69
- protectedHeaders: [
70
- ...protectedIdentityHeaders
71
- ]
72
- });
73
- if (customDerivedHeaders && 'object' == typeof customDerivedHeaders) for (const header of protectedIdentityHeaders){
74
- const customValue = readHeader(customDerivedHeaders, header);
75
- if (void 0 !== customValue) writeHeader(derivedIdentityHeaders, header, customValue);
76
- }
77
- for (const header of protectedIdentityHeaders){
78
- const attemptedValue = readHeader(headers, header);
79
- if (void 0 === attemptedValue) continue;
80
- const violation = {
81
- requestId,
82
- target: environment.target,
83
- header,
84
- attemptedValue,
85
- derivedValue: readHeader(derivedIdentityHeaders, header),
86
- reason: 'client_override_blocked'
87
- };
88
- identityBinding?.onViolation?.(violation);
89
- if (identityBindingStrict) throw new IdentityBindingViolationError(violation);
90
- deleteHeader(headers, header);
91
- }
92
- Object.keys(derivedIdentityHeaders).forEach((header)=>{
93
- isServerTarget ? writeHeader(forwardedHeaders, header, derivedIdentityHeaders[header]) : writeHeader(headers, header, derivedIdentityHeaders[header]);
94
- });
95
- }
96
- if (isServerTarget) {
97
- const resolveHeaders = realResolveHeaders.get(requestId);
98
- if (resolveHeaders) {
99
- const resolvedHeaders = resolveHeaders({
100
- requestId,
101
- allowedHeaders: targetAllowedHeaders,
102
- incomingHeaders: {
103
- ...forwardedHeaders
104
- }
105
- });
106
- if (resolvedHeaders && 'object' == typeof resolvedHeaders) for (const key of targetAllowedHeaders){
107
- const resolvedValue = readHeader(resolvedHeaders, key);
108
- if (void 0 !== resolvedValue) {
109
- if (identityBindingEnabled && protectedIdentityHeaders.includes(key.toLowerCase())) {
110
- writeHeader(forwardedHeaders, key.toLowerCase(), resolvedValue);
111
- continue;
112
- }
113
- writeHeader(forwardedHeaders, key, resolvedValue);
114
- }
115
- }
116
- }
28
+ if (isServerTarget) for (const key of allowedHeaders){
29
+ const value = readHeader(context.incomingHeaders, key);
30
+ if (void 0 !== value) writeHeader(forwardedHeaders, key, value);
117
31
  }
32
+ hooks?.prepareHeaders?.({
33
+ ...context,
34
+ headers,
35
+ forwardedHeaders,
36
+ allowedHeaders
37
+ });
118
38
  if (isServerTarget) for (const [header, value] of Object.entries(forwardedHeaders))writeHeader(headers, header, value);
119
39
  return headers;
120
40
  };
41
+ const dispatch = (context, hooks)=>hooks?.dispatch ? hooks.dispatch(context) : context.fetcher(context.url, context.init);
121
42
  const configure = (options)=>{
122
- const { request, interceptor, allowedHeaders, resolveHeaders, transport, requireEnvelope, allowCrossOriginEnvelope, identityBinding, operationContract, setDomain, requestId = 'default' } = options;
123
- const hasExistingDomain = domainMap.has(requestId);
124
- if ('default' !== requestId && !setDomain && !hasExistingDomain) throw new ProducerDomainNotConfiguredError(requestId);
43
+ const { request, interceptor, allowedHeaders, setDomain, requestId = 'default' } = options;
125
44
  let configuredRequest = request || environment.originFetch;
126
45
  if (interceptor && !request) configuredRequest = interceptor(environment.getFetch());
127
- let resolvedDomain;
128
- if (setDomain) {
129
- resolvedDomain = setDomain({
130
- target: environment.target,
131
- requestId
132
- });
133
- if ('default' !== requestId && isEmptyDomain(resolvedDomain)) throw new ProducerDomainNotConfiguredError(requestId);
134
- }
46
+ const resolvedDomain = setDomain?.({
47
+ target: environment.target,
48
+ requestId
49
+ });
135
50
  realAllowedHeaders.delete(requestId);
136
- realResolveHeaders.delete(requestId);
137
- realTransportResilience.delete(requestId);
138
- realIdentityBinding.delete(requestId);
139
- realOperationContract.delete(requestId);
140
- realRequireEnvelope.delete(requestId);
141
- realAllowCrossOriginEnvelope.delete(requestId);
142
51
  if (Array.isArray(allowedHeaders)) realAllowedHeaders.set(requestId, allowedHeaders);
143
- if (isServerTarget && 'function' == typeof resolveHeaders) realResolveHeaders.set(requestId, resolveHeaders);
144
- if (transport && 'object' == typeof transport) realTransportResilience.set(requestId, transport);
145
- if (identityBinding && 'object' == typeof identityBinding) realIdentityBinding.set(requestId, identityBinding);
146
- if (operationContract && 'object' == typeof operationContract) realOperationContract.set(requestId, operationContract);
147
- if ('boolean' == typeof requireEnvelope) realRequireEnvelope.set(requestId, requireEnvelope);
148
- if ('boolean' == typeof allowCrossOriginEnvelope || 'function' == typeof allowCrossOriginEnvelope) realAllowCrossOriginEnvelope.set(requestId, allowCrossOriginEnvelope);
149
52
  if ('string' == typeof resolvedDomain) domainMap.set(requestId, resolvedDomain);
150
53
  realRequest.set(requestId, configuredRequest);
151
54
  };
@@ -157,22 +60,21 @@ const createRequestFactory = (environment)=>{
157
60
  httpMethodDecider: args[3],
158
61
  fetch: args[4],
159
62
  requestId: args[5],
160
- operationContext: args[6]
63
+ hooks: args[6]
161
64
  };
162
- const { path, method, port, httpMethodDecider = 'functionName', fetch = environment.originFetch, domain, requestId = 'default', operationContext } = options;
65
+ const { path, method, port, httpMethodDecider = 'functionName', fetch = environment.originFetch, domain, requestId = 'default', hooks } = options;
163
66
  const getFinalPath = compile(path, {
164
67
  encode: encodeURIComponent
165
68
  });
166
69
  const keyNames = extractPathParamNames(path);
167
70
  const send = (...senderArgs)=>{
168
- const fetcher = resolveConfiguredRequest(realRequest, requestId, fetch);
169
- const incomingHeaders = environment.readIncomingHeaders();
71
+ const context = startRequest(requestId, hooks);
72
+ const fetcher = realRequest.get(requestId) || fetch;
170
73
  let body;
171
74
  let headers;
172
75
  let url;
173
76
  if ('inputParams' === httpMethodDecider) {
174
77
  const configDomain = domainMap.get(requestId);
175
- if ('default' !== requestId && isEmptyDomain(configDomain)) throw new ProducerDomainNotConfiguredError(requestId);
176
78
  url = environment.resolveRequestUrl({
177
79
  configDomain,
178
80
  domain,
@@ -183,7 +85,7 @@ const createRequestFactory = (environment)=>{
183
85
  headers = {
184
86
  'Content-Type': 'application/json'
185
87
  };
186
- headers = applyIdentityAndForwardedHeaders(headers, requestId, incomingHeaders);
88
+ headers = prepareHeaders(headers, context, hooks);
187
89
  } else {
188
90
  const payload = 'object' == typeof senderArgs[senderArgs.length - 1] ? senderArgs[senderArgs.length - 1] : {};
189
91
  payload.params = payload.params || {};
@@ -201,7 +103,7 @@ const createRequestFactory = (environment)=>{
201
103
  headers = payload.headers ? {
202
104
  ...payload.headers
203
105
  } : {};
204
- headers = applyIdentityAndForwardedHeaders(headers, requestId, incomingHeaders);
106
+ headers = prepareHeaders(headers, context, hooks);
205
107
  if (payload.data) {
206
108
  headers['Content-Type'] = 'application/json';
207
109
  body = 'object' == typeof payload.data ? JSON.stringify(payload.data) : payload.body;
@@ -214,7 +116,6 @@ const createRequestFactory = (environment)=>{
214
116
  body = !isServerTarget && "u" > typeof URLSearchParams && payload.formUrlencoded instanceof URLSearchParams ? payload.formUrlencoded : 'object' == typeof payload.formUrlencoded ? stringify(payload.formUrlencoded) : payload.formUrlencoded;
215
117
  }
216
118
  const configDomain = domainMap.get(requestId);
217
- if ('default' !== requestId && isEmptyDomain(configDomain)) throw new ProducerDomainNotConfiguredError(requestId);
218
119
  url = environment.resolveRequestUrl({
219
120
  configDomain,
220
121
  domain,
@@ -223,20 +124,12 @@ const createRequestFactory = (environment)=>{
223
124
  });
224
125
  }
225
126
  if (!isServerTarget) writeHeader(headers, 'accept', "application/json,*/*;q=0.8");
226
- if (isServerTarget) {
227
- if (void 0 === readHeader(headers, TRACEPARENT_HEADER)) {
228
- const incomingTraceparent = firstHeaderValue(readHeader(incomingHeaders, TRACEPARENT_HEADER));
229
- if ('string' == typeof incomingTraceparent) writeHeader(headers, TRACEPARENT_HEADER, incomingTraceparent);
230
- }
231
- if (void 0 === readHeader(headers, TRACEPARENT_HEADER) && operationContext?.traceparent) writeHeader(headers, TRACEPARENT_HEADER, operationContext.traceparent);
232
- }
233
- attachEnvelopeHeaderIfRequired(headers, requestId, url, incomingHeaders);
234
- attachSecuredOperationHeaders(headers, requestId, method, path, operationContext);
235
127
  if ('get' === method.toLowerCase()) body = void 0;
236
128
  if (isServerTarget) writeHeader(headers, 'accept', "application/json,*/*;q=0.8");
237
- return executeWithResilience({
129
+ return dispatch({
130
+ ...context,
131
+ path,
238
132
  requestId,
239
- target: environment.target,
240
133
  method,
241
134
  url,
242
135
  init: {
@@ -244,19 +137,19 @@ const createRequestFactory = (environment)=>{
244
137
  body,
245
138
  headers
246
139
  },
247
- fetcher: fetcher,
248
- transport: realTransportResilience.get(requestId)
249
- });
140
+ fetcher: fetcher
141
+ }, hooks);
250
142
  };
251
143
  const sender = isServerTarget ? send : async (...senderArgs)=>send(...senderArgs);
252
144
  return sender;
253
145
  };
254
- const createUploader = ({ path, domain, requestId = 'default', operationContext })=>{
146
+ const createUploader = ({ path, domain, requestId = 'default', hooks })=>{
255
147
  const getUploadPath = isServerTarget ? void 0 : compile(path, {
256
148
  encode: encodeURIComponent
257
149
  });
258
150
  const sender = (...args)=>{
259
- const fetcher = resolveConfiguredRequest(realRequest, requestId, environment.originFetch);
151
+ const context = startRequest(requestId, hooks);
152
+ const fetcher = realRequest.get(requestId) || environment.originFetch;
260
153
  const { body, headers: uploadHeaders, params } = getUploadPayload(args);
261
154
  let headers = {
262
155
  ...uploadHeaders
@@ -268,13 +161,11 @@ const createRequestFactory = (environment)=>{
268
161
  domain,
269
162
  path: finalPath
270
163
  });
271
- const incomingHeaders = environment.readIncomingHeaders();
272
- headers = applyIdentityAndForwardedHeaders(headers, requestId, incomingHeaders);
273
- attachEnvelopeHeaderIfRequired(headers, requestId, finalURL, incomingHeaders);
274
- attachSecuredOperationHeaders(headers, requestId, 'POST', path, operationContext);
275
- return executeWithResilience({
164
+ headers = prepareHeaders(headers, context, hooks);
165
+ return dispatch({
166
+ ...context,
167
+ path,
276
168
  requestId,
277
- target: environment.target,
278
169
  method: 'POST',
279
170
  url: finalURL,
280
171
  init: {
@@ -282,9 +173,8 @@ const createRequestFactory = (environment)=>{
282
173
  body,
283
174
  headers
284
175
  },
285
- fetcher: fetcher,
286
- transport: realTransportResilience.get(requestId)
287
- });
176
+ fetcher: fetcher
177
+ }, hooks);
288
178
  };
289
179
  return sender;
290
180
  };
@@ -294,4 +184,4 @@ const createRequestFactory = (environment)=>{
294
184
  createUploader
295
185
  };
296
186
  };
297
- export { createRequestFactory };
187
+ export { createRequestFactory, extractPathParamNames };
@@ -1,11 +1 @@
1
1
  import "node:module";
2
- const BFF_ENVELOPE_HEADER = 'x-modernjs-bff-envelope';
3
- const BFF_OPERATION_CONTEXT_HEADER = 'x-operation-id';
4
- const BFF_OPERATION_CONTEXT_DETAIL_HEADER = 'x-modernjs-bff-operation-context';
5
- const BFF_DEFAULT_PROTECTED_IDENTITY_HEADERS = [
6
- 'x-tenant-id',
7
- 'x-subject-id',
8
- 'x-user-id',
9
- BFF_OPERATION_CONTEXT_HEADER
10
- ];
11
- export { BFF_DEFAULT_PROTECTED_IDENTITY_HEADERS, BFF_ENVELOPE_HEADER, BFF_OPERATION_CONTEXT_DETAIL_HEADER, BFF_OPERATION_CONTEXT_HEADER };
@@ -1,5 +1,3 @@
1
- export { CrossOriginEnvelopePolicyError, IdentityBindingViolationError, OperationContractViolationError, ProducerClientNotInitializedError, ProducerDomainNotConfiguredError, } from './policyCore.js';
1
+ export declare const createClient: () => import("./types.js").RequestClient<typeof fetch>;
2
2
  export declare const configure: (options: import("./types.js").IOptions<typeof fetch>) => void, createRequest: import("./types.js").RequestCreator<typeof fetch>, createUploader: import("./types.js").UploadCreator;
3
- export * from './requestContext.js';
4
- export * from './traceparent.js';
5
3
  export * from './types.js';
@@ -0,0 +1,3 @@
1
+ export declare const findHeaderKey: (headers: Record<string, any>, header: string) => string | undefined;
2
+ export declare const readHeader: (headers: Record<string, any>, header: string) => any;
3
+ export declare const writeHeader: (headers: Record<string, any>, header: string, value: unknown) => void;
@@ -1,5 +1,3 @@
1
- export { CrossOriginEnvelopePolicyError, IdentityBindingViolationError, OperationContractViolationError, ProducerClientNotInitializedError, ProducerDomainNotConfiguredError, } from './policyCore.js';
1
+ export declare const createClient: () => import("./types.js").RequestClient<typeof fetch>;
2
2
  export declare const configure: (options: import("./types.js").IOptions<typeof fetch>) => void, createRequest: import("./types.js").RequestCreator<typeof fetch>, createUploader: import("./types.js").UploadCreator;
3
- export * from './requestContext.js';
4
- export * from './traceparent.js';
5
3
  export * from './types.js';
@@ -1,5 +1,5 @@
1
- import type { IOptions, RequestCreator, TransportTarget, UploadCreator } from './types.js';
2
- type HeaderMap = Record<string, any>;
1
+ import type { RequestClient, RequestHeaders, RequestTarget } from './types.js';
2
+ type HeaderMap = RequestHeaders;
3
3
  type RequestUrlOptions = {
4
4
  configDomain: string | undefined;
5
5
  domain: string | undefined;
@@ -12,18 +12,14 @@ type UploadUrlOptions = {
12
12
  path: string;
13
13
  };
14
14
  type RequestFactoryEnvironment<F> = {
15
- target: TransportTarget;
15
+ target: RequestTarget;
16
16
  getFetch: () => F;
17
17
  originFetch: F;
18
18
  readIncomingHeaders: () => HeaderMap;
19
- resolveSourceOrigin: (incomingHeaders: HeaderMap) => string | undefined;
20
19
  createInputParamsBody: (args: any[]) => any;
21
20
  resolveRequestUrl: (options: RequestUrlOptions) => string;
22
21
  resolveUploadUrl: (options: UploadUrlOptions) => string;
23
22
  };
24
- export declare const createRequestFactory: <F>(environment: RequestFactoryEnvironment<F>) => {
25
- configure: (options: IOptions<F>) => void;
26
- createRequest: RequestCreator<F>;
27
- createUploader: UploadCreator;
28
- };
23
+ export declare const extractPathParamNames: (path: string) => string[];
24
+ export declare const createRequestFactory: <F>(environment: RequestFactoryEnvironment<F>) => RequestClient<F>;
29
25
  export {};
@@ -1,7 +1,3 @@
1
- export declare const BFF_ENVELOPE_HEADER = "x-modernjs-bff-envelope";
2
- export declare const BFF_OPERATION_CONTEXT_HEADER = "x-operation-id";
3
- export declare const BFF_OPERATION_CONTEXT_DETAIL_HEADER = "x-modernjs-bff-operation-context";
4
- export declare const BFF_DEFAULT_PROTECTED_IDENTITY_HEADERS: readonly ['x-tenant-id', 'x-subject-id', 'x-user-id', "x-operation-id"];
5
1
  export type BFFRequestPayload = {
6
2
  params?: Record<string, any>;
7
3
  query?: Record<string, any>;
@@ -17,124 +13,40 @@ export type Sender<F = typeof fetch> = ((...args: any[]) => Promise<any>) & {
17
13
  fetch?: F;
18
14
  };
19
15
  export type HttpMethodDecider = 'functionName' | 'inputParams';
20
- export type ResolveHeadersOptions = {
16
+ /** Target selected by the native browser/server entry. */
17
+ export type RequestTarget = 'server' | 'browser';
18
+ export type RequestHeaders = Record<string, any>;
19
+ export type RequestFetcher = (url: string, init: Record<string, any>) => Promise<any>;
20
+ /** A snapshot of the native configuration at the start of one send. */
21
+ export type RequestStartContext = {
21
22
  requestId: string;
23
+ target: RequestTarget;
24
+ incomingHeaders: RequestHeaders;
25
+ configured: boolean;
26
+ configuredDomain: string | undefined;
27
+ };
28
+ /** Header maps before native allowlisted forwarding is merged into the request. */
29
+ export type RequestHeadersContext = RequestStartContext & {
30
+ headers: RequestHeaders;
31
+ forwardedHeaders: RequestHeaders;
22
32
  allowedHeaders: string[];
23
- incomingHeaders: Record<string, any>;
24
33
  };
25
- export type ResolveHeaders = (options: ResolveHeadersOptions) => Record<string, any> | void;
26
- export type AllowCrossOriginEnvelopeOptions = {
27
- requestId: string;
28
- sourceOrigin?: string;
29
- targetOrigin?: string;
30
- target: 'server' | 'browser';
31
- };
32
- export type AllowCrossOriginEnvelope = boolean | ((options: AllowCrossOriginEnvelopeOptions) => boolean);
33
- export type TransportTarget = 'server' | 'browser';
34
- export type RetryDecisionContext = {
35
- requestId: string;
36
- target: TransportTarget;
37
- method: string;
38
- url: string;
39
- attempt: number;
40
- maxAttempts: number;
41
- error: unknown;
42
- statusCode?: number;
43
- };
44
- export type RetryBackoffOptions = {
45
- retries?: number;
46
- baseDelayMs?: number;
47
- maxDelayMs?: number;
48
- jitterRatio?: number;
49
- retryableStatusCodes?: number[];
50
- shouldRetry?: (context: RetryDecisionContext) => boolean;
51
- };
52
- export type DegradedModeReason = 'timeout' | 'retry' | 'retry_exhausted';
53
- export type DegradedModeEvent = {
54
- requestId: string;
55
- target: TransportTarget;
34
+ export type RequestDispatchContext = RequestStartContext & {
35
+ path: string;
56
36
  method: string;
57
37
  url: string;
58
- reason: DegradedModeReason;
59
- attempt: number;
60
- maxAttempts: number;
61
- timeoutMs?: number;
62
- backoffMs?: number;
63
- statusCode?: number;
64
- error?: unknown;
65
- };
66
- export type TransportResilienceOptions = {
67
- timeoutMs?: number;
68
- retry?: RetryBackoffOptions;
69
- onDegraded?: (event: DegradedModeEvent) => void;
70
- };
71
- export type IdentityBindingViolationReason = 'client_override_blocked' | 'client_override_rejected';
72
- export type IdentityBindingViolation = {
73
- requestId: string;
74
- target: TransportTarget;
75
- header: string;
76
- reason: IdentityBindingViolationReason;
77
- attemptedValue?: unknown;
78
- derivedValue?: unknown;
79
- };
80
- export type DeriveIdentityHeadersOptions = {
81
- requestId: string;
82
- target: TransportTarget;
83
- incomingHeaders: Record<string, any>;
84
- protectedHeaders: string[];
85
- };
86
- export type IdentityBindingOptions = {
87
- enabled?: boolean;
88
- strict?: boolean;
89
- protectedHeaders?: string[];
90
- deriveHeaders?: (options: DeriveIdentityHeadersOptions) => Record<string, any> | void;
91
- onViolation?: (violation: IdentityBindingViolation) => void;
92
- };
93
- export type OperationContractViolationReason = 'missing_schema_hash' | 'missing_operation_version';
94
- export type OperationContractViolation = {
95
- requestId: string;
96
- target: TransportTarget;
97
- operationId: string;
98
- reason: OperationContractViolationReason;
99
- routePath?: string;
100
- method?: string;
101
- schemaHash?: string;
102
- operationVersion?: number;
103
- };
104
- export type CrossProjectOperationContract = {
105
- schemaHash?: string;
106
- operationVersion?: number;
107
- };
108
- export type CrossProjectPolicyViolationReason = 'missing_envelope' | 'invalid_envelope' | 'missing_request_id' | 'namespace_not_allowed' | 'missing_operation_context' | 'operation_context_mismatch' | 'missing_operation_context_details' | 'invalid_operation_context_details' | 'operation_context_details_request_id_mismatch' | 'missing_operation_schema_hash' | 'missing_operation_version' | 'unknown_operation_contract' | 'operation_schema_hash_mismatch' | 'operation_version_mismatch' | 'producer_identity_mismatch';
109
- export type CrossProjectPolicyViolation = {
110
- code: 'BFF_CROSS_PROJECT_POLICY_DENIED';
111
- reason: CrossProjectPolicyViolationReason;
112
- message: string;
113
- status: number;
114
- };
115
- export type OperationContractOptions = {
116
- enabled?: boolean;
117
- strict?: boolean;
118
- requireSchemaHash?: boolean;
119
- requireOperationVersion?: boolean;
120
- onViolation?: (violation: OperationContractViolation) => void;
121
- };
122
- export type OperationContextSource = 'client' | 'server' | 'generated-client' | 'effect-adapter' | 'data-platform' | 'unknown';
123
- export type OperationContext = {
124
- requestId?: string;
125
- operationId?: string;
126
- routePath?: string;
127
- method?: string;
128
- schemaHash?: string;
129
- operationVersion?: number;
130
- locale?: string;
131
- traceparent?: string;
132
- traceId?: string;
133
- spanId?: string;
134
- source?: OperationContextSource;
135
- scope?: Record<string, unknown>;
136
- sessionClaims?: Record<string, unknown>;
137
- attributes?: Record<string, unknown>;
38
+ init: {
39
+ method: string;
40
+ body: any;
41
+ headers: RequestHeaders;
42
+ };
43
+ fetcher: RequestFetcher;
44
+ };
45
+ /** Optional, instance-local callbacks around native request construction. */
46
+ export type RequestHooks = {
47
+ onStart?: (context: RequestStartContext) => void;
48
+ prepareHeaders?: (context: RequestHeadersContext) => void;
49
+ dispatch?: (context: RequestDispatchContext) => Promise<any>;
138
50
  };
139
51
  export type RequestCreatorOptions<F = typeof fetch> = {
140
52
  path: string;
@@ -144,32 +56,31 @@ export type RequestCreatorOptions<F = typeof fetch> = {
144
56
  fetch?: F;
145
57
  domain?: string;
146
58
  requestId?: string;
147
- operationContext?: OperationContext;
59
+ hooks?: RequestHooks;
148
60
  };
149
61
  export type RequestCreator<F = typeof fetch> = {
150
62
  (options: RequestCreatorOptions<F>): Sender;
151
- (path: string, method: string, port: number, httpMethodDecider?: HttpMethodDecider, fetch?: F, requestId?: string, operationContext?: OperationContext): Sender;
63
+ (path: string, method: string, port: number, httpMethodDecider?: HttpMethodDecider, fetch?: F, requestId?: string, hooks?: RequestHooks): Sender;
152
64
  };
153
65
  export type UploadCreatorOptions = {
154
66
  path: string;
155
67
  domain?: string;
156
68
  requestId?: string;
157
- operationContext?: OperationContext;
69
+ hooks?: RequestHooks;
158
70
  };
159
71
  export type UploadCreator = (options: UploadCreatorOptions) => Sender;
160
72
  export type IOptions<F = typeof fetch> = {
161
73
  request?: F;
162
74
  interceptor?: (request: F) => F;
163
75
  allowedHeaders?: string[];
164
- resolveHeaders?: ResolveHeaders;
165
- transport?: TransportResilienceOptions;
166
- requireEnvelope?: boolean;
167
- allowCrossOriginEnvelope?: AllowCrossOriginEnvelope;
168
- identityBinding?: IdentityBindingOptions;
169
- operationContract?: OperationContractOptions;
170
76
  setDomain?: (ops?: {
171
- target: 'server' | 'browser';
77
+ target: RequestTarget;
172
78
  requestId?: string;
173
79
  }) => string;
174
80
  requestId?: string;
175
81
  };
82
+ export type RequestClient<F = typeof fetch> = {
83
+ configure: (options: IOptions<F>) => void;
84
+ createRequest: RequestCreator<F>;
85
+ createUploader: UploadCreator;
86
+ };
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.9.0-ultramodern.2",
20
+ "version": "3.9.0-ultramodern.21",
21
21
  "modern:source": "./src/node.ts",
22
22
  "types": "./dist/types/browser.d.ts",
23
23
  "main": "./dist/cjs/node.js",
@@ -65,18 +65,18 @@
65
65
  }
66
66
  },
67
67
  "dependencies": {
68
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.2",
69
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.2",
68
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.21",
69
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.21",
70
70
  "@swc/helpers": "^0.5.23",
71
71
  "encoding": "^0.1.13",
72
72
  "path-to-regexp": "^8.4.2",
73
73
  "qs": "^6.16.0"
74
74
  },
75
75
  "devDependencies": {
76
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.2",
77
- "@rslib/core": "1.0.0",
76
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.21",
77
+ "@rslib/core": "1.0.2",
78
78
  "@scripts/rstest-config": "2.66.0",
79
- "@types/node": "^26.4.1",
79
+ "@types/node": "^26.6.2",
80
80
  "@types/qs": "^6.15.1",
81
81
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
82
82
  "nock": "^14.0.17",
@@ -89,6 +89,6 @@
89
89
  "scripts": {
90
90
  "dev": "rslib build --watch",
91
91
  "build": "rslib build",
92
- "test": "rstest --passWithNoTests"
92
+ "test": "rstest"
93
93
  }
94
94
  }