@bleedingdev/modern-js-bff-effect 3.8.3-ultramodern.2

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 (230) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/cjs/data-platform/batch/protocol.js +130 -0
  4. package/dist/cjs/data-platform/batch/queue.js +623 -0
  5. package/dist/cjs/data-platform/batch/request.js +121 -0
  6. package/dist/cjs/data-platform/batch/response.js +65 -0
  7. package/dist/cjs/data-platform/batch/telemetry.js +71 -0
  8. package/dist/cjs/data-platform/batch/url.js +69 -0
  9. package/dist/cjs/data-platform/batch-transport.js +59 -0
  10. package/dist/cjs/data-platform/codec.js +155 -0
  11. package/dist/cjs/data-platform/index.js +124 -0
  12. package/dist/cjs/data-platform/invalidation.js +61 -0
  13. package/dist/cjs/data-platform/trace.js +73 -0
  14. package/dist/cjs/data-platform/types.js +48 -0
  15. package/dist/cjs/data-platform/validation.js +178 -0
  16. package/dist/cjs/effect/context.js +59 -0
  17. package/dist/cjs/effect/dispatch.js +101 -0
  18. package/dist/cjs/effect/edge-context.js +61 -0
  19. package/dist/cjs/effect/edge-dispatcher.js +354 -0
  20. package/dist/cjs/effect/edge.js +62 -0
  21. package/dist/cjs/effect/endpoint-contracts.js +121 -0
  22. package/dist/cjs/effect/entry-shape-registry.js +31 -0
  23. package/dist/cjs/effect/entry-shape.js +113 -0
  24. package/dist/cjs/effect/handler/batch-handler.js +270 -0
  25. package/dist/cjs/effect/handler/batch.js +170 -0
  26. package/dist/cjs/effect/handler/definition.js +99 -0
  27. package/dist/cjs/effect/handler/envelope.js +104 -0
  28. package/dist/cjs/effect/handler/http.js +107 -0
  29. package/dist/cjs/effect/handler/openapi.js +58 -0
  30. package/dist/cjs/effect/handler/routing.js +100 -0
  31. package/dist/cjs/effect/handler/rpc.js +63 -0
  32. package/dist/cjs/effect/handler/types.js +43 -0
  33. package/dist/cjs/effect/handler.js +173 -0
  34. package/dist/cjs/effect/index.js +137 -0
  35. package/dist/cjs/effect/module.js +118 -0
  36. package/dist/cjs/effect/operation-context.js +116 -0
  37. package/dist/cjs/effect/rpcSerialization.js +54 -0
  38. package/dist/cjs/effect-client/batch.js +73 -0
  39. package/dist/cjs/effect-client/envelope.js +145 -0
  40. package/dist/cjs/effect-client/guards.js +44 -0
  41. package/dist/cjs/effect-client/index.js +208 -0
  42. package/dist/cjs/effect-client/request-context.js +64 -0
  43. package/dist/cjs/effect-client/request.js +66 -0
  44. package/dist/cjs/effect-client/runtime.js +100 -0
  45. package/dist/cjs/effect-client/types.js +18 -0
  46. package/dist/cjs/headers.js +46 -0
  47. package/dist/cjs/index.js +83 -0
  48. package/dist/esm/data-platform/batch/protocol.mjs +71 -0
  49. package/dist/esm/data-platform/batch/queue.mjs +582 -0
  50. package/dist/esm/data-platform/batch/request.mjs +68 -0
  51. package/dist/esm/data-platform/batch/response.mjs +24 -0
  52. package/dist/esm/data-platform/batch/telemetry.mjs +27 -0
  53. package/dist/esm/data-platform/batch/url.mjs +25 -0
  54. package/dist/esm/data-platform/batch-transport.mjs +12 -0
  55. package/dist/esm/data-platform/codec.mjs +90 -0
  56. package/dist/esm/data-platform/index.mjs +7 -0
  57. package/dist/esm/data-platform/invalidation.mjs +20 -0
  58. package/dist/esm/data-platform/trace.mjs +29 -0
  59. package/dist/esm/data-platform/types.mjs +4 -0
  60. package/dist/esm/data-platform/validation.mjs +128 -0
  61. package/dist/esm/effect/context.mjs +12 -0
  62. package/dist/esm/effect/dispatch.mjs +63 -0
  63. package/dist/esm/effect/edge-context.mjs +14 -0
  64. package/dist/esm/effect/edge-dispatcher.mjs +115 -0
  65. package/dist/esm/effect/edge.mjs +1 -0
  66. package/dist/esm/effect/endpoint-contracts.mjs +68 -0
  67. package/dist/esm/effect/entry-shape-registry.mjs +17 -0
  68. package/dist/esm/effect/entry-shape.mjs +51 -0
  69. package/dist/esm/effect/handler/batch-handler.mjs +232 -0
  70. package/dist/esm/effect/handler/batch.mjs +105 -0
  71. package/dist/esm/effect/handler/definition.mjs +58 -0
  72. package/dist/esm/effect/handler/envelope.mjs +63 -0
  73. package/dist/esm/effect/handler/http.mjs +69 -0
  74. package/dist/esm/effect/handler/openapi.mjs +20 -0
  75. package/dist/esm/effect/handler/routing.mjs +50 -0
  76. package/dist/esm/effect/handler/rpc.mjs +22 -0
  77. package/dist/esm/effect/handler/types.mjs +5 -0
  78. package/dist/esm/effect/handler.mjs +13 -0
  79. package/dist/esm/effect/index.mjs +7 -0
  80. package/dist/esm/effect/module.mjs +80 -0
  81. package/dist/esm/effect/operation-context.mjs +75 -0
  82. package/dist/esm/effect/rpcSerialization.mjs +16 -0
  83. package/dist/esm/effect-client/batch.mjs +35 -0
  84. package/dist/esm/effect-client/envelope.mjs +104 -0
  85. package/dist/esm/effect-client/guards.mjs +3 -0
  86. package/dist/esm/effect-client/index.mjs +110 -0
  87. package/dist/esm/effect-client/request-context.mjs +23 -0
  88. package/dist/esm/effect-client/request.mjs +28 -0
  89. package/dist/esm/effect-client/runtime.mjs +62 -0
  90. package/dist/esm/effect-client/types.mjs +0 -0
  91. package/dist/esm/headers.mjs +8 -0
  92. package/dist/esm/index.mjs +4 -0
  93. package/dist/esm/rslib-runtime.mjs +18 -0
  94. package/dist/esm-node/data-platform/batch/protocol.mjs +72 -0
  95. package/dist/esm-node/data-platform/batch/queue.mjs +583 -0
  96. package/dist/esm-node/data-platform/batch/request.mjs +69 -0
  97. package/dist/esm-node/data-platform/batch/response.mjs +25 -0
  98. package/dist/esm-node/data-platform/batch/telemetry.mjs +28 -0
  99. package/dist/esm-node/data-platform/batch/url.mjs +26 -0
  100. package/dist/esm-node/data-platform/batch-transport.mjs +13 -0
  101. package/dist/esm-node/data-platform/codec.mjs +91 -0
  102. package/dist/esm-node/data-platform/index.mjs +8 -0
  103. package/dist/esm-node/data-platform/invalidation.mjs +21 -0
  104. package/dist/esm-node/data-platform/trace.mjs +30 -0
  105. package/dist/esm-node/data-platform/types.mjs +5 -0
  106. package/dist/esm-node/data-platform/validation.mjs +129 -0
  107. package/dist/esm-node/effect/context.mjs +13 -0
  108. package/dist/esm-node/effect/dispatch.mjs +64 -0
  109. package/dist/esm-node/effect/edge-context.mjs +15 -0
  110. package/dist/esm-node/effect/edge-dispatcher.mjs +116 -0
  111. package/dist/esm-node/effect/edge.mjs +2 -0
  112. package/dist/esm-node/effect/endpoint-contracts.mjs +69 -0
  113. package/dist/esm-node/effect/entry-shape-registry.mjs +18 -0
  114. package/dist/esm-node/effect/entry-shape.mjs +63 -0
  115. package/dist/esm-node/effect/handler/batch-handler.mjs +233 -0
  116. package/dist/esm-node/effect/handler/batch.mjs +106 -0
  117. package/dist/esm-node/effect/handler/definition.mjs +59 -0
  118. package/dist/esm-node/effect/handler/envelope.mjs +64 -0
  119. package/dist/esm-node/effect/handler/http.mjs +70 -0
  120. package/dist/esm-node/effect/handler/openapi.mjs +21 -0
  121. package/dist/esm-node/effect/handler/routing.mjs +51 -0
  122. package/dist/esm-node/effect/handler/rpc.mjs +23 -0
  123. package/dist/esm-node/effect/handler/types.mjs +6 -0
  124. package/dist/esm-node/effect/handler.mjs +14 -0
  125. package/dist/esm-node/effect/index.mjs +8 -0
  126. package/dist/esm-node/effect/module.mjs +81 -0
  127. package/dist/esm-node/effect/operation-context.mjs +76 -0
  128. package/dist/esm-node/effect/rpcSerialization.mjs +17 -0
  129. package/dist/esm-node/effect-client/batch.mjs +36 -0
  130. package/dist/esm-node/effect-client/envelope.mjs +105 -0
  131. package/dist/esm-node/effect-client/guards.mjs +4 -0
  132. package/dist/esm-node/effect-client/index.mjs +111 -0
  133. package/dist/esm-node/effect-client/request-context.mjs +24 -0
  134. package/dist/esm-node/effect-client/request.mjs +29 -0
  135. package/dist/esm-node/effect-client/runtime.mjs +63 -0
  136. package/dist/esm-node/effect-client/types.mjs +1 -0
  137. package/dist/esm-node/headers.mjs +9 -0
  138. package/dist/esm-node/index.mjs +5 -0
  139. package/dist/esm-node/rslib-runtime.mjs +19 -0
  140. package/dist/types/data-platform/batch/protocol.d.ts +9 -0
  141. package/dist/types/data-platform/batch/queue.d.ts +47 -0
  142. package/dist/types/data-platform/batch/request.d.ts +19 -0
  143. package/dist/types/data-platform/batch/response.d.ts +3 -0
  144. package/dist/types/data-platform/batch/telemetry.d.ts +4 -0
  145. package/dist/types/data-platform/batch/url.d.ts +4 -0
  146. package/dist/types/data-platform/batch-transport.d.ts +3 -0
  147. package/dist/types/data-platform/codec.d.ts +17 -0
  148. package/dist/types/data-platform/index.d.ts +8 -0
  149. package/dist/types/data-platform/invalidation.d.ts +8 -0
  150. package/dist/types/data-platform/trace.d.ts +4 -0
  151. package/dist/types/data-platform/types.d.ts +151 -0
  152. package/dist/types/data-platform/validation.d.ts +29 -0
  153. package/dist/types/effect/context.d.ts +5 -0
  154. package/dist/types/effect/dispatch.d.ts +13 -0
  155. package/dist/types/effect/edge-context.d.ts +5 -0
  156. package/dist/types/effect/edge-dispatcher.d.ts +37 -0
  157. package/dist/types/effect/edge.d.ts +1 -0
  158. package/dist/types/effect/endpoint-contracts.d.ts +67 -0
  159. package/dist/types/effect/entry-shape.d.ts +32 -0
  160. package/dist/types/effect/handler/batch-handler.d.ts +11 -0
  161. package/dist/types/effect/handler/batch.d.ts +15 -0
  162. package/dist/types/effect/handler/definition.d.ts +7 -0
  163. package/dist/types/effect/handler/envelope.d.ts +13 -0
  164. package/dist/types/effect/handler/http.d.ts +15 -0
  165. package/dist/types/effect/handler/openapi.d.ts +4 -0
  166. package/dist/types/effect/handler/routing.d.ts +5 -0
  167. package/dist/types/effect/handler/rpc.d.ts +7 -0
  168. package/dist/types/effect/handler/types.d.ts +153 -0
  169. package/dist/types/effect/handler.d.ts +13 -0
  170. package/dist/types/effect/index.d.ts +6 -0
  171. package/dist/types/effect/module.d.ts +36 -0
  172. package/dist/types/effect/operation-context.d.ts +15 -0
  173. package/dist/types/effect/rpcSerialization.d.ts +3 -0
  174. package/dist/types/effect-client/batch.d.ts +2 -0
  175. package/dist/types/effect-client/envelope.d.ts +15 -0
  176. package/dist/types/effect-client/guards.d.ts +2 -0
  177. package/dist/types/effect-client/index.d.ts +58 -0
  178. package/dist/types/effect-client/request-context.d.ts +3 -0
  179. package/dist/types/effect-client/request.d.ts +1 -0
  180. package/dist/types/effect-client/runtime.d.ts +5 -0
  181. package/dist/types/effect-client/types.d.ts +68 -0
  182. package/dist/types/headers.d.ts +1 -0
  183. package/dist/types/index.d.ts +3 -0
  184. package/package.json +164 -0
  185. package/src/data-platform/batch/protocol.ts +112 -0
  186. package/src/data-platform/batch/queue.ts +876 -0
  187. package/src/data-platform/batch/request.ts +130 -0
  188. package/src/data-platform/batch/response.ts +74 -0
  189. package/src/data-platform/batch/telemetry.ts +47 -0
  190. package/src/data-platform/batch/url.ts +54 -0
  191. package/src/data-platform/batch-transport.ts +22 -0
  192. package/src/data-platform/codec.ts +145 -0
  193. package/src/data-platform/index.ts +63 -0
  194. package/src/data-platform/invalidation.ts +57 -0
  195. package/src/data-platform/trace.ts +49 -0
  196. package/src/data-platform/types.ts +186 -0
  197. package/src/data-platform/validation.ts +303 -0
  198. package/src/effect/context.ts +37 -0
  199. package/src/effect/dispatch.ts +132 -0
  200. package/src/effect/edge-context.ts +51 -0
  201. package/src/effect/edge-dispatcher.ts +310 -0
  202. package/src/effect/edge.ts +1 -0
  203. package/src/effect/endpoint-contracts.ts +191 -0
  204. package/src/effect/entry-shape-registry.cjs +16 -0
  205. package/src/effect/entry-shape-registry.d.cts +6 -0
  206. package/src/effect/entry-shape.ts +155 -0
  207. package/src/effect/handler/batch-handler.ts +475 -0
  208. package/src/effect/handler/batch.ts +182 -0
  209. package/src/effect/handler/definition.ts +121 -0
  210. package/src/effect/handler/envelope.ts +133 -0
  211. package/src/effect/handler/http.ts +133 -0
  212. package/src/effect/handler/openapi.ts +45 -0
  213. package/src/effect/handler/routing.ts +91 -0
  214. package/src/effect/handler/rpc.ts +45 -0
  215. package/src/effect/handler/types.ts +255 -0
  216. package/src/effect/handler.ts +33 -0
  217. package/src/effect/index.ts +22 -0
  218. package/src/effect/module.ts +186 -0
  219. package/src/effect/operation-context.ts +144 -0
  220. package/src/effect/rpcSerialization.ts +20 -0
  221. package/src/effect-client/batch.ts +50 -0
  222. package/src/effect-client/envelope.ts +238 -0
  223. package/src/effect-client/guards.ts +6 -0
  224. package/src/effect-client/index.ts +325 -0
  225. package/src/effect-client/request-context.ts +54 -0
  226. package/src/effect-client/request.ts +51 -0
  227. package/src/effect-client/runtime.ts +122 -0
  228. package/src/effect-client/types.ts +84 -0
  229. package/src/headers.ts +7 -0
  230. package/src/index.ts +3 -0
@@ -0,0 +1,121 @@
1
+ // @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off globalDate:off globalTimers:off newPromise:off strictBooleanExpressions:off
2
+ import type { HttpApi } from 'effect/unstable/httpapi';
3
+ import type { Rpc } from 'effect/unstable/rpc';
4
+
5
+ import { registerValidatorAwareHandlerFactory } from '../entry-shape';
6
+ import { mergeDataPlatformOptions } from './envelope';
7
+ import { createHttpApiHandler } from './http';
8
+ import { createRpcApiHandler } from './rpc';
9
+ import type {
10
+ EffectApiPromiseClientFromApi,
11
+ EffectBffDefinition,
12
+ EffectBffHandlerFactory,
13
+ EffectBffRuntime,
14
+ EffectDataPlatformValidationOptions,
15
+ EffectRpcBffDefinition,
16
+ EffectRpcBffHandlerFactory,
17
+ EffectRpcRuntimeLayer,
18
+ EffectRuntimeLayer,
19
+ } from './types';
20
+
21
+ export function defineEffectBff<
22
+ TApi extends HttpApi.Constraint,
23
+ TLayer extends EffectRuntimeLayer,
24
+ TRpcs extends Rpc.Any = Rpc.Any,
25
+ >(
26
+ definition: EffectBffDefinition<TApi, TLayer, TRpcs>,
27
+ ): EffectBffDefinition<TApi, TLayer, TRpcs> & EffectBffRuntime<TApi, TLayer> {
28
+ const createHandler = registerValidatorAwareHandlerFactory<
29
+ EffectBffHandlerFactory<TApi, TLayer>
30
+ >(options => {
31
+ const rpcDefinition = definition.rpc;
32
+ let mergedRpcOptions: EffectRpcBffDefinition<TRpcs> | undefined =
33
+ rpcDefinition;
34
+ if (rpcDefinition && options?.rpc) {
35
+ mergedRpcOptions = {
36
+ ...rpcDefinition,
37
+ ...options.rpc,
38
+ };
39
+ }
40
+
41
+ return createHttpApiHandler<TApi, TRpcs>({
42
+ api: definition.api,
43
+ layer: definition.layer,
44
+ openapi: options?.openapi,
45
+ rpc: mergedRpcOptions,
46
+ dataPlatform: mergeDataPlatformOptions(
47
+ definition.dataPlatform,
48
+ options?.dataPlatform,
49
+ ),
50
+ interceptRequest: definition.interceptRequest,
51
+ validateRequest: options?.validateRequest,
52
+ });
53
+ });
54
+ const client = createLoaderMaterializedClientPlaceholder<TApi>();
55
+ return {
56
+ ...definition,
57
+ createHandler,
58
+ client,
59
+ };
60
+ }
61
+
62
+ const LOADER_CLIENT_IGNORED_KEYS = new Set<PropertyKey>([
63
+ 'then',
64
+ 'catch',
65
+ 'finally',
66
+ 'toJSON',
67
+ '$$typeof',
68
+ ]);
69
+
70
+ /**
71
+ * `defineEffectBff(...).client` is only materialized when the entry module
72
+ * is imported through the `@api/index` transform (the webpack/rspack
73
+ * loader swaps the module for generated client code). Importing the entry
74
+ * directly (server code, scripts, tests) used to return a `client` typed as
75
+ * fully functional but `undefined` at runtime — a bare TypeError with zero
76
+ * explanation. This placeholder keeps the type contract while failing with
77
+ * an actionable error on first property access.
78
+ */
79
+ function createLoaderMaterializedClientPlaceholder<
80
+ TApi extends HttpApi.Constraint,
81
+ >(): EffectApiPromiseClientFromApi<TApi> {
82
+ const explain = (property: PropertyKey): never => {
83
+ throw new Error(
84
+ `[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.`,
85
+ );
86
+ };
87
+
88
+ return new Proxy(Object.create(null), {
89
+ get(_target, property) {
90
+ // Keep async/inspection protocols (await, console.log, React internals)
91
+ // from throwing so the placeholder stays inert until really used.
92
+ if (
93
+ typeof property === 'symbol' ||
94
+ LOADER_CLIENT_IGNORED_KEYS.has(property)
95
+ ) {
96
+ return undefined;
97
+ }
98
+ return explain(property);
99
+ },
100
+ }) as EffectApiPromiseClientFromApi<TApi>;
101
+ }
102
+
103
+ export function defineEffectRpcBff<
104
+ TRpcs extends Rpc.Any = Rpc.Any,
105
+ TLayer extends EffectRpcRuntimeLayer<TRpcs> = EffectRpcRuntimeLayer<TRpcs>,
106
+ >(
107
+ definition: EffectRpcBffDefinition<TRpcs, TLayer>,
108
+ ): EffectRpcBffDefinition<TRpcs, TLayer> & {
109
+ createHandler: EffectRpcBffHandlerFactory<TRpcs>;
110
+ } {
111
+ const createHandler: EffectRpcBffHandlerFactory<TRpcs> = options =>
112
+ createRpcApiHandler({
113
+ ...definition,
114
+ ...options,
115
+ });
116
+
117
+ return {
118
+ ...definition,
119
+ createHandler,
120
+ };
121
+ }
@@ -0,0 +1,133 @@
1
+ // @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off globalDate:off globalTimers:off newPromise:off strictBooleanExpressions:off
2
+ import {
3
+ DEFAULT_DATA_ENVELOPE_HEADER,
4
+ decodeRequestEnvelopeHeader,
5
+ validateRequestEnvelope,
6
+ validateSelectionPlan,
7
+ } from '../../data-platform';
8
+
9
+ import { getExpectedEnvelopeOrigin } from './routing';
10
+ import type { EffectDataPlatformValidationOptions } from './types';
11
+
12
+ function createInvalidEnvelopeResponse(message: string, errors?: string[]) {
13
+ return new Response(
14
+ JSON.stringify({
15
+ message,
16
+ ...(errors && errors.length > 0 ? { errors } : {}),
17
+ }),
18
+ {
19
+ status: 400,
20
+ headers: {
21
+ 'content-type': 'application/json; charset=utf-8',
22
+ },
23
+ },
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Validates the `x-modernjs-data-envelope` header (the data-platform
29
+ * transport/cache contract: protocol version, scope, selection plan).
30
+ *
31
+ * This is NOT a security boundary: the envelope is client-constructed cache
32
+ * metadata used for batching, scope keys and hydration. Cross-project
33
+ * authorization runs through the bff-core policy evaluator wired in via
34
+ * `validateRequest` (see `EffectRequestValidator`), which both the direct
35
+ * and batched request paths hit before this check.
36
+ */
37
+
38
+ export function validateDataPlatformRequestEnvelope(
39
+ request: Request,
40
+ options: EffectDataPlatformValidationOptions | undefined,
41
+ ) {
42
+ const isEnabled = options?.enabled ?? true;
43
+ if (!isEnabled) {
44
+ return null;
45
+ }
46
+
47
+ const envelopeHeader =
48
+ options?.envelopeHeader || DEFAULT_DATA_ENVELOPE_HEADER;
49
+ const encodedEnvelope = request.headers.get(envelopeHeader);
50
+
51
+ if (!encodedEnvelope) {
52
+ if (options?.requireEnvelope) {
53
+ return createInvalidEnvelopeResponse(
54
+ `Missing required data envelope header: ${envelopeHeader}`,
55
+ );
56
+ }
57
+ return null;
58
+ }
59
+
60
+ const envelope = decodeRequestEnvelopeHeader(encodedEnvelope);
61
+ if (!envelope) {
62
+ return createInvalidEnvelopeResponse(
63
+ `Invalid data envelope header format: ${envelopeHeader}`,
64
+ );
65
+ }
66
+
67
+ const validation = validateRequestEnvelope(envelope, {
68
+ expectedProtocolVersion: 1,
69
+ expectedNamespace: options?.expectedNamespace,
70
+ expectedOrigin:
71
+ options?.validateOrigin === false
72
+ ? undefined
73
+ : getExpectedEnvelopeOrigin(request),
74
+ requireTraceContext: options?.requireTraceContext,
75
+ });
76
+
77
+ if (!validation.ok) {
78
+ return createInvalidEnvelopeResponse(
79
+ 'Invalid data envelope',
80
+ validation.errors,
81
+ );
82
+ }
83
+
84
+ if (envelope.selectionPlan) {
85
+ const selectionValidation = validateSelectionPlan(envelope.selectionPlan, {
86
+ maxDepth: options?.selection?.maxDepth,
87
+ maxFields: options?.selection?.maxFields,
88
+ allowedLeafPaths: options?.selection?.allowedLeafPaths,
89
+ });
90
+
91
+ if (!selectionValidation.ok) {
92
+ return createInvalidEnvelopeResponse(
93
+ 'Invalid data envelope selection plan',
94
+ selectionValidation.errors,
95
+ );
96
+ }
97
+ }
98
+
99
+ return null;
100
+ }
101
+
102
+ export function mergeDataPlatformOptions(
103
+ base: EffectDataPlatformValidationOptions | undefined,
104
+ override: Partial<EffectDataPlatformValidationOptions> | undefined,
105
+ ): EffectDataPlatformValidationOptions | undefined {
106
+ if (!base && !override) {
107
+ return undefined;
108
+ }
109
+
110
+ const baseSelection = base?.selection;
111
+ const overrideSelection = override?.selection;
112
+ const baseBatch = base?.batch;
113
+ const overrideBatch = override?.batch;
114
+
115
+ return {
116
+ ...base,
117
+ ...override,
118
+ selection:
119
+ baseSelection || overrideSelection
120
+ ? {
121
+ ...baseSelection,
122
+ ...overrideSelection,
123
+ }
124
+ : undefined,
125
+ batch:
126
+ baseBatch || overrideBatch
127
+ ? {
128
+ ...baseBatch,
129
+ ...overrideBatch,
130
+ }
131
+ : undefined,
132
+ };
133
+ }
@@ -0,0 +1,133 @@
1
+ // @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off globalDate:off globalTimers:off newPromise:off strictBooleanExpressions:off
2
+ import * as Layer from 'effect/Layer';
3
+ import { HttpRouter, HttpServer } from 'effect/unstable/http';
4
+ import type { HttpApi } from 'effect/unstable/httpapi';
5
+ import type { Rpc } from 'effect/unstable/rpc';
6
+
7
+ import { prepareJsonRequestBody } from './batch';
8
+ import { createDataPlatformBatchRequestHandler } from './batch-handler';
9
+ import { validateDataPlatformRequestEnvelope } from './envelope';
10
+ import { createOpenApiLayer } from './openapi';
11
+ import { getRequestPathname, isRpcRequest } from './routing';
12
+ import { createRpcApiHandler, normalizeRpcPath } from './rpc';
13
+ import type {
14
+ EffectBffOpenApiConfig,
15
+ EffectDataPlatformValidationOptions,
16
+ EffectRequestInterceptor,
17
+ EffectRequestValidator,
18
+ EffectRpcBffDefinition,
19
+ EffectRuntimeLayer,
20
+ } from './types';
21
+ import { toEffectServiceContext } from './types';
22
+
23
+ export function createHttpApiHandler<
24
+ TApi extends HttpApi.Constraint = HttpApi.Top,
25
+ TRpcs extends Rpc.Any = Rpc.Any,
26
+ >(options: {
27
+ api: TApi;
28
+ layer: EffectRuntimeLayer;
29
+ openapi?: EffectBffOpenApiConfig;
30
+ rpc?: EffectRpcBffDefinition<TRpcs>;
31
+ dataPlatform?: EffectDataPlatformValidationOptions;
32
+ interceptRequest?: EffectRequestInterceptor;
33
+ validateRequest?: EffectRequestValidator;
34
+ }) {
35
+ const apiLayer = options.layer.pipe(Layer.provide(HttpServer.layerServices));
36
+ const openApiLayer = createOpenApiLayer(options.api, options.openapi);
37
+ const mergedLayer = openApiLayer
38
+ ? Layer.mergeAll(apiLayer, openApiLayer)
39
+ : apiLayer;
40
+ const httpApiHandler = HttpRouter.toWebHandler(mergedLayer);
41
+
42
+ const dispatchHttpApiRequest = async (
43
+ request: Request,
44
+ context?: Parameters<typeof httpApiHandler.handler>[1],
45
+ ) => {
46
+ const preparedRequest = await prepareJsonRequestBody(request);
47
+ if (preparedRequest instanceof Response) {
48
+ return preparedRequest;
49
+ }
50
+ const validationError = validateDataPlatformRequestEnvelope(
51
+ preparedRequest,
52
+ options.dataPlatform,
53
+ );
54
+ if (validationError) {
55
+ return validationError;
56
+ }
57
+ return httpApiHandler.handler(
58
+ preparedRequest,
59
+ toEffectServiceContext(context),
60
+ );
61
+ };
62
+
63
+ const withRequestPolicy = async (
64
+ request: Request,
65
+ context?: Parameters<typeof httpApiHandler.handler>[1],
66
+ ) => {
67
+ // Policy seam first: every HttpApi request, direct or batched item,
68
+ // passes through here, so batch fan-out cannot bypass validator. The
69
+ // interceptor receives the original request and may own malformed or
70
+ // legacy payloads; delegated requests still pass through body validation.
71
+ const policyDenial = options.validateRequest?.(request);
72
+ if (policyDenial) {
73
+ return policyDenial;
74
+ }
75
+ const next = () => dispatchHttpApiRequest(request, context);
76
+ return options.interceptRequest
77
+ ? options.interceptRequest({ request, next })
78
+ : next();
79
+ };
80
+
81
+ const batchHandler = createDataPlatformBatchRequestHandler({
82
+ dataPlatform: options.dataPlatform,
83
+ handleItem: withRequestPolicy,
84
+ });
85
+
86
+ const handleHttpApiRequest = async (
87
+ request: Request,
88
+ context?: Parameters<typeof httpApiHandler.handler>[1],
89
+ ) => {
90
+ const pathname = getRequestPathname(request);
91
+ if (batchHandler.enabled && pathname === batchHandler.path) {
92
+ // Outer batch POST transport, not an API operation: each batched item is
93
+ // dispatched through withDataPlatformValidation and hits policy handling.
94
+ return batchHandler.handle(request, context);
95
+ }
96
+ return withRequestPolicy(request, context);
97
+ };
98
+
99
+ if (!options.rpc) {
100
+ return {
101
+ handler: handleHttpApiRequest,
102
+ dispose: async () => {
103
+ await httpApiHandler.dispose();
104
+ },
105
+ };
106
+ }
107
+
108
+ const rpcPath = normalizeRpcPath(options.rpc.path);
109
+ const rpcHandler = createRpcApiHandler(options.rpc);
110
+
111
+ return {
112
+ handler: async (
113
+ request: Request,
114
+ context?: Parameters<typeof httpApiHandler.handler>[1],
115
+ ) => {
116
+ if (isRpcRequest(request, rpcPath)) {
117
+ const policyDenial = options.validateRequest?.(request);
118
+ if (policyDenial) {
119
+ return policyDenial;
120
+ }
121
+ const next = () =>
122
+ rpcHandler.handler(request, toEffectServiceContext(context));
123
+ return options.interceptRequest
124
+ ? options.interceptRequest({ request, next })
125
+ : next();
126
+ }
127
+ return handleHttpApiRequest(request, context);
128
+ },
129
+ dispose: async () => {
130
+ await Promise.all([httpApiHandler.dispose(), rpcHandler.dispose()]);
131
+ },
132
+ };
133
+ }
@@ -0,0 +1,45 @@
1
+ // @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off globalDate:off globalTimers:off newPromise:off strictBooleanExpressions:off
2
+ import { HttpRouter, HttpServerResponse } from 'effect/unstable/http';
3
+ import { type HttpApi, OpenApi } from 'effect/unstable/httpapi';
4
+
5
+ import type { EffectBffOpenApiConfig } from './types';
6
+
7
+ const DEFAULT_OPENAPI_PATH = '/openapi.json';
8
+
9
+ function normalizeOpenApiPath(pathname: string) {
10
+ if (!pathname.startsWith('/')) {
11
+ return `/${pathname}` as `/${string}`;
12
+ }
13
+ return pathname as `/${string}`;
14
+ }
15
+
16
+ function getOpenApiOptions(openapi: EffectBffOpenApiConfig | undefined) {
17
+ if (openapi === false || openapi === undefined) {
18
+ return undefined;
19
+ }
20
+
21
+ const path = openapi === true ? DEFAULT_OPENAPI_PATH : openapi.path;
22
+ return {
23
+ path: normalizeOpenApiPath(path ?? DEFAULT_OPENAPI_PATH),
24
+ };
25
+ }
26
+
27
+ export function createOpenApiLayer(
28
+ api: HttpApi.Constraint,
29
+ openapi: EffectBffOpenApiConfig | undefined,
30
+ ) {
31
+ const openApiOptions = getOpenApiOptions(openapi);
32
+ if (!openApiOptions) {
33
+ return null;
34
+ }
35
+
36
+ // effect 4.0.0-beta.98 made `HttpApi`'s `Groups` parameter invariant, so no
37
+ // concrete api is assignable to the widened `HttpApi.Top` instance type.
38
+ // `HttpApi.Constraint` is the erased bound every api satisfies; `fromApi`
39
+ // only reads runtime properties, which `Top` preserves.
40
+ return HttpRouter.add(
41
+ 'GET',
42
+ openApiOptions.path,
43
+ HttpServerResponse.jsonUnsafe(OpenApi.fromApi(api as HttpApi.Top)),
44
+ );
45
+ }
@@ -0,0 +1,91 @@
1
+ // @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off globalDate:off globalTimers:off newPromise:off strictBooleanExpressions:off
2
+ import { isPlainObject } from '../../data-platform';
3
+
4
+ export function getRequestPathname(request: Request) {
5
+ try {
6
+ return new URL(request.url).pathname;
7
+ } catch {
8
+ return new URL(request.url, 'http://localhost').pathname;
9
+ }
10
+ }
11
+
12
+ function normalizeMountPrefix(prefix: string) {
13
+ if (!prefix || prefix === '/') {
14
+ return '';
15
+ }
16
+ return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
17
+ }
18
+
19
+ export function getMountedPrefixFromContext(
20
+ request: Request,
21
+ context: unknown,
22
+ ): string {
23
+ if (!isPlainObject(context) || typeof context.path !== 'string') {
24
+ return '';
25
+ }
26
+
27
+ const contextPath = normalizeMountPrefix(context.path);
28
+ const requestPath = normalizeMountPrefix(getRequestPathname(request));
29
+
30
+ if (
31
+ !contextPath ||
32
+ !requestPath ||
33
+ contextPath === requestPath ||
34
+ !contextPath.endsWith(requestPath)
35
+ ) {
36
+ return '';
37
+ }
38
+
39
+ return normalizeMountPrefix(
40
+ contextPath.slice(0, contextPath.length - requestPath.length),
41
+ );
42
+ }
43
+
44
+ export function removeMountedPrefixFromBatchPath(
45
+ pathWithQuery: string,
46
+ prefix: string,
47
+ ) {
48
+ const normalizedPrefix = normalizeMountPrefix(prefix);
49
+ if (!normalizedPrefix) {
50
+ return pathWithQuery;
51
+ }
52
+
53
+ const [pathname, ...queryParts] = pathWithQuery.split('?');
54
+ if (!pathname) {
55
+ return pathWithQuery;
56
+ }
57
+
58
+ let nextPathname = pathname;
59
+ if (pathname === normalizedPrefix) {
60
+ nextPathname = '/';
61
+ } else if (pathname.startsWith(`${normalizedPrefix}/`)) {
62
+ const sliced = pathname.slice(normalizedPrefix.length);
63
+ nextPathname = sliced.startsWith('/') ? sliced : `/${sliced}`;
64
+ }
65
+
66
+ if (queryParts.length === 0) {
67
+ return nextPathname;
68
+ }
69
+ return `${nextPathname}?${queryParts.join('?')}`;
70
+ }
71
+
72
+ function getRequestOrigin(request: Request) {
73
+ try {
74
+ return new URL(request.url).origin;
75
+ } catch {
76
+ return new URL(request.url, 'http://localhost').origin;
77
+ }
78
+ }
79
+
80
+ export function getExpectedEnvelopeOrigin(request: Request) {
81
+ const origin = request.headers.get('origin');
82
+ if (origin && origin !== 'null') {
83
+ return origin;
84
+ }
85
+ return getRequestOrigin(request);
86
+ }
87
+
88
+ export function isRpcRequest(request: Request, rpcPath: `/${string}`) {
89
+ const pathname = getRequestPathname(request);
90
+ return pathname === rpcPath || pathname.startsWith(`${rpcPath}/`);
91
+ }
@@ -0,0 +1,45 @@
1
+ // @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off globalDate:off globalTimers:off newPromise:off strictBooleanExpressions:off
2
+ import * as Layer from 'effect/Layer';
3
+ import { HttpRouter } from 'effect/unstable/http';
4
+ import { type Rpc, RpcServer } from 'effect/unstable/rpc';
5
+
6
+ import { getRpcSerializationLayer } from '../rpcSerialization';
7
+ import type { EffectRpcBffDefinition } from './types';
8
+
9
+ export function normalizeRpcPath(pathname: string | undefined) {
10
+ if (!pathname || pathname === '/') {
11
+ return '/rpc' as `/${string}`;
12
+ }
13
+ if (!pathname.startsWith('/')) {
14
+ return `/${pathname}` as `/${string}`;
15
+ }
16
+ return pathname as `/${string}`;
17
+ }
18
+
19
+ export function createRpcApiHandler<TRpcs extends Rpc.Any = Rpc.Any>(
20
+ options: EffectRpcBffDefinition<TRpcs>,
21
+ ) {
22
+ const rpcPath = normalizeRpcPath(options.path);
23
+ const rpcLayer = Layer.provide(
24
+ RpcServer.layerHttp({
25
+ group: options.group,
26
+ path: rpcPath,
27
+ protocol: 'http',
28
+ disableTracing: options.disableTracing,
29
+ spanPrefix: options.spanPrefix,
30
+ spanAttributes: options.spanAttributes,
31
+ }),
32
+ Layer.mergeAll(
33
+ options.layer,
34
+ getRpcSerializationLayer(options.serialization),
35
+ ),
36
+ );
37
+
38
+ return HttpRouter.toWebHandler<
39
+ never,
40
+ unknown,
41
+ HttpRouter.HttpRouter,
42
+ never,
43
+ never
44
+ >(rpcLayer);
45
+ }