@bleedingdev/modern-js-plugin-bff 3.4.0-ultramodern.2 → 3.4.0-ultramodern.20

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.
@@ -39,7 +39,6 @@ __webpack_require__.r(__webpack_exports__);
39
39
  __webpack_require__.d(__webpack_exports__, {
40
40
  EffectAdapter: ()=>EffectAdapter
41
41
  });
42
- const bff_core_namespaceObject = require("@modern-js/bff-core");
43
42
  const utils_namespaceObject = require("@modern-js/utils");
44
43
  const httpapi_namespaceObject = require("effect/unstable/httpapi");
45
44
  const external_path_namespaceObject = require("path");
@@ -94,43 +93,12 @@ class EffectAdapter {
94
93
  const { appDirectory, apiDirectory } = this.api.getServerContext();
95
94
  const bffConfig = this.api.getServerConfig()?.bff;
96
95
  const configuredEntry = bffConfig?.effect?.entry;
97
- const defaultEntry = external_path_default().resolve(appDirectory || process.cwd(), apiDirectory || utils_namespaceObject.API_DIR, 'effect', 'index');
98
- const entryWithoutExt = configuredEntry ? external_path_default().isAbsolute(configuredEntry) ? configuredEntry : external_path_default().resolve(appDirectory || process.cwd(), configuredEntry) : defaultEntry;
99
- return resolveJsOrTsEntry(entryWithoutExt);
100
- }
101
- isApiRequestPath(requestPath, prefix, enableHandleWeb) {
102
- if (!enableHandleWeb) return true;
103
- const normalized = normalizePrefix(prefix);
104
- if (!normalized) return true;
105
- return requestPath === normalized || requestPath.startsWith(`${normalized}/`);
106
- }
107
- async collectLambdaContractSources() {
108
- try {
109
- const serverContext = this.api.getServerContext();
110
- const appDir = serverContext.distDirectory || serverContext.appDirectory;
111
- if (!appDir) return [];
112
- const apiDir = 'string' == typeof serverContext.apiDirectory ? serverContext.apiDirectory : external_path_default().resolve(appDir, utils_namespaceObject.API_DIR);
113
- const lambdaDir = 'string' == typeof serverContext.lambdaDirectory ? serverContext.lambdaDirectory : external_path_default().join(apiDir, 'lambda');
114
- if (!await utils_namespaceObject.fs.pathExists(lambdaDir)) return [];
115
- const apiRouter = new bff_core_namespaceObject.ApiRouter({
116
- appDir,
117
- apiDir,
118
- lambdaDir,
119
- prefix: this.prefix,
120
- httpMethodDecider: this.api.getServerConfig()?.bff?.httpMethodDecider
121
- });
122
- const handlerInfos = await apiRouter.getApiHandlers();
123
- return handlerInfos.map((info)=>({
124
- name: info.name,
125
- httpMethod: info.httpMethod,
126
- routePath: info.routePath,
127
- filename: info.filename,
128
- handler: info.handler
129
- }));
130
- } catch (error) {
131
- utils_namespaceObject.logger.warn(`[BFF][Effect] Failed to derive lambda operation contracts for the cross-project policy: ${String(error)}`);
132
- return [];
96
+ if (configuredEntry) {
97
+ const entryWithoutExt = external_path_default().isAbsolute(configuredEntry) ? configuredEntry : external_path_default().resolve(appDirectory || process.cwd(), configuredEntry);
98
+ return resolveJsOrTsEntry(entryWithoutExt);
133
99
  }
100
+ const apiRoot = external_path_default().resolve(appDirectory || process.cwd(), apiDirectory || utils_namespaceObject.API_DIR);
101
+ return resolveJsOrTsEntry(external_path_default().resolve(apiRoot, 'index'));
134
102
  }
135
103
  async refreshCrossProjectPolicy(mod) {
136
104
  let contractSources = [];
@@ -146,24 +114,15 @@ class EffectAdapter {
146
114
  } catch (error) {
147
115
  utils_namespaceObject.logger.warn(`[BFF][Effect] Failed to reflect HttpApi endpoints for the cross-project policy: ${String(error)}`);
148
116
  }
149
- let policy = (0, crossProjectServerPolicy_js_namespaceObject.resolveAdapterCrossProjectPolicy)(this.api, contractSources);
150
- if (policy?.enabled) {
151
- const lambdaSources = await this.collectLambdaContractSources();
152
- if (lambdaSources.length > 0) {
153
- contractSources = [
154
- ...contractSources,
155
- ...lambdaSources
156
- ];
157
- policy = (0, crossProjectServerPolicy_js_namespaceObject.resolveAdapterCrossProjectPolicy)(this.api, contractSources);
158
- }
159
- }
117
+ const policy = (0, crossProjectServerPolicy_js_namespaceObject.resolveAdapterCrossProjectPolicy)(this.api, contractSources);
160
118
  this.crossProjectPolicy = policy;
161
119
  if (this.crossProjectPolicy?.enabled && 0 === contractSources.length) utils_namespaceObject.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).');
162
120
  }
163
121
  async loadEffectHandlerFromModule(mod) {
122
+ const effectConfig = this.api.getServerConfig()?.bff?.effect;
164
123
  return (0, external_module_js_namespaceObject.resolveEffectBffModuleHandler)(mod, {
165
- openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
166
- dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform,
124
+ openapi: effectConfig?.openapi,
125
+ dataPlatform: effectConfig?.dataPlatform,
167
126
  validateRequest: (request)=>(0, crossProjectServerPolicy_js_namespaceObject.checkCrossProjectPolicyForRequest)(request, this.crossProjectPolicy),
168
127
  onWarning: (message)=>{
169
128
  utils_namespaceObject.logger.warn(message);
@@ -197,13 +156,12 @@ class EffectAdapter {
197
156
  await this.refreshCrossProjectPolicy(mod);
198
157
  const loaded = await this.loadEffectHandlerFromModule(mod);
199
158
  if (!loaded) {
200
- utils_namespaceObject.logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer } or handler.`);
159
+ utils_namespaceObject.logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export defineEffectBff(...) or a { api, layer } HttpApi module.`);
201
160
  this.handler = null;
202
161
  return;
203
162
  }
204
163
  this.handler = loaded.handler;
205
164
  this.dispose = loaded.dispose || null;
206
- this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
207
165
  }
208
166
  async disposeCurrentHandler() {
209
167
  if (!this.dispose) return;
@@ -255,7 +213,6 @@ class EffectAdapter {
255
213
  this.handler = null;
256
214
  this.dispose = null;
257
215
  this.prefix = '/api';
258
- this.policyEnforcedInMiddleware = false;
259
216
  this.registerMiddleware = async (options)=>{
260
217
  const { prefix, enableHandleWeb } = options;
261
218
  const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
@@ -266,7 +223,7 @@ class EffectAdapter {
266
223
  this.prefix = prefix || this.prefix;
267
224
  await this.reloadHandler();
268
225
  this.effectMiddleware = {
269
- name: 'effect-bff-handler',
226
+ name: 'effect-api-handler',
270
227
  path: enableHandleWeb ? '*' : `${prefix}/*`,
271
228
  method: 'all',
272
229
  order: 'post',
@@ -274,11 +231,7 @@ class EffectAdapter {
274
231
  handler: async (c, next)=>{
275
232
  if (!this.handler) {
276
233
  if (enableHandleWeb) return void await next();
277
- return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/effect/index or configure bff.effect.entry.'), c);
278
- }
279
- if (this.crossProjectPolicy?.enabled && this.policyEnforcedInMiddleware && this.isApiRequestPath(c.req.path, prefix, enableHandleWeb)) {
280
- const denial = (0, crossProjectServerPolicy_js_namespaceObject.checkCrossProjectPolicyForRequest)(c.req.raw, this.crossProjectPolicy);
281
- if (denial) return denial;
234
+ return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/index or configure bff.effect.entry.'), c);
282
235
  }
283
236
  let response;
284
237
  try {
@@ -138,7 +138,7 @@ var __webpack_exports__ = {};
138
138
  dataPlatform: options.dataPlatform,
139
139
  onWarning: options.onWarning
140
140
  });
141
- if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }, createHandler, or handler.');
141
+ if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export defineEffectBff(...) or a { api, layer } HttpApi module.');
142
142
  return {
143
143
  handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
144
144
  ...dispatchOptions,
@@ -202,6 +202,41 @@ var __webpack_exports__ = {};
202
202
  }
203
203
  });
204
204
  }
205
+ function createJsonValidationResponse(message, status = 400) {
206
+ return new Response(JSON.stringify({
207
+ message
208
+ }), {
209
+ status,
210
+ headers: {
211
+ 'content-type': 'application/json; charset=utf-8'
212
+ }
213
+ });
214
+ }
215
+ function cloneWithoutJsonBodyHeaders(request) {
216
+ const headers = new Headers(request.headers);
217
+ headers.delete('content-type');
218
+ headers.delete('content-length');
219
+ return new Request(request.url, {
220
+ method: request.method,
221
+ headers,
222
+ signal: request.signal
223
+ });
224
+ }
225
+ async function prepareJsonRequestBody(request) {
226
+ const method = normalizeItemMethod(request.method);
227
+ if ('GET' === method || 'HEAD' === method) return request;
228
+ const contentType = (request.headers.get('content-type') || '').toLowerCase();
229
+ if (!contentType.includes('application/json')) return request;
230
+ if (null === request.body) return cloneWithoutJsonBodyHeaders(request);
231
+ try {
232
+ const bodyText = await request.clone().text();
233
+ if ('' === bodyText) return cloneWithoutJsonBodyHeaders(request);
234
+ JSON.parse(bodyText);
235
+ } catch {
236
+ return createJsonValidationResponse('Invalid JSON request body');
237
+ }
238
+ return request;
239
+ }
205
240
  function toBatchItemError(id, status, message) {
206
241
  return {
207
242
  id,
@@ -422,7 +457,7 @@ var __webpack_exports__ = {};
422
457
  ]);
423
458
  function createLoaderMaterializedClientPlaceholder() {
424
459
  const explain = (property)=>{
425
- 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.`);
460
+ throw new Error(`[BFF][Effect] client.${String(property)} is not available here: the typed client only exists when the API entry is imported through the "@api/index" transformed path (the BFF loader replaces it with generated client code). On the server, use HttpApiClient or call the Effect layer directly.`);
426
461
  };
427
462
  return new Proxy(Object.create(null), {
428
463
  get (_target, property) {
@@ -459,9 +494,11 @@ var __webpack_exports__ = {};
459
494
  const withDataPlatformValidation = async (request, context)=>{
460
495
  const policyDenial = options.validateRequest?.(request);
461
496
  if (policyDenial) return policyDenial;
462
- const validationError = validateDataPlatformRequestEnvelope(request, options.dataPlatform);
497
+ const preparedRequest = await prepareJsonRequestBody(request);
498
+ if (preparedRequest instanceof Response) return preparedRequest;
499
+ const validationError = validateDataPlatformRequestEnvelope(preparedRequest, options.dataPlatform);
463
500
  if (validationError) return validationError;
464
- return httpApiHandler.handler(request, context ?? emptyEffectServiceContext);
501
+ return httpApiHandler.handler(preparedRequest, context ?? emptyEffectServiceContext);
465
502
  };
466
503
  const handleBatchRequest = async (request, context)=>{
467
504
  const mountedPrefix = getMountedPrefixFromContext(request, context);
@@ -49,16 +49,18 @@ function isEffectServiceContext(context) {
49
49
  return 'object' == typeof context && null !== context && 'mapUnsafe' in context;
50
50
  }
51
51
  const emptyEffectServiceContext = Context_namespaceObject.empty();
52
+ const strictEffectApproachMessage = '[BFF][Effect] strictEffectApproach is enforced: Effect API entries must export defineEffectBff(...) or a { api, layer } HttpApi module. Raw handler exports, default request handlers, and unbranded custom createHandler factories are not valid Effect API entries.';
53
+ function rejectLegacyEffectModuleShape(options, shape) {
54
+ options.onWarning?.(`${strictEffectApproachMessage} Rejected ${shape}.`);
55
+ return true;
56
+ }
52
57
  function callEffectBffRequestHandler(handler, request, context) {
53
58
  return void 0 === context ? handler(request) : handler(request, context);
54
59
  }
55
- function createLoadedHandler(webHandler, appliesRequestValidator) {
60
+ function createLoadedHandler(webHandler) {
56
61
  return {
57
62
  handler: (request, context)=>callEffectBffRequestHandler(webHandler.handler, request, context),
58
- dispose: webHandler.dispose,
59
- ...appliesRequestValidator ? {
60
- appliesRequestValidator: true
61
- } : {}
63
+ dispose: webHandler.dispose
62
64
  };
63
65
  }
64
66
  function createLoadedHttpApiHandler(webHandler) {
@@ -67,18 +69,23 @@ function createLoadedHttpApiHandler(webHandler) {
67
69
  const effectContext = isEffectServiceContext(context) ? context : emptyEffectServiceContext;
68
70
  return webHandler.handler(request, effectContext);
69
71
  },
70
- dispose: webHandler.dispose,
71
- appliesRequestValidator: true
72
+ dispose: webHandler.dispose
72
73
  };
73
74
  }
74
75
  function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {}) {
75
- if (isRequestHandler(normalizedModule.handler)) return {
76
- handler: normalizedModule.handler
77
- };
76
+ if (isRequestHandler(normalizedModule.handler)) {
77
+ if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
78
+ return {
79
+ handler: normalizedModule.handler
80
+ };
81
+ }
78
82
  const entry = normalizedModule.default;
79
- if (isRequestHandler(entry)) return {
80
- handler: entry
81
- };
83
+ if (isRequestHandler(entry)) {
84
+ if (rejectLegacyEffectModuleShape(options, 'default request handler')) return null;
85
+ return {
86
+ handler: entry
87
+ };
88
+ }
82
89
  if (isRecord(entry)) normalizedModule = {
83
90
  ...normalizedModule,
84
91
  ...entry
@@ -90,19 +97,22 @@ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {})
90
97
  handler: maybeHandler
91
98
  };
92
99
  }
93
- if (isRequestHandler(normalizedModule.handler)) return {
94
- handler: normalizedModule.handler
95
- };
100
+ if (isRequestHandler(normalizedModule.handler)) {
101
+ if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
102
+ return {
103
+ handler: normalizedModule.handler
104
+ };
105
+ }
96
106
  if ('function' == typeof normalizedModule.createHandler) {
97
107
  const factory = normalizedModule.createHandler;
98
108
  const validatorAware = (0, external_handler_js_namespaceObject.isValidatorAwareHandlerFactory)(factory);
99
- 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.');
109
+ if (!validatorAware && rejectLegacyEffectModuleShape(options, 'unbranded `createHandler` export')) return null;
100
110
  const webHandler = factory({
101
111
  openapi: options.openapi,
102
112
  dataPlatform: options.dataPlatform,
103
113
  validateRequest: options.validateRequest
104
114
  });
105
- return createLoadedHandler(webHandler, validatorAware);
115
+ return createLoadedHandler(webHandler);
106
116
  }
107
117
  if (isEffectApiDefinition(normalizedModule)) {
108
118
  options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
@@ -126,17 +136,26 @@ function resolveEffectBffModuleHandler(mod, options = {}) {
126
136
  ...value
127
137
  };
128
138
  };
129
- if (isRequestHandler(normalizedModule.handler)) return Promise.resolve({
130
- handler: normalizedModule.handler
131
- });
139
+ if (isRequestHandler(normalizedModule.handler)) {
140
+ if (rejectLegacyEffectModuleShape(options, '`handler` export')) return Promise.resolve(null);
141
+ return Promise.resolve({
142
+ handler: normalizedModule.handler
143
+ });
144
+ }
132
145
  const entry = normalizedModule.default;
133
- if (isRequestHandler(entry)) return Promise.resolve({
134
- handler: entry
135
- });
146
+ if (isRequestHandler(entry)) {
147
+ if (rejectLegacyEffectModuleShape(options, 'default request handler')) return Promise.resolve(null);
148
+ return Promise.resolve({
149
+ handler: entry
150
+ });
151
+ }
136
152
  if ('function' == typeof entry && 0 === entry.length) return Promise.resolve(entry()).then((out)=>{
137
- if (isRequestHandler(out)) return {
138
- handler: out
139
- };
153
+ if (isRequestHandler(out)) {
154
+ if (rejectLegacyEffectModuleShape(options, 'default factory returning a request handler')) return null;
155
+ return {
156
+ handler: out
157
+ };
158
+ }
140
159
  mergeRuntimeExports(out);
141
160
  return resolveNormalizedEffectBffModuleHandler(normalizedModule, options);
142
161
  });
@@ -35,6 +35,7 @@ __webpack_require__.d(__webpack_exports__, {
35
35
  HttpApiEndpoint: ()=>httpapi_namespaceObject.HttpApiEndpoint,
36
36
  HttpApiGroup: ()=>httpapi_namespaceObject.HttpApiGroup,
37
37
  HttpApiSchema: ()=>httpapi_namespaceObject.HttpApiSchema,
38
+ HttpClientError: ()=>HttpClientError_namespaceObject,
38
39
  Layer: ()=>Layer_namespaceObject,
39
40
  Rpc: ()=>rpc_namespaceObject.Rpc,
40
41
  RpcClient: ()=>rpc_namespaceObject.RpcClient,
@@ -60,6 +61,7 @@ const http_namespaceObject = require("effect/unstable/http");
60
61
  const httpapi_namespaceObject = require("effect/unstable/httpapi");
61
62
  const rpc_namespaceObject = require("effect/unstable/rpc");
62
63
  const Schema_namespaceObject = require("effect/Schema");
64
+ const HttpClientError_namespaceObject = require("effect/unstable/http/HttpClientError");
63
65
  class EffectRpcClientError extends Data_namespaceObject.TaggedError('EffectRpcClientError') {
64
66
  }
65
67
  function isRecord(value) {
@@ -162,6 +164,7 @@ exports.HttpApiClient = __webpack_exports__.HttpApiClient;
162
164
  exports.HttpApiEndpoint = __webpack_exports__.HttpApiEndpoint;
163
165
  exports.HttpApiGroup = __webpack_exports__.HttpApiGroup;
164
166
  exports.HttpApiSchema = __webpack_exports__.HttpApiSchema;
167
+ exports.HttpClientError = __webpack_exports__.HttpClientError;
165
168
  exports.Layer = __webpack_exports__.Layer;
166
169
  exports.Rpc = __webpack_exports__.Rpc;
167
170
  exports.RpcClient = __webpack_exports__.RpcClient;
@@ -183,6 +186,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
183
186
  "HttpApiEndpoint",
184
187
  "HttpApiGroup",
185
188
  "HttpApiSchema",
189
+ "HttpClientError",
186
190
  "Layer",
187
191
  "Rpc",
188
192
  "RpcClient",
@@ -172,14 +172,14 @@ const __generated = createGeneratedEffectClient(__manifest, __config, __requestR
172
172
  const client = __generated.client;
173
173
  const operationManifest = __generated.operationManifest;
174
174
  const createEffectRequestContext = __generated.createEffectRequestContext;
175
- const effectBffModule = {
175
+ const apiModule = {
176
176
  client,
177
177
  operationManifest,
178
178
  createEffectRequestContext,
179
179
  };
180
180
 
181
181
  export { client, createEffectRequestContext, operationManifest };
182
- export default effectBffModule;
182
+ export default apiModule;
183
183
  `;
184
184
  }
185
185
  function renderClientShape(endpoints, valueType) {
@@ -252,13 +252,13 @@ export declare const createEffectRequestContext: (
252
252
  requestContext: Record<string, unknown>,
253
253
  ) => EffectRequestContext;
254
254
  export declare const operationManifest: GeneratedEffectOperationManifest;
255
- declare const effectBffModule: {
255
+ declare const apiModule: {
256
256
  client: GeneratedEffectClient;
257
257
  createEffectRequestContext: typeof createEffectRequestContext;
258
258
  operationManifest: GeneratedEffectOperationManifest;
259
259
  };
260
260
 
261
- export default effectBffModule;
261
+ export default apiModule;
262
262
  `;
263
263
  }
264
264
  async function generateEffectClient(options) {
@@ -281,10 +281,15 @@ async function generateEffectClientCode(options) {
281
281
  }
282
282
  function resolveEffectEntryFile(options) {
283
283
  const { appDir, apiDir, effectEntry } = options;
284
- const defaultEntry = external_path_default().resolve(apiDir, 'effect', 'index');
285
- const entryWithoutExt = effectEntry ? external_path_default().isAbsolute(effectEntry) ? effectEntry : external_path_default().resolve(appDir, effectEntry) : defaultEntry;
286
- if (external_path_default().extname(entryWithoutExt)) return utils_namespaceObject.fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
287
- return (0, utils_namespaceObject.findExists)(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
284
+ const resolveEntry = (entryWithoutExt)=>{
285
+ if (external_path_default().extname(entryWithoutExt)) return utils_namespaceObject.fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
286
+ return (0, utils_namespaceObject.findExists)(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
287
+ };
288
+ if (effectEntry) {
289
+ const entryWithoutExt = external_path_default().isAbsolute(effectEntry) ? effectEntry : external_path_default().resolve(appDir, effectEntry);
290
+ return resolveEntry(entryWithoutExt);
291
+ }
292
+ return resolveEntry(external_path_default().resolve(apiDir, 'index'));
288
293
  }
289
294
  exports.generateEffectClient = __webpack_exports__.generateEffectClient;
290
295
  exports.generateEffectClientCode = __webpack_exports__.generateEffectClientCode;
@@ -1,4 +1,3 @@
1
- import { ApiRouter } from "@modern-js/bff-core";
2
1
  import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
3
2
  import { HttpApi } from "effect/unstable/httpapi";
4
3
  import path from "path";
@@ -52,43 +51,12 @@ class EffectAdapter {
52
51
  const { appDirectory, apiDirectory } = this.api.getServerContext();
53
52
  const bffConfig = this.api.getServerConfig()?.bff;
54
53
  const configuredEntry = bffConfig?.effect?.entry;
55
- const defaultEntry = path.resolve(appDirectory || process.cwd(), apiDirectory || API_DIR, 'effect', 'index');
56
- const entryWithoutExt = configuredEntry ? path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry) : defaultEntry;
57
- return resolveJsOrTsEntry(entryWithoutExt);
58
- }
59
- isApiRequestPath(requestPath, prefix, enableHandleWeb) {
60
- if (!enableHandleWeb) return true;
61
- const normalized = normalizePrefix(prefix);
62
- if (!normalized) return true;
63
- return requestPath === normalized || requestPath.startsWith(`${normalized}/`);
64
- }
65
- async collectLambdaContractSources() {
66
- try {
67
- const serverContext = this.api.getServerContext();
68
- const appDir = serverContext.distDirectory || serverContext.appDirectory;
69
- if (!appDir) return [];
70
- const apiDir = 'string' == typeof serverContext.apiDirectory ? serverContext.apiDirectory : path.resolve(appDir, API_DIR);
71
- const lambdaDir = 'string' == typeof serverContext.lambdaDirectory ? serverContext.lambdaDirectory : path.join(apiDir, 'lambda');
72
- if (!await fs.pathExists(lambdaDir)) return [];
73
- const apiRouter = new ApiRouter({
74
- appDir,
75
- apiDir,
76
- lambdaDir,
77
- prefix: this.prefix,
78
- httpMethodDecider: this.api.getServerConfig()?.bff?.httpMethodDecider
79
- });
80
- const handlerInfos = await apiRouter.getApiHandlers();
81
- return handlerInfos.map((info)=>({
82
- name: info.name,
83
- httpMethod: info.httpMethod,
84
- routePath: info.routePath,
85
- filename: info.filename,
86
- handler: info.handler
87
- }));
88
- } catch (error) {
89
- logger.warn(`[BFF][Effect] Failed to derive lambda operation contracts for the cross-project policy: ${String(error)}`);
90
- return [];
54
+ if (configuredEntry) {
55
+ const entryWithoutExt = path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry);
56
+ return resolveJsOrTsEntry(entryWithoutExt);
91
57
  }
58
+ const apiRoot = path.resolve(appDirectory || process.cwd(), apiDirectory || API_DIR);
59
+ return resolveJsOrTsEntry(path.resolve(apiRoot, 'index'));
92
60
  }
93
61
  async refreshCrossProjectPolicy(mod) {
94
62
  let contractSources = [];
@@ -104,24 +72,15 @@ class EffectAdapter {
104
72
  } catch (error) {
105
73
  logger.warn(`[BFF][Effect] Failed to reflect HttpApi endpoints for the cross-project policy: ${String(error)}`);
106
74
  }
107
- let policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
108
- if (policy?.enabled) {
109
- const lambdaSources = await this.collectLambdaContractSources();
110
- if (lambdaSources.length > 0) {
111
- contractSources = [
112
- ...contractSources,
113
- ...lambdaSources
114
- ];
115
- policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
116
- }
117
- }
75
+ const policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
118
76
  this.crossProjectPolicy = policy;
119
77
  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).');
120
78
  }
121
79
  async loadEffectHandlerFromModule(mod) {
80
+ const effectConfig = this.api.getServerConfig()?.bff?.effect;
122
81
  return resolveEffectBffModuleHandler(mod, {
123
- openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
124
- dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform,
82
+ openapi: effectConfig?.openapi,
83
+ dataPlatform: effectConfig?.dataPlatform,
125
84
  validateRequest: (request)=>checkCrossProjectPolicyForRequest(request, this.crossProjectPolicy),
126
85
  onWarning: (message)=>{
127
86
  logger.warn(message);
@@ -155,13 +114,12 @@ class EffectAdapter {
155
114
  await this.refreshCrossProjectPolicy(mod);
156
115
  const loaded = await this.loadEffectHandlerFromModule(mod);
157
116
  if (!loaded) {
158
- logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer } or handler.`);
117
+ logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export defineEffectBff(...) or a { api, layer } HttpApi module.`);
159
118
  this.handler = null;
160
119
  return;
161
120
  }
162
121
  this.handler = loaded.handler;
163
122
  this.dispose = loaded.dispose || null;
164
- this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
165
123
  }
166
124
  async disposeCurrentHandler() {
167
125
  if (!this.dispose) return;
@@ -213,7 +171,6 @@ class EffectAdapter {
213
171
  this.handler = null;
214
172
  this.dispose = null;
215
173
  this.prefix = '/api';
216
- this.policyEnforcedInMiddleware = false;
217
174
  this.registerMiddleware = async (options)=>{
218
175
  const { prefix, enableHandleWeb } = options;
219
176
  const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
@@ -224,7 +181,7 @@ class EffectAdapter {
224
181
  this.prefix = prefix || this.prefix;
225
182
  await this.reloadHandler();
226
183
  this.effectMiddleware = {
227
- name: 'effect-bff-handler',
184
+ name: 'effect-api-handler',
228
185
  path: enableHandleWeb ? '*' : `${prefix}/*`,
229
186
  method: 'all',
230
187
  order: 'post',
@@ -232,11 +189,7 @@ class EffectAdapter {
232
189
  handler: async (c, next)=>{
233
190
  if (!this.handler) {
234
191
  if (enableHandleWeb) return void await next();
235
- return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/effect/index or configure bff.effect.entry.'), c);
236
- }
237
- if (this.crossProjectPolicy?.enabled && this.policyEnforcedInMiddleware && this.isApiRequestPath(c.req.path, prefix, enableHandleWeb)) {
238
- const denial = checkCrossProjectPolicyForRequest(c.req.raw, this.crossProjectPolicy);
239
- if (denial) return denial;
192
+ return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/index or configure bff.effect.entry.'), c);
240
193
  }
241
194
  let response;
242
195
  try {
@@ -68,7 +68,7 @@ async function createEffectBffEdgeHandler(options) {
68
68
  dataPlatform: options.dataPlatform,
69
69
  onWarning: options.onWarning
70
70
  });
71
- if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }, createHandler, or handler.');
71
+ if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export defineEffectBff(...) or a { api, layer } HttpApi module.');
72
72
  return {
73
73
  handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
74
74
  ...dispatchOptions,
@@ -74,6 +74,41 @@ function createBatchValidationResponse(message, status = 400) {
74
74
  }
75
75
  });
76
76
  }
77
+ function createJsonValidationResponse(message, status = 400) {
78
+ return new Response(JSON.stringify({
79
+ message
80
+ }), {
81
+ status,
82
+ headers: {
83
+ 'content-type': 'application/json; charset=utf-8'
84
+ }
85
+ });
86
+ }
87
+ function cloneWithoutJsonBodyHeaders(request) {
88
+ const headers = new Headers(request.headers);
89
+ headers.delete('content-type');
90
+ headers.delete('content-length');
91
+ return new Request(request.url, {
92
+ method: request.method,
93
+ headers,
94
+ signal: request.signal
95
+ });
96
+ }
97
+ async function prepareJsonRequestBody(request) {
98
+ const method = normalizeItemMethod(request.method);
99
+ if ('GET' === method || 'HEAD' === method) return request;
100
+ const contentType = (request.headers.get('content-type') || '').toLowerCase();
101
+ if (!contentType.includes('application/json')) return request;
102
+ if (null === request.body) return cloneWithoutJsonBodyHeaders(request);
103
+ try {
104
+ const bodyText = await request.clone().text();
105
+ if ('' === bodyText) return cloneWithoutJsonBodyHeaders(request);
106
+ JSON.parse(bodyText);
107
+ } catch {
108
+ return createJsonValidationResponse('Invalid JSON request body');
109
+ }
110
+ return request;
111
+ }
77
112
  function toBatchItemError(id, status, message) {
78
113
  return {
79
114
  id,
@@ -294,7 +329,7 @@ const LOADER_CLIENT_IGNORED_KEYS = new Set([
294
329
  ]);
295
330
  function createLoaderMaterializedClientPlaceholder() {
296
331
  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.`);
332
+ throw new Error(`[BFF][Effect] client.${String(property)} is not available here: the typed client only exists when the API entry is imported through the "@api/index" transformed path (the BFF loader replaces it with generated client code). On the server, use HttpApiClient or call the Effect layer directly.`);
298
333
  };
299
334
  return new Proxy(Object.create(null), {
300
335
  get (_target, property) {
@@ -331,9 +366,11 @@ function createHttpApiHandler(options) {
331
366
  const withDataPlatformValidation = async (request, context)=>{
332
367
  const policyDenial = options.validateRequest?.(request);
333
368
  if (policyDenial) return policyDenial;
334
- const validationError = validateDataPlatformRequestEnvelope(request, options.dataPlatform);
369
+ const preparedRequest = await prepareJsonRequestBody(request);
370
+ if (preparedRequest instanceof Response) return preparedRequest;
371
+ const validationError = validateDataPlatformRequestEnvelope(preparedRequest, options.dataPlatform);
335
372
  if (validationError) return validationError;
336
- return httpApiHandler.handler(request, context ?? emptyEffectServiceContext);
373
+ return httpApiHandler.handler(preparedRequest, context ?? emptyEffectServiceContext);
337
374
  };
338
375
  const handleBatchRequest = async (request, context)=>{
339
376
  const mountedPrefix = getMountedPrefixFromContext(request, context);