@bleedingdev/modern-js-plugin-bff 3.2.0-ultramodern.98 → 3.4.0-ultramodern.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/cli.js +9 -5
- package/dist/cjs/constants.js +13 -9
- package/dist/cjs/index.js +9 -5
- package/dist/cjs/loader.js +9 -5
- package/dist/cjs/runtime/create-request/index.js +9 -5
- package/dist/cjs/runtime/data-platform/index.js +11 -18
- package/dist/cjs/runtime/effect/adapter.js +87 -14
- package/dist/cjs/runtime/effect/context.js +9 -5
- package/dist/cjs/runtime/effect/edge.js +26 -26
- package/dist/cjs/runtime/effect/endpoint-contracts.js +130 -0
- package/dist/cjs/runtime/effect/handler.js +107 -63
- package/dist/cjs/runtime/effect/index.js +28 -16
- package/dist/cjs/runtime/effect/module.js +71 -35
- package/dist/cjs/runtime/effect/operation-context.js +10 -18
- package/dist/cjs/runtime/effect-client/index.js +10 -6
- package/dist/cjs/runtime/effect-client/runtime.js +266 -0
- package/dist/cjs/runtime/hono/adapter.js +30 -14
- package/dist/cjs/runtime/hono/index.js +9 -5
- package/dist/cjs/runtime/hono/operators.js +9 -5
- package/dist/cjs/runtime/safe-failure.js +83 -0
- package/dist/cjs/server.js +9 -5
- package/dist/cjs/utils/clientGenerator.js +13 -9
- package/dist/cjs/utils/createHonoRoutes.js +9 -5
- package/dist/cjs/utils/crossProjectApiPlugin.js +9 -5
- package/dist/cjs/utils/crossProjectServerPolicy.js +104 -0
- package/dist/cjs/utils/effectClientGenerator.js +99 -488
- package/dist/cjs/utils/pluginGenerator.js +9 -5
- package/dist/cjs/utils/runtimeGenerator.js +9 -5
- package/dist/esm/runtime/data-platform/index.mjs +2 -13
- package/dist/esm/runtime/effect/adapter.mjs +78 -9
- package/dist/esm/runtime/effect/edge.mjs +2 -9
- package/dist/esm/runtime/effect/endpoint-contracts.mjs +68 -0
- package/dist/esm/runtime/effect/handler.mjs +41 -8
- package/dist/esm/runtime/effect/index.mjs +2 -0
- package/dist/esm/runtime/effect/module.mjs +63 -31
- package/dist/esm/runtime/effect/operation-context.mjs +1 -13
- package/dist/esm/runtime/effect-client/index.mjs +1 -1
- package/dist/esm/runtime/effect-client/runtime.mjs +228 -0
- package/dist/esm/runtime/hono/adapter.mjs +21 -9
- package/dist/esm/runtime/safe-failure.mjs +45 -0
- package/dist/esm/utils/clientGenerator.mjs +5 -5
- package/dist/esm/utils/crossProjectServerPolicy.mjs +50 -0
- package/dist/esm/utils/effectClientGenerator.mjs +88 -484
- package/dist/esm-node/runtime/data-platform/index.mjs +2 -13
- package/dist/esm-node/runtime/effect/adapter.mjs +78 -9
- package/dist/esm-node/runtime/effect/edge.mjs +2 -9
- package/dist/esm-node/runtime/effect/endpoint-contracts.mjs +69 -0
- package/dist/esm-node/runtime/effect/handler.mjs +41 -8
- package/dist/esm-node/runtime/effect/index.mjs +2 -0
- package/dist/esm-node/runtime/effect/module.mjs +63 -31
- package/dist/esm-node/runtime/effect/operation-context.mjs +1 -13
- package/dist/esm-node/runtime/effect-client/index.mjs +1 -1
- package/dist/esm-node/runtime/effect-client/runtime.mjs +229 -0
- package/dist/esm-node/runtime/hono/adapter.mjs +21 -9
- package/dist/esm-node/runtime/safe-failure.mjs +46 -0
- package/dist/esm-node/utils/clientGenerator.mjs +5 -5
- package/dist/esm-node/utils/crossProjectServerPolicy.mjs +52 -0
- package/dist/esm-node/utils/effectClientGenerator.mjs +88 -484
- package/dist/types/runtime/effect/adapter.d.ts +25 -0
- package/dist/types/runtime/effect/context.d.ts +1 -1
- package/dist/types/runtime/effect/endpoint-contracts.d.ts +62 -0
- package/dist/types/runtime/effect/handler.d.ts +37 -4
- package/dist/types/runtime/effect/index.d.ts +1 -0
- package/dist/types/runtime/effect/module.d.ts +22 -2
- package/dist/types/runtime/effect-client/runtime.d.ts +71 -0
- package/dist/types/runtime/hono/adapter.d.ts +3 -0
- package/dist/types/runtime/safe-failure.d.ts +1 -0
- package/dist/types/server.d.ts +1 -1
- package/dist/types/utils/createHonoRoutes.d.ts +3 -3
- package/dist/types/utils/crossProjectServerPolicy.d.ts +35 -0
- package/dist/types/utils/effectClientGenerator.d.ts +16 -2
- package/package.json +40 -27
|
@@ -10,11 +10,15 @@ var __webpack_require__ = {};
|
|
|
10
10
|
};
|
|
11
11
|
})();
|
|
12
12
|
(()=>{
|
|
13
|
-
__webpack_require__.d = (exports1,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
(()=>{
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseTraceparent } from "@modern-js/create-request";
|
|
1
2
|
import { trace as api_trace } from "@opentelemetry/api";
|
|
2
3
|
const DATA_BATCH_TRANSPORT_OTEL_EVENT = 'modernjs.data.batch';
|
|
3
4
|
function createDataBatchTransportTelemetryAttributes(event) {
|
|
@@ -23,7 +24,6 @@ function emitDataBatchTransportEvent(onEvent, event) {
|
|
|
23
24
|
const DEFAULT_DATA_ENVELOPE_HEADER = 'x-modernjs-data-envelope';
|
|
24
25
|
const DEFAULT_DATA_BATCH_ENDPOINT = '/_data/batch';
|
|
25
26
|
const DEFAULT_DATA_BATCH_HEADER = 'x-modernjs-data-batch';
|
|
26
|
-
const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
|
|
27
27
|
function isPlainObject(value) {
|
|
28
28
|
if ('object' != typeof value || null === value || Array.isArray(value)) return false;
|
|
29
29
|
const proto = Object.getPrototypeOf(value);
|
|
@@ -120,18 +120,7 @@ function isValidHex(value, length) {
|
|
|
120
120
|
return value.length === length && /^[0-9a-f]+$/.test(value);
|
|
121
121
|
}
|
|
122
122
|
function parseTraceparentHeader(header) {
|
|
123
|
-
|
|
124
|
-
if (!match) return null;
|
|
125
|
-
const traceId = match[1].toLowerCase();
|
|
126
|
-
const spanId = match[2].toLowerCase();
|
|
127
|
-
const flags = match[3].toLowerCase();
|
|
128
|
-
if (isAllZeroHex(traceId) || isAllZeroHex(spanId)) return null;
|
|
129
|
-
const sampled = (0x1 & Number.parseInt(flags, 16)) === 1;
|
|
130
|
-
return {
|
|
131
|
-
traceId,
|
|
132
|
-
spanId,
|
|
133
|
-
sampled
|
|
134
|
-
};
|
|
123
|
+
return parseTraceparent(header) ?? null;
|
|
135
124
|
}
|
|
136
125
|
function formatTraceparentHeader(trace) {
|
|
137
126
|
const traceId = trace.traceId.toLowerCase();
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { ApiRouter } from "@modern-js/bff-core";
|
|
1
2
|
import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
|
|
3
|
+
import { HttpApi } from "effect/unstable/httpapi";
|
|
2
4
|
import path from "path";
|
|
5
|
+
import { checkCrossProjectPolicyForRequest, resolveAdapterCrossProjectPolicy } from "../../utils/crossProjectServerPolicy.mjs";
|
|
6
|
+
import { createSafeFailureResponse } from "../safe-failure.mjs";
|
|
3
7
|
import { createEffectOperationContext, runWithEffectContext } from "./context.mjs";
|
|
8
|
+
import { collectEffectEndpoints, extractHttpApiFromModule, toOperationContractSources } from "./endpoint-contracts.mjs";
|
|
4
9
|
import { resolveEffectBffModuleHandler } from "./module.mjs";
|
|
5
10
|
const before = [
|
|
6
11
|
'custom-server-hook',
|
|
@@ -46,10 +51,73 @@ class EffectAdapter {
|
|
|
46
51
|
const entryWithoutExt = configuredEntry ? path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry) : defaultEntry;
|
|
47
52
|
return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
|
|
48
53
|
}
|
|
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
|
|
74
|
+
});
|
|
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 [];
|
|
86
|
+
}
|
|
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)}`);
|
|
101
|
+
}
|
|
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
|
+
}
|
|
49
116
|
async loadEffectHandlerFromModule(mod) {
|
|
50
117
|
return resolveEffectBffModuleHandler(mod, {
|
|
51
118
|
openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
|
|
52
119
|
dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform,
|
|
120
|
+
validateRequest: (request)=>checkCrossProjectPolicyForRequest(request, this.crossProjectPolicy),
|
|
53
121
|
onWarning: (message)=>{
|
|
54
122
|
logger.warn(message);
|
|
55
123
|
}
|
|
@@ -79,6 +147,7 @@ class EffectAdapter {
|
|
|
79
147
|
this.handler = null;
|
|
80
148
|
return;
|
|
81
149
|
}
|
|
150
|
+
await this.refreshCrossProjectPolicy(mod);
|
|
82
151
|
const loaded = await this.loadEffectHandlerFromModule(mod);
|
|
83
152
|
if (!loaded) {
|
|
84
153
|
logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer } or handler.`);
|
|
@@ -87,6 +156,7 @@ class EffectAdapter {
|
|
|
87
156
|
}
|
|
88
157
|
this.handler = loaded.handler;
|
|
89
158
|
this.dispose = loaded.dispose || null;
|
|
159
|
+
this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
|
|
90
160
|
}
|
|
91
161
|
async disposeCurrentHandler() {
|
|
92
162
|
if (!this.dispose) return;
|
|
@@ -110,15 +180,7 @@ class EffectAdapter {
|
|
|
110
180
|
} catch (configError) {
|
|
111
181
|
logger.error(`Error in serverConfig.onError handler: ${configError}`);
|
|
112
182
|
}
|
|
113
|
-
|
|
114
|
-
return new Response(JSON.stringify({
|
|
115
|
-
message: error instanceof Error ? error.message : '[BFF] Internal Server Error'
|
|
116
|
-
}), {
|
|
117
|
-
status,
|
|
118
|
-
headers: {
|
|
119
|
-
'content-type': 'application/json; charset=utf-8'
|
|
120
|
-
}
|
|
121
|
-
});
|
|
183
|
+
return createSafeFailureResponse(error);
|
|
122
184
|
}
|
|
123
185
|
ensureJsonContext(c) {
|
|
124
186
|
const maybeJsonContext = c;
|
|
@@ -145,6 +207,8 @@ class EffectAdapter {
|
|
|
145
207
|
this.effectMiddleware = null;
|
|
146
208
|
this.handler = null;
|
|
147
209
|
this.dispose = null;
|
|
210
|
+
this.prefix = '/api';
|
|
211
|
+
this.policyEnforcedInMiddleware = false;
|
|
148
212
|
this.registerMiddleware = async (options)=>{
|
|
149
213
|
const { prefix, enableHandleWeb } = options;
|
|
150
214
|
const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
|
|
@@ -152,6 +216,7 @@ class EffectAdapter {
|
|
|
152
216
|
this.isEffect = false;
|
|
153
217
|
return;
|
|
154
218
|
}
|
|
219
|
+
this.prefix = prefix || this.prefix;
|
|
155
220
|
await this.reloadHandler();
|
|
156
221
|
this.effectMiddleware = {
|
|
157
222
|
name: 'effect-bff-handler',
|
|
@@ -164,6 +229,10 @@ class EffectAdapter {
|
|
|
164
229
|
if (enableHandleWeb) return void await next();
|
|
165
230
|
return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/effect/index or configure bff.effect.entry.'), c);
|
|
166
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
|
+
}
|
|
167
236
|
let response;
|
|
168
237
|
try {
|
|
169
238
|
const effectRequest = createRequestForMountedPrefix(c.req.raw, prefix);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createSafeFailureResponse } from "../safe-failure.mjs";
|
|
1
2
|
import { resolveEffectBffModuleHandler } from "./module.mjs";
|
|
2
3
|
import { createEffectOperationContext } from "./operation-context.mjs";
|
|
3
4
|
export * from "./handler.mjs";
|
|
@@ -39,15 +40,7 @@ function createEdgeEffectContext(originalRequest, effectRequest, options) {
|
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
function createRuntimeErrorResponse(error) {
|
|
42
|
-
|
|
43
|
-
return new Response(JSON.stringify({
|
|
44
|
-
message: error instanceof Error ? error.message : '[BFF] Internal Server Error'
|
|
45
|
-
}), {
|
|
46
|
-
status,
|
|
47
|
-
headers: {
|
|
48
|
-
'content-type': 'application/json; charset=utf-8'
|
|
49
|
-
}
|
|
50
|
-
});
|
|
43
|
+
return createSafeFailureResponse(error);
|
|
51
44
|
}
|
|
52
45
|
async function dispatchEffectBffRequest(handler, request, options = {}) {
|
|
53
46
|
const requestPathname = new URL(request.url).pathname;
|
|
@@ -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 };
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import * as __rspack_external_effect_Layer_16f7a8fc from "effect/Layer";
|
|
2
|
-
import { HttpRouter, HttpServerResponse, HttpTraceContext } from "effect/unstable/http";
|
|
2
|
+
import { HttpRouter, HttpServer, HttpServerResponse, HttpTraceContext } from "effect/unstable/http";
|
|
3
3
|
import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi";
|
|
4
4
|
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
|
|
5
5
|
import { DEFAULT_DATA_BATCH_ENDPOINT, DEFAULT_DATA_BATCH_HEADER, DEFAULT_DATA_ENVELOPE_HEADER, decodeRequestEnvelopeHeader, validateRequestEnvelope, validateSelectionPlan } from "../data-platform/index.mjs";
|
|
6
|
-
import * as __rspack_external__effect_opentelemetry_8bbbb5af from "@effect/opentelemetry";
|
|
7
6
|
import * as __rspack_external_effect_Config_29be8a92 from "effect/Config";
|
|
8
7
|
import * as __rspack_external_effect_Effect_194ac36c from "effect/Effect";
|
|
9
8
|
import * as __rspack_external_effect_Option_4d691636 from "effect/Option";
|
|
@@ -11,6 +10,12 @@ import * as __rspack_external_effect_Schema_f8472650 from "effect/Schema";
|
|
|
11
10
|
export * from "effect/unstable/http";
|
|
12
11
|
export * from "effect/unstable/httpapi";
|
|
13
12
|
export * from "effect/unstable/rpc";
|
|
13
|
+
import * as __rspack_external_effect_Context_f1289ca3 from "effect/Context";
|
|
14
|
+
const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
|
|
15
|
+
const EFFECT_VALIDATOR_AWARE_FACTORY = Symbol.for('modernjs.effect.validatorAware');
|
|
16
|
+
function isValidatorAwareHandlerFactory(factory) {
|
|
17
|
+
return 'function' == typeof factory && true === factory[EFFECT_VALIDATOR_AWARE_FACTORY];
|
|
18
|
+
}
|
|
14
19
|
function normalizeOpenApiPath(pathname) {
|
|
15
20
|
if (!pathname.startsWith('/')) return `/${pathname}`;
|
|
16
21
|
return pathname;
|
|
@@ -266,16 +271,38 @@ function defineEffectBff(definition) {
|
|
|
266
271
|
layer: definition.layer,
|
|
267
272
|
openapi: options?.openapi,
|
|
268
273
|
rpc: mergedRpcOptions,
|
|
269
|
-
dataPlatform: mergeDataPlatformOptions(definition.dataPlatform, options?.dataPlatform)
|
|
274
|
+
dataPlatform: mergeDataPlatformOptions(definition.dataPlatform, options?.dataPlatform),
|
|
275
|
+
validateRequest: options?.validateRequest
|
|
270
276
|
});
|
|
271
277
|
};
|
|
272
|
-
|
|
278
|
+
Object.defineProperty(createHandler, EFFECT_VALIDATOR_AWARE_FACTORY, {
|
|
279
|
+
value: true
|
|
280
|
+
});
|
|
281
|
+
const client = createLoaderMaterializedClientPlaceholder();
|
|
273
282
|
return {
|
|
274
283
|
...definition,
|
|
275
284
|
createHandler,
|
|
276
285
|
client
|
|
277
286
|
};
|
|
278
287
|
}
|
|
288
|
+
const LOADER_CLIENT_IGNORED_KEYS = new Set([
|
|
289
|
+
'then',
|
|
290
|
+
'catch',
|
|
291
|
+
'finally',
|
|
292
|
+
'toJSON',
|
|
293
|
+
'$$typeof'
|
|
294
|
+
]);
|
|
295
|
+
function createLoaderMaterializedClientPlaceholder() {
|
|
296
|
+
const explain = (property)=>{
|
|
297
|
+
throw new Error(`[BFF][Effect] effectBff.client.${String(property)} is not available here: the typed client only exists when this module is imported through the "@api/effect/*" transformed path (the BFF loader replaces it with generated client code). On the server, use HttpApiClient or call the Effect layer directly.`);
|
|
298
|
+
};
|
|
299
|
+
return new Proxy(Object.create(null), {
|
|
300
|
+
get (_target, property) {
|
|
301
|
+
if ('symbol' == typeof property || LOADER_CLIENT_IGNORED_KEYS.has(property)) return;
|
|
302
|
+
return explain(property);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
279
306
|
function defineEffectRpcBff(definition) {
|
|
280
307
|
const createHandler = (options)=>createRpcApiHandler({
|
|
281
308
|
...definition,
|
|
@@ -287,7 +314,7 @@ function defineEffectRpcBff(definition) {
|
|
|
287
314
|
};
|
|
288
315
|
}
|
|
289
316
|
function createHttpApiHandler(options) {
|
|
290
|
-
const apiLayer = options.layer;
|
|
317
|
+
const apiLayer = options.layer.pipe(__rspack_external_effect_Layer_16f7a8fc.provide(HttpServer.layerServices));
|
|
291
318
|
const openApiLayer = createOpenApiLayer(options.api, options.openapi);
|
|
292
319
|
const mergedLayer = openApiLayer ? __rspack_external_effect_Layer_16f7a8fc.mergeAll(apiLayer, openApiLayer) : apiLayer;
|
|
293
320
|
const httpApiHandler = HttpRouter.toWebHandler(mergedLayer);
|
|
@@ -302,9 +329,11 @@ function createHttpApiHandler(options) {
|
|
|
302
329
|
const envelopeHeader = options.dataPlatform?.envelopeHeader || DEFAULT_DATA_ENVELOPE_HEADER;
|
|
303
330
|
const normalizedEnvelopeHeader = envelopeHeader.toLowerCase();
|
|
304
331
|
const withDataPlatformValidation = async (request, context)=>{
|
|
332
|
+
const policyDenial = options.validateRequest?.(request);
|
|
333
|
+
if (policyDenial) return policyDenial;
|
|
305
334
|
const validationError = validateDataPlatformRequestEnvelope(request, options.dataPlatform);
|
|
306
335
|
if (validationError) return validationError;
|
|
307
|
-
return httpApiHandler.handler(request, context);
|
|
336
|
+
return httpApiHandler.handler(request, context ?? emptyEffectServiceContext);
|
|
308
337
|
};
|
|
309
338
|
const handleBatchRequest = async (request, context)=>{
|
|
310
339
|
const mountedPrefix = getMountedPrefixFromContext(request, context);
|
|
@@ -423,7 +452,11 @@ function createHttpApiHandler(options) {
|
|
|
423
452
|
const rpcHandler = createRpcApiHandler(options.rpc);
|
|
424
453
|
return {
|
|
425
454
|
handler: async (request, context)=>{
|
|
426
|
-
if (isRpcRequest(request, rpcPath))
|
|
455
|
+
if (isRpcRequest(request, rpcPath)) {
|
|
456
|
+
const policyDenial = options.validateRequest?.(request);
|
|
457
|
+
if (policyDenial) return policyDenial;
|
|
458
|
+
return rpcHandler.handler(request, context ?? emptyEffectServiceContext);
|
|
459
|
+
}
|
|
427
460
|
return handleHttpApiRequest(request);
|
|
428
461
|
},
|
|
429
462
|
dispose: async ()=>{
|
|
@@ -434,4 +467,4 @@ function createHttpApiHandler(options) {
|
|
|
434
467
|
}
|
|
435
468
|
};
|
|
436
469
|
}
|
|
437
|
-
export { HttpApiBuilder, HttpTraceContext,
|
|
470
|
+
export { EFFECT_VALIDATOR_AWARE_FACTORY, HttpApiBuilder, HttpTraceContext, __rspack_external_effect_Config_29be8a92 as Config, __rspack_external_effect_Effect_194ac36c as Effect, __rspack_external_effect_Layer_16f7a8fc as Layer, __rspack_external_effect_Option_4d691636 as Option, __rspack_external_effect_Schema_f8472650 as Schema, createHttpApiHandler, defineEffectBff, defineEffectRpcBff, isValidatorAwareHandlerFactory };
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
import * as __rspack_external__effect_opentelemetry_8bbbb5af from "@effect/opentelemetry";
|
|
1
2
|
export * from "./handler.mjs";
|
|
2
3
|
export { createEffectOperationContext, useEffectContext, useOperationContext } from "./context.mjs";
|
|
4
|
+
export { __rspack_external__effect_opentelemetry_8bbbb5af as OpenTelemetry };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HttpApi } from "effect/unstable/httpapi";
|
|
2
|
-
import { createHttpApiHandler } from "./handler.mjs";
|
|
2
|
+
import { createHttpApiHandler, isValidatorAwareHandlerFactory } from "./handler.mjs";
|
|
3
|
+
import * as __rspack_external_effect_Context_f1289ca3 from "effect/Context";
|
|
3
4
|
function isRecord(value) {
|
|
4
5
|
return 'object' == typeof value && null !== value;
|
|
5
6
|
}
|
|
@@ -12,15 +13,33 @@ function isRequestHandler(value) {
|
|
|
12
13
|
function isEffectApiDefinition(module) {
|
|
13
14
|
return HttpApi.isHttpApi(module.api) && void 0 !== module.layer;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
function isEffectServiceContext(context) {
|
|
17
|
+
return 'object' == typeof context && null !== context && 'mapUnsafe' in context;
|
|
18
|
+
}
|
|
19
|
+
const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
|
|
20
|
+
function callEffectBffRequestHandler(handler, request, context) {
|
|
21
|
+
return void 0 === context ? handler(request) : handler(request, context);
|
|
22
|
+
}
|
|
23
|
+
function createLoadedHandler(webHandler, appliesRequestValidator) {
|
|
24
|
+
return {
|
|
25
|
+
handler: (request, context)=>callEffectBffRequestHandler(webHandler.handler, request, context),
|
|
26
|
+
dispose: webHandler.dispose,
|
|
27
|
+
...appliesRequestValidator ? {
|
|
28
|
+
appliesRequestValidator: true
|
|
29
|
+
} : {}
|
|
23
30
|
};
|
|
31
|
+
}
|
|
32
|
+
function createLoadedHttpApiHandler(webHandler) {
|
|
33
|
+
return {
|
|
34
|
+
handler: (request, context)=>{
|
|
35
|
+
const effectContext = isEffectServiceContext(context) ? context : emptyEffectServiceContext;
|
|
36
|
+
return webHandler.handler(request, effectContext);
|
|
37
|
+
},
|
|
38
|
+
dispose: webHandler.dispose,
|
|
39
|
+
appliesRequestValidator: true
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {}) {
|
|
24
43
|
if (isRequestHandler(normalizedModule.handler)) return {
|
|
25
44
|
handler: normalizedModule.handler
|
|
26
45
|
};
|
|
@@ -28,13 +47,6 @@ async function resolveEffectBffModuleHandler(mod, options = {}) {
|
|
|
28
47
|
if (isRequestHandler(entry)) return {
|
|
29
48
|
handler: entry
|
|
30
49
|
};
|
|
31
|
-
if ('function' == typeof entry && 0 === entry.length) {
|
|
32
|
-
const out = await entry();
|
|
33
|
-
if (isRequestHandler(out)) return {
|
|
34
|
-
handler: out
|
|
35
|
-
};
|
|
36
|
-
mergeRuntimeExports(out);
|
|
37
|
-
}
|
|
38
50
|
if (isRecord(entry)) normalizedModule = {
|
|
39
51
|
...normalizedModule,
|
|
40
52
|
...entry
|
|
@@ -50,16 +62,15 @@ async function resolveEffectBffModuleHandler(mod, options = {}) {
|
|
|
50
62
|
handler: normalizedModule.handler
|
|
51
63
|
};
|
|
52
64
|
if ('function' == typeof normalizedModule.createHandler) {
|
|
53
|
-
const
|
|
65
|
+
const factory = normalizedModule.createHandler;
|
|
66
|
+
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.');
|
|
68
|
+
const webHandler = factory({
|
|
54
69
|
openapi: options.openapi,
|
|
55
|
-
dataPlatform: options.dataPlatform
|
|
70
|
+
dataPlatform: options.dataPlatform,
|
|
71
|
+
validateRequest: options.validateRequest
|
|
56
72
|
});
|
|
57
|
-
return
|
|
58
|
-
handler: async (request, context)=>webHandler.handler(request, context),
|
|
59
|
-
dispose: async ()=>{
|
|
60
|
-
await webHandler.dispose();
|
|
61
|
-
}
|
|
62
|
-
};
|
|
73
|
+
return createLoadedHandler(webHandler, validatorAware);
|
|
63
74
|
}
|
|
64
75
|
if (isEffectApiDefinition(normalizedModule)) {
|
|
65
76
|
options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
|
|
@@ -67,15 +78,36 @@ async function resolveEffectBffModuleHandler(mod, options = {}) {
|
|
|
67
78
|
api: normalizedModule.api,
|
|
68
79
|
layer: normalizedModule.layer,
|
|
69
80
|
openapi: options.openapi,
|
|
70
|
-
dataPlatform: options.dataPlatform
|
|
81
|
+
dataPlatform: options.dataPlatform,
|
|
82
|
+
validateRequest: options.validateRequest
|
|
71
83
|
});
|
|
72
|
-
return
|
|
73
|
-
handler: async (request, context)=>webHandler.handler(request, context),
|
|
74
|
-
dispose: async ()=>{
|
|
75
|
-
await webHandler.dispose();
|
|
76
|
-
}
|
|
77
|
-
};
|
|
84
|
+
return createLoadedHttpApiHandler(webHandler);
|
|
78
85
|
}
|
|
79
86
|
return null;
|
|
80
87
|
}
|
|
88
|
+
function resolveEffectBffModuleHandler(mod, options = {}) {
|
|
89
|
+
let normalizedModule = mod;
|
|
90
|
+
const mergeRuntimeExports = (value)=>{
|
|
91
|
+
if (!isRecord(value) || !includesRuntimeExports(value)) return;
|
|
92
|
+
normalizedModule = {
|
|
93
|
+
...normalizedModule,
|
|
94
|
+
...value
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
if (isRequestHandler(normalizedModule.handler)) return Promise.resolve({
|
|
98
|
+
handler: normalizedModule.handler
|
|
99
|
+
});
|
|
100
|
+
const entry = normalizedModule.default;
|
|
101
|
+
if (isRequestHandler(entry)) return Promise.resolve({
|
|
102
|
+
handler: entry
|
|
103
|
+
});
|
|
104
|
+
if ('function' == typeof entry && 0 === entry.length) return Promise.resolve(entry()).then((out)=>{
|
|
105
|
+
if (isRequestHandler(out)) return {
|
|
106
|
+
handler: out
|
|
107
|
+
};
|
|
108
|
+
mergeRuntimeExports(out);
|
|
109
|
+
return resolveNormalizedEffectBffModuleHandler(normalizedModule, options);
|
|
110
|
+
});
|
|
111
|
+
return Promise.resolve(resolveNormalizedEffectBffModuleHandler(normalizedModule, options));
|
|
112
|
+
}
|
|
81
113
|
export { resolveEffectBffModuleHandler };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { BFF_LOCALE_HEADER, BFF_OPERATION_CONTEXT_DETAIL_HEADER, BFF_OPERATION_CONTEXT_HEADER, BFF_TRACEPARENT_HEADER } from "@modern-js/create-request";
|
|
2
|
-
const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/i;
|
|
1
|
+
import { BFF_LOCALE_HEADER, BFF_OPERATION_CONTEXT_DETAIL_HEADER, BFF_OPERATION_CONTEXT_HEADER, BFF_TRACEPARENT_HEADER, parseTraceparent } from "@modern-js/create-request";
|
|
3
2
|
const readHeader = (headers, header)=>{
|
|
4
3
|
const value = headers.get(header);
|
|
5
4
|
return value && value.length > 0 ? value : void 0;
|
|
@@ -8,17 +7,6 @@ const copyStringField = (target, details, key)=>{
|
|
|
8
7
|
const value = details[key];
|
|
9
8
|
if ('string' == typeof value && value.length > 0) target[key] = value;
|
|
10
9
|
};
|
|
11
|
-
const parseTraceparent = (traceparent)=>{
|
|
12
|
-
if (!traceparent) return;
|
|
13
|
-
const match = traceparent.trim().match(TRACEPARENT_REGEX);
|
|
14
|
-
if (!match) return;
|
|
15
|
-
const [, traceId, spanId] = match;
|
|
16
|
-
if (!traceId || !spanId) return;
|
|
17
|
-
return {
|
|
18
|
-
traceId: traceId.toLowerCase(),
|
|
19
|
-
spanId: spanId.toLowerCase()
|
|
20
|
-
};
|
|
21
|
-
};
|
|
22
10
|
const readOperationContextDetails = (request)=>{
|
|
23
11
|
const rawDetails = readHeader(request.headers, BFF_OPERATION_CONTEXT_DETAIL_HEADER);
|
|
24
12
|
if (!rawDetails) return {};
|
|
@@ -55,7 +55,7 @@ function makeEffectHttpApiClient(api, options) {
|
|
|
55
55
|
for (const [header, value] of Object.entries(requestContextHeaders))if (void 0 === nextRequest.headers[header.toLowerCase()]) nextRequest = HttpClientRequest.setHeader(nextRequest, header, value);
|
|
56
56
|
return nextRequest;
|
|
57
57
|
}));
|
|
58
|
-
return options?.transformClient ? options.transformClient(contextClient) : contextClient;
|
|
58
|
+
return 'function' == typeof options?.transformClient ? options.transformClient(contextClient) : contextClient;
|
|
59
59
|
};
|
|
60
60
|
return HttpApiClient.make(api, {
|
|
61
61
|
baseUrl: options?.baseUrl,
|