@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.1 → 3.9.0-ultramodern.10

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 (198) hide show
  1. package/dist/cjs/buildIdentity.js +73 -0
  2. package/dist/cjs/contextExtensions.js +74 -0
  3. package/dist/cjs/headRuntime.js +91 -0
  4. package/dist/cjs/helmetContext.js +54 -0
  5. package/dist/cjs/index.js +18 -4
  6. package/dist/cjs/localisedLoaderIdentity.js +51 -0
  7. package/dist/cjs/prefetchPolicy.js +52 -0
  8. package/dist/cjs/reactContext.js +45 -0
  9. package/dist/cjs/request-policy/browser.js +139 -0
  10. package/dist/cjs/request-policy/client.js +234 -0
  11. package/dist/cjs/request-policy/node.js +149 -0
  12. package/dist/cjs/request-policy/policyCore.js +266 -0
  13. package/dist/cjs/request-policy/requestContext.js +137 -0
  14. package/dist/cjs/request-policy/traceparent.js +56 -0
  15. package/dist/cjs/request-policy/transport.js +234 -0
  16. package/dist/cjs/request-policy/types.js +57 -0
  17. package/dist/cjs/routeHydration.js +127 -0
  18. package/dist/cjs/routerPrefetchPolicy.js +153 -0
  19. package/dist/cjs/routerProvider.js +105 -0
  20. package/dist/cjs/routerState.js +204 -0
  21. package/dist/cjs/routerStatePlugin.js +86 -0
  22. package/dist/cjs/routerStateTypes.js +18 -0
  23. package/dist/cjs/rsc-disabled/rscDisabledClientBrowserRuntime.js +69 -0
  24. package/dist/cjs/rsc-disabled/rscDisabledClientEdgeRuntime.js +61 -0
  25. package/dist/cjs/rsc-disabled/rscDisabledClientNodeRuntime.js +65 -0
  26. package/dist/cjs/rsc-disabled/rscDisabledRuntimeError.js +42 -0
  27. package/dist/cjs/rsc-disabled/rscDisabledServerEdgeRuntime.js +93 -0
  28. package/dist/cjs/rsc-disabled/rscDisabledServerNodeRuntime.js +101 -0
  29. package/dist/cjs/runtimeContextProjection.js +112 -0
  30. package/dist/cjs/server-config/bffRuntime.js +18 -0
  31. package/dist/cjs/server-config/index.js +18 -0
  32. package/dist/cjs/server-config/serverTelemetry.js +18 -0
  33. package/dist/cjs/shellStream.js +34 -2
  34. package/dist/cjs/workspaceEvents.js +159 -0
  35. package/dist/esm/buildIdentity.mjs +35 -0
  36. package/dist/esm/contextExtensions.mjs +33 -0
  37. package/dist/esm/headRuntime.mjs +43 -0
  38. package/dist/esm/helmetContext.mjs +13 -0
  39. package/dist/esm/index.mjs +2 -0
  40. package/dist/esm/localisedLoaderIdentity.mjs +13 -0
  41. package/dist/esm/prefetchPolicy.mjs +11 -0
  42. package/dist/esm/reactContext.mjs +7 -0
  43. package/dist/esm/request-policy/browser.mjs +11 -0
  44. package/dist/esm/request-policy/client.mjs +195 -0
  45. package/dist/esm/request-policy/policyCore.mjs +171 -0
  46. package/dist/esm/request-policy/requestContext.mjs +75 -0
  47. package/dist/esm/request-policy/traceparent.mjs +18 -0
  48. package/dist/esm/request-policy/transport.mjs +196 -0
  49. package/dist/esm/request-policy/types.mjs +10 -0
  50. package/dist/esm/routeHydration.mjs +77 -0
  51. package/dist/esm/routerPrefetchPolicy.mjs +115 -0
  52. package/dist/esm/routerProvider.mjs +58 -0
  53. package/dist/esm/routerState.mjs +139 -0
  54. package/dist/esm/routerStatePlugin.mjs +48 -0
  55. package/dist/esm/routerStateTypes.mjs +0 -0
  56. package/dist/esm/rsc-disabled/rscDisabledClientBrowserRuntime.mjs +10 -0
  57. package/dist/esm/rsc-disabled/rscDisabledClientEdgeRuntime.mjs +8 -0
  58. package/dist/esm/rsc-disabled/rscDisabledClientNodeRuntime.mjs +9 -0
  59. package/dist/esm/rsc-disabled/rscDisabledRuntimeError.mjs +4 -0
  60. package/dist/esm/rsc-disabled/rscDisabledServerEdgeRuntime.mjs +16 -0
  61. package/dist/esm/rsc-disabled/rscDisabledServerNodeRuntime.mjs +18 -0
  62. package/dist/esm/runtimeContextProjection.mjs +74 -0
  63. package/dist/esm/server-config/bffRuntime.mjs +0 -0
  64. package/dist/esm/server-config/index.mjs +0 -0
  65. package/dist/esm/server-config/serverTelemetry.mjs +0 -0
  66. package/dist/esm/shellStream.mjs +34 -2
  67. package/dist/esm/workspaceEvents.mjs +66 -0
  68. package/dist/esm-node/buildIdentity.mjs +36 -0
  69. package/dist/esm-node/contextExtensions.mjs +34 -0
  70. package/dist/esm-node/headRuntime.mjs +44 -0
  71. package/dist/esm-node/helmetContext.mjs +14 -0
  72. package/dist/esm-node/index.mjs +2 -0
  73. package/dist/esm-node/localisedLoaderIdentity.mjs +14 -0
  74. package/dist/esm-node/prefetchPolicy.mjs +12 -0
  75. package/dist/esm-node/reactContext.mjs +8 -0
  76. package/dist/esm-node/request-policy/browser.mjs +12 -0
  77. package/dist/esm-node/request-policy/client.mjs +196 -0
  78. package/dist/esm-node/request-policy/node.mjs +22 -0
  79. package/dist/esm-node/request-policy/policyCore.mjs +172 -0
  80. package/dist/esm-node/request-policy/requestContext.mjs +76 -0
  81. package/dist/esm-node/request-policy/traceparent.mjs +19 -0
  82. package/dist/esm-node/request-policy/transport.mjs +197 -0
  83. package/dist/esm-node/request-policy/types.mjs +11 -0
  84. package/dist/esm-node/routeHydration.mjs +78 -0
  85. package/dist/esm-node/routerPrefetchPolicy.mjs +116 -0
  86. package/dist/esm-node/routerProvider.mjs +59 -0
  87. package/dist/esm-node/routerState.mjs +140 -0
  88. package/dist/esm-node/routerStatePlugin.mjs +49 -0
  89. package/dist/esm-node/routerStateTypes.mjs +1 -0
  90. package/dist/esm-node/rsc-disabled/rscDisabledClientBrowserRuntime.mjs +11 -0
  91. package/dist/esm-node/rsc-disabled/rscDisabledClientEdgeRuntime.mjs +9 -0
  92. package/dist/esm-node/rsc-disabled/rscDisabledClientNodeRuntime.mjs +10 -0
  93. package/dist/esm-node/rsc-disabled/rscDisabledRuntimeError.mjs +5 -0
  94. package/dist/esm-node/rsc-disabled/rscDisabledServerEdgeRuntime.mjs +17 -0
  95. package/dist/esm-node/rsc-disabled/rscDisabledServerNodeRuntime.mjs +19 -0
  96. package/dist/esm-node/runtimeContextProjection.mjs +75 -0
  97. package/dist/esm-node/server-config/bffRuntime.mjs +1 -0
  98. package/dist/esm-node/server-config/index.mjs +1 -0
  99. package/dist/esm-node/server-config/serverTelemetry.mjs +1 -0
  100. package/dist/esm-node/shellStream.mjs +34 -2
  101. package/dist/esm-node/workspaceEvents.mjs +67 -0
  102. package/dist/types/buildIdentity.d.ts +37 -0
  103. package/dist/types/contextExtensions.d.ts +34 -0
  104. package/dist/types/headRuntime.d.ts +12 -0
  105. package/dist/types/helmetContext.d.ts +10 -0
  106. package/dist/types/index.d.ts +2 -0
  107. package/dist/types/localisedLoaderIdentity.d.ts +10 -0
  108. package/dist/types/prefetchPolicy.d.ts +4 -0
  109. package/dist/types/reactContext.d.ts +6 -0
  110. package/dist/types/request-policy/browser.d.ts +10 -0
  111. package/dist/types/request-policy/client.d.ts +14 -0
  112. package/dist/types/request-policy/node.d.ts +10 -0
  113. package/dist/types/request-policy/policyCore.d.ts +107 -0
  114. package/dist/types/request-policy/requestContext.d.ts +22 -0
  115. package/dist/types/request-policy/traceparent.d.ts +10 -0
  116. package/dist/types/request-policy/transport.d.ts +12 -0
  117. package/dist/types/request-policy/types.d.ts +144 -0
  118. package/dist/types/routeHydration.d.ts +24 -0
  119. package/dist/types/routerPrefetchPolicy.d.ts +28 -0
  120. package/dist/types/routerProvider.d.ts +55 -0
  121. package/dist/types/routerState.d.ts +18 -0
  122. package/dist/types/routerStatePlugin.d.ts +19 -0
  123. package/dist/types/routerStateTypes.d.ts +40 -0
  124. package/dist/types/rsc-disabled/rscDisabledClientBrowserRuntime.d.ts +9 -0
  125. package/dist/types/rsc-disabled/rscDisabledClientEdgeRuntime.d.ts +7 -0
  126. package/dist/types/rsc-disabled/rscDisabledClientNodeRuntime.d.ts +8 -0
  127. package/dist/types/rsc-disabled/rscDisabledRuntimeError.d.ts +1 -0
  128. package/dist/types/rsc-disabled/rscDisabledServerEdgeRuntime.d.ts +15 -0
  129. package/dist/types/rsc-disabled/rscDisabledServerNodeRuntime.d.ts +17 -0
  130. package/dist/types/runtimeContextProjection.d.ts +30 -0
  131. package/dist/types/server-config/bffRuntime.d.ts +122 -0
  132. package/dist/types/server-config/index.d.ts +2 -0
  133. package/dist/types/server-config/serverTelemetry.d.ts +322 -0
  134. package/dist/types/workspaceEvents.d.ts +62 -0
  135. package/package.json +239 -14
  136. package/rslib.config.mts +17 -2
  137. package/rstest.config.mts +33 -2
  138. package/src/buildIdentity.ts +61 -0
  139. package/src/contextExtensions.ts +81 -0
  140. package/src/headRuntime.tsx +77 -0
  141. package/src/helmetContext.ts +31 -0
  142. package/src/index.ts +2 -0
  143. package/src/localisedLoaderIdentity.ts +30 -0
  144. package/src/prefetchPolicy.ts +33 -0
  145. package/src/reactContext.ts +16 -0
  146. package/src/request-policy/browser.ts +22 -0
  147. package/src/request-policy/client.ts +404 -0
  148. package/src/request-policy/node.ts +41 -0
  149. package/src/request-policy/policyCore.ts +424 -0
  150. package/src/request-policy/requestContext.ts +149 -0
  151. package/src/request-policy/traceparent.ts +43 -0
  152. package/src/request-policy/transport.ts +349 -0
  153. package/src/request-policy/types.ts +229 -0
  154. package/src/routeHydration.ts +208 -0
  155. package/src/routerPrefetchPolicy.ts +195 -0
  156. package/src/routerProvider.ts +160 -0
  157. package/src/routerState.ts +290 -0
  158. package/src/routerStatePlugin.ts +86 -0
  159. package/src/routerStateTypes.ts +46 -0
  160. package/src/rsc-disabled/rscDisabledClientBrowserRuntime.ts +10 -0
  161. package/src/rsc-disabled/rscDisabledClientEdgeRuntime.ts +8 -0
  162. package/src/rsc-disabled/rscDisabledClientNodeRuntime.ts +9 -0
  163. package/src/rsc-disabled/rscDisabledRuntimeError.ts +3 -0
  164. package/src/rsc-disabled/rscDisabledServerEdgeRuntime.ts +16 -0
  165. package/src/rsc-disabled/rscDisabledServerNodeRuntime.ts +18 -0
  166. package/src/runtimeContextProjection.ts +105 -0
  167. package/src/server-config/bffRuntime.ts +136 -0
  168. package/src/server-config/index.ts +2 -0
  169. package/src/server-config/serverTelemetry.ts +337 -0
  170. package/src/shellStream.ts +55 -25
  171. package/src/workspaceEvents.ts +324 -0
  172. package/tests/buildIdentity.test.ts +51 -0
  173. package/tests/contextExtensions.test.ts +27 -0
  174. package/tests/headRuntime.test.tsx +229 -0
  175. package/tests/rendererHead.test.ts +1 -113
  176. package/tests/request-policy/browser-policy.test.ts +108 -0
  177. package/tests/request-policy/client-isolation.test.ts +109 -0
  178. package/tests/request-policy/node-policy.test.ts +447 -0
  179. package/tests/request-policy/policyCore.test.ts +129 -0
  180. package/tests/request-policy/requestContext.test.ts +66 -0
  181. package/tests/request-policy/requestFactory.outbound-contract.test.ts +290 -0
  182. package/tests/request-policy/traceparent.test.ts +52 -0
  183. package/tests/request-policy/transport-retry.test.ts +367 -0
  184. package/tests/routeHydration.test.ts +180 -0
  185. package/tests/routerPrefetchPolicy.test.ts +102 -0
  186. package/tests/routerProvider.test.ts +29 -0
  187. package/tests/routerState.test.ts +78 -0
  188. package/tests/routerStatePlugin.test.ts +63 -0
  189. package/tests/runtimeContextProjection.test.ts +83 -0
  190. package/tests/safeFailure.test.ts +10 -28
  191. package/tests/shellStream.test.ts +37 -0
  192. package/tests/workspaceEvents.test.ts +221 -0
  193. package/dist/cjs/rscHtmlStream.js +0 -128
  194. package/dist/esm/rscHtmlStream.mjs +0 -86
  195. package/dist/esm-node/rscHtmlStream.mjs +0 -87
  196. package/dist/types/rscHtmlStream.d.ts +0 -4
  197. package/src/rscHtmlStream.ts +0 -142
  198. package/tests/rscHtmlStream.test.ts +0 -153
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Shared (browser/node) core for the fork-added BFF producer-client policy:
3
+ * envelope construction, operation-context derivation, identity/operation
4
+ * contract violations and header utilities.
5
+ *
6
+ * This module is environment-neutral by design: it must not import node
7
+ * builtins (it is bundled into the browser entry) nor touch `window`
8
+ * directly (it is bundled into the node entry). Environment-specific
9
+ * behaviour (origin resolution, incoming-header forwarding, fetch wiring)
10
+ * stays in `browser.ts` / `node.ts`.
11
+ *
12
+ * Honest scope note: everything produced here (envelope, operation context,
13
+ * schema hash, operation version) is asserted by the CLIENT. It protects
14
+ * against version skew and misconfiguration, not against malicious callers —
15
+ * the server-side policy evaluator in `@modern-js/bff-core` is the actual
16
+ * gate, and only when it is bound to a verified identity.
17
+ */
18
+ import { parseTraceparent as parseTraceparentHeader } from './traceparent';
19
+ import type {
20
+ IdentityBindingViolation,
21
+ OperationContext,
22
+ OperationContractOptions,
23
+ OperationContractViolation,
24
+ TransportTarget,
25
+ } from './types';
26
+
27
+ declare const process:
28
+ | { readonly env?: Readonly<Record<string, string | undefined>> }
29
+ | undefined;
30
+
31
+ export const TRACEPARENT_HEADER = 'traceparent';
32
+
33
+ const readProcessEnv = (key: string) => {
34
+ if (
35
+ typeof process === 'undefined' ||
36
+ typeof process.env === 'undefined' ||
37
+ typeof process.env[key] !== 'string'
38
+ ) {
39
+ return undefined;
40
+ }
41
+
42
+ return process.env[key];
43
+ };
44
+
45
+ const isStrictDefaultRequestIdEnabled = () =>
46
+ readProcessEnv('MODERN_BFF_STRICT_DEFAULT_REQUEST_ID') === 'true';
47
+
48
+ export const isSecuredRequestId = (requestId: string) =>
49
+ requestId !== 'default' || isStrictDefaultRequestIdEnabled();
50
+
51
+ export const isEmptyDomain = (domain?: string) =>
52
+ typeof domain !== 'string' || domain.trim() === '';
53
+
54
+ export const firstHeaderValue = (value: unknown) =>
55
+ Array.isArray(value) ? value[0] : value;
56
+
57
+ export const findHeaderKey = (headers: Record<string, any>, header: string) => {
58
+ const normalized = header.toLowerCase();
59
+ return Object.keys(headers).find(key => key.toLowerCase() === normalized);
60
+ };
61
+
62
+ export const readHeader = (headers: Record<string, any>, header: string) => {
63
+ const key = findHeaderKey(headers, header);
64
+ return typeof key === 'string' ? headers[key] : undefined;
65
+ };
66
+
67
+ export const writeHeader = (
68
+ headers: Record<string, any>,
69
+ header: string,
70
+ value: unknown,
71
+ ) => {
72
+ if (typeof value === 'undefined') {
73
+ return;
74
+ }
75
+ const key = findHeaderKey(headers, header);
76
+ if (typeof key === 'string' && key !== header) {
77
+ delete headers[key];
78
+ }
79
+ headers[header] = value;
80
+ };
81
+
82
+ export const deleteHeader = (headers: Record<string, any>, header: string) => {
83
+ const key = findHeaderKey(headers, header);
84
+ if (typeof key === 'string') {
85
+ delete headers[key];
86
+ }
87
+ };
88
+
89
+ export const toOrigin = (value?: string) => {
90
+ if (!value) {
91
+ return undefined;
92
+ }
93
+ try {
94
+ return new URL(value).origin;
95
+ } catch (error) {
96
+ return undefined;
97
+ }
98
+ };
99
+
100
+ export const parseTraceparentValue = (value: unknown) =>
101
+ parseTraceparentHeader(firstHeaderValue(value) as string | undefined);
102
+
103
+ export const buildOperationContext = ({
104
+ requestId,
105
+ method,
106
+ path,
107
+ operationContext,
108
+ traceparent,
109
+ }: {
110
+ requestId: string;
111
+ method: string;
112
+ path: string;
113
+ operationContext?: OperationContext | undefined;
114
+ traceparent?: unknown;
115
+ }) => {
116
+ const routePath = operationContext?.routePath || path;
117
+ const operationMethod = (
118
+ operationContext?.method ||
119
+ method ||
120
+ 'GET'
121
+ ).toUpperCase();
122
+ const rawOperationId =
123
+ operationContext?.operationId || `${operationMethod}:${routePath}`;
124
+ const operationId = rawOperationId.startsWith(`${requestId}:`)
125
+ ? rawOperationId
126
+ : `${requestId}:${rawOperationId}`;
127
+ const traceparentValue =
128
+ operationContext?.traceparent ||
129
+ (typeof firstHeaderValue(traceparent) === 'string'
130
+ ? String(firstHeaderValue(traceparent))
131
+ : undefined);
132
+ const parsedTraceContext =
133
+ operationContext?.traceId && operationContext?.spanId
134
+ ? {
135
+ traceId: operationContext.traceId,
136
+ spanId: operationContext.spanId,
137
+ }
138
+ : parseTraceparentValue(traceparentValue);
139
+
140
+ return {
141
+ requestId,
142
+ operationId,
143
+ routePath,
144
+ method: operationMethod,
145
+ ...(operationContext?.schemaHash
146
+ ? { schemaHash: operationContext.schemaHash }
147
+ : {}),
148
+ ...(typeof operationContext?.operationVersion === 'number'
149
+ ? { operationVersion: operationContext.operationVersion }
150
+ : {}),
151
+ ...(traceparentValue ? { traceparent: traceparentValue } : {}),
152
+ ...(parsedTraceContext
153
+ ? {
154
+ traceId: parsedTraceContext.traceId,
155
+ spanId: parsedTraceContext.spanId,
156
+ }
157
+ : {}),
158
+ };
159
+ };
160
+
161
+ type OperationContextPayload = ReturnType<typeof buildOperationContext>;
162
+
163
+ export class ProducerClientNotInitializedError extends Error {
164
+ readonly code = 'BFF_PRODUCER_CLIENT_NOT_INITIALIZED';
165
+
166
+ constructor(requestId: string) {
167
+ super(
168
+ `Producer client "${requestId}" is not initialized. Call initProducerClient() (or configure()) before using generated APIs for this requestId.`,
169
+ );
170
+ this.name = 'ProducerClientNotInitializedError';
171
+ }
172
+ }
173
+
174
+ export class ProducerDomainNotConfiguredError extends Error {
175
+ readonly code = 'BFF_PRODUCER_DOMAIN_NOT_CONFIGURED';
176
+
177
+ constructor(requestId: string) {
178
+ super(
179
+ `Producer client "${requestId}" must provide setDomain() during configure().`,
180
+ );
181
+ this.name = 'ProducerDomainNotConfiguredError';
182
+ }
183
+ }
184
+
185
+ export class CrossOriginEnvelopePolicyError extends Error {
186
+ readonly code = 'BFF_CROSS_ORIGIN_ENVELOPE_NOT_ALLOWED';
187
+
188
+ constructor(requestId: string, sourceOrigin?: string, targetOrigin?: string) {
189
+ super(
190
+ `Cross-origin envelope is not allowed for producer "${requestId}" (${sourceOrigin || 'unknown-origin'} -> ${targetOrigin || 'unknown-origin'}). Configure allowCrossOriginEnvelope to explicitly allow this flow.`,
191
+ );
192
+ this.name = 'CrossOriginEnvelopePolicyError';
193
+ }
194
+ }
195
+
196
+ export class IdentityBindingViolationError extends Error {
197
+ readonly code = 'BFF_IDENTITY_BINDING_VIOLATION';
198
+
199
+ readonly violation: IdentityBindingViolation;
200
+
201
+ constructor(violation: IdentityBindingViolation) {
202
+ super(
203
+ `Identity header "${violation.header}" for producer "${violation.requestId}" was rejected by server-derived identity binding.`,
204
+ );
205
+ this.name = 'IdentityBindingViolationError';
206
+ this.violation = violation;
207
+ }
208
+ }
209
+
210
+ export class OperationContractViolationError extends Error {
211
+ readonly code = 'BFF_OPERATION_CONTRACT_VIOLATION';
212
+
213
+ readonly violation: OperationContractViolation;
214
+
215
+ constructor(violation: OperationContractViolation) {
216
+ super(
217
+ `Operation contract violation "${violation.reason}" for producer "${violation.requestId}" operation "${violation.operationId}".`,
218
+ );
219
+ this.name = 'OperationContractViolationError';
220
+ this.violation = violation;
221
+ }
222
+ }
223
+
224
+ export const validateOperationContract = ({
225
+ requestId,
226
+ target,
227
+ contextPayload,
228
+ operationContract,
229
+ }: {
230
+ requestId: string;
231
+ target: TransportTarget;
232
+ contextPayload: OperationContextPayload;
233
+ operationContract: OperationContractOptions | undefined;
234
+ }) => {
235
+ const operationContractEnabled =
236
+ operationContract?.enabled ?? isSecuredRequestId(requestId);
237
+
238
+ if (!operationContractEnabled) {
239
+ return;
240
+ }
241
+
242
+ const strict = operationContract?.strict ?? true;
243
+ const requireSchemaHash = operationContract?.requireSchemaHash ?? true;
244
+ const requireOperationVersion =
245
+ operationContract?.requireOperationVersion ?? true;
246
+
247
+ const maybeReportViolation = (
248
+ reason: OperationContractViolation['reason'],
249
+ ) => {
250
+ const violation: OperationContractViolation = {
251
+ requestId,
252
+ target,
253
+ operationId: contextPayload.operationId,
254
+ routePath: contextPayload.routePath,
255
+ method: contextPayload.method,
256
+ schemaHash:
257
+ typeof contextPayload.schemaHash === 'string'
258
+ ? contextPayload.schemaHash
259
+ : undefined,
260
+ operationVersion:
261
+ typeof contextPayload.operationVersion === 'number'
262
+ ? contextPayload.operationVersion
263
+ : undefined,
264
+ reason,
265
+ };
266
+ operationContract?.onViolation?.(violation);
267
+ if (strict) {
268
+ throw new OperationContractViolationError(violation);
269
+ }
270
+ };
271
+
272
+ if (requireSchemaHash && typeof contextPayload.schemaHash !== 'string') {
273
+ maybeReportViolation('missing_schema_hash');
274
+ }
275
+
276
+ if (
277
+ requireOperationVersion &&
278
+ typeof contextPayload.operationVersion !== 'number'
279
+ ) {
280
+ maybeReportViolation('missing_operation_version');
281
+ }
282
+ };
283
+
284
+ export const resolveConfiguredRequest = <F>(
285
+ configuredRequests: Map<string, F>,
286
+ requestId: string,
287
+ fallback: F,
288
+ ): F => {
289
+ const configuredRequest = configuredRequests.get(requestId);
290
+ if (configuredRequest) {
291
+ return configuredRequest;
292
+ }
293
+
294
+ if (requestId !== 'default') {
295
+ throw new ProducerClientNotInitializedError(requestId);
296
+ }
297
+
298
+ return fallback;
299
+ };
300
+
301
+ /**
302
+ * Builds the JSON value for the `x-modernjs-bff-envelope` header and throws
303
+ * when the cross-origin envelope policy denies the flow. Returns the
304
+ * serialized envelope.
305
+ */
306
+ export const buildEnvelopeHeaderValue = ({
307
+ requestId,
308
+ target,
309
+ sourceOrigin,
310
+ targetOrigin,
311
+ traceContext,
312
+ allowCrossOriginEnvelope,
313
+ }: {
314
+ requestId: string;
315
+ target: TransportTarget;
316
+ sourceOrigin: string | undefined;
317
+ targetOrigin: string | undefined;
318
+ traceContext: { traceId: string; spanId: string } | null | undefined;
319
+ allowCrossOriginEnvelope:
320
+ | boolean
321
+ | ((options: {
322
+ requestId: string;
323
+ sourceOrigin?: string;
324
+ targetOrigin?: string;
325
+ target: TransportTarget;
326
+ }) => boolean)
327
+ | undefined;
328
+ }) => {
329
+ const isCrossOrigin =
330
+ Boolean(sourceOrigin) &&
331
+ Boolean(targetOrigin) &&
332
+ sourceOrigin !== targetOrigin;
333
+ if (isCrossOrigin) {
334
+ const isAllowed =
335
+ typeof allowCrossOriginEnvelope === 'function'
336
+ ? allowCrossOriginEnvelope({
337
+ requestId,
338
+ sourceOrigin,
339
+ targetOrigin,
340
+ target,
341
+ }) === true
342
+ : allowCrossOriginEnvelope === true;
343
+ if (!isAllowed) {
344
+ throw new CrossOriginEnvelopePolicyError(
345
+ requestId,
346
+ sourceOrigin,
347
+ targetOrigin,
348
+ );
349
+ }
350
+ }
351
+
352
+ return JSON.stringify({
353
+ requestId,
354
+ target,
355
+ timestamp: Date.now(),
356
+ sourceOrigin,
357
+ targetOrigin,
358
+ ...(traceContext
359
+ ? {
360
+ traceId: traceContext.traceId,
361
+ spanId: traceContext.spanId,
362
+ }
363
+ : {}),
364
+ });
365
+ };
366
+
367
+ /**
368
+ * Derives the operation context for a secured requestId, validates it against
369
+ * the configured operation contract and writes the `x-operation-id` and
370
+ * `x-modernjs-bff-operation-context` headers (without clobbering caller
371
+ * provided values for the id header).
372
+ */
373
+ export const attachOperationContextHeaders = ({
374
+ headers,
375
+ requestId,
376
+ target,
377
+ method,
378
+ path,
379
+ operationContext,
380
+ operationContract,
381
+ operationContextHeader,
382
+ operationContextDetailHeader,
383
+ }: {
384
+ headers: Record<string, any>;
385
+ requestId: string;
386
+ target: TransportTarget;
387
+ method: string;
388
+ path: string;
389
+ operationContext: OperationContext | undefined;
390
+ operationContract: OperationContractOptions | undefined;
391
+ operationContextHeader: string;
392
+ operationContextDetailHeader: string;
393
+ }) => {
394
+ if (
395
+ typeof readHeader(headers, TRACEPARENT_HEADER) === 'undefined' &&
396
+ operationContext?.traceparent
397
+ ) {
398
+ writeHeader(headers, TRACEPARENT_HEADER, operationContext.traceparent);
399
+ }
400
+ const contextPayload = buildOperationContext({
401
+ requestId,
402
+ method,
403
+ path,
404
+ operationContext,
405
+ traceparent: readHeader(headers, TRACEPARENT_HEADER),
406
+ });
407
+ validateOperationContract({
408
+ requestId,
409
+ target,
410
+ contextPayload,
411
+ operationContract,
412
+ });
413
+
414
+ if (typeof readHeader(headers, operationContextHeader) === 'undefined') {
415
+ writeHeader(headers, operationContextHeader, contextPayload.operationId);
416
+ }
417
+ writeHeader(
418
+ headers,
419
+ operationContextDetailHeader,
420
+ JSON.stringify(contextPayload),
421
+ );
422
+
423
+ return contextPayload;
424
+ };
@@ -0,0 +1,149 @@
1
+ import { parseTraceparent } from './traceparent';
2
+ import type { OperationContext } from './types';
3
+
4
+ export const BFF_LOCALE_HEADER = 'accept-language';
5
+ export const BFF_TRACEPARENT_HEADER = 'traceparent';
6
+
7
+ export type RequestContextInput = {
8
+ headers?: Record<string, unknown>;
9
+ locale?: string;
10
+ traceparent?: string;
11
+ operationContext?: OperationContext;
12
+ };
13
+
14
+ export type RequestContextSnapshot = {
15
+ headers: Record<string, string>;
16
+ locale?: string;
17
+ traceparent?: string;
18
+ traceId?: string;
19
+ spanId?: string;
20
+ operationContext?: OperationContext;
21
+ };
22
+
23
+ const readHeader = (
24
+ headers: Record<string, unknown> | undefined,
25
+ header: string,
26
+ ) => {
27
+ if (!headers) {
28
+ return undefined;
29
+ }
30
+
31
+ const normalized = header.toLowerCase();
32
+ const key = Object.keys(headers).find(
33
+ current => current.toLowerCase() === normalized,
34
+ );
35
+ if (!key) {
36
+ return undefined;
37
+ }
38
+
39
+ const value = headers[key];
40
+ return Array.isArray(value) ? value[0] : value;
41
+ };
42
+
43
+ const readString = (value: unknown) =>
44
+ typeof value === 'string' && value.length > 0 ? value : undefined;
45
+
46
+ function createOperationContextSnapshot(
47
+ operationContext: OperationContext | undefined,
48
+ safeContext: Pick<
49
+ OperationContext,
50
+ 'locale' | 'traceparent' | 'traceId' | 'spanId'
51
+ >,
52
+ ): OperationContext | undefined {
53
+ if (!operationContext) {
54
+ return undefined;
55
+ }
56
+
57
+ const snapshot: OperationContext = {
58
+ ...operationContext,
59
+ ...(safeContext.locale || operationContext?.locale
60
+ ? { locale: safeContext.locale || operationContext?.locale }
61
+ : {}),
62
+ ...(safeContext.traceparent || operationContext?.traceparent
63
+ ? {
64
+ traceparent: safeContext.traceparent || operationContext?.traceparent,
65
+ }
66
+ : {}),
67
+ ...(safeContext.traceId || operationContext?.traceId
68
+ ? { traceId: safeContext.traceId || operationContext?.traceId }
69
+ : {}),
70
+ ...(safeContext.spanId || operationContext?.spanId
71
+ ? { spanId: safeContext.spanId || operationContext?.spanId }
72
+ : {}),
73
+ };
74
+
75
+ return Object.keys(snapshot).some(
76
+ key => typeof snapshot[key as keyof OperationContext] !== 'undefined',
77
+ )
78
+ ? snapshot
79
+ : undefined;
80
+ }
81
+
82
+ export function createRequestContextSnapshot(
83
+ input: RequestContextInput = {},
84
+ ): RequestContextSnapshot {
85
+ const locale =
86
+ readString(input.locale) ||
87
+ readString(readHeader(input.headers, BFF_LOCALE_HEADER));
88
+ const traceparent =
89
+ readString(input.traceparent) ||
90
+ readString(input.operationContext?.traceparent) ||
91
+ readString(readHeader(input.headers, BFF_TRACEPARENT_HEADER));
92
+ const parsedTraceparent =
93
+ input.operationContext?.traceId && input.operationContext?.spanId
94
+ ? {
95
+ traceId: input.operationContext.traceId,
96
+ spanId: input.operationContext.spanId,
97
+ }
98
+ : parseTraceparent(traceparent);
99
+
100
+ const headers: Record<string, string> = {};
101
+ if (locale) {
102
+ headers[BFF_LOCALE_HEADER] = locale;
103
+ }
104
+ if (traceparent) {
105
+ headers[BFF_TRACEPARENT_HEADER] = traceparent;
106
+ }
107
+
108
+ const operationContext = createOperationContextSnapshot(
109
+ input.operationContext,
110
+ {
111
+ ...(locale ? { locale } : {}),
112
+ ...(traceparent ? { traceparent } : {}),
113
+ ...(parsedTraceparent
114
+ ? {
115
+ traceId: parsedTraceparent.traceId,
116
+ spanId: parsedTraceparent.spanId,
117
+ }
118
+ : {}),
119
+ },
120
+ );
121
+
122
+ return {
123
+ headers,
124
+ ...(locale ? { locale } : {}),
125
+ ...(traceparent ? { traceparent } : {}),
126
+ ...(parsedTraceparent
127
+ ? {
128
+ traceId: parsedTraceparent.traceId,
129
+ spanId: parsedTraceparent.spanId,
130
+ }
131
+ : {}),
132
+ ...(operationContext ? { operationContext } : {}),
133
+ };
134
+ }
135
+
136
+ export function createRequestContextHeaders(
137
+ input: RequestContextInput = {},
138
+ ): Record<string, string> {
139
+ return createRequestContextSnapshot(input).headers;
140
+ }
141
+
142
+ export { parseTraceparent, type TraceparentContext } from './traceparent';
143
+ export type * from './types';
144
+ export {
145
+ BFF_DEFAULT_PROTECTED_IDENTITY_HEADERS,
146
+ BFF_ENVELOPE_HEADER,
147
+ BFF_OPERATION_CONTEXT_DETAIL_HEADER,
148
+ BFF_OPERATION_CONTEXT_HEADER,
149
+ } from './types';
@@ -0,0 +1,43 @@
1
+ const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
2
+
3
+ export type TraceparentContext = {
4
+ traceId: string;
5
+ spanId: string;
6
+ sampled: boolean;
7
+ };
8
+
9
+ const isAllZeroHex = (value: string) => /^0+$/.test(value);
10
+
11
+ /**
12
+ * Parse a W3C trace context `traceparent` header (version 00).
13
+ * All-zero trace ids and span ids are rejected per the W3C spec.
14
+ */
15
+ export function parseTraceparent(
16
+ traceparent: string | undefined | null,
17
+ ): TraceparentContext | undefined {
18
+ if (!traceparent) {
19
+ return undefined;
20
+ }
21
+
22
+ const match = traceparent.trim().match(TRACEPARENT_REGEX);
23
+ if (!match) {
24
+ return undefined;
25
+ }
26
+
27
+ const [, rawTraceId, rawSpanId, rawFlags] = match;
28
+ if (!rawTraceId || !rawSpanId || !rawFlags) {
29
+ return undefined;
30
+ }
31
+
32
+ const traceId = rawTraceId.toLowerCase();
33
+ const spanId = rawSpanId.toLowerCase();
34
+ if (isAllZeroHex(traceId) || isAllZeroHex(spanId)) {
35
+ return undefined;
36
+ }
37
+
38
+ return {
39
+ traceId,
40
+ spanId,
41
+ sampled: (Number.parseInt(rawFlags, 16) & 0x1) === 1,
42
+ };
43
+ }