@bleedingdev/modern-js-plugin-bff 3.2.0-ultramodern.12 → 3.2.0-ultramodern.121

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 (80) 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 +32 -5
  5. package/dist/cjs/runtime/create-request/index.js +9 -5
  6. package/dist/cjs/runtime/data-platform/index.js +50 -26
  7. package/dist/cjs/runtime/effect/adapter.js +99 -93
  8. package/dist/cjs/runtime/effect/context.js +19 -7
  9. package/dist/cjs/runtime/effect/edge.js +169 -0
  10. package/dist/cjs/runtime/effect/endpoint-contracts.js +130 -0
  11. package/dist/cjs/runtime/effect/handler.js +642 -0
  12. package/dist/cjs/runtime/effect/index.js +30 -547
  13. package/dist/cjs/runtime/effect/module.js +151 -0
  14. package/dist/cjs/runtime/effect/operation-context.js +103 -0
  15. package/dist/cjs/runtime/effect-client/index.js +22 -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 +116 -488
  27. package/dist/cjs/utils/pluginGenerator.js +9 -5
  28. package/dist/cjs/utils/runtimeGenerator.js +9 -5
  29. package/dist/esm/loader.mjs +23 -0
  30. package/dist/esm/runtime/data-platform/index.mjs +33 -22
  31. package/dist/esm/runtime/effect/adapter.mjs +91 -89
  32. package/dist/esm/runtime/effect/context.mjs +3 -1
  33. package/dist/esm/runtime/effect/edge.mjs +83 -0
  34. package/dist/esm/runtime/effect/endpoint-contracts.mjs +68 -0
  35. package/dist/esm/runtime/effect/handler.mjs +470 -0
  36. package/dist/esm/runtime/effect/index.mjs +3 -437
  37. package/dist/esm/runtime/effect/module.mjs +113 -0
  38. package/dist/esm/runtime/effect/operation-context.mjs +65 -0
  39. package/dist/esm/runtime/effect-client/index.mjs +14 -2
  40. package/dist/esm/runtime/effect-client/runtime.mjs +228 -0
  41. package/dist/esm/runtime/hono/adapter.mjs +21 -9
  42. package/dist/esm/runtime/safe-failure.mjs +45 -0
  43. package/dist/esm/utils/clientGenerator.mjs +5 -5
  44. package/dist/esm/utils/crossProjectServerPolicy.mjs +50 -0
  45. package/dist/esm/utils/effectClientGenerator.mjs +105 -484
  46. package/dist/esm-node/loader.mjs +23 -0
  47. package/dist/esm-node/runtime/data-platform/index.mjs +33 -22
  48. package/dist/esm-node/runtime/effect/adapter.mjs +91 -89
  49. package/dist/esm-node/runtime/effect/context.mjs +3 -1
  50. package/dist/esm-node/runtime/effect/edge.mjs +84 -0
  51. package/dist/esm-node/runtime/effect/endpoint-contracts.mjs +69 -0
  52. package/dist/esm-node/runtime/effect/handler.mjs +471 -0
  53. package/dist/esm-node/runtime/effect/index.mjs +3 -437
  54. package/dist/esm-node/runtime/effect/module.mjs +114 -0
  55. package/dist/esm-node/runtime/effect/operation-context.mjs +66 -0
  56. package/dist/esm-node/runtime/effect-client/index.mjs +14 -2
  57. package/dist/esm-node/runtime/effect-client/runtime.mjs +229 -0
  58. package/dist/esm-node/runtime/hono/adapter.mjs +21 -9
  59. package/dist/esm-node/runtime/safe-failure.mjs +46 -0
  60. package/dist/esm-node/utils/clientGenerator.mjs +5 -5
  61. package/dist/esm-node/utils/crossProjectServerPolicy.mjs +52 -0
  62. package/dist/esm-node/utils/effectClientGenerator.mjs +105 -484
  63. package/dist/types/runtime/create-request/index.d.ts +1 -0
  64. package/dist/types/runtime/data-platform/index.d.ts +4 -0
  65. package/dist/types/runtime/effect/adapter.d.ts +25 -0
  66. package/dist/types/runtime/effect/context.d.ts +3 -6
  67. package/dist/types/runtime/effect/edge.d.ts +25 -0
  68. package/dist/types/runtime/effect/endpoint-contracts.d.ts +62 -0
  69. package/dist/types/runtime/effect/handler.d.ts +203 -0
  70. package/dist/types/runtime/effect/index.d.ts +2 -170
  71. package/dist/types/runtime/effect/module.d.ts +48 -0
  72. package/dist/types/runtime/effect/operation-context.d.ts +10 -0
  73. package/dist/types/runtime/effect-client/index.d.ts +6 -1
  74. package/dist/types/runtime/effect-client/runtime.d.ts +71 -0
  75. package/dist/types/runtime/hono/adapter.d.ts +3 -0
  76. package/dist/types/runtime/safe-failure.d.ts +1 -0
  77. package/dist/types/utils/createHonoRoutes.d.ts +3 -3
  78. package/dist/types/utils/crossProjectServerPolicy.d.ts +35 -0
  79. package/dist/types/utils/effectClientGenerator.d.ts +16 -2
  80. package/package.json +41 -20
@@ -4,6 +4,24 @@ import { generateClient } from "@modern-js/bff-core";
4
4
  import { logger } from "@modern-js/utils";
5
5
  import path from "path";
6
6
  import { generateEffectClientCode, resolveEffectEntryFile } from "./utils/effectClientGenerator.mjs";
7
+ async function transformEffectRuntimeSource(source, filename) {
8
+ const swc = await import("@swc/core");
9
+ const result = await swc.transform(source, {
10
+ filename,
11
+ sourceMaps: false,
12
+ jsc: {
13
+ parser: {
14
+ syntax: "typescript",
15
+ tsx: filename.endsWith('.tsx') || filename.endsWith('.jsx')
16
+ },
17
+ target: 'es2022'
18
+ },
19
+ module: {
20
+ type: 'es6'
21
+ }
22
+ });
23
+ return result.code;
24
+ }
7
25
  async function loader(source) {
8
26
  this.cacheable();
9
27
  const { resourcePath } = this;
@@ -15,6 +33,11 @@ async function loader(source) {
15
33
  apiDir: draftOptions.apiDir,
16
34
  effectEntry: draftOptions.effectEntry
17
35
  });
36
+ if ('effect' === draftOptions.bffRuntimeFramework && effectEntryFile && path.resolve(effectEntryFile) === path.resolve(resourcePath) && this.resourceQuery.includes('modern-bff-runtime')) {
37
+ const code = await transformEffectRuntimeSource(source, resourcePath);
38
+ callback(void 0, code);
39
+ return;
40
+ }
18
41
  if ('effect' === draftOptions.bffRuntimeFramework && effectEntryFile && path.resolve(effectEntryFile) === path.resolve(resourcePath)) {
19
42
  const code = await generateEffectClientCode({
20
43
  appDir: draftOptions.appDir,
@@ -1,8 +1,30 @@
1
1
  import "node:module";
2
+ import { parseTraceparent } from "@modern-js/create-request";
3
+ import { trace as api_trace } from "@opentelemetry/api";
4
+ const DATA_BATCH_TRANSPORT_OTEL_EVENT = 'modernjs.data.batch';
5
+ function createDataBatchTransportTelemetryAttributes(event) {
6
+ return {
7
+ 'modernjs.data.batch.type': event.type,
8
+ 'modernjs.data.batch.endpoint': event.endpoint,
9
+ 'modernjs.data.batch.degraded': 'fallback' === event.type || 'disable' === event.type,
10
+ ...event.batchId ? {
11
+ 'modernjs.data.batch.id': event.batchId
12
+ } : {},
13
+ ...'number' == typeof event.size ? {
14
+ 'modernjs.data.batch.size': event.size
15
+ } : {},
16
+ ...event.reason ? {
17
+ 'modernjs.data.batch.reason': event.reason
18
+ } : {}
19
+ };
20
+ }
21
+ function emitDataBatchTransportEvent(onEvent, event) {
22
+ onEvent?.(event);
23
+ api_trace.getActiveSpan()?.addEvent(DATA_BATCH_TRANSPORT_OTEL_EVENT, createDataBatchTransportTelemetryAttributes(event));
24
+ }
2
25
  const DEFAULT_DATA_ENVELOPE_HEADER = 'x-modernjs-data-envelope';
3
26
  const DEFAULT_DATA_BATCH_ENDPOINT = '/_data/batch';
4
27
  const DEFAULT_DATA_BATCH_HEADER = 'x-modernjs-data-batch';
5
- const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
6
28
  function isPlainObject(value) {
7
29
  if ('object' != typeof value || null === value || Array.isArray(value)) return false;
8
30
  const proto = Object.getPrototypeOf(value);
@@ -99,18 +121,7 @@ function isValidHex(value, length) {
99
121
  return value.length === length && /^[0-9a-f]+$/.test(value);
100
122
  }
101
123
  function parseTraceparentHeader(header) {
102
- const match = header.trim().match(TRACEPARENT_REGEX);
103
- if (!match) return null;
104
- const traceId = match[1].toLowerCase();
105
- const spanId = match[2].toLowerCase();
106
- const flags = match[3].toLowerCase();
107
- if (isAllZeroHex(traceId) || isAllZeroHex(spanId)) return null;
108
- const sampled = (0x1 & Number.parseInt(flags, 16)) === 1;
109
- return {
110
- traceId,
111
- spanId,
112
- sampled
113
- };
124
+ return parseTraceparent(header) ?? null;
114
125
  }
115
126
  function formatTraceparentHeader(trace) {
116
127
  const traceId = trace.traceId.toLowerCase();
@@ -416,7 +427,7 @@ function createDataBatchTransport(options = {}) {
416
427
  bucket.items = [];
417
428
  bucket.bytes = 0;
418
429
  if (1 === items.length || disabledEndpoints.has(endpoint)) {
419
- onEvent?.({
430
+ emitDataBatchTransportEvent(onEvent, {
420
431
  type: disabledEndpoints.has(endpoint) ? 'fallback' : 'flush',
421
432
  endpoint,
422
433
  size: items.length,
@@ -433,7 +444,7 @@ function createDataBatchTransport(options = {}) {
433
444
  sentAt: Date.now(),
434
445
  items: items.map((item)=>item.item)
435
446
  };
436
- onEvent?.({
447
+ emitDataBatchTransportEvent(onEvent, {
437
448
  type: 'flush',
438
449
  endpoint,
439
450
  batchId,
@@ -460,7 +471,7 @@ function createDataBatchTransport(options = {}) {
460
471
  requestInit.signal = controller.signal;
461
472
  timeoutHandle = setTimeout(()=>{
462
473
  controller.abort();
463
- onEvent?.({
474
+ emitDataBatchTransportEvent(onEvent, {
464
475
  type: 'fallback',
465
476
  endpoint,
466
477
  batchId,
@@ -473,13 +484,13 @@ function createDataBatchTransport(options = {}) {
473
484
  if (!response.ok) {
474
485
  if (404 === response.status || 405 === response.status) {
475
486
  disabledEndpoints.add(endpoint);
476
- onEvent?.({
487
+ emitDataBatchTransportEvent(onEvent, {
477
488
  type: 'disable',
478
489
  endpoint,
479
490
  batchId,
480
491
  reason: `batch-endpoint-unavailable-${String(response.status)}`
481
492
  });
482
- } else onEvent?.({
493
+ } else emitDataBatchTransportEvent(onEvent, {
483
494
  type: 'fallback',
484
495
  endpoint,
485
496
  batchId,
@@ -492,7 +503,7 @@ function createDataBatchTransport(options = {}) {
492
503
  }
493
504
  const result = await response.json();
494
505
  if (!isBatchResponsePayload(result)) {
495
- onEvent?.({
506
+ emitDataBatchTransportEvent(onEvent, {
496
507
  type: 'fallback',
497
508
  endpoint,
498
509
  batchId,
@@ -515,7 +526,7 @@ function createDataBatchTransport(options = {}) {
515
526
  return parseResponseLikeCreateRequest(reconstructedResponse);
516
527
  });
517
528
  } catch (error) {
518
- onEvent?.({
529
+ emitDataBatchTransportEvent(onEvent, {
519
530
  type: 'fallback',
520
531
  endpoint,
521
532
  batchId,
@@ -582,7 +593,7 @@ function createDataBatchTransport(options = {}) {
582
593
  };
583
594
  bucket.items.push(queued);
584
595
  bucket.bytes += size;
585
- onEvent?.({
596
+ emitDataBatchTransportEvent(onEvent, {
586
597
  type: 'enqueue',
587
598
  endpoint,
588
599
  size: bucket.items.length
@@ -597,4 +608,4 @@ function createDataBatchTransport(options = {}) {
597
608
  return promise;
598
609
  };
599
610
  }
600
- export { DEFAULT_DATA_BATCH_ENDPOINT, DEFAULT_DATA_BATCH_HEADER, DEFAULT_DATA_ENVELOPE_HEADER, buildQueryKey, buildScopeKey, createDataBatchTransport, createHydrationEnvelope, createInvalidationEvent, createOperationId, createRequestEnvelope, decodeRequestEnvelopeHeader, deriveChildTraceContext, encodeRequestEnvelopeHeader, formatTraceparentHeader, normalizeOrigin, parseTraceparentHeader, shouldApplyInvalidation, stableStringify, validateHydrationEnvelope, validateRequestEnvelope, validateSelectionPlan };
611
+ export { DATA_BATCH_TRANSPORT_OTEL_EVENT, DEFAULT_DATA_BATCH_ENDPOINT, DEFAULT_DATA_BATCH_HEADER, DEFAULT_DATA_ENVELOPE_HEADER, buildQueryKey, buildScopeKey, createDataBatchTransport, createDataBatchTransportTelemetryAttributes, createHydrationEnvelope, createInvalidationEvent, createOperationId, createRequestEnvelope, decodeRequestEnvelopeHeader, deriveChildTraceContext, emitDataBatchTransportEvent, encodeRequestEnvelopeHeader, formatTraceparentHeader, normalizeOrigin, parseTraceparentHeader, shouldApplyInvalidation, stableStringify, validateHydrationEnvelope, validateRequestEnvelope, validateSelectionPlan };
@@ -1,10 +1,14 @@
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";
4
5
  import { HttpApi } from "effect/unstable/httpapi";
5
6
  import path from "path";
6
- import { runWithEffectContext } from "./context.mjs";
7
- import { createHttpApiHandler } from "./index.mjs";
7
+ import { checkCrossProjectPolicyForRequest, resolveAdapterCrossProjectPolicy } from "../../utils/crossProjectServerPolicy.mjs";
8
+ import { createSafeFailureResponse } from "../safe-failure.mjs";
9
+ import { createEffectOperationContext, runWithEffectContext } from "./context.mjs";
10
+ import { collectEffectEndpoints, extractHttpApiFromModule, toOperationContractSources } from "./endpoint-contracts.mjs";
11
+ import { resolveEffectBffModuleHandler } from "./module.mjs";
8
12
  const before = [
9
13
  'custom-server-hook',
10
14
  'custom-server-middleware',
@@ -37,24 +41,9 @@ function createRequestForMountedPrefix(req, prefix) {
37
41
  url.pathname = nextPath;
38
42
  return new Request(url, req);
39
43
  }
40
- function isRequestHandler(value) {
41
- return 'function' == typeof value;
42
- }
43
44
  function maybeResponse(value) {
44
45
  return value instanceof Response;
45
46
  }
46
- function isRecord(value) {
47
- return 'object' == typeof value && null !== value;
48
- }
49
- function includesRuntimeExports(value) {
50
- return 'api' in value || 'layer' in value || 'createHandler' in value || 'handler' in value;
51
- }
52
- function isHttpApiWithProps(value) {
53
- return HttpApi.isHttpApi(value) && isRecord(value) && 'string' == typeof value.identifier && isRecord(value.groups);
54
- }
55
- function isEffectApiDefinition(module) {
56
- return isHttpApiWithProps(module.api) && void 0 !== module.layer;
57
- }
58
47
  class EffectAdapter {
59
48
  resolveEntryFile() {
60
49
  const { appDirectory, apiDirectory } = this.api.getServerContext();
@@ -64,71 +53,77 @@ class EffectAdapter {
64
53
  const entryWithoutExt = configuredEntry ? path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry) : defaultEntry;
65
54
  return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
66
55
  }
67
- async loadEffectHandlerFromModule(mod) {
68
- let normalizedModule = mod;
69
- const mergeRuntimeExports = (value)=>{
70
- if (!isRecord(value) || !includesRuntimeExports(value)) return;
71
- normalizedModule = {
72
- ...normalizedModule,
73
- ...value
74
- };
75
- };
76
- if (isRequestHandler(normalizedModule.handler)) return {
77
- handler: normalizedModule.handler
78
- };
79
- const entry = normalizedModule.default;
80
- if (isRequestHandler(entry)) return {
81
- handler: entry
82
- };
83
- if ('function' == typeof entry && 0 === entry.length) {
84
- const out = await entry();
85
- if (isRequestHandler(out)) return {
86
- handler: out
87
- };
88
- mergeRuntimeExports(out);
89
- }
90
- if (isRecord(entry)) normalizedModule = {
91
- ...normalizedModule,
92
- ...entry
93
- };
94
- if (isRecord(entry) && 'handler' in entry) {
95
- const maybeHandler = entry.handler;
96
- if (isRequestHandler(maybeHandler)) normalizedModule = {
97
- ...normalizedModule,
98
- handler: maybeHandler
99
- };
100
- }
101
- if (isRequestHandler(normalizedModule.handler)) return {
102
- handler: normalizedModule.handler
103
- };
104
- if ('function' == typeof normalizedModule.createHandler) {
105
- const webHandler = normalizedModule.createHandler({
106
- openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
107
- dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform
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
108
76
  });
109
- return {
110
- handler: async (request)=>webHandler.handler(request),
111
- dispose: async ()=>{
112
- await webHandler.dispose();
113
- }
114
- };
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 [];
115
88
  }
116
- if (isEffectApiDefinition(normalizedModule)) {
117
- logger.warn('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
118
- const webHandler = createHttpApiHandler({
119
- api: normalizedModule.api,
120
- layer: normalizedModule.layer,
121
- openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
122
- dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform
123
- });
124
- return {
125
- handler: async (request)=>webHandler.handler(request),
126
- dispose: async ()=>{
127
- await webHandler.dispose();
128
- }
129
- };
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)}`);
130
103
  }
131
- return null;
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
+ }
118
+ async loadEffectHandlerFromModule(mod) {
119
+ return resolveEffectBffModuleHandler(mod, {
120
+ openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
121
+ dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform,
122
+ validateRequest: (request)=>checkCrossProjectPolicyForRequest(request, this.crossProjectPolicy),
123
+ onWarning: (message)=>{
124
+ logger.warn(message);
125
+ }
126
+ });
132
127
  }
133
128
  async reloadHandler() {
134
129
  if (!this.isEffect) return;
@@ -154,6 +149,7 @@ class EffectAdapter {
154
149
  this.handler = null;
155
150
  return;
156
151
  }
152
+ await this.refreshCrossProjectPolicy(mod);
157
153
  const loaded = await this.loadEffectHandlerFromModule(mod);
158
154
  if (!loaded) {
159
155
  logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer } or handler.`);
@@ -162,6 +158,7 @@ class EffectAdapter {
162
158
  }
163
159
  this.handler = loaded.handler;
164
160
  this.dispose = loaded.dispose || null;
161
+ this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
165
162
  }
166
163
  async disposeCurrentHandler() {
167
164
  if (!this.dispose) return;
@@ -185,15 +182,7 @@ class EffectAdapter {
185
182
  } catch (configError) {
186
183
  logger.error(`Error in serverConfig.onError handler: ${configError}`);
187
184
  }
188
- const status = 'object' == typeof error && null !== error && 'status' in error && 'number' == typeof error.status ? error.status : 500;
189
- return new Response(JSON.stringify({
190
- message: error instanceof Error ? error.message : '[BFF] Internal Server Error'
191
- }), {
192
- status,
193
- headers: {
194
- 'content-type': 'application/json; charset=utf-8'
195
- }
196
- });
185
+ return createSafeFailureResponse(error);
197
186
  }
198
187
  ensureJsonContext(c) {
199
188
  const maybeJsonContext = c;
@@ -220,6 +209,8 @@ class EffectAdapter {
220
209
  this.effectMiddleware = null;
221
210
  this.handler = null;
222
211
  this.dispose = null;
212
+ this.prefix = '/api';
213
+ this.policyEnforcedInMiddleware = false;
223
214
  this.registerMiddleware = async (options)=>{
224
215
  const { prefix, enableHandleWeb } = options;
225
216
  const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
@@ -227,6 +218,7 @@ class EffectAdapter {
227
218
  this.isEffect = false;
228
219
  return;
229
220
  }
221
+ this.prefix = prefix || this.prefix;
230
222
  await this.reloadHandler();
231
223
  this.effectMiddleware = {
232
224
  name: 'effect-bff-handler',
@@ -239,6 +231,10 @@ class EffectAdapter {
239
231
  if (enableHandleWeb) return void await next();
240
232
  return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/effect/index or configure bff.effect.entry.'), c);
241
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
+ }
242
238
  let response;
243
239
  try {
244
240
  const effectRequest = createRequestForMountedPrefix(c.req.raw, prefix);
@@ -246,7 +242,13 @@ class EffectAdapter {
246
242
  request: effectRequest,
247
243
  env: c.env,
248
244
  path: c.req.path,
249
- method: c.req.method
245
+ method: c.req.method,
246
+ operationContext: createEffectOperationContext({
247
+ request: effectRequest,
248
+ env: c.env,
249
+ path: c.req.path,
250
+ method: c.req.method
251
+ })
250
252
  };
251
253
  response = await runWithEffectContext(effectContext, ()=>this.handler.length > 1 ? this.handler(effectRequest, effectContext) : this.handler(effectRequest));
252
254
  } catch (error) {
@@ -9,4 +9,6 @@ const useEffectContext = ()=>{
9
9
  if (!context) throw new Error("Can't call useEffectContext out of Effect runtime scope");
10
10
  return context;
11
11
  };
12
- export { runWithEffectContext, useEffectContext };
12
+ const useOperationContext = ()=>useEffectContext().operationContext;
13
+ export { createEffectOperationContext } from "./operation-context.mjs";
14
+ export { runWithEffectContext, useEffectContext, useOperationContext };
@@ -0,0 +1,84 @@
1
+ import "node:module";
2
+ import { createSafeFailureResponse } from "../safe-failure.mjs";
3
+ import { resolveEffectBffModuleHandler } from "./module.mjs";
4
+ import { createEffectOperationContext } from "./operation-context.mjs";
5
+ export * from "./handler.mjs";
6
+ function normalizePrefix(prefix) {
7
+ if (!prefix || '/' === prefix) return '';
8
+ return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
9
+ }
10
+ function removePrefixFromPath(pathname, prefix) {
11
+ const normalized = normalizePrefix(prefix);
12
+ if (!normalized || pathname !== normalized && !pathname.startsWith(`${normalized}/`)) return pathname;
13
+ const sliced = pathname.slice(normalized.length);
14
+ return sliced.startsWith('/') ? sliced : `/${sliced}`;
15
+ }
16
+ function matchesPrefix(pathname, prefix) {
17
+ const normalized = normalizePrefix(prefix);
18
+ return !normalized || pathname === normalized || pathname.startsWith(`${normalized}/`);
19
+ }
20
+ function createRequestForMountedPrefix(req, prefix) {
21
+ const url = new URL(req.url);
22
+ const nextPath = removePrefixFromPath(url.pathname, prefix);
23
+ if (nextPath === url.pathname) return req;
24
+ url.pathname = nextPath;
25
+ return new Request(url, req);
26
+ }
27
+ function createEdgeEffectContext(originalRequest, effectRequest, options) {
28
+ const originalPath = options.path || new URL(originalRequest.url).pathname;
29
+ const method = options.method || originalRequest.method;
30
+ return {
31
+ request: effectRequest,
32
+ env: options.env || {},
33
+ path: originalPath,
34
+ method,
35
+ operationContext: createEffectOperationContext({
36
+ request: effectRequest,
37
+ env: options.env || {},
38
+ path: originalPath,
39
+ method
40
+ })
41
+ };
42
+ }
43
+ function createRuntimeErrorResponse(error) {
44
+ return createSafeFailureResponse(error);
45
+ }
46
+ async function dispatchEffectBffRequest(handler, request, options = {}) {
47
+ const requestPathname = new URL(request.url).pathname;
48
+ if (!matchesPrefix(requestPathname, options.prefix)) return new Response(null, {
49
+ status: 404
50
+ });
51
+ const effectRequest = createRequestForMountedPrefix(request, options.prefix);
52
+ const effectContext = createEdgeEffectContext(request, effectRequest, options);
53
+ try {
54
+ const response = handler.length > 1 ? await handler(effectRequest, effectContext) : await handler(effectRequest);
55
+ if (!(response instanceof Response)) throw new Error('[BFF][Effect] Effect handler must return a Response instance.');
56
+ return new Response(response.body, response);
57
+ } catch (error) {
58
+ if (error instanceof Response) return new Response(error.body, error);
59
+ if (options.onError) {
60
+ const errorResponse = await options.onError(error, effectContext);
61
+ if (errorResponse instanceof Response) return errorResponse;
62
+ }
63
+ return createRuntimeErrorResponse(error);
64
+ }
65
+ }
66
+ async function createEffectBffEdgeHandler(options) {
67
+ const loaded = await resolveEffectBffModuleHandler(options.module, {
68
+ openapi: options.openapi,
69
+ dataPlatform: options.dataPlatform,
70
+ onWarning: options.onWarning
71
+ });
72
+ if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }, createHandler, or handler.');
73
+ return {
74
+ handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
75
+ ...dispatchOptions,
76
+ prefix: options.prefix,
77
+ onError: options.onError
78
+ }),
79
+ dispose: async ()=>{
80
+ await loaded.dispose?.();
81
+ }
82
+ };
83
+ }
84
+ export { createEffectBffEdgeHandler, createEffectOperationContext, dispatchEffectBffRequest };
@@ -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 };