@bleedingdev/modern-js-plugin-bff 3.4.0-ultramodern.9 → 3.5.0-ultramodern.1

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.
@@ -13,6 +13,8 @@ export * from "effect/unstable/httpapi";
13
13
  export * from "effect/unstable/rpc";
14
14
  import * as __rspack_external_effect_Context_f1289ca3 from "effect/Context";
15
15
  const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
16
+ const isEffectServiceContext = (context)=>'object' == typeof context && null !== context && 'mapUnsafe' in context;
17
+ const toEffectServiceContext = (context)=>isEffectServiceContext(context) ? context : emptyEffectServiceContext;
16
18
  const EFFECT_VALIDATOR_AWARE_FACTORY = Symbol.for('modernjs.effect.validatorAware');
17
19
  function isValidatorAwareHandlerFactory(factory) {
18
20
  return 'function' == typeof factory && true === factory[EFFECT_VALIDATOR_AWARE_FACTORY];
@@ -330,7 +332,7 @@ const LOADER_CLIENT_IGNORED_KEYS = new Set([
330
332
  ]);
331
333
  function createLoaderMaterializedClientPlaceholder() {
332
334
  const explain = (property)=>{
333
- throw new Error(`[BFF][Effect] effectBff.client.${String(property)} is not available here: the typed client only exists when this module is imported through the "@api/effect/*" transformed path (the BFF loader replaces it with generated client code). On the server, use HttpApiClient or call the Effect layer directly.`);
335
+ throw new Error(`[BFF][Effect] client.${String(property)} is not available here: the typed client only exists when the API entry is imported through the "@api/index" transformed path (the BFF loader replaces it with generated client code). On the server, use HttpApiClient or call the Effect layer directly.`);
334
336
  };
335
337
  return new Proxy(Object.create(null), {
336
338
  get (_target, property) {
@@ -371,7 +373,7 @@ function createHttpApiHandler(options) {
371
373
  if (preparedRequest instanceof Response) return preparedRequest;
372
374
  const validationError = validateDataPlatformRequestEnvelope(preparedRequest, options.dataPlatform);
373
375
  if (validationError) return validationError;
374
- return httpApiHandler.handler(preparedRequest, context ?? emptyEffectServiceContext);
376
+ return httpApiHandler.handler(preparedRequest, toEffectServiceContext(context));
375
377
  };
376
378
  const handleBatchRequest = async (request, context)=>{
377
379
  const mountedPrefix = getMountedPrefixFromContext(request, context);
@@ -493,7 +495,7 @@ function createHttpApiHandler(options) {
493
495
  if (isRpcRequest(request, rpcPath)) {
494
496
  const policyDenial = options.validateRequest?.(request);
495
497
  if (policyDenial) return policyDenial;
496
- return rpcHandler.handler(request, context ?? emptyEffectServiceContext);
498
+ return rpcHandler.handler(request, toEffectServiceContext(context));
497
499
  }
498
500
  return handleHttpApiRequest(request);
499
501
  },
@@ -1,5 +1,6 @@
1
1
  import "node:module";
2
2
  import * as __rspack_external__effect_opentelemetry_8bbbb5af from "@effect/opentelemetry";
3
3
  export * from "./handler.mjs";
4
- export { createEffectOperationContext, useEffectContext, useOperationContext } from "./context.mjs";
4
+ export { createEffectOperationContext, runWithEffectContext, useEffectContext, useOperationContext } from "./context.mjs";
5
+ export { createEffectBffEdgeHandler, createEffectBffTestHandler, dispatchEffectBffRequest } from "./edge.mjs";
5
6
  export { __rspack_external__effect_opentelemetry_8bbbb5af as OpenTelemetry };
@@ -18,16 +18,18 @@ function isEffectServiceContext(context) {
18
18
  return 'object' == typeof context && null !== context && 'mapUnsafe' in context;
19
19
  }
20
20
  const emptyEffectServiceContext = __rspack_external_effect_Context_f1289ca3.empty();
21
+ const strictEffectApproachMessage = '[BFF][Effect] strictEffectApproach is enforced: Effect API entries must export defineEffectBff(...) or a { api, layer } HttpApi module. Raw handler exports, default request handlers, and unbranded custom createHandler factories are not valid Effect API entries.';
22
+ function rejectLegacyEffectModuleShape(options, shape) {
23
+ options.onWarning?.(`${strictEffectApproachMessage} Rejected ${shape}.`);
24
+ return true;
25
+ }
21
26
  function callEffectBffRequestHandler(handler, request, context) {
22
27
  return void 0 === context ? handler(request) : handler(request, context);
23
28
  }
24
- function createLoadedHandler(webHandler, appliesRequestValidator) {
29
+ function createLoadedHandler(webHandler) {
25
30
  return {
26
31
  handler: (request, context)=>callEffectBffRequestHandler(webHandler.handler, request, context),
27
- dispose: webHandler.dispose,
28
- ...appliesRequestValidator ? {
29
- appliesRequestValidator: true
30
- } : {}
32
+ dispose: webHandler.dispose
31
33
  };
32
34
  }
33
35
  function createLoadedHttpApiHandler(webHandler) {
@@ -36,18 +38,23 @@ function createLoadedHttpApiHandler(webHandler) {
36
38
  const effectContext = isEffectServiceContext(context) ? context : emptyEffectServiceContext;
37
39
  return webHandler.handler(request, effectContext);
38
40
  },
39
- dispose: webHandler.dispose,
40
- appliesRequestValidator: true
41
+ dispose: webHandler.dispose
41
42
  };
42
43
  }
43
44
  function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {}) {
44
- if (isRequestHandler(normalizedModule.handler)) return {
45
- handler: normalizedModule.handler
46
- };
45
+ if (isRequestHandler(normalizedModule.handler)) {
46
+ if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
47
+ return {
48
+ handler: normalizedModule.handler
49
+ };
50
+ }
47
51
  const entry = normalizedModule.default;
48
- if (isRequestHandler(entry)) return {
49
- handler: entry
50
- };
52
+ if (isRequestHandler(entry)) {
53
+ if (rejectLegacyEffectModuleShape(options, 'default request handler')) return null;
54
+ return {
55
+ handler: entry
56
+ };
57
+ }
51
58
  if (isRecord(entry)) normalizedModule = {
52
59
  ...normalizedModule,
53
60
  ...entry
@@ -59,19 +66,22 @@ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {})
59
66
  handler: maybeHandler
60
67
  };
61
68
  }
62
- if (isRequestHandler(normalizedModule.handler)) return {
63
- handler: normalizedModule.handler
64
- };
69
+ if (isRequestHandler(normalizedModule.handler)) {
70
+ if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
71
+ return {
72
+ handler: normalizedModule.handler
73
+ };
74
+ }
65
75
  if ('function' == typeof normalizedModule.createHandler) {
66
76
  const factory = normalizedModule.createHandler;
67
77
  const validatorAware = isValidatorAwareHandlerFactory(factory);
68
- if (!validatorAware && void 0 !== options.validateRequest) options.onWarning?.('[BFF][Effect] Custom createHandler export detected: it cannot be verified to apply validateRequest (cross-project policy), so the policy is enforced by the adapter middleware on the outer request. Batched calls will be denied at the batch POST (it carries no per-operation contract); export defineEffectBff(...) to get per-batch-item enforcement.');
78
+ if (!validatorAware && rejectLegacyEffectModuleShape(options, 'unbranded `createHandler` export')) return null;
69
79
  const webHandler = factory({
70
80
  openapi: options.openapi,
71
81
  dataPlatform: options.dataPlatform,
72
82
  validateRequest: options.validateRequest
73
83
  });
74
- return createLoadedHandler(webHandler, validatorAware);
84
+ return createLoadedHandler(webHandler);
75
85
  }
76
86
  if (isEffectApiDefinition(normalizedModule)) {
77
87
  options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
@@ -95,17 +105,26 @@ function resolveEffectBffModuleHandler(mod, options = {}) {
95
105
  ...value
96
106
  };
97
107
  };
98
- if (isRequestHandler(normalizedModule.handler)) return Promise.resolve({
99
- handler: normalizedModule.handler
100
- });
108
+ if (isRequestHandler(normalizedModule.handler)) {
109
+ if (rejectLegacyEffectModuleShape(options, '`handler` export')) return Promise.resolve(null);
110
+ return Promise.resolve({
111
+ handler: normalizedModule.handler
112
+ });
113
+ }
101
114
  const entry = normalizedModule.default;
102
- if (isRequestHandler(entry)) return Promise.resolve({
103
- handler: entry
104
- });
115
+ if (isRequestHandler(entry)) {
116
+ if (rejectLegacyEffectModuleShape(options, 'default request handler')) return Promise.resolve(null);
117
+ return Promise.resolve({
118
+ handler: entry
119
+ });
120
+ }
105
121
  if ('function' == typeof entry && 0 === entry.length) return Promise.resolve(entry()).then((out)=>{
106
- if (isRequestHandler(out)) return {
107
- handler: out
108
- };
122
+ if (isRequestHandler(out)) {
123
+ if (rejectLegacyEffectModuleShape(options, 'default factory returning a request handler')) return null;
124
+ return {
125
+ handler: out
126
+ };
127
+ }
109
128
  mergeRuntimeExports(out);
110
129
  return resolveNormalizedEffectBffModuleHandler(normalizedModule, options);
111
130
  });
@@ -129,14 +129,14 @@ const __generated = createGeneratedEffectClient(__manifest, __config, __requestR
129
129
  const client = __generated.client;
130
130
  const operationManifest = __generated.operationManifest;
131
131
  const createEffectRequestContext = __generated.createEffectRequestContext;
132
- const effectBffModule = {
132
+ const apiModule = {
133
133
  client,
134
134
  operationManifest,
135
135
  createEffectRequestContext,
136
136
  };
137
137
 
138
138
  export { client, createEffectRequestContext, operationManifest };
139
- export default effectBffModule;
139
+ export default apiModule;
140
140
  `;
141
141
  }
142
142
  function renderClientShape(endpoints, valueType) {
@@ -209,13 +209,13 @@ export declare const createEffectRequestContext: (
209
209
  requestContext: Record<string, unknown>,
210
210
  ) => EffectRequestContext;
211
211
  export declare const operationManifest: GeneratedEffectOperationManifest;
212
- declare const effectBffModule: {
212
+ declare const apiModule: {
213
213
  client: GeneratedEffectClient;
214
214
  createEffectRequestContext: typeof createEffectRequestContext;
215
215
  operationManifest: GeneratedEffectOperationManifest;
216
216
  };
217
217
 
218
- export default effectBffModule;
218
+ export default apiModule;
219
219
  `;
220
220
  }
221
221
  async function generateEffectClient(options) {
@@ -238,9 +238,14 @@ async function generateEffectClientCode(options) {
238
238
  }
239
239
  function resolveEffectEntryFile(options) {
240
240
  const { appDir, apiDir, effectEntry } = options;
241
- const defaultEntry = path.resolve(apiDir, 'effect', 'index');
242
- const entryWithoutExt = effectEntry ? path.isAbsolute(effectEntry) ? effectEntry : path.resolve(appDir, effectEntry) : defaultEntry;
243
- if (path.extname(entryWithoutExt)) return fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
244
- return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
241
+ const resolveEntry = (entryWithoutExt)=>{
242
+ if (path.extname(entryWithoutExt)) return fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
243
+ return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
244
+ };
245
+ if (effectEntry) {
246
+ const entryWithoutExt = path.isAbsolute(effectEntry) ? effectEntry : path.resolve(appDir, effectEntry);
247
+ return resolveEntry(entryWithoutExt);
248
+ }
249
+ return resolveEntry(path.resolve(apiDir, 'index'));
245
250
  }
246
251
  export { generateEffectClient, generateEffectClientCode, renderEffectClientDeclaration, resolveEffectEntryFile };
@@ -12,30 +12,14 @@ export declare class EffectAdapter {
12
12
  private handler;
13
13
  private dispose;
14
14
  private prefix;
15
- /**
16
- * True when the loaded handler does NOT run the policy seam internally
17
- * (plain `handler` exports); the adapter middleware enforces it instead.
18
- */
19
- private policyEnforcedInMiddleware;
20
15
  constructor(api: ServerPluginAPI);
21
16
  registerMiddleware: (options: MiddlewareOptions) => Promise<void>;
22
17
  onApiHandlersUpdated: () => Promise<void>;
23
18
  private resolveEntryFile;
24
- private isApiRequestPath;
25
- /**
26
- * Contract sources for the lambda-lane handlers hosted alongside the
27
- * effect entry. Cross-project SDKs generate per-operation contracts for
28
- * `api/lambda` handlers with the exact same `ApiRouter` derivation used
29
- * here, so the server-side expected-contract map must include them —
30
- * otherwise every lambda-lane operation of a hosted producer SDK would be
31
- * denied as `unknown_operation_contract`.
32
- */
33
- private collectLambdaContractSources;
34
19
  /**
35
- * Resolves the cross-project policy from the reflected HttpApi endpoints
36
- * (plus any hosted lambda-lane handlers) so the expected operation
37
- * contracts match the hashes the client generators stamp into generated
38
- * SDKs.
20
+ * Resolves the cross-project policy from the reflected HttpApi endpoints so
21
+ * the expected operation contracts match the hashes the client generators
22
+ * stamp into generated SDKs.
39
23
  */
40
24
  private refreshCrossProjectPolicy;
41
25
  private loadEffectHandlerFromModule;
@@ -0,0 +1,5 @@
1
+ import type { EffectContext } from './operation-context';
2
+ export { type CreateEffectOperationContextOptions, createEffectOperationContext, type EffectContext, } from './operation-context';
3
+ export declare const runWithEffectContext: <T>(context: EffectContext, cb: () => T) => T;
4
+ export declare const useEffectContext: () => EffectContext;
5
+ export declare const useOperationContext: () => import("@modern-js/create-request").OperationContext;
@@ -1,6 +1,7 @@
1
1
  import type { EffectBffOpenApiConfig, EffectDataPlatformValidationOptions } from './handler';
2
2
  import { type EffectApiModule, type EffectBffRequestHandler } from './module';
3
3
  import { type EffectContext } from './operation-context';
4
+ export { runWithEffectContext, useEffectContext, useOperationContext, } from './edge-context';
4
5
  export * from './handler';
5
6
  export { type CreateEffectOperationContextOptions, createEffectOperationContext, type EffectContext, } from './operation-context';
6
7
  export type EffectBffEdgeDispatchOptions = {
@@ -19,6 +20,7 @@ export type EffectBffEdgeHandlerOptions = {
19
20
  onWarning?: (message: string) => void;
20
21
  };
21
22
  export declare function dispatchEffectBffRequest(handler: EffectBffRequestHandler, request: Request, options?: EffectBffEdgeDispatchOptions): Promise<Response>;
23
+ export declare const createEffectBffTestHandler: typeof createEffectBffEdgeHandler;
22
24
  export declare function createEffectBffEdgeHandler(options: EffectBffEdgeHandlerOptions): Promise<{
23
25
  handler: (request: Request, dispatchOptions?: Omit<EffectBffEdgeDispatchOptions, 'prefix' | 'onError'>) => Promise<Response>;
24
26
  dispose: () => Promise<void>;
@@ -144,11 +144,8 @@ export type EffectRequestValidator = (request: Request) => Response | null | und
144
144
  * seam runs inside the produced handler, including per batched item.
145
145
  *
146
146
  * `defineEffectBff` brands its factory automatically. Hand-written factories
147
- * are NOT assumed to honor `validateRequest` (the option is newer than the
148
- * factory shape), so the effect adapter falls back to middleware enforcement
149
- * for unbranded factories instead of silently skipping the policy. Custom
150
- * factories that do forward `validateRequest` into `createHttpApiHandler`
151
- * may opt in by setting this symbol property to `true`.
147
+ * are NOT assumed to honor `validateRequest`, so strict module resolution
148
+ * rejects unbranded factories instead of silently skipping the policy.
152
149
  *
153
150
  * Registered via `Symbol.for` so independently loaded runtime copies agree.
154
151
  */
@@ -1,3 +1,4 @@
1
1
  export * as OpenTelemetry from '@effect/opentelemetry';
2
- export { type CreateEffectOperationContextOptions, createEffectOperationContext, type EffectContext, useEffectContext, useOperationContext, } from './context';
2
+ export { type CreateEffectOperationContextOptions, createEffectOperationContext, type EffectContext, runWithEffectContext, useEffectContext, useOperationContext, } from './context';
3
+ export { createEffectBffEdgeHandler, createEffectBffTestHandler, dispatchEffectBffRequest, } from './edge';
3
4
  export * from './handler';
@@ -20,27 +20,14 @@ export type EffectApiModule = {
20
20
  export type LoadedEffectBffHandler = {
21
21
  handler: EffectBffRequestHandler;
22
22
  dispose?: () => Promise<void>;
23
- /**
24
- * True when the handler is known to apply `validateRequest` internally
25
- * (including per batched item): `{ api, layer }` modules resolved through
26
- * `createHttpApiHandler`, and `createHandler` factories branded by
27
- * `defineEffectBff` (see `EFFECT_VALIDATOR_AWARE_FACTORY`). Plain
28
- * `handler` exports and unbranded custom factories leave this unset; the
29
- * adapter middleware must enforce the policy itself for those.
30
- */
31
- appliesRequestValidator?: boolean;
32
23
  };
33
24
  export type ResolveEffectBffModuleHandlerOptions = {
34
25
  openapi?: EffectBffOpenApiConfig;
35
26
  dataPlatform?: EffectDataPlatformValidationOptions;
36
27
  /**
37
- * Server-seam request validator (cross-project policy). Applied by
38
- * `createHttpApiHandler` to direct, batched and rpc requests.
39
- *
40
- * Note: plain `handler` exports and unbranded custom `createHandler`
41
- * factories bypass this seam — the effect adapter enforces the policy in
42
- * its middleware for those shapes instead (see
43
- * `LoadedEffectBffHandler.appliesRequestValidator`).
28
+ * Server-seam request validator (cross-project policy). Strict Effect
29
+ * entries forward it into `createHttpApiHandler`, where it applies to
30
+ * direct, batched and rpc requests.
44
31
  */
45
32
  validateRequest?: EffectRequestValidator;
46
33
  onWarning?: (message: string) => void;
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.4.0-ultramodern.9",
20
+ "version": "3.5.0-ultramodern.1",
21
21
  "types": "./dist/types/cli.d.ts",
22
22
  "main": "./dist/cjs/cli.js",
23
23
  "exports": {
@@ -157,12 +157,12 @@
157
157
  "effect": "4.0.0-beta.89",
158
158
  "qs": "^6.15.3",
159
159
  "type-is": "^2.1.0",
160
- "@modern-js/bff-core": "npm:@bleedingdev/modern-js-bff-core@3.4.0-ultramodern.9",
161
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.4.0-ultramodern.9",
162
- "@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.4.0-ultramodern.9",
163
- "@modern-js/create-request": "npm:@bleedingdev/modern-js-create-request@3.4.0-ultramodern.9",
164
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.4.0-ultramodern.9",
165
- "@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.4.0-ultramodern.9"
160
+ "@modern-js/bff-core": "npm:@bleedingdev/modern-js-bff-core@3.5.0-ultramodern.1",
161
+ "@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.5.0-ultramodern.1",
162
+ "@modern-js/create-request": "npm:@bleedingdev/modern-js-create-request@3.5.0-ultramodern.1",
163
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.1",
164
+ "@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.5.0-ultramodern.1",
165
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.1"
166
166
  },
167
167
  "devDependencies": {
168
168
  "@rsbuild/core": "2.1.0",
@@ -174,12 +174,12 @@
174
174
  "memfs": "^4.57.8",
175
175
  "typescript": "^6.0.3",
176
176
  "zod": "^4.4.3",
177
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.4.0-ultramodern.9",
178
- "@modern-js/bff-runtime": "npm:@bleedingdev/modern-js-bff-runtime@3.4.0-ultramodern.9",
179
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.4.0-ultramodern.9",
180
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.4.0-ultramodern.9",
181
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.4.0-ultramodern.9",
182
- "@scripts/rstest-config": "2.66.0"
177
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.1",
178
+ "@modern-js/bff-runtime": "npm:@bleedingdev/modern-js-bff-runtime@3.5.0-ultramodern.1",
179
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.1",
180
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.1",
181
+ "@scripts/rstest-config": "2.66.0",
182
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.1"
183
183
  },
184
184
  "sideEffects": false,
185
185
  "publishConfig": {