@bleedingdev/modern-js-plugin-bff 3.2.0-ultramodern.99 → 3.4.0-ultramodern.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.
Files changed (72) hide show
  1. package/dist/cjs/cli.js +9 -5
  2. package/dist/cjs/constants.js +13 -9
  3. package/dist/cjs/index.js +9 -5
  4. package/dist/cjs/loader.js +9 -5
  5. package/dist/cjs/runtime/create-request/index.js +9 -5
  6. package/dist/cjs/runtime/data-platform/index.js +11 -18
  7. package/dist/cjs/runtime/effect/adapter.js +87 -14
  8. package/dist/cjs/runtime/effect/context.js +9 -5
  9. package/dist/cjs/runtime/effect/edge.js +26 -26
  10. package/dist/cjs/runtime/effect/endpoint-contracts.js +130 -0
  11. package/dist/cjs/runtime/effect/handler.js +107 -63
  12. package/dist/cjs/runtime/effect/index.js +28 -16
  13. package/dist/cjs/runtime/effect/module.js +71 -35
  14. package/dist/cjs/runtime/effect/operation-context.js +10 -18
  15. package/dist/cjs/runtime/effect-client/index.js +10 -6
  16. package/dist/cjs/runtime/effect-client/runtime.js +266 -0
  17. package/dist/cjs/runtime/hono/adapter.js +30 -14
  18. package/dist/cjs/runtime/hono/index.js +9 -5
  19. package/dist/cjs/runtime/hono/operators.js +9 -5
  20. package/dist/cjs/runtime/safe-failure.js +83 -0
  21. package/dist/cjs/server.js +9 -5
  22. package/dist/cjs/utils/clientGenerator.js +13 -9
  23. package/dist/cjs/utils/createHonoRoutes.js +9 -5
  24. package/dist/cjs/utils/crossProjectApiPlugin.js +9 -5
  25. package/dist/cjs/utils/crossProjectServerPolicy.js +104 -0
  26. package/dist/cjs/utils/effectClientGenerator.js +99 -488
  27. package/dist/cjs/utils/pluginGenerator.js +9 -5
  28. package/dist/cjs/utils/runtimeGenerator.js +9 -5
  29. package/dist/esm/runtime/data-platform/index.mjs +2 -13
  30. package/dist/esm/runtime/effect/adapter.mjs +78 -9
  31. package/dist/esm/runtime/effect/edge.mjs +2 -9
  32. package/dist/esm/runtime/effect/endpoint-contracts.mjs +68 -0
  33. package/dist/esm/runtime/effect/handler.mjs +41 -8
  34. package/dist/esm/runtime/effect/index.mjs +2 -0
  35. package/dist/esm/runtime/effect/module.mjs +63 -31
  36. package/dist/esm/runtime/effect/operation-context.mjs +1 -13
  37. package/dist/esm/runtime/effect-client/index.mjs +1 -1
  38. package/dist/esm/runtime/effect-client/runtime.mjs +228 -0
  39. package/dist/esm/runtime/hono/adapter.mjs +21 -9
  40. package/dist/esm/runtime/safe-failure.mjs +45 -0
  41. package/dist/esm/utils/clientGenerator.mjs +5 -5
  42. package/dist/esm/utils/crossProjectServerPolicy.mjs +50 -0
  43. package/dist/esm/utils/effectClientGenerator.mjs +88 -484
  44. package/dist/esm-node/runtime/data-platform/index.mjs +2 -13
  45. package/dist/esm-node/runtime/effect/adapter.mjs +78 -9
  46. package/dist/esm-node/runtime/effect/edge.mjs +2 -9
  47. package/dist/esm-node/runtime/effect/endpoint-contracts.mjs +69 -0
  48. package/dist/esm-node/runtime/effect/handler.mjs +41 -8
  49. package/dist/esm-node/runtime/effect/index.mjs +2 -0
  50. package/dist/esm-node/runtime/effect/module.mjs +63 -31
  51. package/dist/esm-node/runtime/effect/operation-context.mjs +1 -13
  52. package/dist/esm-node/runtime/effect-client/index.mjs +1 -1
  53. package/dist/esm-node/runtime/effect-client/runtime.mjs +229 -0
  54. package/dist/esm-node/runtime/hono/adapter.mjs +21 -9
  55. package/dist/esm-node/runtime/safe-failure.mjs +46 -0
  56. package/dist/esm-node/utils/clientGenerator.mjs +5 -5
  57. package/dist/esm-node/utils/crossProjectServerPolicy.mjs +52 -0
  58. package/dist/esm-node/utils/effectClientGenerator.mjs +88 -484
  59. package/dist/types/runtime/effect/adapter.d.ts +25 -0
  60. package/dist/types/runtime/effect/context.d.ts +1 -1
  61. package/dist/types/runtime/effect/endpoint-contracts.d.ts +62 -0
  62. package/dist/types/runtime/effect/handler.d.ts +37 -4
  63. package/dist/types/runtime/effect/index.d.ts +1 -0
  64. package/dist/types/runtime/effect/module.d.ts +22 -2
  65. package/dist/types/runtime/effect-client/runtime.d.ts +71 -0
  66. package/dist/types/runtime/hono/adapter.d.ts +3 -0
  67. package/dist/types/runtime/safe-failure.d.ts +1 -0
  68. package/dist/types/server.d.ts +1 -1
  69. package/dist/types/utils/createHonoRoutes.d.ts +3 -3
  70. package/dist/types/utils/crossProjectServerPolicy.d.ts +35 -0
  71. package/dist/types/utils/effectClientGenerator.d.ts +16 -2
  72. package/package.json +40 -27
@@ -1,4 +1,5 @@
1
1
  import "node:module";
2
+ import { parseTraceparent } from "@modern-js/create-request";
2
3
  import { trace as api_trace } from "@opentelemetry/api";
3
4
  const DATA_BATCH_TRANSPORT_OTEL_EVENT = 'modernjs.data.batch';
4
5
  function createDataBatchTransportTelemetryAttributes(event) {
@@ -24,7 +25,6 @@ function emitDataBatchTransportEvent(onEvent, event) {
24
25
  const DEFAULT_DATA_ENVELOPE_HEADER = 'x-modernjs-data-envelope';
25
26
  const DEFAULT_DATA_BATCH_ENDPOINT = '/_data/batch';
26
27
  const DEFAULT_DATA_BATCH_HEADER = 'x-modernjs-data-batch';
27
- const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
28
28
  function isPlainObject(value) {
29
29
  if ('object' != typeof value || null === value || Array.isArray(value)) return false;
30
30
  const proto = Object.getPrototypeOf(value);
@@ -121,18 +121,7 @@ function isValidHex(value, length) {
121
121
  return value.length === length && /^[0-9a-f]+$/.test(value);
122
122
  }
123
123
  function parseTraceparentHeader(header) {
124
- const match = header.trim().match(TRACEPARENT_REGEX);
125
- if (!match) return null;
126
- const traceId = match[1].toLowerCase();
127
- const spanId = match[2].toLowerCase();
128
- const flags = match[3].toLowerCase();
129
- if (isAllZeroHex(traceId) || isAllZeroHex(spanId)) return null;
130
- const sampled = (0x1 & Number.parseInt(flags, 16)) === 1;
131
- return {
132
- traceId,
133
- spanId,
134
- sampled
135
- };
124
+ return parseTraceparent(header) ?? null;
136
125
  }
137
126
  function formatTraceparentHeader(trace) {
138
127
  const traceId = trace.traceId.toLowerCase();
@@ -1,8 +1,13 @@
1
1
  import __rslib_shim_module__ from "node:module";
2
2
  const require = /*#__PURE__*/ __rslib_shim_module__.createRequire(/*#__PURE__*/ (()=>import.meta.url)());
3
+ import { ApiRouter } from "@modern-js/bff-core";
3
4
  import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
5
+ import { HttpApi } from "effect/unstable/httpapi";
4
6
  import path from "path";
7
+ import { checkCrossProjectPolicyForRequest, resolveAdapterCrossProjectPolicy } from "../../utils/crossProjectServerPolicy.mjs";
8
+ import { createSafeFailureResponse } from "../safe-failure.mjs";
5
9
  import { createEffectOperationContext, runWithEffectContext } from "./context.mjs";
10
+ import { collectEffectEndpoints, extractHttpApiFromModule, toOperationContractSources } from "./endpoint-contracts.mjs";
6
11
  import { resolveEffectBffModuleHandler } from "./module.mjs";
7
12
  const before = [
8
13
  'custom-server-hook',
@@ -48,10 +53,73 @@ class EffectAdapter {
48
53
  const entryWithoutExt = configuredEntry ? path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry) : defaultEntry;
49
54
  return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
50
55
  }
56
+ isApiRequestPath(requestPath, prefix, enableHandleWeb) {
57
+ if (!enableHandleWeb) return true;
58
+ const normalized = normalizePrefix(prefix);
59
+ if (!normalized) return true;
60
+ return requestPath === normalized || requestPath.startsWith(`${normalized}/`);
61
+ }
62
+ async collectLambdaContractSources() {
63
+ try {
64
+ const serverContext = this.api.getServerContext();
65
+ const appDir = serverContext.distDirectory || serverContext.appDirectory;
66
+ if (!appDir) return [];
67
+ const apiDir = 'string' == typeof serverContext.apiDirectory ? serverContext.apiDirectory : path.resolve(appDir, API_DIR);
68
+ const lambdaDir = 'string' == typeof serverContext.lambdaDirectory ? serverContext.lambdaDirectory : path.join(apiDir, 'lambda');
69
+ if (!await fs.pathExists(lambdaDir)) return [];
70
+ const apiRouter = new ApiRouter({
71
+ appDir,
72
+ apiDir,
73
+ lambdaDir,
74
+ prefix: this.prefix,
75
+ httpMethodDecider: this.api.getServerConfig()?.bff?.httpMethodDecider
76
+ });
77
+ const handlerInfos = await apiRouter.getApiHandlers();
78
+ return handlerInfos.map((info)=>({
79
+ name: info.name,
80
+ httpMethod: info.httpMethod,
81
+ routePath: info.routePath,
82
+ filename: info.filename,
83
+ handler: info.handler
84
+ }));
85
+ } catch (error) {
86
+ logger.warn(`[BFF][Effect] Failed to derive lambda operation contracts for the cross-project policy: ${String(error)}`);
87
+ return [];
88
+ }
89
+ }
90
+ async refreshCrossProjectPolicy(mod) {
91
+ let contractSources = [];
92
+ if (mod) try {
93
+ const api = await extractHttpApiFromModule(mod, HttpApi.isHttpApi);
94
+ if (api) {
95
+ const reflect = (apiValue, handlers)=>HttpApi.reflect(apiValue, {
96
+ onGroup: handlers.onGroup ?? (()=>{}),
97
+ onEndpoint: handlers.onEndpoint
98
+ });
99
+ contractSources = toOperationContractSources(collectEffectEndpoints(reflect, api, this.prefix));
100
+ }
101
+ } catch (error) {
102
+ logger.warn(`[BFF][Effect] Failed to reflect HttpApi endpoints for the cross-project policy: ${String(error)}`);
103
+ }
104
+ let policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
105
+ if (policy?.enabled) {
106
+ const lambdaSources = await this.collectLambdaContractSources();
107
+ if (lambdaSources.length > 0) {
108
+ contractSources = [
109
+ ...contractSources,
110
+ ...lambdaSources
111
+ ];
112
+ policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
113
+ }
114
+ }
115
+ this.crossProjectPolicy = policy;
116
+ 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).');
117
+ }
51
118
  async loadEffectHandlerFromModule(mod) {
52
119
  return resolveEffectBffModuleHandler(mod, {
53
120
  openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
54
121
  dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform,
122
+ validateRequest: (request)=>checkCrossProjectPolicyForRequest(request, this.crossProjectPolicy),
55
123
  onWarning: (message)=>{
56
124
  logger.warn(message);
57
125
  }
@@ -81,6 +149,7 @@ class EffectAdapter {
81
149
  this.handler = null;
82
150
  return;
83
151
  }
152
+ await this.refreshCrossProjectPolicy(mod);
84
153
  const loaded = await this.loadEffectHandlerFromModule(mod);
85
154
  if (!loaded) {
86
155
  logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer } or handler.`);
@@ -89,6 +158,7 @@ class EffectAdapter {
89
158
  }
90
159
  this.handler = loaded.handler;
91
160
  this.dispose = loaded.dispose || null;
161
+ this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
92
162
  }
93
163
  async disposeCurrentHandler() {
94
164
  if (!this.dispose) return;
@@ -112,15 +182,7 @@ class EffectAdapter {
112
182
  } catch (configError) {
113
183
  logger.error(`Error in serverConfig.onError handler: ${configError}`);
114
184
  }
115
- const status = 'object' == typeof error && null !== error && 'status' in error && 'number' == typeof error.status ? error.status : 500;
116
- return new Response(JSON.stringify({
117
- message: error instanceof Error ? error.message : '[BFF] Internal Server Error'
118
- }), {
119
- status,
120
- headers: {
121
- 'content-type': 'application/json; charset=utf-8'
122
- }
123
- });
185
+ return createSafeFailureResponse(error);
124
186
  }
125
187
  ensureJsonContext(c) {
126
188
  const maybeJsonContext = c;
@@ -147,6 +209,8 @@ class EffectAdapter {
147
209
  this.effectMiddleware = null;
148
210
  this.handler = null;
149
211
  this.dispose = null;
212
+ this.prefix = '/api';
213
+ this.policyEnforcedInMiddleware = false;
150
214
  this.registerMiddleware = async (options)=>{
151
215
  const { prefix, enableHandleWeb } = options;
152
216
  const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
@@ -154,6 +218,7 @@ class EffectAdapter {
154
218
  this.isEffect = false;
155
219
  return;
156
220
  }
221
+ this.prefix = prefix || this.prefix;
157
222
  await this.reloadHandler();
158
223
  this.effectMiddleware = {
159
224
  name: 'effect-bff-handler',
@@ -166,6 +231,10 @@ class EffectAdapter {
166
231
  if (enableHandleWeb) return void await next();
167
232
  return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/effect/index or configure bff.effect.entry.'), c);
168
233
  }
234
+ if (this.crossProjectPolicy?.enabled && this.policyEnforcedInMiddleware && this.isApiRequestPath(c.req.path, prefix, enableHandleWeb)) {
235
+ const denial = checkCrossProjectPolicyForRequest(c.req.raw, this.crossProjectPolicy);
236
+ if (denial) return denial;
237
+ }
169
238
  let response;
170
239
  try {
171
240
  const effectRequest = createRequestForMountedPrefix(c.req.raw, prefix);
@@ -1,4 +1,5 @@
1
1
  import "node:module";
2
+ import { createSafeFailureResponse } from "../safe-failure.mjs";
2
3
  import { resolveEffectBffModuleHandler } from "./module.mjs";
3
4
  import { createEffectOperationContext } from "./operation-context.mjs";
4
5
  export * from "./handler.mjs";
@@ -40,15 +41,7 @@ function createEdgeEffectContext(originalRequest, effectRequest, options) {
40
41
  };
41
42
  }
42
43
  function createRuntimeErrorResponse(error) {
43
- const status = 'object' == typeof error && null !== error && 'status' in error && 'number' == typeof error.status ? error.status : 500;
44
- return new Response(JSON.stringify({
45
- message: error instanceof Error ? error.message : '[BFF] Internal Server Error'
46
- }), {
47
- status,
48
- headers: {
49
- 'content-type': 'application/json; charset=utf-8'
50
- }
51
- });
44
+ return createSafeFailureResponse(error);
52
45
  }
53
46
  async function dispatchEffectBffRequest(handler, request, options = {}) {
54
47
  const requestPathname = new URL(request.url).pathname;
@@ -0,0 +1,69 @@
1
+ import "node:module";
2
+ import { createOperationContractHash } from "@modern-js/bff-core";
3
+ function ensureLeadingSlash(pathname) {
4
+ return pathname.startsWith('/') ? pathname : `/${pathname}`;
5
+ }
6
+ function normalizeEffectPrefix(prefix) {
7
+ if ('/' === prefix) return '';
8
+ return ensureLeadingSlash(prefix || '/api');
9
+ }
10
+ function getEffectRoutePath(prefix, endpointPath) {
11
+ const normalizedPrefix = normalizeEffectPrefix(prefix);
12
+ const normalizedEndpointPath = ensureLeadingSlash(endpointPath);
13
+ const finalEndpointPath = '/' === normalizedEndpointPath ? '' : endpointPath;
14
+ if (!normalizedPrefix && !finalEndpointPath) return '/';
15
+ return `${normalizedPrefix}${finalEndpointPath || ''}`;
16
+ }
17
+ function resolveEffectApiId(api) {
18
+ const fallback = 'EffectHttpApi';
19
+ if ('identifier' in api && 'string' == typeof api.identifier && api.identifier) return api.identifier;
20
+ return fallback;
21
+ }
22
+ function collectEffectEndpoints(reflect, api, prefix) {
23
+ const endpoints = [];
24
+ const apiId = resolveEffectApiId(api);
25
+ reflect(api, {
26
+ onGroup: ()=>{},
27
+ onEndpoint: ({ group, endpoint })=>{
28
+ endpoints.push({
29
+ apiId,
30
+ groupName: String(group.identifier),
31
+ endpointName: String(endpoint.name),
32
+ method: String(endpoint.method).toUpperCase(),
33
+ routePath: getEffectRoutePath(prefix, String(endpoint.path))
34
+ });
35
+ }
36
+ });
37
+ return endpoints.sort((a, b)=>{
38
+ if (a.groupName === b.groupName) return a.endpointName.localeCompare(b.endpointName);
39
+ return a.groupName.localeCompare(b.groupName);
40
+ });
41
+ }
42
+ function toOperationContractSources(endpoints) {
43
+ return endpoints.map(createEffectOperationContractSource);
44
+ }
45
+ function createEffectOperationContractSource(endpoint) {
46
+ return {
47
+ name: endpoint.endpointName,
48
+ httpMethod: endpoint.method,
49
+ routePath: endpoint.routePath
50
+ };
51
+ }
52
+ function isRecord(value) {
53
+ return 'object' == typeof value && null !== value;
54
+ }
55
+ async function extractHttpApiFromModule(mod, isHttpApi) {
56
+ if (!isRecord(mod)) return null;
57
+ if (isHttpApi(mod.api)) return mod.api;
58
+ const entry = mod.default;
59
+ if (isRecord(entry) && isHttpApi(entry.api)) return entry.api;
60
+ if ('function' == typeof entry && 0 === entry.length) {
61
+ const output = await entry();
62
+ if (isRecord(output) && isHttpApi(output.api)) return output.api;
63
+ }
64
+ return null;
65
+ }
66
+ function createEffectEndpointContractHash(endpoint, requestId) {
67
+ return createOperationContractHash(createEffectOperationContractSource(endpoint), requestId);
68
+ }
69
+ export { collectEffectEndpoints, createEffectEndpointContractHash, createEffectOperationContractSource, ensureLeadingSlash, extractHttpApiFromModule, getEffectRoutePath, normalizeEffectPrefix, resolveEffectApiId, toOperationContractSources };
@@ -1,10 +1,9 @@
1
1
  import "node:module";
2
2
  import * as __rspack_external_effect_Layer_16f7a8fc from "effect/Layer";
3
- import { HttpRouter, HttpServerResponse, HttpTraceContext } from "effect/unstable/http";
3
+ import { HttpRouter, HttpServer, HttpServerResponse, HttpTraceContext } from "effect/unstable/http";
4
4
  import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi";
5
5
  import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
6
6
  import { DEFAULT_DATA_BATCH_ENDPOINT, DEFAULT_DATA_BATCH_HEADER, DEFAULT_DATA_ENVELOPE_HEADER, decodeRequestEnvelopeHeader, validateRequestEnvelope, validateSelectionPlan } from "../data-platform/index.mjs";
7
- import * as __rspack_external__effect_opentelemetry_8bbbb5af from "@effect/opentelemetry";
8
7
  import * as __rspack_external_effect_Config_29be8a92 from "effect/Config";
9
8
  import * as __rspack_external_effect_Effect_194ac36c from "effect/Effect";
10
9
  import * as __rspack_external_effect_Option_4d691636 from "effect/Option";
@@ -12,6 +11,12 @@ import * as __rspack_external_effect_Schema_f8472650 from "effect/Schema";
12
11
  export * from "effect/unstable/http";
13
12
  export * from "effect/unstable/httpapi";
14
13
  export * from "effect/unstable/rpc";
14
+ import * as __rspack_external_effect_Context_f1289ca3 from "effect/Context";
15
+ const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
16
+ const EFFECT_VALIDATOR_AWARE_FACTORY = Symbol.for('modernjs.effect.validatorAware');
17
+ function isValidatorAwareHandlerFactory(factory) {
18
+ return 'function' == typeof factory && true === factory[EFFECT_VALIDATOR_AWARE_FACTORY];
19
+ }
15
20
  function normalizeOpenApiPath(pathname) {
16
21
  if (!pathname.startsWith('/')) return `/${pathname}`;
17
22
  return pathname;
@@ -267,16 +272,38 @@ function defineEffectBff(definition) {
267
272
  layer: definition.layer,
268
273
  openapi: options?.openapi,
269
274
  rpc: mergedRpcOptions,
270
- dataPlatform: mergeDataPlatformOptions(definition.dataPlatform, options?.dataPlatform)
275
+ dataPlatform: mergeDataPlatformOptions(definition.dataPlatform, options?.dataPlatform),
276
+ validateRequest: options?.validateRequest
271
277
  });
272
278
  };
273
- const client = void 0;
279
+ Object.defineProperty(createHandler, EFFECT_VALIDATOR_AWARE_FACTORY, {
280
+ value: true
281
+ });
282
+ const client = createLoaderMaterializedClientPlaceholder();
274
283
  return {
275
284
  ...definition,
276
285
  createHandler,
277
286
  client
278
287
  };
279
288
  }
289
+ const LOADER_CLIENT_IGNORED_KEYS = new Set([
290
+ 'then',
291
+ 'catch',
292
+ 'finally',
293
+ 'toJSON',
294
+ '$$typeof'
295
+ ]);
296
+ function createLoaderMaterializedClientPlaceholder() {
297
+ const explain = (property)=>{
298
+ 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.`);
299
+ };
300
+ return new Proxy(Object.create(null), {
301
+ get (_target, property) {
302
+ if ('symbol' == typeof property || LOADER_CLIENT_IGNORED_KEYS.has(property)) return;
303
+ return explain(property);
304
+ }
305
+ });
306
+ }
280
307
  function defineEffectRpcBff(definition) {
281
308
  const createHandler = (options)=>createRpcApiHandler({
282
309
  ...definition,
@@ -288,7 +315,7 @@ function defineEffectRpcBff(definition) {
288
315
  };
289
316
  }
290
317
  function createHttpApiHandler(options) {
291
- const apiLayer = options.layer;
318
+ const apiLayer = options.layer.pipe(__rspack_external_effect_Layer_16f7a8fc.provide(HttpServer.layerServices));
292
319
  const openApiLayer = createOpenApiLayer(options.api, options.openapi);
293
320
  const mergedLayer = openApiLayer ? __rspack_external_effect_Layer_16f7a8fc.mergeAll(apiLayer, openApiLayer) : apiLayer;
294
321
  const httpApiHandler = HttpRouter.toWebHandler(mergedLayer);
@@ -303,9 +330,11 @@ function createHttpApiHandler(options) {
303
330
  const envelopeHeader = options.dataPlatform?.envelopeHeader || DEFAULT_DATA_ENVELOPE_HEADER;
304
331
  const normalizedEnvelopeHeader = envelopeHeader.toLowerCase();
305
332
  const withDataPlatformValidation = async (request, context)=>{
333
+ const policyDenial = options.validateRequest?.(request);
334
+ if (policyDenial) return policyDenial;
306
335
  const validationError = validateDataPlatformRequestEnvelope(request, options.dataPlatform);
307
336
  if (validationError) return validationError;
308
- return httpApiHandler.handler(request, context);
337
+ return httpApiHandler.handler(request, context ?? emptyEffectServiceContext);
309
338
  };
310
339
  const handleBatchRequest = async (request, context)=>{
311
340
  const mountedPrefix = getMountedPrefixFromContext(request, context);
@@ -424,7 +453,11 @@ function createHttpApiHandler(options) {
424
453
  const rpcHandler = createRpcApiHandler(options.rpc);
425
454
  return {
426
455
  handler: async (request, context)=>{
427
- if (isRpcRequest(request, rpcPath)) return rpcHandler.handler(request, context);
456
+ if (isRpcRequest(request, rpcPath)) {
457
+ const policyDenial = options.validateRequest?.(request);
458
+ if (policyDenial) return policyDenial;
459
+ return rpcHandler.handler(request, context ?? emptyEffectServiceContext);
460
+ }
428
461
  return handleHttpApiRequest(request);
429
462
  },
430
463
  dispose: async ()=>{
@@ -435,4 +468,4 @@ function createHttpApiHandler(options) {
435
468
  }
436
469
  };
437
470
  }
438
- export { HttpApiBuilder, HttpTraceContext, __rspack_external__effect_opentelemetry_8bbbb5af as OpenTelemetry, __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 };
471
+ 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,3 +1,5 @@
1
1
  import "node:module";
2
+ import * as __rspack_external__effect_opentelemetry_8bbbb5af from "@effect/opentelemetry";
2
3
  export * from "./handler.mjs";
3
4
  export { createEffectOperationContext, useEffectContext, useOperationContext } from "./context.mjs";
5
+ export { __rspack_external__effect_opentelemetry_8bbbb5af as OpenTelemetry };
@@ -1,6 +1,7 @@
1
1
  import "node:module";
2
2
  import { HttpApi } from "effect/unstable/httpapi";
3
- import { createHttpApiHandler } from "./handler.mjs";
3
+ import { createHttpApiHandler, isValidatorAwareHandlerFactory } from "./handler.mjs";
4
+ import * as __rspack_external_effect_Context_f1289ca3 from "effect/Context";
4
5
  function isRecord(value) {
5
6
  return 'object' == typeof value && null !== value;
6
7
  }
@@ -13,15 +14,33 @@ function isRequestHandler(value) {
13
14
  function isEffectApiDefinition(module) {
14
15
  return HttpApi.isHttpApi(module.api) && void 0 !== module.layer;
15
16
  }
16
- async function resolveEffectBffModuleHandler(mod, options = {}) {
17
- let normalizedModule = mod;
18
- const mergeRuntimeExports = (value)=>{
19
- if (!isRecord(value) || !includesRuntimeExports(value)) return;
20
- normalizedModule = {
21
- ...normalizedModule,
22
- ...value
23
- };
17
+ function isEffectServiceContext(context) {
18
+ return 'object' == typeof context && null !== context && 'mapUnsafe' in context;
19
+ }
20
+ const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
21
+ function callEffectBffRequestHandler(handler, request, context) {
22
+ return void 0 === context ? handler(request) : handler(request, context);
23
+ }
24
+ function createLoadedHandler(webHandler, appliesRequestValidator) {
25
+ return {
26
+ handler: (request, context)=>callEffectBffRequestHandler(webHandler.handler, request, context),
27
+ dispose: webHandler.dispose,
28
+ ...appliesRequestValidator ? {
29
+ appliesRequestValidator: true
30
+ } : {}
24
31
  };
32
+ }
33
+ function createLoadedHttpApiHandler(webHandler) {
34
+ return {
35
+ handler: (request, context)=>{
36
+ const effectContext = isEffectServiceContext(context) ? context : emptyEffectServiceContext;
37
+ return webHandler.handler(request, effectContext);
38
+ },
39
+ dispose: webHandler.dispose,
40
+ appliesRequestValidator: true
41
+ };
42
+ }
43
+ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {}) {
25
44
  if (isRequestHandler(normalizedModule.handler)) return {
26
45
  handler: normalizedModule.handler
27
46
  };
@@ -29,13 +48,6 @@ async function resolveEffectBffModuleHandler(mod, options = {}) {
29
48
  if (isRequestHandler(entry)) return {
30
49
  handler: entry
31
50
  };
32
- if ('function' == typeof entry && 0 === entry.length) {
33
- const out = await entry();
34
- if (isRequestHandler(out)) return {
35
- handler: out
36
- };
37
- mergeRuntimeExports(out);
38
- }
39
51
  if (isRecord(entry)) normalizedModule = {
40
52
  ...normalizedModule,
41
53
  ...entry
@@ -51,16 +63,15 @@ async function resolveEffectBffModuleHandler(mod, options = {}) {
51
63
  handler: normalizedModule.handler
52
64
  };
53
65
  if ('function' == typeof normalizedModule.createHandler) {
54
- const webHandler = normalizedModule.createHandler({
66
+ const factory = normalizedModule.createHandler;
67
+ const validatorAware = isValidatorAwareHandlerFactory(factory);
68
+ 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.');
69
+ const webHandler = factory({
55
70
  openapi: options.openapi,
56
- dataPlatform: options.dataPlatform
71
+ dataPlatform: options.dataPlatform,
72
+ validateRequest: options.validateRequest
57
73
  });
58
- return {
59
- handler: async (request, context)=>webHandler.handler(request, context),
60
- dispose: async ()=>{
61
- await webHandler.dispose();
62
- }
63
- };
74
+ return createLoadedHandler(webHandler, validatorAware);
64
75
  }
65
76
  if (isEffectApiDefinition(normalizedModule)) {
66
77
  options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
@@ -68,15 +79,36 @@ async function resolveEffectBffModuleHandler(mod, options = {}) {
68
79
  api: normalizedModule.api,
69
80
  layer: normalizedModule.layer,
70
81
  openapi: options.openapi,
71
- dataPlatform: options.dataPlatform
82
+ dataPlatform: options.dataPlatform,
83
+ validateRequest: options.validateRequest
72
84
  });
73
- return {
74
- handler: async (request, context)=>webHandler.handler(request, context),
75
- dispose: async ()=>{
76
- await webHandler.dispose();
77
- }
78
- };
85
+ return createLoadedHttpApiHandler(webHandler);
79
86
  }
80
87
  return null;
81
88
  }
89
+ function resolveEffectBffModuleHandler(mod, options = {}) {
90
+ let normalizedModule = mod;
91
+ const mergeRuntimeExports = (value)=>{
92
+ if (!isRecord(value) || !includesRuntimeExports(value)) return;
93
+ normalizedModule = {
94
+ ...normalizedModule,
95
+ ...value
96
+ };
97
+ };
98
+ if (isRequestHandler(normalizedModule.handler)) return Promise.resolve({
99
+ handler: normalizedModule.handler
100
+ });
101
+ const entry = normalizedModule.default;
102
+ if (isRequestHandler(entry)) return Promise.resolve({
103
+ handler: entry
104
+ });
105
+ if ('function' == typeof entry && 0 === entry.length) return Promise.resolve(entry()).then((out)=>{
106
+ if (isRequestHandler(out)) return {
107
+ handler: out
108
+ };
109
+ mergeRuntimeExports(out);
110
+ return resolveNormalizedEffectBffModuleHandler(normalizedModule, options);
111
+ });
112
+ return Promise.resolve(resolveNormalizedEffectBffModuleHandler(normalizedModule, options));
113
+ }
82
114
  export { resolveEffectBffModuleHandler };
@@ -1,6 +1,5 @@
1
1
  import "node:module";
2
- import { BFF_LOCALE_HEADER, BFF_OPERATION_CONTEXT_DETAIL_HEADER, BFF_OPERATION_CONTEXT_HEADER, BFF_TRACEPARENT_HEADER } from "@modern-js/create-request";
3
- const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/i;
2
+ import { BFF_LOCALE_HEADER, BFF_OPERATION_CONTEXT_DETAIL_HEADER, BFF_OPERATION_CONTEXT_HEADER, BFF_TRACEPARENT_HEADER, parseTraceparent } from "@modern-js/create-request";
4
3
  const readHeader = (headers, header)=>{
5
4
  const value = headers.get(header);
6
5
  return value && value.length > 0 ? value : void 0;
@@ -9,17 +8,6 @@ const copyStringField = (target, details, key)=>{
9
8
  const value = details[key];
10
9
  if ('string' == typeof value && value.length > 0) target[key] = value;
11
10
  };
12
- const parseTraceparent = (traceparent)=>{
13
- if (!traceparent) return;
14
- const match = traceparent.trim().match(TRACEPARENT_REGEX);
15
- if (!match) return;
16
- const [, traceId, spanId] = match;
17
- if (!traceId || !spanId) return;
18
- return {
19
- traceId: traceId.toLowerCase(),
20
- spanId: spanId.toLowerCase()
21
- };
22
- };
23
11
  const readOperationContextDetails = (request)=>{
24
12
  const rawDetails = readHeader(request.headers, BFF_OPERATION_CONTEXT_DETAIL_HEADER);
25
13
  if (!rawDetails) return {};
@@ -56,7 +56,7 @@ function makeEffectHttpApiClient(api, options) {
56
56
  for (const [header, value] of Object.entries(requestContextHeaders))if (void 0 === nextRequest.headers[header.toLowerCase()]) nextRequest = HttpClientRequest.setHeader(nextRequest, header, value);
57
57
  return nextRequest;
58
58
  }));
59
- return options?.transformClient ? options.transformClient(contextClient) : contextClient;
59
+ return 'function' == typeof options?.transformClient ? options.transformClient(contextClient) : contextClient;
60
60
  };
61
61
  return HttpApiClient.make(api, {
62
62
  baseUrl: options?.baseUrl,