@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
@@ -10,11 +10,15 @@ var __webpack_require__ = {};
10
10
  };
11
11
  })();
12
12
  (()=>{
13
- __webpack_require__.d = (exports1, definition)=>{
14
- for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
15
- enumerable: true,
16
- get: definition[key]
17
- });
13
+ __webpack_require__.d = (exports1, getters, values)=>{
14
+ var define = (defs, kind)=>{
15
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
16
+ enumerable: true,
17
+ [kind]: defs[key]
18
+ });
19
+ };
20
+ define(getters, "get");
21
+ define(values, "value");
18
22
  };
19
23
  })();
20
24
  (()=>{
@@ -10,11 +10,15 @@ var __webpack_require__ = {};
10
10
  };
11
11
  })();
12
12
  (()=>{
13
- __webpack_require__.d = (exports1, definition)=>{
14
- for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
15
- enumerable: true,
16
- get: definition[key]
17
- });
13
+ __webpack_require__.d = (exports1, getters, values)=>{
14
+ var define = (defs, kind)=>{
15
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
16
+ enumerable: true,
17
+ [kind]: defs[key]
18
+ });
19
+ };
20
+ define(getters, "get");
21
+ define(values, "value");
18
22
  };
19
23
  })();
20
24
  (()=>{
@@ -2,6 +2,24 @@ import { generateClient } from "@modern-js/bff-core";
2
2
  import { logger } from "@modern-js/utils";
3
3
  import path from "path";
4
4
  import { generateEffectClientCode, resolveEffectEntryFile } from "./utils/effectClientGenerator.mjs";
5
+ async function transformEffectRuntimeSource(source, filename) {
6
+ const swc = await import("@swc/core");
7
+ const result = await swc.transform(source, {
8
+ filename,
9
+ sourceMaps: false,
10
+ jsc: {
11
+ parser: {
12
+ syntax: "typescript",
13
+ tsx: filename.endsWith('.tsx') || filename.endsWith('.jsx')
14
+ },
15
+ target: 'es2022'
16
+ },
17
+ module: {
18
+ type: 'es6'
19
+ }
20
+ });
21
+ return result.code;
22
+ }
5
23
  async function loader(source) {
6
24
  this.cacheable();
7
25
  const { resourcePath } = this;
@@ -13,6 +31,11 @@ async function loader(source) {
13
31
  apiDir: draftOptions.apiDir,
14
32
  effectEntry: draftOptions.effectEntry
15
33
  });
34
+ if ('effect' === draftOptions.bffRuntimeFramework && effectEntryFile && path.resolve(effectEntryFile) === path.resolve(resourcePath) && this.resourceQuery.includes('modern-bff-runtime')) {
35
+ const code = await transformEffectRuntimeSource(source, resourcePath);
36
+ callback(void 0, code);
37
+ return;
38
+ }
16
39
  if ('effect' === draftOptions.bffRuntimeFramework && effectEntryFile && path.resolve(effectEntryFile) === path.resolve(resourcePath)) {
17
40
  const code = await generateEffectClientCode({
18
41
  appDir: draftOptions.appDir,
@@ -1,7 +1,29 @@
1
+ import { parseTraceparent } from "@modern-js/create-request";
2
+ import { trace as api_trace } from "@opentelemetry/api";
3
+ const DATA_BATCH_TRANSPORT_OTEL_EVENT = 'modernjs.data.batch';
4
+ function createDataBatchTransportTelemetryAttributes(event) {
5
+ return {
6
+ 'modernjs.data.batch.type': event.type,
7
+ 'modernjs.data.batch.endpoint': event.endpoint,
8
+ 'modernjs.data.batch.degraded': 'fallback' === event.type || 'disable' === event.type,
9
+ ...event.batchId ? {
10
+ 'modernjs.data.batch.id': event.batchId
11
+ } : {},
12
+ ...'number' == typeof event.size ? {
13
+ 'modernjs.data.batch.size': event.size
14
+ } : {},
15
+ ...event.reason ? {
16
+ 'modernjs.data.batch.reason': event.reason
17
+ } : {}
18
+ };
19
+ }
20
+ function emitDataBatchTransportEvent(onEvent, event) {
21
+ onEvent?.(event);
22
+ api_trace.getActiveSpan()?.addEvent(DATA_BATCH_TRANSPORT_OTEL_EVENT, createDataBatchTransportTelemetryAttributes(event));
23
+ }
1
24
  const DEFAULT_DATA_ENVELOPE_HEADER = 'x-modernjs-data-envelope';
2
25
  const DEFAULT_DATA_BATCH_ENDPOINT = '/_data/batch';
3
26
  const DEFAULT_DATA_BATCH_HEADER = 'x-modernjs-data-batch';
4
- const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
5
27
  function isPlainObject(value) {
6
28
  if ('object' != typeof value || null === value || Array.isArray(value)) return false;
7
29
  const proto = Object.getPrototypeOf(value);
@@ -98,18 +120,7 @@ function isValidHex(value, length) {
98
120
  return value.length === length && /^[0-9a-f]+$/.test(value);
99
121
  }
100
122
  function parseTraceparentHeader(header) {
101
- const match = header.trim().match(TRACEPARENT_REGEX);
102
- if (!match) return null;
103
- const traceId = match[1].toLowerCase();
104
- const spanId = match[2].toLowerCase();
105
- const flags = match[3].toLowerCase();
106
- if (isAllZeroHex(traceId) || isAllZeroHex(spanId)) return null;
107
- const sampled = (0x1 & Number.parseInt(flags, 16)) === 1;
108
- return {
109
- traceId,
110
- spanId,
111
- sampled
112
- };
123
+ return parseTraceparent(header) ?? null;
113
124
  }
114
125
  function formatTraceparentHeader(trace) {
115
126
  const traceId = trace.traceId.toLowerCase();
@@ -415,7 +426,7 @@ function createDataBatchTransport(options = {}) {
415
426
  bucket.items = [];
416
427
  bucket.bytes = 0;
417
428
  if (1 === items.length || disabledEndpoints.has(endpoint)) {
418
- onEvent?.({
429
+ emitDataBatchTransportEvent(onEvent, {
419
430
  type: disabledEndpoints.has(endpoint) ? 'fallback' : 'flush',
420
431
  endpoint,
421
432
  size: items.length,
@@ -432,7 +443,7 @@ function createDataBatchTransport(options = {}) {
432
443
  sentAt: Date.now(),
433
444
  items: items.map((item)=>item.item)
434
445
  };
435
- onEvent?.({
446
+ emitDataBatchTransportEvent(onEvent, {
436
447
  type: 'flush',
437
448
  endpoint,
438
449
  batchId,
@@ -459,7 +470,7 @@ function createDataBatchTransport(options = {}) {
459
470
  requestInit.signal = controller.signal;
460
471
  timeoutHandle = setTimeout(()=>{
461
472
  controller.abort();
462
- onEvent?.({
473
+ emitDataBatchTransportEvent(onEvent, {
463
474
  type: 'fallback',
464
475
  endpoint,
465
476
  batchId,
@@ -472,13 +483,13 @@ function createDataBatchTransport(options = {}) {
472
483
  if (!response.ok) {
473
484
  if (404 === response.status || 405 === response.status) {
474
485
  disabledEndpoints.add(endpoint);
475
- onEvent?.({
486
+ emitDataBatchTransportEvent(onEvent, {
476
487
  type: 'disable',
477
488
  endpoint,
478
489
  batchId,
479
490
  reason: `batch-endpoint-unavailable-${String(response.status)}`
480
491
  });
481
- } else onEvent?.({
492
+ } else emitDataBatchTransportEvent(onEvent, {
482
493
  type: 'fallback',
483
494
  endpoint,
484
495
  batchId,
@@ -491,7 +502,7 @@ function createDataBatchTransport(options = {}) {
491
502
  }
492
503
  const result = await response.json();
493
504
  if (!isBatchResponsePayload(result)) {
494
- onEvent?.({
505
+ emitDataBatchTransportEvent(onEvent, {
495
506
  type: 'fallback',
496
507
  endpoint,
497
508
  batchId,
@@ -514,7 +525,7 @@ function createDataBatchTransport(options = {}) {
514
525
  return parseResponseLikeCreateRequest(reconstructedResponse);
515
526
  });
516
527
  } catch (error) {
517
- onEvent?.({
528
+ emitDataBatchTransportEvent(onEvent, {
518
529
  type: 'fallback',
519
530
  endpoint,
520
531
  batchId,
@@ -581,7 +592,7 @@ function createDataBatchTransport(options = {}) {
581
592
  };
582
593
  bucket.items.push(queued);
583
594
  bucket.bytes += size;
584
- onEvent?.({
595
+ emitDataBatchTransportEvent(onEvent, {
585
596
  type: 'enqueue',
586
597
  endpoint,
587
598
  size: bucket.items.length
@@ -596,4 +607,4 @@ function createDataBatchTransport(options = {}) {
596
607
  return promise;
597
608
  };
598
609
  }
599
- 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 };
610
+ 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,8 +1,12 @@
1
+ import { ApiRouter } from "@modern-js/bff-core";
1
2
  import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
2
3
  import { HttpApi } from "effect/unstable/httpapi";
3
4
  import path from "path";
4
- import { runWithEffectContext } from "./context.mjs";
5
- import { createHttpApiHandler } from "./index.mjs";
5
+ import { checkCrossProjectPolicyForRequest, resolveAdapterCrossProjectPolicy } from "../../utils/crossProjectServerPolicy.mjs";
6
+ import { createSafeFailureResponse } from "../safe-failure.mjs";
7
+ import { createEffectOperationContext, runWithEffectContext } from "./context.mjs";
8
+ import { collectEffectEndpoints, extractHttpApiFromModule, toOperationContractSources } from "./endpoint-contracts.mjs";
9
+ import { resolveEffectBffModuleHandler } from "./module.mjs";
6
10
  const before = [
7
11
  'custom-server-hook',
8
12
  'custom-server-middleware',
@@ -35,24 +39,9 @@ function createRequestForMountedPrefix(req, prefix) {
35
39
  url.pathname = nextPath;
36
40
  return new Request(url, req);
37
41
  }
38
- function isRequestHandler(value) {
39
- return 'function' == typeof value;
40
- }
41
42
  function maybeResponse(value) {
42
43
  return value instanceof Response;
43
44
  }
44
- function isRecord(value) {
45
- return 'object' == typeof value && null !== value;
46
- }
47
- function includesRuntimeExports(value) {
48
- return 'api' in value || 'layer' in value || 'createHandler' in value || 'handler' in value;
49
- }
50
- function isHttpApiWithProps(value) {
51
- return HttpApi.isHttpApi(value) && isRecord(value) && 'string' == typeof value.identifier && isRecord(value.groups);
52
- }
53
- function isEffectApiDefinition(module) {
54
- return isHttpApiWithProps(module.api) && void 0 !== module.layer;
55
- }
56
45
  class EffectAdapter {
57
46
  resolveEntryFile() {
58
47
  const { appDirectory, apiDirectory } = this.api.getServerContext();
@@ -62,71 +51,77 @@ class EffectAdapter {
62
51
  const entryWithoutExt = configuredEntry ? path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry) : defaultEntry;
63
52
  return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
64
53
  }
65
- async loadEffectHandlerFromModule(mod) {
66
- let normalizedModule = mod;
67
- const mergeRuntimeExports = (value)=>{
68
- if (!isRecord(value) || !includesRuntimeExports(value)) return;
69
- normalizedModule = {
70
- ...normalizedModule,
71
- ...value
72
- };
73
- };
74
- if (isRequestHandler(normalizedModule.handler)) return {
75
- handler: normalizedModule.handler
76
- };
77
- const entry = normalizedModule.default;
78
- if (isRequestHandler(entry)) return {
79
- handler: entry
80
- };
81
- if ('function' == typeof entry && 0 === entry.length) {
82
- const out = await entry();
83
- if (isRequestHandler(out)) return {
84
- handler: out
85
- };
86
- mergeRuntimeExports(out);
87
- }
88
- if (isRecord(entry)) normalizedModule = {
89
- ...normalizedModule,
90
- ...entry
91
- };
92
- if (isRecord(entry) && 'handler' in entry) {
93
- const maybeHandler = entry.handler;
94
- if (isRequestHandler(maybeHandler)) normalizedModule = {
95
- ...normalizedModule,
96
- handler: maybeHandler
97
- };
98
- }
99
- if (isRequestHandler(normalizedModule.handler)) return {
100
- handler: normalizedModule.handler
101
- };
102
- if ('function' == typeof normalizedModule.createHandler) {
103
- const webHandler = normalizedModule.createHandler({
104
- openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
105
- dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform
54
+ isApiRequestPath(requestPath, prefix, enableHandleWeb) {
55
+ if (!enableHandleWeb) return true;
56
+ const normalized = normalizePrefix(prefix);
57
+ if (!normalized) return true;
58
+ return requestPath === normalized || requestPath.startsWith(`${normalized}/`);
59
+ }
60
+ async collectLambdaContractSources() {
61
+ try {
62
+ const serverContext = this.api.getServerContext();
63
+ const appDir = serverContext.distDirectory || serverContext.appDirectory;
64
+ if (!appDir) return [];
65
+ const apiDir = 'string' == typeof serverContext.apiDirectory ? serverContext.apiDirectory : path.resolve(appDir, API_DIR);
66
+ const lambdaDir = 'string' == typeof serverContext.lambdaDirectory ? serverContext.lambdaDirectory : path.join(apiDir, 'lambda');
67
+ if (!await fs.pathExists(lambdaDir)) return [];
68
+ const apiRouter = new ApiRouter({
69
+ appDir,
70
+ apiDir,
71
+ lambdaDir,
72
+ prefix: this.prefix,
73
+ httpMethodDecider: this.api.getServerConfig()?.bff?.httpMethodDecider
106
74
  });
107
- return {
108
- handler: async (request)=>webHandler.handler(request),
109
- dispose: async ()=>{
110
- await webHandler.dispose();
111
- }
112
- };
75
+ const handlerInfos = await apiRouter.getApiHandlers();
76
+ return handlerInfos.map((info)=>({
77
+ name: info.name,
78
+ httpMethod: info.httpMethod,
79
+ routePath: info.routePath,
80
+ filename: info.filename,
81
+ handler: info.handler
82
+ }));
83
+ } catch (error) {
84
+ logger.warn(`[BFF][Effect] Failed to derive lambda operation contracts for the cross-project policy: ${String(error)}`);
85
+ return [];
113
86
  }
114
- if (isEffectApiDefinition(normalizedModule)) {
115
- logger.warn('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
116
- const webHandler = createHttpApiHandler({
117
- api: normalizedModule.api,
118
- layer: normalizedModule.layer,
119
- openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
120
- dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform
121
- });
122
- return {
123
- handler: async (request)=>webHandler.handler(request),
124
- dispose: async ()=>{
125
- await webHandler.dispose();
126
- }
127
- };
87
+ }
88
+ async refreshCrossProjectPolicy(mod) {
89
+ let contractSources = [];
90
+ if (mod) try {
91
+ const api = await extractHttpApiFromModule(mod, HttpApi.isHttpApi);
92
+ if (api) {
93
+ const reflect = (apiValue, handlers)=>HttpApi.reflect(apiValue, {
94
+ onGroup: handlers.onGroup ?? (()=>{}),
95
+ onEndpoint: handlers.onEndpoint
96
+ });
97
+ contractSources = toOperationContractSources(collectEffectEndpoints(reflect, api, this.prefix));
98
+ }
99
+ } catch (error) {
100
+ logger.warn(`[BFF][Effect] Failed to reflect HttpApi endpoints for the cross-project policy: ${String(error)}`);
128
101
  }
129
- return null;
102
+ let policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
103
+ if (policy?.enabled) {
104
+ const lambdaSources = await this.collectLambdaContractSources();
105
+ if (lambdaSources.length > 0) {
106
+ contractSources = [
107
+ ...contractSources,
108
+ ...lambdaSources
109
+ ];
110
+ policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
111
+ }
112
+ }
113
+ this.crossProjectPolicy = policy;
114
+ if (this.crossProjectPolicy?.enabled && 0 === contractSources.length) logger.warn('[BFF][Effect] Cross-project policy is enabled but no HttpApi endpoints could be reflected; operation-contract matching is disabled for this server (envelope and operation-context checks still apply).');
115
+ }
116
+ async loadEffectHandlerFromModule(mod) {
117
+ return resolveEffectBffModuleHandler(mod, {
118
+ openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
119
+ dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform,
120
+ validateRequest: (request)=>checkCrossProjectPolicyForRequest(request, this.crossProjectPolicy),
121
+ onWarning: (message)=>{
122
+ logger.warn(message);
123
+ }
124
+ });
130
125
  }
131
126
  async reloadHandler() {
132
127
  if (!this.isEffect) return;
@@ -152,6 +147,7 @@ class EffectAdapter {
152
147
  this.handler = null;
153
148
  return;
154
149
  }
150
+ await this.refreshCrossProjectPolicy(mod);
155
151
  const loaded = await this.loadEffectHandlerFromModule(mod);
156
152
  if (!loaded) {
157
153
  logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer } or handler.`);
@@ -160,6 +156,7 @@ class EffectAdapter {
160
156
  }
161
157
  this.handler = loaded.handler;
162
158
  this.dispose = loaded.dispose || null;
159
+ this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
163
160
  }
164
161
  async disposeCurrentHandler() {
165
162
  if (!this.dispose) return;
@@ -183,15 +180,7 @@ class EffectAdapter {
183
180
  } catch (configError) {
184
181
  logger.error(`Error in serverConfig.onError handler: ${configError}`);
185
182
  }
186
- const status = 'object' == typeof error && null !== error && 'status' in error && 'number' == typeof error.status ? error.status : 500;
187
- return new Response(JSON.stringify({
188
- message: error instanceof Error ? error.message : '[BFF] Internal Server Error'
189
- }), {
190
- status,
191
- headers: {
192
- 'content-type': 'application/json; charset=utf-8'
193
- }
194
- });
183
+ return createSafeFailureResponse(error);
195
184
  }
196
185
  ensureJsonContext(c) {
197
186
  const maybeJsonContext = c;
@@ -218,6 +207,8 @@ class EffectAdapter {
218
207
  this.effectMiddleware = null;
219
208
  this.handler = null;
220
209
  this.dispose = null;
210
+ this.prefix = '/api';
211
+ this.policyEnforcedInMiddleware = false;
221
212
  this.registerMiddleware = async (options)=>{
222
213
  const { prefix, enableHandleWeb } = options;
223
214
  const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
@@ -225,6 +216,7 @@ class EffectAdapter {
225
216
  this.isEffect = false;
226
217
  return;
227
218
  }
219
+ this.prefix = prefix || this.prefix;
228
220
  await this.reloadHandler();
229
221
  this.effectMiddleware = {
230
222
  name: 'effect-bff-handler',
@@ -237,6 +229,10 @@ class EffectAdapter {
237
229
  if (enableHandleWeb) return void await next();
238
230
  return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/effect/index or configure bff.effect.entry.'), c);
239
231
  }
232
+ if (this.crossProjectPolicy?.enabled && this.policyEnforcedInMiddleware && this.isApiRequestPath(c.req.path, prefix, enableHandleWeb)) {
233
+ const denial = checkCrossProjectPolicyForRequest(c.req.raw, this.crossProjectPolicy);
234
+ if (denial) return denial;
235
+ }
240
236
  let response;
241
237
  try {
242
238
  const effectRequest = createRequestForMountedPrefix(c.req.raw, prefix);
@@ -244,7 +240,13 @@ class EffectAdapter {
244
240
  request: effectRequest,
245
241
  env: c.env,
246
242
  path: c.req.path,
247
- method: c.req.method
243
+ method: c.req.method,
244
+ operationContext: createEffectOperationContext({
245
+ request: effectRequest,
246
+ env: c.env,
247
+ path: c.req.path,
248
+ method: c.req.method
249
+ })
248
250
  };
249
251
  response = await runWithEffectContext(effectContext, ()=>this.handler.length > 1 ? this.handler(effectRequest, effectContext) : this.handler(effectRequest));
250
252
  } catch (error) {
@@ -8,4 +8,6 @@ const useEffectContext = ()=>{
8
8
  if (!context) throw new Error("Can't call useEffectContext out of Effect runtime scope");
9
9
  return context;
10
10
  };
11
- export { runWithEffectContext, useEffectContext };
11
+ const useOperationContext = ()=>useEffectContext().operationContext;
12
+ export { createEffectOperationContext } from "./operation-context.mjs";
13
+ export { runWithEffectContext, useEffectContext, useOperationContext };
@@ -0,0 +1,83 @@
1
+ import { createSafeFailureResponse } from "../safe-failure.mjs";
2
+ import { resolveEffectBffModuleHandler } from "./module.mjs";
3
+ import { createEffectOperationContext } from "./operation-context.mjs";
4
+ export * from "./handler.mjs";
5
+ function normalizePrefix(prefix) {
6
+ if (!prefix || '/' === prefix) return '';
7
+ return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
8
+ }
9
+ function removePrefixFromPath(pathname, prefix) {
10
+ const normalized = normalizePrefix(prefix);
11
+ if (!normalized || pathname !== normalized && !pathname.startsWith(`${normalized}/`)) return pathname;
12
+ const sliced = pathname.slice(normalized.length);
13
+ return sliced.startsWith('/') ? sliced : `/${sliced}`;
14
+ }
15
+ function matchesPrefix(pathname, prefix) {
16
+ const normalized = normalizePrefix(prefix);
17
+ return !normalized || pathname === normalized || pathname.startsWith(`${normalized}/`);
18
+ }
19
+ function createRequestForMountedPrefix(req, prefix) {
20
+ const url = new URL(req.url);
21
+ const nextPath = removePrefixFromPath(url.pathname, prefix);
22
+ if (nextPath === url.pathname) return req;
23
+ url.pathname = nextPath;
24
+ return new Request(url, req);
25
+ }
26
+ function createEdgeEffectContext(originalRequest, effectRequest, options) {
27
+ const originalPath = options.path || new URL(originalRequest.url).pathname;
28
+ const method = options.method || originalRequest.method;
29
+ return {
30
+ request: effectRequest,
31
+ env: options.env || {},
32
+ path: originalPath,
33
+ method,
34
+ operationContext: createEffectOperationContext({
35
+ request: effectRequest,
36
+ env: options.env || {},
37
+ path: originalPath,
38
+ method
39
+ })
40
+ };
41
+ }
42
+ function createRuntimeErrorResponse(error) {
43
+ return createSafeFailureResponse(error);
44
+ }
45
+ async function dispatchEffectBffRequest(handler, request, options = {}) {
46
+ const requestPathname = new URL(request.url).pathname;
47
+ if (!matchesPrefix(requestPathname, options.prefix)) return new Response(null, {
48
+ status: 404
49
+ });
50
+ const effectRequest = createRequestForMountedPrefix(request, options.prefix);
51
+ const effectContext = createEdgeEffectContext(request, effectRequest, options);
52
+ try {
53
+ const response = handler.length > 1 ? await handler(effectRequest, effectContext) : await handler(effectRequest);
54
+ if (!(response instanceof Response)) throw new Error('[BFF][Effect] Effect handler must return a Response instance.');
55
+ return new Response(response.body, response);
56
+ } catch (error) {
57
+ if (error instanceof Response) return new Response(error.body, error);
58
+ if (options.onError) {
59
+ const errorResponse = await options.onError(error, effectContext);
60
+ if (errorResponse instanceof Response) return errorResponse;
61
+ }
62
+ return createRuntimeErrorResponse(error);
63
+ }
64
+ }
65
+ async function createEffectBffEdgeHandler(options) {
66
+ const loaded = await resolveEffectBffModuleHandler(options.module, {
67
+ openapi: options.openapi,
68
+ dataPlatform: options.dataPlatform,
69
+ onWarning: options.onWarning
70
+ });
71
+ if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }, createHandler, or handler.');
72
+ return {
73
+ handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
74
+ ...dispatchOptions,
75
+ prefix: options.prefix,
76
+ onError: options.onError
77
+ }),
78
+ dispose: async ()=>{
79
+ await loaded.dispose?.();
80
+ }
81
+ };
82
+ }
83
+ export { createEffectBffEdgeHandler, createEffectOperationContext, dispatchEffectBffRequest };
@@ -0,0 +1,68 @@
1
+ import { createOperationContractHash } from "@modern-js/bff-core";
2
+ function ensureLeadingSlash(pathname) {
3
+ return pathname.startsWith('/') ? pathname : `/${pathname}`;
4
+ }
5
+ function normalizeEffectPrefix(prefix) {
6
+ if ('/' === prefix) return '';
7
+ return ensureLeadingSlash(prefix || '/api');
8
+ }
9
+ function getEffectRoutePath(prefix, endpointPath) {
10
+ const normalizedPrefix = normalizeEffectPrefix(prefix);
11
+ const normalizedEndpointPath = ensureLeadingSlash(endpointPath);
12
+ const finalEndpointPath = '/' === normalizedEndpointPath ? '' : endpointPath;
13
+ if (!normalizedPrefix && !finalEndpointPath) return '/';
14
+ return `${normalizedPrefix}${finalEndpointPath || ''}`;
15
+ }
16
+ function resolveEffectApiId(api) {
17
+ const fallback = 'EffectHttpApi';
18
+ if ('identifier' in api && 'string' == typeof api.identifier && api.identifier) return api.identifier;
19
+ return fallback;
20
+ }
21
+ function collectEffectEndpoints(reflect, api, prefix) {
22
+ const endpoints = [];
23
+ const apiId = resolveEffectApiId(api);
24
+ reflect(api, {
25
+ onGroup: ()=>{},
26
+ onEndpoint: ({ group, endpoint })=>{
27
+ endpoints.push({
28
+ apiId,
29
+ groupName: String(group.identifier),
30
+ endpointName: String(endpoint.name),
31
+ method: String(endpoint.method).toUpperCase(),
32
+ routePath: getEffectRoutePath(prefix, String(endpoint.path))
33
+ });
34
+ }
35
+ });
36
+ return endpoints.sort((a, b)=>{
37
+ if (a.groupName === b.groupName) return a.endpointName.localeCompare(b.endpointName);
38
+ return a.groupName.localeCompare(b.groupName);
39
+ });
40
+ }
41
+ function toOperationContractSources(endpoints) {
42
+ return endpoints.map(createEffectOperationContractSource);
43
+ }
44
+ function createEffectOperationContractSource(endpoint) {
45
+ return {
46
+ name: endpoint.endpointName,
47
+ httpMethod: endpoint.method,
48
+ routePath: endpoint.routePath
49
+ };
50
+ }
51
+ function isRecord(value) {
52
+ return 'object' == typeof value && null !== value;
53
+ }
54
+ async function extractHttpApiFromModule(mod, isHttpApi) {
55
+ if (!isRecord(mod)) return null;
56
+ if (isHttpApi(mod.api)) return mod.api;
57
+ const entry = mod.default;
58
+ if (isRecord(entry) && isHttpApi(entry.api)) return entry.api;
59
+ if ('function' == typeof entry && 0 === entry.length) {
60
+ const output = await entry();
61
+ if (isRecord(output) && isHttpApi(output.api)) return output.api;
62
+ }
63
+ return null;
64
+ }
65
+ function createEffectEndpointContractHash(endpoint, requestId) {
66
+ return createOperationContractHash(createEffectOperationContractSource(endpoint), requestId);
67
+ }
68
+ export { collectEffectEndpoints, createEffectEndpointContractHash, createEffectOperationContractSource, ensureLeadingSlash, extractHttpApiFromModule, getEffectRoutePath, normalizeEffectPrefix, resolveEffectApiId, toOperationContractSources };