@bleedingdev/modern-js-plugin-bff 3.4.0-ultramodern.19 → 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,
@@ -457,7 +457,7 @@ var __webpack_exports__ = {};
457
457
  ]);
458
458
  function createLoaderMaterializedClientPlaceholder() {
459
459
  const explain = (property)=>{
460
- 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.`);
461
461
  };
462
462
  return new Proxy(Object.create(null), {
463
463
  get (_target, property) {
@@ -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
  });
@@ -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,
@@ -329,7 +329,7 @@ const LOADER_CLIENT_IGNORED_KEYS = new Set([
329
329
  ]);
330
330
  function createLoaderMaterializedClientPlaceholder() {
331
331
  const explain = (property)=>{
332
- 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.`);
333
333
  };
334
334
  return new Proxy(Object.create(null), {
335
335
  get (_target, property) {
@@ -17,16 +17,18 @@ function isEffectServiceContext(context) {
17
17
  return 'object' == typeof context && null !== context && 'mapUnsafe' in context;
18
18
  }
19
19
  const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
20
+ 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.';
21
+ function rejectLegacyEffectModuleShape(options, shape) {
22
+ options.onWarning?.(`${strictEffectApproachMessage} Rejected ${shape}.`);
23
+ return true;
24
+ }
20
25
  function callEffectBffRequestHandler(handler, request, context) {
21
26
  return void 0 === context ? handler(request) : handler(request, context);
22
27
  }
23
- function createLoadedHandler(webHandler, appliesRequestValidator) {
28
+ function createLoadedHandler(webHandler) {
24
29
  return {
25
30
  handler: (request, context)=>callEffectBffRequestHandler(webHandler.handler, request, context),
26
- dispose: webHandler.dispose,
27
- ...appliesRequestValidator ? {
28
- appliesRequestValidator: true
29
- } : {}
31
+ dispose: webHandler.dispose
30
32
  };
31
33
  }
32
34
  function createLoadedHttpApiHandler(webHandler) {
@@ -35,18 +37,23 @@ function createLoadedHttpApiHandler(webHandler) {
35
37
  const effectContext = isEffectServiceContext(context) ? context : emptyEffectServiceContext;
36
38
  return webHandler.handler(request, effectContext);
37
39
  },
38
- dispose: webHandler.dispose,
39
- appliesRequestValidator: true
40
+ dispose: webHandler.dispose
40
41
  };
41
42
  }
42
43
  function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {}) {
43
- if (isRequestHandler(normalizedModule.handler)) return {
44
- handler: normalizedModule.handler
45
- };
44
+ if (isRequestHandler(normalizedModule.handler)) {
45
+ if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
46
+ return {
47
+ handler: normalizedModule.handler
48
+ };
49
+ }
46
50
  const entry = normalizedModule.default;
47
- if (isRequestHandler(entry)) return {
48
- handler: entry
49
- };
51
+ if (isRequestHandler(entry)) {
52
+ if (rejectLegacyEffectModuleShape(options, 'default request handler')) return null;
53
+ return {
54
+ handler: entry
55
+ };
56
+ }
50
57
  if (isRecord(entry)) normalizedModule = {
51
58
  ...normalizedModule,
52
59
  ...entry
@@ -58,19 +65,22 @@ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {})
58
65
  handler: maybeHandler
59
66
  };
60
67
  }
61
- if (isRequestHandler(normalizedModule.handler)) return {
62
- handler: normalizedModule.handler
63
- };
68
+ if (isRequestHandler(normalizedModule.handler)) {
69
+ if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
70
+ return {
71
+ handler: normalizedModule.handler
72
+ };
73
+ }
64
74
  if ('function' == typeof normalizedModule.createHandler) {
65
75
  const factory = normalizedModule.createHandler;
66
76
  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.');
77
+ if (!validatorAware && rejectLegacyEffectModuleShape(options, 'unbranded `createHandler` export')) return null;
68
78
  const webHandler = factory({
69
79
  openapi: options.openapi,
70
80
  dataPlatform: options.dataPlatform,
71
81
  validateRequest: options.validateRequest
72
82
  });
73
- return createLoadedHandler(webHandler, validatorAware);
83
+ return createLoadedHandler(webHandler);
74
84
  }
75
85
  if (isEffectApiDefinition(normalizedModule)) {
76
86
  options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
@@ -94,17 +104,26 @@ function resolveEffectBffModuleHandler(mod, options = {}) {
94
104
  ...value
95
105
  };
96
106
  };
97
- if (isRequestHandler(normalizedModule.handler)) return Promise.resolve({
98
- handler: normalizedModule.handler
99
- });
107
+ if (isRequestHandler(normalizedModule.handler)) {
108
+ if (rejectLegacyEffectModuleShape(options, '`handler` export')) return Promise.resolve(null);
109
+ return Promise.resolve({
110
+ handler: normalizedModule.handler
111
+ });
112
+ }
100
113
  const entry = normalizedModule.default;
101
- if (isRequestHandler(entry)) return Promise.resolve({
102
- handler: entry
103
- });
114
+ if (isRequestHandler(entry)) {
115
+ if (rejectLegacyEffectModuleShape(options, 'default request handler')) return Promise.resolve(null);
116
+ return Promise.resolve({
117
+ handler: entry
118
+ });
119
+ }
104
120
  if ('function' == typeof entry && 0 === entry.length) return Promise.resolve(entry()).then((out)=>{
105
- if (isRequestHandler(out)) return {
106
- handler: out
107
- };
121
+ if (isRequestHandler(out)) {
122
+ if (rejectLegacyEffectModuleShape(options, 'default factory returning a request handler')) return null;
123
+ return {
124
+ handler: out
125
+ };
126
+ }
108
127
  mergeRuntimeExports(out);
109
128
  return resolveNormalizedEffectBffModuleHandler(normalizedModule, options);
110
129
  });
@@ -127,14 +127,14 @@ const __generated = createGeneratedEffectClient(__manifest, __config, __requestR
127
127
  const client = __generated.client;
128
128
  const operationManifest = __generated.operationManifest;
129
129
  const createEffectRequestContext = __generated.createEffectRequestContext;
130
- const effectBffModule = {
130
+ const apiModule = {
131
131
  client,
132
132
  operationManifest,
133
133
  createEffectRequestContext,
134
134
  };
135
135
 
136
136
  export { client, createEffectRequestContext, operationManifest };
137
- export default effectBffModule;
137
+ export default apiModule;
138
138
  `;
139
139
  }
140
140
  function renderClientShape(endpoints, valueType) {
@@ -207,13 +207,13 @@ export declare const createEffectRequestContext: (
207
207
  requestContext: Record<string, unknown>,
208
208
  ) => EffectRequestContext;
209
209
  export declare const operationManifest: GeneratedEffectOperationManifest;
210
- declare const effectBffModule: {
210
+ declare const apiModule: {
211
211
  client: GeneratedEffectClient;
212
212
  createEffectRequestContext: typeof createEffectRequestContext;
213
213
  operationManifest: GeneratedEffectOperationManifest;
214
214
  };
215
215
 
216
- export default effectBffModule;
216
+ export default apiModule;
217
217
  `;
218
218
  }
219
219
  async function generateEffectClient(options) {
@@ -236,9 +236,14 @@ async function generateEffectClientCode(options) {
236
236
  }
237
237
  function resolveEffectEntryFile(options) {
238
238
  const { appDir, apiDir, effectEntry } = options;
239
- const defaultEntry = path.resolve(apiDir, 'effect', 'index');
240
- const entryWithoutExt = effectEntry ? path.isAbsolute(effectEntry) ? effectEntry : path.resolve(appDir, effectEntry) : defaultEntry;
241
- if (path.extname(entryWithoutExt)) return fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
242
- return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
239
+ const resolveEntry = (entryWithoutExt)=>{
240
+ if (path.extname(entryWithoutExt)) return fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
241
+ return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
242
+ };
243
+ if (effectEntry) {
244
+ const entryWithoutExt = path.isAbsolute(effectEntry) ? effectEntry : path.resolve(appDir, effectEntry);
245
+ return resolveEntry(entryWithoutExt);
246
+ }
247
+ return resolveEntry(path.resolve(apiDir, 'index'));
243
248
  }
244
249
  export { generateEffectClient, generateEffectClientCode, renderEffectClientDeclaration, resolveEffectEntryFile };
@@ -1,6 +1,5 @@
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";
4
3
  import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
5
4
  import { HttpApi } from "effect/unstable/httpapi";
6
5
  import path from "path";
@@ -54,43 +53,12 @@ class EffectAdapter {
54
53
  const { appDirectory, apiDirectory } = this.api.getServerContext();
55
54
  const bffConfig = this.api.getServerConfig()?.bff;
56
55
  const configuredEntry = bffConfig?.effect?.entry;
57
- const defaultEntry = path.resolve(appDirectory || process.cwd(), apiDirectory || API_DIR, 'effect', 'index');
58
- const entryWithoutExt = configuredEntry ? path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry) : defaultEntry;
59
- return resolveJsOrTsEntry(entryWithoutExt);
60
- }
61
- isApiRequestPath(requestPath, prefix, enableHandleWeb) {
62
- if (!enableHandleWeb) return true;
63
- const normalized = normalizePrefix(prefix);
64
- if (!normalized) return true;
65
- return requestPath === normalized || requestPath.startsWith(`${normalized}/`);
66
- }
67
- async collectLambdaContractSources() {
68
- try {
69
- const serverContext = this.api.getServerContext();
70
- const appDir = serverContext.distDirectory || serverContext.appDirectory;
71
- if (!appDir) return [];
72
- const apiDir = 'string' == typeof serverContext.apiDirectory ? serverContext.apiDirectory : path.resolve(appDir, API_DIR);
73
- const lambdaDir = 'string' == typeof serverContext.lambdaDirectory ? serverContext.lambdaDirectory : path.join(apiDir, 'lambda');
74
- if (!await fs.pathExists(lambdaDir)) return [];
75
- const apiRouter = new ApiRouter({
76
- appDir,
77
- apiDir,
78
- lambdaDir,
79
- prefix: this.prefix,
80
- httpMethodDecider: this.api.getServerConfig()?.bff?.httpMethodDecider
81
- });
82
- const handlerInfos = await apiRouter.getApiHandlers();
83
- return handlerInfos.map((info)=>({
84
- name: info.name,
85
- httpMethod: info.httpMethod,
86
- routePath: info.routePath,
87
- filename: info.filename,
88
- handler: info.handler
89
- }));
90
- } catch (error) {
91
- logger.warn(`[BFF][Effect] Failed to derive lambda operation contracts for the cross-project policy: ${String(error)}`);
92
- return [];
56
+ if (configuredEntry) {
57
+ const entryWithoutExt = path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry);
58
+ return resolveJsOrTsEntry(entryWithoutExt);
93
59
  }
60
+ const apiRoot = path.resolve(appDirectory || process.cwd(), apiDirectory || API_DIR);
61
+ return resolveJsOrTsEntry(path.resolve(apiRoot, 'index'));
94
62
  }
95
63
  async refreshCrossProjectPolicy(mod) {
96
64
  let contractSources = [];
@@ -106,24 +74,15 @@ class EffectAdapter {
106
74
  } catch (error) {
107
75
  logger.warn(`[BFF][Effect] Failed to reflect HttpApi endpoints for the cross-project policy: ${String(error)}`);
108
76
  }
109
- let policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
110
- if (policy?.enabled) {
111
- const lambdaSources = await this.collectLambdaContractSources();
112
- if (lambdaSources.length > 0) {
113
- contractSources = [
114
- ...contractSources,
115
- ...lambdaSources
116
- ];
117
- policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
118
- }
119
- }
77
+ const policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
120
78
  this.crossProjectPolicy = policy;
121
79
  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).');
122
80
  }
123
81
  async loadEffectHandlerFromModule(mod) {
82
+ const effectConfig = this.api.getServerConfig()?.bff?.effect;
124
83
  return resolveEffectBffModuleHandler(mod, {
125
- openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
126
- dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform,
84
+ openapi: effectConfig?.openapi,
85
+ dataPlatform: effectConfig?.dataPlatform,
127
86
  validateRequest: (request)=>checkCrossProjectPolicyForRequest(request, this.crossProjectPolicy),
128
87
  onWarning: (message)=>{
129
88
  logger.warn(message);
@@ -157,13 +116,12 @@ class EffectAdapter {
157
116
  await this.refreshCrossProjectPolicy(mod);
158
117
  const loaded = await this.loadEffectHandlerFromModule(mod);
159
118
  if (!loaded) {
160
- logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer } or handler.`);
119
+ logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export defineEffectBff(...) or a { api, layer } HttpApi module.`);
161
120
  this.handler = null;
162
121
  return;
163
122
  }
164
123
  this.handler = loaded.handler;
165
124
  this.dispose = loaded.dispose || null;
166
- this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
167
125
  }
168
126
  async disposeCurrentHandler() {
169
127
  if (!this.dispose) return;
@@ -215,7 +173,6 @@ class EffectAdapter {
215
173
  this.handler = null;
216
174
  this.dispose = null;
217
175
  this.prefix = '/api';
218
- this.policyEnforcedInMiddleware = false;
219
176
  this.registerMiddleware = async (options)=>{
220
177
  const { prefix, enableHandleWeb } = options;
221
178
  const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
@@ -226,7 +183,7 @@ class EffectAdapter {
226
183
  this.prefix = prefix || this.prefix;
227
184
  await this.reloadHandler();
228
185
  this.effectMiddleware = {
229
- name: 'effect-bff-handler',
186
+ name: 'effect-api-handler',
230
187
  path: enableHandleWeb ? '*' : `${prefix}/*`,
231
188
  method: 'all',
232
189
  order: 'post',
@@ -234,11 +191,7 @@ class EffectAdapter {
234
191
  handler: async (c, next)=>{
235
192
  if (!this.handler) {
236
193
  if (enableHandleWeb) return void await next();
237
- return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/effect/index or configure bff.effect.entry.'), c);
238
- }
239
- if (this.crossProjectPolicy?.enabled && this.policyEnforcedInMiddleware && this.isApiRequestPath(c.req.path, prefix, enableHandleWeb)) {
240
- const denial = checkCrossProjectPolicyForRequest(c.req.raw, this.crossProjectPolicy);
241
- if (denial) return denial;
194
+ return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/index or configure bff.effect.entry.'), c);
242
195
  }
243
196
  let response;
244
197
  try {
@@ -69,7 +69,7 @@ async function createEffectBffEdgeHandler(options) {
69
69
  dataPlatform: options.dataPlatform,
70
70
  onWarning: options.onWarning
71
71
  });
72
- if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }, createHandler, or handler.');
72
+ if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export defineEffectBff(...) or a { api, layer } HttpApi module.');
73
73
  return {
74
74
  handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
75
75
  ...dispatchOptions,
@@ -330,7 +330,7 @@ const LOADER_CLIENT_IGNORED_KEYS = new Set([
330
330
  ]);
331
331
  function createLoaderMaterializedClientPlaceholder() {
332
332
  const explain = (property)=>{
333
- 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.`);
333
+ 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.`);
334
334
  };
335
335
  return new Proxy(Object.create(null), {
336
336
  get (_target, property) {
@@ -18,16 +18,18 @@ function isEffectServiceContext(context) {
18
18
  return 'object' == typeof context && null !== context && 'mapUnsafe' in context;
19
19
  }
20
20
  const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
21
+ 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.';
22
+ function rejectLegacyEffectModuleShape(options, shape) {
23
+ options.onWarning?.(`${strictEffectApproachMessage} Rejected ${shape}.`);
24
+ return true;
25
+ }
21
26
  function callEffectBffRequestHandler(handler, request, context) {
22
27
  return void 0 === context ? handler(request) : handler(request, context);
23
28
  }
24
- function createLoadedHandler(webHandler, appliesRequestValidator) {
29
+ function createLoadedHandler(webHandler) {
25
30
  return {
26
31
  handler: (request, context)=>callEffectBffRequestHandler(webHandler.handler, request, context),
27
- dispose: webHandler.dispose,
28
- ...appliesRequestValidator ? {
29
- appliesRequestValidator: true
30
- } : {}
32
+ dispose: webHandler.dispose
31
33
  };
32
34
  }
33
35
  function createLoadedHttpApiHandler(webHandler) {
@@ -36,18 +38,23 @@ function createLoadedHttpApiHandler(webHandler) {
36
38
  const effectContext = isEffectServiceContext(context) ? context : emptyEffectServiceContext;
37
39
  return webHandler.handler(request, effectContext);
38
40
  },
39
- dispose: webHandler.dispose,
40
- appliesRequestValidator: true
41
+ dispose: webHandler.dispose
41
42
  };
42
43
  }
43
44
  function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {}) {
44
- if (isRequestHandler(normalizedModule.handler)) return {
45
- handler: normalizedModule.handler
46
- };
45
+ if (isRequestHandler(normalizedModule.handler)) {
46
+ if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
47
+ return {
48
+ handler: normalizedModule.handler
49
+ };
50
+ }
47
51
  const entry = normalizedModule.default;
48
- if (isRequestHandler(entry)) return {
49
- handler: entry
50
- };
52
+ if (isRequestHandler(entry)) {
53
+ if (rejectLegacyEffectModuleShape(options, 'default request handler')) return null;
54
+ return {
55
+ handler: entry
56
+ };
57
+ }
51
58
  if (isRecord(entry)) normalizedModule = {
52
59
  ...normalizedModule,
53
60
  ...entry
@@ -59,19 +66,22 @@ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {})
59
66
  handler: maybeHandler
60
67
  };
61
68
  }
62
- if (isRequestHandler(normalizedModule.handler)) return {
63
- handler: normalizedModule.handler
64
- };
69
+ if (isRequestHandler(normalizedModule.handler)) {
70
+ if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
71
+ return {
72
+ handler: normalizedModule.handler
73
+ };
74
+ }
65
75
  if ('function' == typeof normalizedModule.createHandler) {
66
76
  const factory = normalizedModule.createHandler;
67
77
  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.');
78
+ if (!validatorAware && rejectLegacyEffectModuleShape(options, 'unbranded `createHandler` export')) return null;
69
79
  const webHandler = factory({
70
80
  openapi: options.openapi,
71
81
  dataPlatform: options.dataPlatform,
72
82
  validateRequest: options.validateRequest
73
83
  });
74
- return createLoadedHandler(webHandler, validatorAware);
84
+ return createLoadedHandler(webHandler);
75
85
  }
76
86
  if (isEffectApiDefinition(normalizedModule)) {
77
87
  options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
@@ -95,17 +105,26 @@ function resolveEffectBffModuleHandler(mod, options = {}) {
95
105
  ...value
96
106
  };
97
107
  };
98
- if (isRequestHandler(normalizedModule.handler)) return Promise.resolve({
99
- handler: normalizedModule.handler
100
- });
108
+ if (isRequestHandler(normalizedModule.handler)) {
109
+ if (rejectLegacyEffectModuleShape(options, '`handler` export')) return Promise.resolve(null);
110
+ return Promise.resolve({
111
+ handler: normalizedModule.handler
112
+ });
113
+ }
101
114
  const entry = normalizedModule.default;
102
- if (isRequestHandler(entry)) return Promise.resolve({
103
- handler: entry
104
- });
115
+ if (isRequestHandler(entry)) {
116
+ if (rejectLegacyEffectModuleShape(options, 'default request handler')) return Promise.resolve(null);
117
+ return Promise.resolve({
118
+ handler: entry
119
+ });
120
+ }
105
121
  if ('function' == typeof entry && 0 === entry.length) return Promise.resolve(entry()).then((out)=>{
106
- if (isRequestHandler(out)) return {
107
- handler: out
108
- };
122
+ if (isRequestHandler(out)) {
123
+ if (rejectLegacyEffectModuleShape(options, 'default factory returning a request handler')) return null;
124
+ return {
125
+ handler: out
126
+ };
127
+ }
109
128
  mergeRuntimeExports(out);
110
129
  return resolveNormalizedEffectBffModuleHandler(normalizedModule, options);
111
130
  });
@@ -129,14 +129,14 @@ const __generated = createGeneratedEffectClient(__manifest, __config, __requestR
129
129
  const client = __generated.client;
130
130
  const operationManifest = __generated.operationManifest;
131
131
  const createEffectRequestContext = __generated.createEffectRequestContext;
132
- const effectBffModule = {
132
+ const apiModule = {
133
133
  client,
134
134
  operationManifest,
135
135
  createEffectRequestContext,
136
136
  };
137
137
 
138
138
  export { client, createEffectRequestContext, operationManifest };
139
- export default effectBffModule;
139
+ export default apiModule;
140
140
  `;
141
141
  }
142
142
  function renderClientShape(endpoints, valueType) {
@@ -209,13 +209,13 @@ export declare const createEffectRequestContext: (
209
209
  requestContext: Record<string, unknown>,
210
210
  ) => EffectRequestContext;
211
211
  export declare const operationManifest: GeneratedEffectOperationManifest;
212
- declare const effectBffModule: {
212
+ declare const apiModule: {
213
213
  client: GeneratedEffectClient;
214
214
  createEffectRequestContext: typeof createEffectRequestContext;
215
215
  operationManifest: GeneratedEffectOperationManifest;
216
216
  };
217
217
 
218
- export default effectBffModule;
218
+ export default apiModule;
219
219
  `;
220
220
  }
221
221
  async function generateEffectClient(options) {
@@ -238,9 +238,14 @@ async function generateEffectClientCode(options) {
238
238
  }
239
239
  function resolveEffectEntryFile(options) {
240
240
  const { appDir, apiDir, effectEntry } = options;
241
- const defaultEntry = path.resolve(apiDir, 'effect', 'index');
242
- const entryWithoutExt = effectEntry ? path.isAbsolute(effectEntry) ? effectEntry : path.resolve(appDir, effectEntry) : defaultEntry;
243
- if (path.extname(entryWithoutExt)) return fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
244
- return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
241
+ const resolveEntry = (entryWithoutExt)=>{
242
+ if (path.extname(entryWithoutExt)) return fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
243
+ return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
244
+ };
245
+ if (effectEntry) {
246
+ const entryWithoutExt = path.isAbsolute(effectEntry) ? effectEntry : path.resolve(appDir, effectEntry);
247
+ return resolveEntry(entryWithoutExt);
248
+ }
249
+ return resolveEntry(path.resolve(apiDir, 'index'));
245
250
  }
246
251
  export { generateEffectClient, generateEffectClientCode, renderEffectClientDeclaration, resolveEffectEntryFile };
@@ -12,30 +12,14 @@ export declare class EffectAdapter {
12
12
  private handler;
13
13
  private dispose;
14
14
  private prefix;
15
- /**
16
- * True when the loaded handler does NOT run the policy seam internally
17
- * (plain `handler` exports); the adapter middleware enforces it instead.
18
- */
19
- private policyEnforcedInMiddleware;
20
15
  constructor(api: ServerPluginAPI);
21
16
  registerMiddleware: (options: MiddlewareOptions) => Promise<void>;
22
17
  onApiHandlersUpdated: () => Promise<void>;
23
18
  private resolveEntryFile;
24
- private isApiRequestPath;
25
- /**
26
- * Contract sources for the lambda-lane handlers hosted alongside the
27
- * effect entry. Cross-project SDKs generate per-operation contracts for
28
- * `api/lambda` handlers with the exact same `ApiRouter` derivation used
29
- * here, so the server-side expected-contract map must include them —
30
- * otherwise every lambda-lane operation of a hosted producer SDK would be
31
- * denied as `unknown_operation_contract`.
32
- */
33
- private collectLambdaContractSources;
34
19
  /**
35
- * Resolves the cross-project policy from the reflected HttpApi endpoints
36
- * (plus any hosted lambda-lane handlers) so the expected operation
37
- * contracts match the hashes the client generators stamp into generated
38
- * SDKs.
20
+ * Resolves the cross-project policy from the reflected HttpApi endpoints so
21
+ * the expected operation contracts match the hashes the client generators
22
+ * stamp into generated SDKs.
39
23
  */
40
24
  private refreshCrossProjectPolicy;
41
25
  private loadEffectHandlerFromModule;
@@ -144,11 +144,8 @@ export type EffectRequestValidator = (request: Request) => Response | null | und
144
144
  * seam runs inside the produced handler, including per batched item.
145
145
  *
146
146
  * `defineEffectBff` brands its factory automatically. Hand-written factories
147
- * are NOT assumed to honor `validateRequest` (the option is newer than the
148
- * factory shape), so the effect adapter falls back to middleware enforcement
149
- * for unbranded factories instead of silently skipping the policy. Custom
150
- * factories that do forward `validateRequest` into `createHttpApiHandler`
151
- * may opt in by setting this symbol property to `true`.
147
+ * are NOT assumed to honor `validateRequest`, so strict module resolution
148
+ * rejects unbranded factories instead of silently skipping the policy.
152
149
  *
153
150
  * Registered via `Symbol.for` so independently loaded runtime copies agree.
154
151
  */
@@ -20,27 +20,14 @@ export type EffectApiModule = {
20
20
  export type LoadedEffectBffHandler = {
21
21
  handler: EffectBffRequestHandler;
22
22
  dispose?: () => Promise<void>;
23
- /**
24
- * True when the handler is known to apply `validateRequest` internally
25
- * (including per batched item): `{ api, layer }` modules resolved through
26
- * `createHttpApiHandler`, and `createHandler` factories branded by
27
- * `defineEffectBff` (see `EFFECT_VALIDATOR_AWARE_FACTORY`). Plain
28
- * `handler` exports and unbranded custom factories leave this unset; the
29
- * adapter middleware must enforce the policy itself for those.
30
- */
31
- appliesRequestValidator?: boolean;
32
23
  };
33
24
  export type ResolveEffectBffModuleHandlerOptions = {
34
25
  openapi?: EffectBffOpenApiConfig;
35
26
  dataPlatform?: EffectDataPlatformValidationOptions;
36
27
  /**
37
- * Server-seam request validator (cross-project policy). Applied by
38
- * `createHttpApiHandler` to direct, batched and rpc requests.
39
- *
40
- * Note: plain `handler` exports and unbranded custom `createHandler`
41
- * factories bypass this seam — the effect adapter enforces the policy in
42
- * its middleware for those shapes instead (see
43
- * `LoadedEffectBffHandler.appliesRequestValidator`).
28
+ * Server-seam request validator (cross-project policy). Strict Effect
29
+ * entries forward it into `createHttpApiHandler`, where it applies to
30
+ * direct, batched and rpc requests.
44
31
  */
45
32
  validateRequest?: EffectRequestValidator;
46
33
  onWarning?: (message: string) => void;
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.4.0-ultramodern.19",
20
+ "version": "3.4.0-ultramodern.20",
21
21
  "types": "./dist/types/cli.d.ts",
22
22
  "main": "./dist/cjs/cli.js",
23
23
  "exports": {
@@ -157,12 +157,12 @@
157
157
  "effect": "4.0.0-beta.89",
158
158
  "qs": "^6.15.3",
159
159
  "type-is": "^2.1.0",
160
- "@modern-js/bff-core": "npm:@bleedingdev/modern-js-bff-core@3.4.0-ultramodern.19",
161
- "@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.4.0-ultramodern.19",
162
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.4.0-ultramodern.19",
163
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.4.0-ultramodern.19",
164
- "@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.4.0-ultramodern.19",
165
- "@modern-js/create-request": "npm:@bleedingdev/modern-js-create-request@3.4.0-ultramodern.19"
160
+ "@modern-js/bff-core": "npm:@bleedingdev/modern-js-bff-core@3.4.0-ultramodern.20",
161
+ "@modern-js/create-request": "npm:@bleedingdev/modern-js-create-request@3.4.0-ultramodern.20",
162
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.4.0-ultramodern.20",
163
+ "@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.4.0-ultramodern.20",
164
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.4.0-ultramodern.20",
165
+ "@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.4.0-ultramodern.20"
166
166
  },
167
167
  "devDependencies": {
168
168
  "@rsbuild/core": "2.1.0",
@@ -174,12 +174,12 @@
174
174
  "memfs": "^4.57.8",
175
175
  "typescript": "^6.0.3",
176
176
  "zod": "^4.4.3",
177
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.4.0-ultramodern.19",
178
- "@modern-js/bff-runtime": "npm:@bleedingdev/modern-js-bff-runtime@3.4.0-ultramodern.19",
179
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.4.0-ultramodern.19",
177
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.4.0-ultramodern.20",
178
+ "@modern-js/bff-runtime": "npm:@bleedingdev/modern-js-bff-runtime@3.4.0-ultramodern.20",
179
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.4.0-ultramodern.20",
180
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.4.0-ultramodern.20",
180
181
  "@scripts/rstest-config": "2.66.0",
181
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.4.0-ultramodern.19",
182
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.4.0-ultramodern.19"
182
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.4.0-ultramodern.20"
183
183
  },
184
184
  "sideEffects": false,
185
185
  "publishConfig": {