@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.2 → 3.9.0-ultramodern.20

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 (251) 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/localisedUrls/config.js +49 -0
  8. package/dist/cjs/localisedUrls/index.js +95 -0
  9. package/dist/cjs/localisedUrls/normalise.js +87 -0
  10. package/dist/cjs/localisedUrls/pathname.js +68 -0
  11. package/dist/cjs/localisedUrls/patterns.js +214 -0
  12. package/dist/cjs/localisedUrls/redirect.js +100 -0
  13. package/dist/cjs/localisedUrls/resolve.js +78 -0
  14. package/dist/cjs/localisedUrls/routeIdentity.js +92 -0
  15. package/dist/cjs/localisedUrls/routes.js +217 -0
  16. package/dist/cjs/localisedUrls/types.js +18 -0
  17. package/dist/cjs/prefetchPolicy.js +52 -0
  18. package/dist/cjs/rendererHead.js +18 -5
  19. package/dist/cjs/request-policy/browser.js +139 -0
  20. package/dist/cjs/request-policy/client.js +234 -0
  21. package/dist/cjs/request-policy/node.js +149 -0
  22. package/dist/cjs/request-policy/policyCore.js +266 -0
  23. package/dist/cjs/request-policy/requestContext.js +137 -0
  24. package/dist/cjs/request-policy/traceparent.js +56 -0
  25. package/dist/cjs/request-policy/transport.js +234 -0
  26. package/dist/cjs/request-policy/types.js +57 -0
  27. package/dist/cjs/routeHydration.js +127 -0
  28. package/dist/cjs/routerPrefetchPolicy.js +153 -0
  29. package/dist/cjs/routerProvider.js +105 -0
  30. package/dist/cjs/routerState.js +175 -0
  31. package/dist/cjs/routerStatePlugin.js +113 -0
  32. package/dist/cjs/routerStateTypes.js +18 -0
  33. package/dist/cjs/rsc-disabled/rscDisabledClientBrowserRuntime.js +69 -0
  34. package/dist/cjs/rsc-disabled/rscDisabledClientEdgeRuntime.js +61 -0
  35. package/dist/cjs/rsc-disabled/rscDisabledClientNodeRuntime.js +65 -0
  36. package/dist/cjs/rsc-disabled/rscDisabledRuntimeError.js +42 -0
  37. package/dist/cjs/rsc-disabled/rscDisabledServerEdgeRuntime.js +93 -0
  38. package/dist/cjs/rsc-disabled/rscDisabledServerNodeRuntime.js +101 -0
  39. package/dist/cjs/runtimeContextProjection.js +112 -0
  40. package/dist/cjs/server-config/bffRuntime.js +18 -0
  41. package/dist/cjs/server-config/index.js +18 -0
  42. package/dist/cjs/server-config/serverTelemetry.js +18 -0
  43. package/dist/cjs/shellStream.js +34 -2
  44. package/dist/cjs/workspaceEvents.js +159 -0
  45. package/dist/esm/buildIdentity.mjs +35 -0
  46. package/dist/esm/contextExtensions.mjs +33 -0
  47. package/dist/esm/headRuntime.mjs +43 -0
  48. package/dist/esm/helmetContext.mjs +13 -0
  49. package/dist/esm/index.mjs +2 -0
  50. package/dist/esm/localisedLoaderIdentity.mjs +13 -0
  51. package/dist/esm/localisedUrls/config.mjs +11 -0
  52. package/dist/esm/localisedUrls/index.mjs +8 -0
  53. package/dist/esm/localisedUrls/normalise.mjs +37 -0
  54. package/dist/esm/localisedUrls/pathname.mjs +24 -0
  55. package/dist/esm/localisedUrls/patterns.mjs +152 -0
  56. package/dist/esm/localisedUrls/redirect.mjs +53 -0
  57. package/dist/esm/localisedUrls/resolve.mjs +37 -0
  58. package/dist/esm/localisedUrls/routeIdentity.mjs +54 -0
  59. package/dist/esm/localisedUrls/routes.mjs +176 -0
  60. package/dist/esm/localisedUrls/types.mjs +0 -0
  61. package/dist/esm/prefetchPolicy.mjs +11 -0
  62. package/dist/esm/rendererHead.mjs +14 -5
  63. package/dist/esm/request-policy/browser.mjs +11 -0
  64. package/dist/esm/request-policy/client.mjs +195 -0
  65. package/dist/esm/request-policy/policyCore.mjs +171 -0
  66. package/dist/esm/request-policy/requestContext.mjs +75 -0
  67. package/dist/esm/request-policy/traceparent.mjs +18 -0
  68. package/dist/esm/request-policy/transport.mjs +196 -0
  69. package/dist/esm/request-policy/types.mjs +10 -0
  70. package/dist/esm/routeHydration.mjs +77 -0
  71. package/dist/esm/routerPrefetchPolicy.mjs +115 -0
  72. package/dist/esm/routerProvider.mjs +58 -0
  73. package/dist/esm/routerState.mjs +113 -0
  74. package/dist/esm/routerStatePlugin.mjs +75 -0
  75. package/dist/esm/routerStateTypes.mjs +0 -0
  76. package/dist/esm/rsc-disabled/rscDisabledClientBrowserRuntime.mjs +10 -0
  77. package/dist/esm/rsc-disabled/rscDisabledClientEdgeRuntime.mjs +8 -0
  78. package/dist/esm/rsc-disabled/rscDisabledClientNodeRuntime.mjs +9 -0
  79. package/dist/esm/rsc-disabled/rscDisabledRuntimeError.mjs +4 -0
  80. package/dist/esm/rsc-disabled/rscDisabledServerEdgeRuntime.mjs +16 -0
  81. package/dist/esm/rsc-disabled/rscDisabledServerNodeRuntime.mjs +18 -0
  82. package/dist/esm/runtimeContextProjection.mjs +74 -0
  83. package/dist/esm/server-config/bffRuntime.mjs +0 -0
  84. package/dist/esm/server-config/index.mjs +0 -0
  85. package/dist/esm/server-config/serverTelemetry.mjs +0 -0
  86. package/dist/esm/shellStream.mjs +34 -2
  87. package/dist/esm/workspaceEvents.mjs +66 -0
  88. package/dist/esm-node/buildIdentity.mjs +36 -0
  89. package/dist/esm-node/contextExtensions.mjs +34 -0
  90. package/dist/esm-node/headRuntime.mjs +44 -0
  91. package/dist/esm-node/helmetContext.mjs +14 -0
  92. package/dist/esm-node/index.mjs +2 -0
  93. package/dist/esm-node/localisedLoaderIdentity.mjs +14 -0
  94. package/dist/esm-node/localisedUrls/config.mjs +12 -0
  95. package/dist/esm-node/localisedUrls/index.mjs +9 -0
  96. package/dist/esm-node/localisedUrls/normalise.mjs +38 -0
  97. package/dist/esm-node/localisedUrls/pathname.mjs +25 -0
  98. package/dist/esm-node/localisedUrls/patterns.mjs +153 -0
  99. package/dist/esm-node/localisedUrls/redirect.mjs +54 -0
  100. package/dist/esm-node/localisedUrls/resolve.mjs +38 -0
  101. package/dist/esm-node/localisedUrls/routeIdentity.mjs +55 -0
  102. package/dist/esm-node/localisedUrls/routes.mjs +177 -0
  103. package/dist/esm-node/localisedUrls/types.mjs +1 -0
  104. package/dist/esm-node/prefetchPolicy.mjs +12 -0
  105. package/dist/esm-node/rendererHead.mjs +14 -5
  106. package/dist/esm-node/request-policy/browser.mjs +12 -0
  107. package/dist/esm-node/request-policy/client.mjs +196 -0
  108. package/dist/esm-node/request-policy/node.mjs +22 -0
  109. package/dist/esm-node/request-policy/policyCore.mjs +172 -0
  110. package/dist/esm-node/request-policy/requestContext.mjs +76 -0
  111. package/dist/esm-node/request-policy/traceparent.mjs +19 -0
  112. package/dist/esm-node/request-policy/transport.mjs +197 -0
  113. package/dist/esm-node/request-policy/types.mjs +11 -0
  114. package/dist/esm-node/routeHydration.mjs +78 -0
  115. package/dist/esm-node/routerPrefetchPolicy.mjs +116 -0
  116. package/dist/esm-node/routerProvider.mjs +59 -0
  117. package/dist/esm-node/routerState.mjs +114 -0
  118. package/dist/esm-node/routerStatePlugin.mjs +76 -0
  119. package/dist/esm-node/routerStateTypes.mjs +1 -0
  120. package/dist/esm-node/rsc-disabled/rscDisabledClientBrowserRuntime.mjs +11 -0
  121. package/dist/esm-node/rsc-disabled/rscDisabledClientEdgeRuntime.mjs +9 -0
  122. package/dist/esm-node/rsc-disabled/rscDisabledClientNodeRuntime.mjs +10 -0
  123. package/dist/esm-node/rsc-disabled/rscDisabledRuntimeError.mjs +5 -0
  124. package/dist/esm-node/rsc-disabled/rscDisabledServerEdgeRuntime.mjs +17 -0
  125. package/dist/esm-node/rsc-disabled/rscDisabledServerNodeRuntime.mjs +19 -0
  126. package/dist/esm-node/runtimeContextProjection.mjs +75 -0
  127. package/dist/esm-node/server-config/bffRuntime.mjs +1 -0
  128. package/dist/esm-node/server-config/index.mjs +1 -0
  129. package/dist/esm-node/server-config/serverTelemetry.mjs +1 -0
  130. package/dist/esm-node/shellStream.mjs +34 -2
  131. package/dist/esm-node/workspaceEvents.mjs +67 -0
  132. package/dist/types/buildIdentity.d.ts +37 -0
  133. package/dist/types/contextExtensions.d.ts +34 -0
  134. package/dist/types/headRuntime.d.ts +12 -0
  135. package/dist/types/helmetCompat.d.ts +1 -1
  136. package/dist/types/helmetContext.d.ts +10 -0
  137. package/dist/types/index.d.ts +2 -0
  138. package/dist/types/localisedLoaderIdentity.d.ts +10 -0
  139. package/dist/types/localisedUrls/config.d.ts +9 -0
  140. package/dist/types/localisedUrls/index.d.ts +10 -0
  141. package/dist/types/localisedUrls/normalise.d.ts +10 -0
  142. package/dist/types/localisedUrls/pathname.d.ts +4 -0
  143. package/dist/types/localisedUrls/patterns.d.ts +11 -0
  144. package/dist/types/localisedUrls/redirect.d.ts +19 -0
  145. package/dist/types/localisedUrls/resolve.d.ts +7 -0
  146. package/dist/types/localisedUrls/routeIdentity.d.ts +22 -0
  147. package/dist/types/localisedUrls/routes.d.ts +3 -0
  148. package/dist/types/localisedUrls/types.d.ts +14 -0
  149. package/dist/types/prefetchPolicy.d.ts +4 -0
  150. package/dist/types/rendererHead.d.ts +5 -3
  151. package/dist/types/request-policy/browser.d.ts +10 -0
  152. package/dist/types/request-policy/client.d.ts +14 -0
  153. package/dist/types/request-policy/node.d.ts +10 -0
  154. package/dist/types/request-policy/policyCore.d.ts +107 -0
  155. package/dist/types/request-policy/requestContext.d.ts +22 -0
  156. package/dist/types/request-policy/traceparent.d.ts +10 -0
  157. package/dist/types/request-policy/transport.d.ts +12 -0
  158. package/dist/types/request-policy/types.d.ts +144 -0
  159. package/dist/types/routeHydration.d.ts +24 -0
  160. package/dist/types/routerPrefetchPolicy.d.ts +28 -0
  161. package/dist/types/routerProvider.d.ts +55 -0
  162. package/dist/types/routerState.d.ts +17 -0
  163. package/dist/types/routerStatePlugin.d.ts +19 -0
  164. package/dist/types/routerStateTypes.d.ts +70 -0
  165. package/dist/types/rsc-disabled/rscDisabledClientBrowserRuntime.d.ts +9 -0
  166. package/dist/types/rsc-disabled/rscDisabledClientEdgeRuntime.d.ts +7 -0
  167. package/dist/types/rsc-disabled/rscDisabledClientNodeRuntime.d.ts +8 -0
  168. package/dist/types/rsc-disabled/rscDisabledRuntimeError.d.ts +1 -0
  169. package/dist/types/rsc-disabled/rscDisabledServerEdgeRuntime.d.ts +15 -0
  170. package/dist/types/rsc-disabled/rscDisabledServerNodeRuntime.d.ts +17 -0
  171. package/dist/types/runtimeContextProjection.d.ts +30 -0
  172. package/dist/types/server-config/bffRuntime.d.ts +122 -0
  173. package/dist/types/server-config/index.d.ts +2 -0
  174. package/dist/types/server-config/serverTelemetry.d.ts +322 -0
  175. package/dist/types/workspaceEvents.d.ts +62 -0
  176. package/package.json +240 -15
  177. package/rslib.config.mts +17 -2
  178. package/rstest.config.mts +33 -2
  179. package/src/buildIdentity.ts +61 -0
  180. package/src/contextExtensions.ts +81 -0
  181. package/src/headRuntime.tsx +77 -0
  182. package/src/helmetCompat.ts +1 -1
  183. package/src/helmetContext.ts +31 -0
  184. package/src/index.ts +2 -0
  185. package/src/localisedLoaderIdentity.ts +30 -0
  186. package/src/localisedUrls/config.ts +18 -0
  187. package/src/localisedUrls/index.ts +25 -0
  188. package/src/localisedUrls/normalise.ts +62 -0
  189. package/src/localisedUrls/pathname.ts +60 -0
  190. package/src/localisedUrls/patterns.ts +283 -0
  191. package/src/localisedUrls/redirect.ts +107 -0
  192. package/src/localisedUrls/resolve.ts +106 -0
  193. package/src/localisedUrls/routeIdentity.ts +83 -0
  194. package/src/localisedUrls/routes.ts +451 -0
  195. package/src/localisedUrls/types.ts +18 -0
  196. package/src/prefetchPolicy.ts +33 -0
  197. package/src/rendererHead.ts +25 -8
  198. package/src/request-policy/browser.ts +22 -0
  199. package/src/request-policy/client.ts +404 -0
  200. package/src/request-policy/node.ts +41 -0
  201. package/src/request-policy/policyCore.ts +424 -0
  202. package/src/request-policy/requestContext.ts +149 -0
  203. package/src/request-policy/traceparent.ts +43 -0
  204. package/src/request-policy/transport.ts +349 -0
  205. package/src/request-policy/types.ts +229 -0
  206. package/src/routeHydration.ts +208 -0
  207. package/src/routerPrefetchPolicy.ts +195 -0
  208. package/src/routerProvider.ts +160 -0
  209. package/src/routerState.ts +220 -0
  210. package/src/routerStatePlugin.ts +125 -0
  211. package/src/routerStateTypes.ts +76 -0
  212. package/src/rsc-disabled/rscDisabledClientBrowserRuntime.ts +10 -0
  213. package/src/rsc-disabled/rscDisabledClientEdgeRuntime.ts +8 -0
  214. package/src/rsc-disabled/rscDisabledClientNodeRuntime.ts +9 -0
  215. package/src/rsc-disabled/rscDisabledRuntimeError.ts +3 -0
  216. package/src/rsc-disabled/rscDisabledServerEdgeRuntime.ts +16 -0
  217. package/src/rsc-disabled/rscDisabledServerNodeRuntime.ts +18 -0
  218. package/src/runtimeContextProjection.ts +105 -0
  219. package/src/server-config/bffRuntime.ts +136 -0
  220. package/src/server-config/index.ts +2 -0
  221. package/src/server-config/serverTelemetry.ts +337 -0
  222. package/src/shellStream.ts +55 -25
  223. package/src/workspaceEvents.ts +324 -0
  224. package/tests/buildIdentity.test.ts +51 -0
  225. package/tests/contextExtensions.test.ts +27 -0
  226. package/tests/headRuntime.test.tsx +229 -0
  227. package/tests/localisedUrls.test.ts +560 -0
  228. package/tests/rendererHead.test.ts +52 -121
  229. package/tests/request-policy/browser-policy.test.ts +108 -0
  230. package/tests/request-policy/client-isolation.test.ts +109 -0
  231. package/tests/request-policy/node-policy.test.ts +447 -0
  232. package/tests/request-policy/policyCore.test.ts +129 -0
  233. package/tests/request-policy/requestContext.test.ts +66 -0
  234. package/tests/request-policy/requestFactory.outbound-contract.test.ts +290 -0
  235. package/tests/request-policy/traceparent.test.ts +52 -0
  236. package/tests/request-policy/transport-retry.test.ts +367 -0
  237. package/tests/routeHydration.test.ts +180 -0
  238. package/tests/routerPrefetchPolicy.test.ts +102 -0
  239. package/tests/routerProvider.test.ts +29 -0
  240. package/tests/routerState.test.ts +77 -0
  241. package/tests/routerStatePlugin.test.ts +63 -0
  242. package/tests/runtimeContextProjection.test.ts +83 -0
  243. package/tests/safeFailure.test.ts +10 -28
  244. package/tests/shellStream.test.ts +39 -2
  245. package/tests/workspaceEvents.test.ts +221 -0
  246. package/dist/cjs/rscHtmlStream.js +0 -128
  247. package/dist/esm/rscHtmlStream.mjs +0 -86
  248. package/dist/esm-node/rscHtmlStream.mjs +0 -87
  249. package/dist/types/rscHtmlStream.d.ts +0 -4
  250. package/src/rscHtmlStream.ts +0 -142
  251. package/tests/rscHtmlStream.test.ts +0 -153
@@ -0,0 +1,447 @@
1
+ import {
2
+ CrossOriginEnvelopePolicyError,
3
+ configure,
4
+ createRequest,
5
+ IdentityBindingViolationError,
6
+ OperationContractViolationError,
7
+ ProducerClientNotInitializedError,
8
+ ProducerDomainNotConfiguredError,
9
+ } from '@modern-js/runtime-extensions/request-policy';
10
+ import { storage } from '@modern-js/runtime-utils/node';
11
+ import nock from 'nock';
12
+
13
+ describe('configure', () => {
14
+ const path = '/api';
15
+ const method = 'GET';
16
+ const response = {
17
+ code: 200,
18
+ data: {
19
+ message: 'hello Modernjs',
20
+ },
21
+ };
22
+
23
+ const run = (
24
+ headers: Record<string, string>,
25
+ callback: () => Promise<void> | void,
26
+ ) =>
27
+ storage.run(
28
+ {
29
+ headers,
30
+ monitors: {} as any,
31
+ },
32
+ callback,
33
+ );
34
+
35
+ // beforeEach(() => {
36
+ // nock.disableNetConnect();
37
+ // });
38
+
39
+ // afterEach(() => {
40
+ // nock.cleanAll();
41
+ // });
42
+
43
+ test('should support inputParams for non-default requestId with configured domain', async () => {
44
+ const producer = 'producer-input-params';
45
+ const producerUrl = 'http://127.0.0.1:9091';
46
+
47
+ await run({}, async () => {
48
+ nock(producerUrl).post(path, 'modernjs').reply(200, response);
49
+
50
+ configure({
51
+ requestId: producer,
52
+ operationContract: {
53
+ enabled: false,
54
+ },
55
+ setDomain: () => producerUrl,
56
+ });
57
+
58
+ const request = createRequest(
59
+ path,
60
+ 'POST',
61
+ 8080,
62
+ 'inputParams',
63
+ undefined,
64
+ producer,
65
+ );
66
+ const data = await request('modernjs');
67
+ expect(data).toStrictEqual(response);
68
+ });
69
+ });
70
+
71
+ test('should throw for non-default requestId when producer client is not initialized', async () => {
72
+ const request = createRequest({
73
+ path,
74
+ method,
75
+ port: 8080,
76
+ requestId: 'producer-app',
77
+ });
78
+
79
+ expect(() => request()).toThrow(ProducerClientNotInitializedError);
80
+ });
81
+
82
+ test('should require setDomain when configuring non-default requestId', () => {
83
+ expect(() =>
84
+ configure({
85
+ requestId: 'producer-a',
86
+ }),
87
+ ).toThrow(ProducerDomainNotConfiguredError);
88
+ });
89
+
90
+ test('should support secure resolveHeaders callback for non-default requestId', async () => {
91
+ const producer = 'producer-with-resolver';
92
+ const authKey = 'token-def';
93
+ const tenant = 'tenant-a';
94
+ const producerUrl = 'http://127.0.0.1:9084';
95
+
96
+ await run(
97
+ {
98
+ authorization: authKey,
99
+ 'x-tenant-id': tenant,
100
+ },
101
+ async () => {
102
+ nock(producerUrl, {
103
+ reqheaders: {
104
+ authorization: authKey,
105
+ 'x-tenant-id': 'tenant-masked',
106
+ },
107
+ })
108
+ .get(path)
109
+ .reply(200, response);
110
+
111
+ configure({
112
+ requestId: producer,
113
+ allowedHeaders: ['authorization', 'x-tenant-id'],
114
+ operationContract: {
115
+ enabled: false,
116
+ },
117
+ resolveHeaders: ({ incomingHeaders }) => ({
118
+ ...incomingHeaders,
119
+ 'x-tenant-id': 'tenant-masked',
120
+ 'x-injected': 'blocked',
121
+ }),
122
+ setDomain: () => producerUrl,
123
+ });
124
+
125
+ const request = createRequest(
126
+ path,
127
+ method,
128
+ 8080,
129
+ undefined,
130
+ undefined,
131
+ producer,
132
+ );
133
+ const data = await request();
134
+
135
+ expect(data).toStrictEqual(response);
136
+ },
137
+ );
138
+ });
139
+
140
+ test('should reject client-supplied tenant headers by default for non-default producer clients', async () => {
141
+ const producer = 'producer-identity-strip';
142
+ const producerUrl = 'http://127.0.0.1:9085';
143
+
144
+ await run({}, async () => {
145
+ nock(producerUrl, {
146
+ badheaders: ['x-tenant-id'],
147
+ })
148
+ .get(path)
149
+ .reply(200, response);
150
+
151
+ configure({
152
+ requestId: producer,
153
+ operationContract: {
154
+ enabled: false,
155
+ },
156
+ setDomain: () => producerUrl,
157
+ });
158
+
159
+ const request = createRequest(
160
+ path,
161
+ method,
162
+ 8080,
163
+ undefined,
164
+ undefined,
165
+ producer,
166
+ );
167
+
168
+ expect(() =>
169
+ request({
170
+ headers: {
171
+ 'x-tenant-id': 'tenant-client',
172
+ },
173
+ }),
174
+ ).toThrow(IdentityBindingViolationError);
175
+ });
176
+ });
177
+
178
+ test('should enforce server-derived tenant and subject context over client overrides', async () => {
179
+ const producer = 'producer-identity-derived';
180
+ const producerUrl = 'http://127.0.0.1:9086';
181
+
182
+ await run(
183
+ {
184
+ 'x-tenant-id': 'tenant-server',
185
+ 'x-subject-id': 'subject-server',
186
+ },
187
+ async () => {
188
+ nock(producerUrl, {
189
+ reqheaders: {
190
+ 'x-tenant-id': 'tenant-server',
191
+ 'x-subject-id': 'subject-server',
192
+ },
193
+ })
194
+ .get(path)
195
+ .reply(200, response);
196
+
197
+ configure({
198
+ requestId: producer,
199
+ operationContract: {
200
+ enabled: false,
201
+ },
202
+ identityBinding: {
203
+ strict: false,
204
+ },
205
+ setDomain: () => producerUrl,
206
+ });
207
+
208
+ const request = createRequest(
209
+ path,
210
+ method,
211
+ 8080,
212
+ undefined,
213
+ undefined,
214
+ producer,
215
+ );
216
+
217
+ const data = await request({
218
+ headers: {
219
+ 'x-tenant-id': 'tenant-client',
220
+ 'x-subject-id': 'subject-client',
221
+ },
222
+ });
223
+
224
+ expect(data).toStrictEqual(response);
225
+ },
226
+ );
227
+ });
228
+
229
+ test('should reject client identity override in strict identity binding mode', () => {
230
+ const producer = 'producer-identity-strict';
231
+
232
+ configure({
233
+ requestId: producer,
234
+ setDomain: () => 'http://127.0.0.1:9087',
235
+ operationContract: {
236
+ enabled: false,
237
+ },
238
+ identityBinding: {
239
+ strict: true,
240
+ },
241
+ });
242
+
243
+ const request = createRequest(
244
+ path,
245
+ method,
246
+ 8080,
247
+ undefined,
248
+ undefined,
249
+ producer,
250
+ );
251
+
252
+ expect(() =>
253
+ request({
254
+ headers: {
255
+ 'x-tenant-id': 'tenant-client',
256
+ },
257
+ }),
258
+ ).toThrow(IdentityBindingViolationError);
259
+ });
260
+
261
+ test('should require envelope and block cross-origin producer calls in production by default', async () => {
262
+ const producer = 'producer-envelope-default';
263
+ const previousEnv = process.env.NODE_ENV;
264
+ process.env.NODE_ENV = 'production';
265
+
266
+ await run(
267
+ {
268
+ origin: 'https://consumer.internal',
269
+ },
270
+ async () => {
271
+ try {
272
+ configure({
273
+ requestId: producer,
274
+ operationContract: {
275
+ enabled: false,
276
+ },
277
+ setDomain: () => 'https://producer.internal',
278
+ });
279
+
280
+ const request = createRequest(
281
+ path,
282
+ method,
283
+ 8080,
284
+ undefined,
285
+ undefined,
286
+ producer,
287
+ );
288
+
289
+ expect(() => request()).toThrow(CrossOriginEnvelopePolicyError);
290
+ } finally {
291
+ process.env.NODE_ENV = previousEnv;
292
+ }
293
+ },
294
+ );
295
+ });
296
+
297
+ test('should allow explicit cross-origin envelope policy and attach envelope header', async () => {
298
+ const producer = 'producer-envelope-policy';
299
+ const producerUrl = 'https://producer.internal';
300
+ const previousEnv = process.env.NODE_ENV;
301
+ process.env.NODE_ENV = 'production';
302
+
303
+ await run(
304
+ {
305
+ origin: 'https://consumer.internal',
306
+ },
307
+ async () => {
308
+ try {
309
+ nock(producerUrl).get(path).reply(200, response);
310
+ const customRequest = rs.fn((requestPath: any, init: any) =>
311
+ fetch(requestPath, init),
312
+ );
313
+
314
+ configure({
315
+ request: customRequest as unknown as typeof fetch,
316
+ requestId: producer,
317
+ operationContract: {
318
+ enabled: false,
319
+ },
320
+ setDomain: () => producerUrl,
321
+ allowCrossOriginEnvelope: ({
322
+ requestId,
323
+ sourceOrigin,
324
+ targetOrigin,
325
+ }) =>
326
+ requestId === producer &&
327
+ sourceOrigin === 'https://consumer.internal' &&
328
+ targetOrigin === producerUrl,
329
+ });
330
+
331
+ const request = createRequest(
332
+ path,
333
+ method,
334
+ 8080,
335
+ undefined,
336
+ undefined,
337
+ producer,
338
+ );
339
+ const res = await request();
340
+ const data = await res.json();
341
+
342
+ const headers = customRequest.mock.calls[0]?.[1]?.headers;
343
+ const envelope = JSON.parse(headers['x-modernjs-bff-envelope']);
344
+ expect(envelope.requestId).toBe(producer);
345
+ expect(envelope.target).toBe('server');
346
+ expect(data).toStrictEqual(response);
347
+ } finally {
348
+ process.env.NODE_ENV = previousEnv;
349
+ }
350
+ },
351
+ );
352
+ });
353
+
354
+ test('should attach operation context headers for non-default producer client', async () => {
355
+ const producer = 'crm.producer-a';
356
+ const producerUrl = 'http://127.0.0.1:18080';
357
+ const traceparent =
358
+ '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';
359
+
360
+ await run({ traceparent }, async () => {
361
+ nock(producerUrl).get(path).reply(200, response);
362
+ const customRequest = rs.fn((requestPath: any, init: any) =>
363
+ fetch(requestPath, init),
364
+ );
365
+
366
+ configure({
367
+ request: customRequest as unknown as typeof fetch,
368
+ requestId: producer,
369
+ setDomain: () => producerUrl,
370
+ });
371
+
372
+ const request = createRequest(
373
+ path,
374
+ method,
375
+ 8080,
376
+ undefined,
377
+ undefined,
378
+ producer,
379
+ {
380
+ operationId: `GET:${path}`,
381
+ routePath: path,
382
+ method,
383
+ schemaHash: 'schema-test',
384
+ operationVersion: 1,
385
+ traceparent,
386
+ },
387
+ );
388
+ await request();
389
+
390
+ const headers = customRequest.mock.calls[0]?.[1]?.headers;
391
+ expect(headers['x-operation-id']).toBe(`${producer}:GET:${path}`);
392
+ expect(headers.traceparent).toBe(traceparent);
393
+ const operationContext = JSON.parse(
394
+ headers['x-modernjs-bff-operation-context'],
395
+ );
396
+ expect(operationContext.requestId).toBe(producer);
397
+ expect(operationContext.operationId).toBe(`${producer}:GET:${path}`);
398
+ expect(operationContext.traceparent).toBe(traceparent);
399
+ expect(operationContext.traceId).toBe('4bf92f3577b34da6a3ce929d0e0e4736');
400
+ expect(operationContext.spanId).toBe('00f067aa0ba902b7');
401
+ });
402
+ });
403
+
404
+ test('should reject requests missing schema/version operation contract metadata by default', () => {
405
+ const producer = 'producer-operation-contract-default';
406
+
407
+ configure({
408
+ requestId: producer,
409
+ setDomain: () => 'http://127.0.0.1:19080',
410
+ });
411
+
412
+ const request = createRequest(
413
+ path,
414
+ method,
415
+ 8080,
416
+ undefined,
417
+ undefined,
418
+ producer,
419
+ );
420
+
421
+ expect(() => request()).toThrow(OperationContractViolationError);
422
+ });
423
+
424
+ test('should enforce operation contract metadata for default requestId when strict-default mode is enabled', () => {
425
+ process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID = 'true';
426
+ try {
427
+ configure({
428
+ operationContract: {
429
+ enabled: true,
430
+ strict: true,
431
+ requireSchemaHash: true,
432
+ requireOperationVersion: true,
433
+ },
434
+ });
435
+
436
+ const request = createRequest(path, method, 8080);
437
+ expect(() => request()).toThrow(OperationContractViolationError);
438
+ } finally {
439
+ configure({
440
+ operationContract: {
441
+ enabled: false,
442
+ },
443
+ });
444
+ delete process.env.MODERN_BFF_STRICT_DEFAULT_REQUEST_ID;
445
+ }
446
+ });
447
+ });
@@ -0,0 +1,129 @@
1
+ import {
2
+ attachOperationContextHeaders,
3
+ buildEnvelopeHeaderValue,
4
+ buildOperationContext,
5
+ CrossOriginEnvelopePolicyError,
6
+ deleteHeader,
7
+ findHeaderKey,
8
+ isSecuredRequestId,
9
+ OperationContractViolationError,
10
+ readHeader,
11
+ validateOperationContract,
12
+ writeHeader,
13
+ } from '../../src/request-policy/policyCore';
14
+
15
+ describe('policyCore (shared browser/node policy module)', () => {
16
+ test('header helpers are case-insensitive and normalize duplicate keys', () => {
17
+ const headers: Record<string, any> = { 'X-Foo': 'a' };
18
+
19
+ expect(findHeaderKey(headers, 'x-foo')).toBe('X-Foo');
20
+ expect(readHeader(headers, 'x-FOO')).toBe('a');
21
+
22
+ writeHeader(headers, 'x-foo', 'b');
23
+ expect(headers).toEqual({ 'x-foo': 'b' });
24
+
25
+ deleteHeader(headers, 'X-FOO');
26
+ expect(headers).toEqual({});
27
+ });
28
+
29
+ test('isSecuredRequestId treats only "default" as unsecured by default', () => {
30
+ expect(isSecuredRequestId('default')).toBe(false);
31
+ expect(isSecuredRequestId('crm-producer')).toBe(true);
32
+ });
33
+
34
+ test('validateOperationContract throws on missing schema hash for secured ids', () => {
35
+ const contextPayload = buildOperationContext({
36
+ requestId: 'crm',
37
+ method: 'GET',
38
+ path: '/hello',
39
+ });
40
+
41
+ expect(() =>
42
+ validateOperationContract({
43
+ requestId: 'crm',
44
+ target: 'browser',
45
+ contextPayload,
46
+ operationContract: undefined,
47
+ }),
48
+ ).toThrow(OperationContractViolationError);
49
+ });
50
+
51
+ test('buildEnvelopeHeaderValue denies cross-origin flows unless allowed', () => {
52
+ expect(() =>
53
+ buildEnvelopeHeaderValue({
54
+ requestId: 'crm',
55
+ target: 'server',
56
+ sourceOrigin: 'https://a.example',
57
+ targetOrigin: 'https://b.example',
58
+ traceContext: undefined,
59
+ allowCrossOriginEnvelope: undefined,
60
+ }),
61
+ ).toThrow(CrossOriginEnvelopePolicyError);
62
+
63
+ const envelope = JSON.parse(
64
+ buildEnvelopeHeaderValue({
65
+ requestId: 'crm',
66
+ target: 'server',
67
+ sourceOrigin: 'https://a.example',
68
+ targetOrigin: 'https://b.example',
69
+ traceContext: { traceId: 't', spanId: 's' },
70
+ allowCrossOriginEnvelope: true,
71
+ }),
72
+ );
73
+ expect(envelope).toMatchObject({
74
+ requestId: 'crm',
75
+ target: 'server',
76
+ sourceOrigin: 'https://a.example',
77
+ targetOrigin: 'https://b.example',
78
+ traceId: 't',
79
+ spanId: 's',
80
+ });
81
+ });
82
+
83
+ test('buildEnvelopeHeaderValue requires cross-origin predicate to return true', () => {
84
+ const allowCrossOriginEnvelope = () => 'yes';
85
+
86
+ expect(() =>
87
+ buildEnvelopeHeaderValue({
88
+ requestId: 'crm',
89
+ target: 'server',
90
+ sourceOrigin: 'https://a.example',
91
+ targetOrigin: 'https://b.example',
92
+ traceContext: undefined,
93
+ allowCrossOriginEnvelope:
94
+ allowCrossOriginEnvelope as unknown as () => boolean,
95
+ }),
96
+ ).toThrow(CrossOriginEnvelopePolicyError);
97
+ });
98
+
99
+ test('attachOperationContextHeaders writes id and detail headers without clobbering caller id', () => {
100
+ const headers: Record<string, any> = {
101
+ 'x-operation-id': 'crm:custom-op',
102
+ };
103
+
104
+ attachOperationContextHeaders({
105
+ headers,
106
+ requestId: 'crm',
107
+ target: 'server',
108
+ method: 'GET',
109
+ path: '/hello',
110
+ operationContext: {
111
+ operationId: 'getHello',
112
+ schemaHash: 'abc',
113
+ operationVersion: 1,
114
+ },
115
+ operationContract: undefined,
116
+ operationContextHeader: 'x-operation-id',
117
+ operationContextDetailHeader: 'x-modernjs-bff-operation-context',
118
+ });
119
+
120
+ expect(headers['x-operation-id']).toBe('crm:custom-op');
121
+ const details = JSON.parse(headers['x-modernjs-bff-operation-context']);
122
+ expect(details).toMatchObject({
123
+ requestId: 'crm',
124
+ operationId: 'crm:getHello',
125
+ schemaHash: 'abc',
126
+ operationVersion: 1,
127
+ });
128
+ });
129
+ });
@@ -0,0 +1,66 @@
1
+ import {
2
+ BFF_LOCALE_HEADER,
3
+ BFF_TRACEPARENT_HEADER,
4
+ createRequestContextHeaders,
5
+ createRequestContextSnapshot,
6
+ } from '@modern-js/runtime-extensions/request-policy/client';
7
+
8
+ describe('request context helpers', () => {
9
+ test('should create propagation headers from explicit locale and traceparent', () => {
10
+ expect(
11
+ createRequestContextHeaders({
12
+ locale: 'cs-CZ',
13
+ traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
14
+ }),
15
+ ).toEqual({
16
+ [BFF_LOCALE_HEADER]: 'cs-CZ',
17
+ [BFF_TRACEPARENT_HEADER]:
18
+ '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
19
+ });
20
+ });
21
+
22
+ test('should derive locale and trace metadata from incoming headers', () => {
23
+ expect(
24
+ createRequestContextSnapshot({
25
+ headers: {
26
+ 'accept-language': 'en-US,en;q=0.8',
27
+ traceparent:
28
+ '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
29
+ },
30
+ }),
31
+ ).toEqual({
32
+ headers: {
33
+ 'accept-language': 'en-US,en;q=0.8',
34
+ traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
35
+ },
36
+ locale: 'en-US,en;q=0.8',
37
+ traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
38
+ traceId: '4bf92f3577b34da6a3ce929d0e0e4736',
39
+ spanId: '00f067aa0ba902b7',
40
+ });
41
+ });
42
+
43
+ test('should keep operation context in snapshots without widening propagation headers', () => {
44
+ const snapshot = createRequestContextSnapshot({
45
+ locale: 'cs-CZ',
46
+ operationContext: {
47
+ operationId: 'shell:list',
48
+ routePath: '/effect/recommendations',
49
+ method: 'GET',
50
+ source: 'generated-client',
51
+ scope: { workspace: 'demo' },
52
+ sessionClaims: { role: 'viewer' },
53
+ traceparent: '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01',
54
+ },
55
+ });
56
+
57
+ expect(Object.keys(snapshot.headers)).toEqual([
58
+ BFF_LOCALE_HEADER,
59
+ BFF_TRACEPARENT_HEADER,
60
+ ]);
61
+ expect(snapshot.operationContext?.traceId).toBe(
62
+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
63
+ );
64
+ expect(snapshot.operationContext?.spanId).toBe('bbbbbbbbbbbbbbbb');
65
+ });
66
+ });