@bleedingdev/modern-js-plugin-bff 3.2.0-ultramodern.120 → 3.2.0-ultramodern.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/cjs/runtime/data-platform/index.js +2 -13
  2. package/dist/cjs/runtime/effect/adapter.js +78 -9
  3. package/dist/cjs/runtime/effect/edge.js +13 -17
  4. package/dist/cjs/runtime/effect/endpoint-contracts.js +130 -0
  5. package/dist/cjs/runtime/effect/handler.js +50 -5
  6. package/dist/cjs/runtime/effect/module.js +16 -7
  7. package/dist/cjs/runtime/effect/operation-context.js +1 -13
  8. package/dist/cjs/runtime/effect-client/runtime.js +266 -0
  9. package/dist/cjs/runtime/hono/adapter.js +21 -9
  10. package/dist/cjs/runtime/safe-failure.js +83 -0
  11. package/dist/cjs/utils/clientGenerator.js +4 -4
  12. package/dist/cjs/utils/crossProjectServerPolicy.js +104 -0
  13. package/dist/cjs/utils/effectClientGenerator.js +90 -483
  14. package/dist/esm/runtime/data-platform/index.mjs +2 -13
  15. package/dist/esm/runtime/effect/adapter.mjs +78 -9
  16. package/dist/esm/runtime/effect/edge.mjs +2 -9
  17. package/dist/esm/runtime/effect/endpoint-contracts.mjs +68 -0
  18. package/dist/esm/runtime/effect/handler.mjs +36 -4
  19. package/dist/esm/runtime/effect/module.mjs +17 -8
  20. package/dist/esm/runtime/effect/operation-context.mjs +1 -13
  21. package/dist/esm/runtime/effect-client/runtime.mjs +228 -0
  22. package/dist/esm/runtime/hono/adapter.mjs +21 -9
  23. package/dist/esm/runtime/safe-failure.mjs +45 -0
  24. package/dist/esm/utils/clientGenerator.mjs +5 -5
  25. package/dist/esm/utils/crossProjectServerPolicy.mjs +50 -0
  26. package/dist/esm/utils/effectClientGenerator.mjs +88 -484
  27. package/dist/esm-node/runtime/data-platform/index.mjs +2 -13
  28. package/dist/esm-node/runtime/effect/adapter.mjs +78 -9
  29. package/dist/esm-node/runtime/effect/edge.mjs +2 -9
  30. package/dist/esm-node/runtime/effect/endpoint-contracts.mjs +69 -0
  31. package/dist/esm-node/runtime/effect/handler.mjs +36 -4
  32. package/dist/esm-node/runtime/effect/module.mjs +17 -8
  33. package/dist/esm-node/runtime/effect/operation-context.mjs +1 -13
  34. package/dist/esm-node/runtime/effect-client/runtime.mjs +229 -0
  35. package/dist/esm-node/runtime/hono/adapter.mjs +21 -9
  36. package/dist/esm-node/runtime/safe-failure.mjs +46 -0
  37. package/dist/esm-node/utils/clientGenerator.mjs +5 -5
  38. package/dist/esm-node/utils/crossProjectServerPolicy.mjs +52 -0
  39. package/dist/esm-node/utils/effectClientGenerator.mjs +88 -484
  40. package/dist/types/runtime/effect/adapter.d.ts +25 -0
  41. package/dist/types/runtime/effect/endpoint-contracts.d.ts +62 -0
  42. package/dist/types/runtime/effect/handler.d.ts +30 -0
  43. package/dist/types/runtime/effect/module.d.ts +21 -1
  44. package/dist/types/runtime/effect-client/runtime.d.ts +71 -0
  45. package/dist/types/runtime/hono/adapter.d.ts +3 -0
  46. package/dist/types/runtime/safe-failure.d.ts +1 -0
  47. package/dist/types/utils/crossProjectServerPolicy.d.ts +35 -0
  48. package/dist/types/utils/effectClientGenerator.d.ts +15 -1
  49. package/package.json +24 -12
@@ -1,3 +1,4 @@
1
+ import { parseTraceparent } from "@modern-js/create-request";
1
2
  import { trace as api_trace } from "@opentelemetry/api";
2
3
  const DATA_BATCH_TRANSPORT_OTEL_EVENT = 'modernjs.data.batch';
3
4
  function createDataBatchTransportTelemetryAttributes(event) {
@@ -23,7 +24,6 @@ function emitDataBatchTransportEvent(onEvent, event) {
23
24
  const DEFAULT_DATA_ENVELOPE_HEADER = 'x-modernjs-data-envelope';
24
25
  const DEFAULT_DATA_BATCH_ENDPOINT = '/_data/batch';
25
26
  const DEFAULT_DATA_BATCH_HEADER = 'x-modernjs-data-batch';
26
- const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
27
27
  function isPlainObject(value) {
28
28
  if ('object' != typeof value || null === value || Array.isArray(value)) return false;
29
29
  const proto = Object.getPrototypeOf(value);
@@ -120,18 +120,7 @@ function isValidHex(value, length) {
120
120
  return value.length === length && /^[0-9a-f]+$/.test(value);
121
121
  }
122
122
  function parseTraceparentHeader(header) {
123
- const match = header.trim().match(TRACEPARENT_REGEX);
124
- if (!match) return null;
125
- const traceId = match[1].toLowerCase();
126
- const spanId = match[2].toLowerCase();
127
- const flags = match[3].toLowerCase();
128
- if (isAllZeroHex(traceId) || isAllZeroHex(spanId)) return null;
129
- const sampled = (0x1 & Number.parseInt(flags, 16)) === 1;
130
- return {
131
- traceId,
132
- spanId,
133
- sampled
134
- };
123
+ return parseTraceparent(header) ?? null;
135
124
  }
136
125
  function formatTraceparentHeader(trace) {
137
126
  const traceId = trace.traceId.toLowerCase();
@@ -1,6 +1,11 @@
1
+ import { ApiRouter } from "@modern-js/bff-core";
1
2
  import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
3
+ import { HttpApi } from "effect/unstable/httpapi";
2
4
  import path from "path";
5
+ import { checkCrossProjectPolicyForRequest, resolveAdapterCrossProjectPolicy } from "../../utils/crossProjectServerPolicy.mjs";
6
+ import { createSafeFailureResponse } from "../safe-failure.mjs";
3
7
  import { createEffectOperationContext, runWithEffectContext } from "./context.mjs";
8
+ import { collectEffectEndpoints, extractHttpApiFromModule, toOperationContractSources } from "./endpoint-contracts.mjs";
4
9
  import { resolveEffectBffModuleHandler } from "./module.mjs";
5
10
  const before = [
6
11
  'custom-server-hook',
@@ -46,10 +51,73 @@ class EffectAdapter {
46
51
  const entryWithoutExt = configuredEntry ? path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry) : defaultEntry;
47
52
  return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
48
53
  }
54
+ isApiRequestPath(requestPath, prefix, enableHandleWeb) {
55
+ if (!enableHandleWeb) return true;
56
+ const normalized = normalizePrefix(prefix);
57
+ if (!normalized) return true;
58
+ return requestPath === normalized || requestPath.startsWith(`${normalized}/`);
59
+ }
60
+ async collectLambdaContractSources() {
61
+ try {
62
+ const serverContext = this.api.getServerContext();
63
+ const appDir = serverContext.distDirectory || serverContext.appDirectory;
64
+ if (!appDir) return [];
65
+ const apiDir = 'string' == typeof serverContext.apiDirectory ? serverContext.apiDirectory : path.resolve(appDir, API_DIR);
66
+ const lambdaDir = 'string' == typeof serverContext.lambdaDirectory ? serverContext.lambdaDirectory : path.join(apiDir, 'lambda');
67
+ if (!await fs.pathExists(lambdaDir)) return [];
68
+ const apiRouter = new ApiRouter({
69
+ appDir,
70
+ apiDir,
71
+ lambdaDir,
72
+ prefix: this.prefix,
73
+ httpMethodDecider: this.api.getServerConfig()?.bff?.httpMethodDecider
74
+ });
75
+ const handlerInfos = await apiRouter.getApiHandlers();
76
+ return handlerInfos.map((info)=>({
77
+ name: info.name,
78
+ httpMethod: info.httpMethod,
79
+ routePath: info.routePath,
80
+ filename: info.filename,
81
+ handler: info.handler
82
+ }));
83
+ } catch (error) {
84
+ logger.warn(`[BFF][Effect] Failed to derive lambda operation contracts for the cross-project policy: ${String(error)}`);
85
+ return [];
86
+ }
87
+ }
88
+ async refreshCrossProjectPolicy(mod) {
89
+ let contractSources = [];
90
+ if (mod) try {
91
+ const api = await extractHttpApiFromModule(mod, HttpApi.isHttpApi);
92
+ if (api) {
93
+ const reflect = (apiValue, handlers)=>HttpApi.reflect(apiValue, {
94
+ onGroup: handlers.onGroup ?? (()=>{}),
95
+ onEndpoint: handlers.onEndpoint
96
+ });
97
+ contractSources = toOperationContractSources(collectEffectEndpoints(reflect, api, this.prefix));
98
+ }
99
+ } catch (error) {
100
+ logger.warn(`[BFF][Effect] Failed to reflect HttpApi endpoints for the cross-project policy: ${String(error)}`);
101
+ }
102
+ let policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
103
+ if (policy?.enabled) {
104
+ const lambdaSources = await this.collectLambdaContractSources();
105
+ if (lambdaSources.length > 0) {
106
+ contractSources = [
107
+ ...contractSources,
108
+ ...lambdaSources
109
+ ];
110
+ policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
111
+ }
112
+ }
113
+ this.crossProjectPolicy = policy;
114
+ if (this.crossProjectPolicy?.enabled && 0 === contractSources.length) logger.warn('[BFF][Effect] Cross-project policy is enabled but no HttpApi endpoints could be reflected; operation-contract matching is disabled for this server (envelope and operation-context checks still apply).');
115
+ }
49
116
  async loadEffectHandlerFromModule(mod) {
50
117
  return resolveEffectBffModuleHandler(mod, {
51
118
  openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
52
119
  dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform,
120
+ validateRequest: (request)=>checkCrossProjectPolicyForRequest(request, this.crossProjectPolicy),
53
121
  onWarning: (message)=>{
54
122
  logger.warn(message);
55
123
  }
@@ -79,6 +147,7 @@ class EffectAdapter {
79
147
  this.handler = null;
80
148
  return;
81
149
  }
150
+ await this.refreshCrossProjectPolicy(mod);
82
151
  const loaded = await this.loadEffectHandlerFromModule(mod);
83
152
  if (!loaded) {
84
153
  logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer } or handler.`);
@@ -87,6 +156,7 @@ class EffectAdapter {
87
156
  }
88
157
  this.handler = loaded.handler;
89
158
  this.dispose = loaded.dispose || null;
159
+ this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
90
160
  }
91
161
  async disposeCurrentHandler() {
92
162
  if (!this.dispose) return;
@@ -110,15 +180,7 @@ class EffectAdapter {
110
180
  } catch (configError) {
111
181
  logger.error(`Error in serverConfig.onError handler: ${configError}`);
112
182
  }
113
- const status = 'object' == typeof error && null !== error && 'status' in error && 'number' == typeof error.status ? error.status : 500;
114
- return new Response(JSON.stringify({
115
- message: error instanceof Error ? error.message : '[BFF] Internal Server Error'
116
- }), {
117
- status,
118
- headers: {
119
- 'content-type': 'application/json; charset=utf-8'
120
- }
121
- });
183
+ return createSafeFailureResponse(error);
122
184
  }
123
185
  ensureJsonContext(c) {
124
186
  const maybeJsonContext = c;
@@ -145,6 +207,8 @@ class EffectAdapter {
145
207
  this.effectMiddleware = null;
146
208
  this.handler = null;
147
209
  this.dispose = null;
210
+ this.prefix = '/api';
211
+ this.policyEnforcedInMiddleware = false;
148
212
  this.registerMiddleware = async (options)=>{
149
213
  const { prefix, enableHandleWeb } = options;
150
214
  const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
@@ -152,6 +216,7 @@ class EffectAdapter {
152
216
  this.isEffect = false;
153
217
  return;
154
218
  }
219
+ this.prefix = prefix || this.prefix;
155
220
  await this.reloadHandler();
156
221
  this.effectMiddleware = {
157
222
  name: 'effect-bff-handler',
@@ -164,6 +229,10 @@ class EffectAdapter {
164
229
  if (enableHandleWeb) return void await next();
165
230
  return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/effect/index or configure bff.effect.entry.'), c);
166
231
  }
232
+ if (this.crossProjectPolicy?.enabled && this.policyEnforcedInMiddleware && this.isApiRequestPath(c.req.path, prefix, enableHandleWeb)) {
233
+ const denial = checkCrossProjectPolicyForRequest(c.req.raw, this.crossProjectPolicy);
234
+ if (denial) return denial;
235
+ }
167
236
  let response;
168
237
  try {
169
238
  const effectRequest = createRequestForMountedPrefix(c.req.raw, prefix);
@@ -1,3 +1,4 @@
1
+ import { createSafeFailureResponse } from "../safe-failure.mjs";
1
2
  import { resolveEffectBffModuleHandler } from "./module.mjs";
2
3
  import { createEffectOperationContext } from "./operation-context.mjs";
3
4
  export * from "./handler.mjs";
@@ -39,15 +40,7 @@ function createEdgeEffectContext(originalRequest, effectRequest, options) {
39
40
  };
40
41
  }
41
42
  function createRuntimeErrorResponse(error) {
42
- const status = 'object' == typeof error && null !== error && 'status' in error && 'number' == typeof error.status ? error.status : 500;
43
- return new Response(JSON.stringify({
44
- message: error instanceof Error ? error.message : '[BFF] Internal Server Error'
45
- }), {
46
- status,
47
- headers: {
48
- 'content-type': 'application/json; charset=utf-8'
49
- }
50
- });
43
+ return createSafeFailureResponse(error);
51
44
  }
52
45
  async function dispatchEffectBffRequest(handler, request, options = {}) {
53
46
  const requestPathname = new URL(request.url).pathname;
@@ -0,0 +1,68 @@
1
+ import { createOperationContractHash } from "@modern-js/bff-core";
2
+ function ensureLeadingSlash(pathname) {
3
+ return pathname.startsWith('/') ? pathname : `/${pathname}`;
4
+ }
5
+ function normalizeEffectPrefix(prefix) {
6
+ if ('/' === prefix) return '';
7
+ return ensureLeadingSlash(prefix || '/api');
8
+ }
9
+ function getEffectRoutePath(prefix, endpointPath) {
10
+ const normalizedPrefix = normalizeEffectPrefix(prefix);
11
+ const normalizedEndpointPath = ensureLeadingSlash(endpointPath);
12
+ const finalEndpointPath = '/' === normalizedEndpointPath ? '' : endpointPath;
13
+ if (!normalizedPrefix && !finalEndpointPath) return '/';
14
+ return `${normalizedPrefix}${finalEndpointPath || ''}`;
15
+ }
16
+ function resolveEffectApiId(api) {
17
+ const fallback = 'EffectHttpApi';
18
+ if ('identifier' in api && 'string' == typeof api.identifier && api.identifier) return api.identifier;
19
+ return fallback;
20
+ }
21
+ function collectEffectEndpoints(reflect, api, prefix) {
22
+ const endpoints = [];
23
+ const apiId = resolveEffectApiId(api);
24
+ reflect(api, {
25
+ onGroup: ()=>{},
26
+ onEndpoint: ({ group, endpoint })=>{
27
+ endpoints.push({
28
+ apiId,
29
+ groupName: String(group.identifier),
30
+ endpointName: String(endpoint.name),
31
+ method: String(endpoint.method).toUpperCase(),
32
+ routePath: getEffectRoutePath(prefix, String(endpoint.path))
33
+ });
34
+ }
35
+ });
36
+ return endpoints.sort((a, b)=>{
37
+ if (a.groupName === b.groupName) return a.endpointName.localeCompare(b.endpointName);
38
+ return a.groupName.localeCompare(b.groupName);
39
+ });
40
+ }
41
+ function toOperationContractSources(endpoints) {
42
+ return endpoints.map(createEffectOperationContractSource);
43
+ }
44
+ function createEffectOperationContractSource(endpoint) {
45
+ return {
46
+ name: endpoint.endpointName,
47
+ httpMethod: endpoint.method,
48
+ routePath: endpoint.routePath
49
+ };
50
+ }
51
+ function isRecord(value) {
52
+ return 'object' == typeof value && null !== value;
53
+ }
54
+ async function extractHttpApiFromModule(mod, isHttpApi) {
55
+ if (!isRecord(mod)) return null;
56
+ if (isHttpApi(mod.api)) return mod.api;
57
+ const entry = mod.default;
58
+ if (isRecord(entry) && isHttpApi(entry.api)) return entry.api;
59
+ if ('function' == typeof entry && 0 === entry.length) {
60
+ const output = await entry();
61
+ if (isRecord(output) && isHttpApi(output.api)) return output.api;
62
+ }
63
+ return null;
64
+ }
65
+ function createEffectEndpointContractHash(endpoint, requestId) {
66
+ return createOperationContractHash(createEffectOperationContractSource(endpoint), requestId);
67
+ }
68
+ export { collectEffectEndpoints, createEffectEndpointContractHash, createEffectOperationContractSource, ensureLeadingSlash, extractHttpApiFromModule, getEffectRoutePath, normalizeEffectPrefix, resolveEffectApiId, toOperationContractSources };
@@ -12,6 +12,10 @@ export * from "effect/unstable/httpapi";
12
12
  export * from "effect/unstable/rpc";
13
13
  import * as __rspack_external_effect_Context_f1289ca3 from "effect/Context";
14
14
  const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
15
+ const EFFECT_VALIDATOR_AWARE_FACTORY = Symbol.for('modernjs.effect.validatorAware');
16
+ function isValidatorAwareHandlerFactory(factory) {
17
+ return 'function' == typeof factory && true === factory[EFFECT_VALIDATOR_AWARE_FACTORY];
18
+ }
15
19
  function normalizeOpenApiPath(pathname) {
16
20
  if (!pathname.startsWith('/')) return `/${pathname}`;
17
21
  return pathname;
@@ -267,16 +271,38 @@ function defineEffectBff(definition) {
267
271
  layer: definition.layer,
268
272
  openapi: options?.openapi,
269
273
  rpc: mergedRpcOptions,
270
- dataPlatform: mergeDataPlatformOptions(definition.dataPlatform, options?.dataPlatform)
274
+ dataPlatform: mergeDataPlatformOptions(definition.dataPlatform, options?.dataPlatform),
275
+ validateRequest: options?.validateRequest
271
276
  });
272
277
  };
273
- const client = void 0;
278
+ Object.defineProperty(createHandler, EFFECT_VALIDATOR_AWARE_FACTORY, {
279
+ value: true
280
+ });
281
+ const client = createLoaderMaterializedClientPlaceholder();
274
282
  return {
275
283
  ...definition,
276
284
  createHandler,
277
285
  client
278
286
  };
279
287
  }
288
+ const LOADER_CLIENT_IGNORED_KEYS = new Set([
289
+ 'then',
290
+ 'catch',
291
+ 'finally',
292
+ 'toJSON',
293
+ '$$typeof'
294
+ ]);
295
+ function createLoaderMaterializedClientPlaceholder() {
296
+ const explain = (property)=>{
297
+ throw new Error(`[BFF][Effect] effectBff.client.${String(property)} is not available here: the typed client only exists when this module is imported through the "@api/effect/*" transformed path (the BFF loader replaces it with generated client code). On the server, use HttpApiClient or call the Effect layer directly.`);
298
+ };
299
+ return new Proxy(Object.create(null), {
300
+ get (_target, property) {
301
+ if ('symbol' == typeof property || LOADER_CLIENT_IGNORED_KEYS.has(property)) return;
302
+ return explain(property);
303
+ }
304
+ });
305
+ }
280
306
  function defineEffectRpcBff(definition) {
281
307
  const createHandler = (options)=>createRpcApiHandler({
282
308
  ...definition,
@@ -303,6 +329,8 @@ function createHttpApiHandler(options) {
303
329
  const envelopeHeader = options.dataPlatform?.envelopeHeader || DEFAULT_DATA_ENVELOPE_HEADER;
304
330
  const normalizedEnvelopeHeader = envelopeHeader.toLowerCase();
305
331
  const withDataPlatformValidation = async (request, context)=>{
332
+ const policyDenial = options.validateRequest?.(request);
333
+ if (policyDenial) return policyDenial;
306
334
  const validationError = validateDataPlatformRequestEnvelope(request, options.dataPlatform);
307
335
  if (validationError) return validationError;
308
336
  return httpApiHandler.handler(request, context ?? emptyEffectServiceContext);
@@ -424,7 +452,11 @@ function createHttpApiHandler(options) {
424
452
  const rpcHandler = createRpcApiHandler(options.rpc);
425
453
  return {
426
454
  handler: async (request, context)=>{
427
- if (isRpcRequest(request, rpcPath)) return rpcHandler.handler(request, context ?? emptyEffectServiceContext);
455
+ if (isRpcRequest(request, rpcPath)) {
456
+ const policyDenial = options.validateRequest?.(request);
457
+ if (policyDenial) return policyDenial;
458
+ return rpcHandler.handler(request, context ?? emptyEffectServiceContext);
459
+ }
428
460
  return handleHttpApiRequest(request);
429
461
  },
430
462
  dispose: async ()=>{
@@ -435,4 +467,4 @@ function createHttpApiHandler(options) {
435
467
  }
436
468
  };
437
469
  }
438
- export { HttpApiBuilder, HttpTraceContext, __rspack_external_effect_Config_29be8a92 as Config, __rspack_external_effect_Effect_194ac36c as Effect, __rspack_external_effect_Layer_16f7a8fc as Layer, __rspack_external_effect_Option_4d691636 as Option, __rspack_external_effect_Schema_f8472650 as Schema, createHttpApiHandler, defineEffectBff, defineEffectRpcBff };
470
+ export { EFFECT_VALIDATOR_AWARE_FACTORY, HttpApiBuilder, HttpTraceContext, __rspack_external_effect_Config_29be8a92 as Config, __rspack_external_effect_Effect_194ac36c as Effect, __rspack_external_effect_Layer_16f7a8fc as Layer, __rspack_external_effect_Option_4d691636 as Option, __rspack_external_effect_Schema_f8472650 as Schema, createHttpApiHandler, defineEffectBff, defineEffectRpcBff, isValidatorAwareHandlerFactory };
@@ -1,5 +1,5 @@
1
1
  import { HttpApi } from "effect/unstable/httpapi";
2
- import { createHttpApiHandler } from "./handler.mjs";
2
+ import { createHttpApiHandler, isValidatorAwareHandlerFactory } from "./handler.mjs";
3
3
  import * as __rspack_external_effect_Context_f1289ca3 from "effect/Context";
4
4
  function isRecord(value) {
5
5
  return 'object' == typeof value && null !== value;
@@ -20,10 +20,13 @@ const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empt
20
20
  function callEffectBffRequestHandler(handler, request, context) {
21
21
  return void 0 === context ? handler(request) : handler(request, context);
22
22
  }
23
- function createLoadedHandler(webHandler) {
23
+ function createLoadedHandler(webHandler, appliesRequestValidator) {
24
24
  return {
25
25
  handler: (request, context)=>callEffectBffRequestHandler(webHandler.handler, request, context),
26
- dispose: webHandler.dispose
26
+ dispose: webHandler.dispose,
27
+ ...appliesRequestValidator ? {
28
+ appliesRequestValidator: true
29
+ } : {}
27
30
  };
28
31
  }
29
32
  function createLoadedHttpApiHandler(webHandler) {
@@ -32,7 +35,8 @@ function createLoadedHttpApiHandler(webHandler) {
32
35
  const effectContext = isEffectServiceContext(context) ? context : emptyEffectServiceContext;
33
36
  return webHandler.handler(request, effectContext);
34
37
  },
35
- dispose: webHandler.dispose
38
+ dispose: webHandler.dispose,
39
+ appliesRequestValidator: true
36
40
  };
37
41
  }
38
42
  function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {}) {
@@ -58,11 +62,15 @@ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {})
58
62
  handler: normalizedModule.handler
59
63
  };
60
64
  if ('function' == typeof normalizedModule.createHandler) {
61
- const webHandler = normalizedModule.createHandler({
65
+ const factory = normalizedModule.createHandler;
66
+ const validatorAware = isValidatorAwareHandlerFactory(factory);
67
+ if (!validatorAware && void 0 !== options.validateRequest) options.onWarning?.('[BFF][Effect] Custom createHandler export detected: it cannot be verified to apply validateRequest (cross-project policy), so the policy is enforced by the adapter middleware on the outer request. Batched calls will be denied at the batch POST (it carries no per-operation contract); export defineEffectBff(...) to get per-batch-item enforcement.');
68
+ const webHandler = factory({
62
69
  openapi: options.openapi,
63
- dataPlatform: options.dataPlatform
70
+ dataPlatform: options.dataPlatform,
71
+ validateRequest: options.validateRequest
64
72
  });
65
- return createLoadedHandler(webHandler);
73
+ return createLoadedHandler(webHandler, validatorAware);
66
74
  }
67
75
  if (isEffectApiDefinition(normalizedModule)) {
68
76
  options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
@@ -70,7 +78,8 @@ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {})
70
78
  api: normalizedModule.api,
71
79
  layer: normalizedModule.layer,
72
80
  openapi: options.openapi,
73
- dataPlatform: options.dataPlatform
81
+ dataPlatform: options.dataPlatform,
82
+ validateRequest: options.validateRequest
74
83
  });
75
84
  return createLoadedHttpApiHandler(webHandler);
76
85
  }
@@ -1,5 +1,4 @@
1
- import { BFF_LOCALE_HEADER, BFF_OPERATION_CONTEXT_DETAIL_HEADER, BFF_OPERATION_CONTEXT_HEADER, BFF_TRACEPARENT_HEADER } from "@modern-js/create-request";
2
- const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/i;
1
+ import { BFF_LOCALE_HEADER, BFF_OPERATION_CONTEXT_DETAIL_HEADER, BFF_OPERATION_CONTEXT_HEADER, BFF_TRACEPARENT_HEADER, parseTraceparent } from "@modern-js/create-request";
3
2
  const readHeader = (headers, header)=>{
4
3
  const value = headers.get(header);
5
4
  return value && value.length > 0 ? value : void 0;
@@ -8,17 +7,6 @@ const copyStringField = (target, details, key)=>{
8
7
  const value = details[key];
9
8
  if ('string' == typeof value && value.length > 0) target[key] = value;
10
9
  };
11
- const parseTraceparent = (traceparent)=>{
12
- if (!traceparent) return;
13
- const match = traceparent.trim().match(TRACEPARENT_REGEX);
14
- if (!match) return;
15
- const [, traceId, spanId] = match;
16
- if (!traceId || !spanId) return;
17
- return {
18
- traceId: traceId.toLowerCase(),
19
- spanId: spanId.toLowerCase()
20
- };
21
- };
22
10
  const readOperationContextDetails = (request)=>{
23
11
  const rawDetails = readHeader(request.headers, BFF_OPERATION_CONTEXT_DETAIL_HEADER);
24
12
  if (!rawDetails) return {};
@@ -0,0 +1,228 @@
1
+ import { DEFAULT_DATA_BATCH_HEADER, DEFAULT_DATA_ENVELOPE_HEADER, createDataBatchTransport, createRequestEnvelope, encodeRequestEnvelopeHeader } from "../data-platform/index.mjs";
2
+ const METHODS_WITHOUT_BODY = new Set([
3
+ 'GET',
4
+ 'DELETE',
5
+ 'HEAD',
6
+ 'OPTIONS'
7
+ ]);
8
+ const DATA_REQUEST_MODES = new Set([
9
+ 'cache-first',
10
+ 'stale-while-revalidate',
11
+ 'network-only'
12
+ ]);
13
+ const DATA_MUTATION_MODES = new Set([
14
+ 'optimistic',
15
+ 'pessimistic',
16
+ 'fire-and-forget'
17
+ ]);
18
+ const isRecord = (value)=>'object' == typeof value && null !== value;
19
+ const stringOrUndefined = (value)=>'string' == typeof value && value.length > 0 ? value : void 0;
20
+ const isDataRequestMode = (value)=>'string' == typeof value && DATA_REQUEST_MODES.has(value);
21
+ const isDataMutationMode = (value)=>'string' == typeof value && DATA_MUTATION_MODES.has(value);
22
+ const normalizeOrigin = (value)=>{
23
+ if ('string' != typeof value || 0 === value.length) return;
24
+ try {
25
+ return new URL(value).origin;
26
+ } catch {
27
+ return;
28
+ }
29
+ };
30
+ const resolveRuntimeFetch = ()=>'function' == typeof fetch ? fetch.bind(globalThis) : void 0;
31
+ const resolveOrigin = (defaultOrigin)=>{
32
+ if ("u" > typeof window && window.location && 'string' == typeof window.location.origin && window.location.origin) return window.location.origin;
33
+ const globalLocation = globalThis?.location;
34
+ if (globalLocation && 'string' == typeof globalLocation.origin && globalLocation.origin) return globalLocation.origin;
35
+ return defaultOrigin;
36
+ };
37
+ const normalizeRequest = (method, request)=>{
38
+ if (!isRecord(request)) return {};
39
+ const payload = {
40
+ ...request
41
+ };
42
+ if (isRecord(request.path) && !isRecord(payload.params)) payload.params = request.path;
43
+ if (isRecord(request.urlParams) && !isRecord(payload.query)) payload.query = request.urlParams;
44
+ if (isRecord(request.headers) && !isRecord(payload.headers)) payload.headers = request.headers;
45
+ if ('payload' in request && void 0 !== request.payload) if ("u" > typeof FormData && request.payload instanceof FormData && !('formData' in payload)) payload.formData = request.payload;
46
+ else if (METHODS_WITHOUT_BODY.has(method)) {
47
+ if (isRecord(request.payload)) payload.query = isRecord(payload.query) ? {
48
+ ...payload.query,
49
+ ...request.payload
50
+ } : request.payload;
51
+ else if (!('body' in payload)) payload.body = request.payload;
52
+ } else if (!isRecord(request.payload) || 'data' in payload) {
53
+ if (!('body' in payload)) payload.body = request.payload;
54
+ } else payload.data = request.payload;
55
+ return payload;
56
+ };
57
+ const resolveTargetOrigin = (dataPlatform, defaultOrigin)=>{
58
+ const explicitTargetOrigin = stringOrUndefined(dataPlatform.targetOrigin) || stringOrUndefined(dataPlatform.endpointOrigin);
59
+ if (explicitTargetOrigin) return explicitTargetOrigin;
60
+ return defaultOrigin;
61
+ };
62
+ const shouldAttachEnvelopeHeader = (dataPlatform, defaultOrigin)=>{
63
+ if (true === dataPlatform.allowCrossOriginEnvelope) return true;
64
+ const currentOrigin = normalizeOrigin(resolveOrigin(defaultOrigin));
65
+ const targetOrigin = normalizeOrigin(resolveTargetOrigin(dataPlatform, defaultOrigin));
66
+ if (!currentOrigin || !targetOrigin) return true;
67
+ return currentOrigin === targetOrigin;
68
+ };
69
+ const toEnvelopeInput = (normalizedRequest)=>{
70
+ const payload = {};
71
+ if (isRecord(normalizedRequest.params)) payload.path = normalizedRequest.params;
72
+ if (isRecord(normalizedRequest.query)) payload.query = normalizedRequest.query;
73
+ if ('data' in normalizedRequest && void 0 !== normalizedRequest.data) payload.data = normalizedRequest.data;
74
+ if ('body' in normalizedRequest && void 0 !== normalizedRequest.body) payload.body = normalizedRequest.body;
75
+ if ("u" > typeof FormData && normalizedRequest.formData instanceof FormData) payload.formData = Array.from(normalizedRequest.formData.entries()).map(([key, value])=>[
76
+ key,
77
+ String(value)
78
+ ]);
79
+ if ("u" > typeof URLSearchParams && normalizedRequest.formUrlencoded instanceof URLSearchParams) payload.formUrlencoded = normalizedRequest.formUrlencoded.toString();
80
+ return payload;
81
+ };
82
+ const createGeneratedEffectClient = (manifest, config, requestRuntime)=>{
83
+ const createRequest = requestRuntime.createRequest;
84
+ const configureRequest = 'function' == typeof requestRuntime.configure ? requestRuntime.configure : void 0;
85
+ const createRequestContextHeaders = 'function' == typeof requestRuntime.createRequestContextHeaders ? requestRuntime.createRequestContextHeaders : void 0;
86
+ const defaultOrigin = config.defaultOrigin;
87
+ const httpMethodDecider = config.httpMethodDecider || 'functionName';
88
+ const port = config.useEnvPort && "u" > typeof process && process.env && process.env.PORT ? process.env.PORT : config.port;
89
+ if (config.requestId && configureRequest) {
90
+ const configurePayload = {
91
+ requestId: config.requestId,
92
+ requireEnvelope: true,
93
+ identityBinding: {
94
+ enabled: true,
95
+ strict: true
96
+ },
97
+ operationContract: {
98
+ enabled: true,
99
+ strict: true,
100
+ requireSchemaHash: true,
101
+ requireOperationVersion: true
102
+ },
103
+ setDomain: ()=>resolveOrigin(defaultOrigin)
104
+ };
105
+ const runtimeFetch = resolveRuntimeFetch();
106
+ if (false !== config.batch.enabled && runtimeFetch) configurePayload.request = createDataBatchTransport({
107
+ fetch: runtimeFetch,
108
+ endpoint: config.batch.endpoint,
109
+ flushIntervalMs: config.batch.flushIntervalMs,
110
+ maxBatchSize: config.batch.maxBatchSize,
111
+ maxBatchBytes: config.batch.maxBatchBytes,
112
+ requestTimeoutMs: config.batch.requestTimeoutMs,
113
+ allowedMethods: config.batch.allowedMethods
114
+ });
115
+ configureRequest(configurePayload);
116
+ }
117
+ const createEffectRequestContext = (requestContext)=>{
118
+ if (!isRecord(requestContext)) return {};
119
+ const headers = createRequestContextHeaders ? createRequestContextHeaders(requestContext) : {};
120
+ return {
121
+ ...requestContext,
122
+ headers
123
+ };
124
+ };
125
+ const applyRequestContext = (normalizedRequest, request)=>{
126
+ if (!isRecord(request) || !isRecord(request.requestContext)) return normalizedRequest;
127
+ const requestContext = createEffectRequestContext(request.requestContext);
128
+ const requestHeaders = isRecord(requestContext.headers) ? requestContext.headers : {};
129
+ if (0 === Object.keys(requestHeaders).length) return normalizedRequest;
130
+ return {
131
+ ...normalizedRequest,
132
+ headers: {
133
+ ...requestHeaders,
134
+ ...isRecord(normalizedRequest.headers) ? normalizedRequest.headers : {}
135
+ }
136
+ };
137
+ };
138
+ const prepareEffectRequest = (endpoint, operation, request)=>{
139
+ const normalizedRequest = applyRequestContext(normalizeRequest(endpoint.method, request), request);
140
+ const dataPlatform = isRecord(request) && isRecord(request.dataPlatform) ? request.dataPlatform : {};
141
+ const strictEnvelope = true === dataPlatform.requireEnvelope || true === dataPlatform.strict;
142
+ if (!strictEnvelope && !shouldAttachEnvelopeHeader(dataPlatform, defaultOrigin)) return normalizedRequest;
143
+ try {
144
+ const namespace = stringOrUndefined(dataPlatform.appNamespace) || config.appNamespace;
145
+ const origin = stringOrUndefined(dataPlatform.origin) || resolveOrigin(defaultOrigin);
146
+ const envelope = createRequestEnvelope({
147
+ operation: {
148
+ ...operation,
149
+ appNamespace: namespace
150
+ },
151
+ scope: {
152
+ appNamespace: namespace,
153
+ origin,
154
+ tenantId: stringOrUndefined(dataPlatform.tenantId),
155
+ userId: stringOrUndefined(dataPlatform.userId),
156
+ sessionId: stringOrUndefined(dataPlatform.sessionId)
157
+ },
158
+ requestInput: {
159
+ method: endpoint.method,
160
+ routePath: endpoint.routePath,
161
+ payload: toEnvelopeInput(normalizedRequest)
162
+ },
163
+ requestMode: isDataRequestMode(dataPlatform.requestMode) ? dataPlatform.requestMode : void 0,
164
+ mutationMode: isDataMutationMode(dataPlatform.mutationMode) ? dataPlatform.mutationMode : void 0,
165
+ selectionPlan: isRecord(dataPlatform.selectionPlan) ? dataPlatform.selectionPlan : void 0,
166
+ traceContext: isRecord(dataPlatform.traceContext) ? dataPlatform.traceContext : void 0,
167
+ requireTraceContext: true === dataPlatform.requireTraceContext
168
+ });
169
+ const headerName = stringOrUndefined(dataPlatform.envelopeHeader) || DEFAULT_DATA_ENVELOPE_HEADER;
170
+ const headers = isRecord(normalizedRequest.headers) ? {
171
+ ...normalizedRequest.headers
172
+ } : {};
173
+ if (false === dataPlatform.batch) headers[DEFAULT_DATA_BATCH_HEADER] = 'off';
174
+ headers[headerName] = encodeRequestEnvelopeHeader(envelope);
175
+ return {
176
+ ...normalizedRequest,
177
+ headers
178
+ };
179
+ } catch (error) {
180
+ if (strictEnvelope) throw error;
181
+ return normalizedRequest;
182
+ }
183
+ };
184
+ const client = {};
185
+ const operationManifest = {};
186
+ for (const endpoint of manifest.endpoints){
187
+ const operationId = `${endpoint.method}:${endpoint.routePath}`;
188
+ const operation = {
189
+ appNamespace: config.appNamespace,
190
+ apiId: endpoint.apiId,
191
+ group: endpoint.group,
192
+ endpoint: endpoint.endpoint,
193
+ operationId,
194
+ routePath: endpoint.routePath,
195
+ method: endpoint.method,
196
+ operationVersion: endpoint.operationVersion,
197
+ schemaHash: endpoint.schemaHash,
198
+ version: endpoint.operationVersion
199
+ };
200
+ const sender = createRequest({
201
+ path: endpoint.routePath,
202
+ method: endpoint.method,
203
+ port,
204
+ operationContext: {
205
+ operationId,
206
+ routePath: endpoint.routePath,
207
+ method: endpoint.method,
208
+ schemaHash: endpoint.schemaHash,
209
+ operationVersion: endpoint.operationVersion
210
+ },
211
+ httpMethodDecider,
212
+ ...config.requestId ? {
213
+ requestId: config.requestId
214
+ } : {}
215
+ });
216
+ const call = (request = {})=>sender(prepareEffectRequest(endpoint, operation, request));
217
+ client[endpoint.group] ??= {};
218
+ client[endpoint.group][endpoint.endpoint] = call;
219
+ operationManifest[endpoint.group] ??= {};
220
+ operationManifest[endpoint.group][endpoint.endpoint] = operation;
221
+ }
222
+ return {
223
+ client,
224
+ operationManifest,
225
+ createEffectRequestContext
226
+ };
227
+ };
228
+ export { createGeneratedEffectClient };