@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.4 → 3.9.0-ultramodern.5

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 (155) hide show
  1. package/dist/cjs/contextExtensions.js +74 -0
  2. package/dist/cjs/headRuntime.js +91 -0
  3. package/dist/cjs/helmetContext.js +54 -0
  4. package/dist/cjs/index.js +18 -4
  5. package/dist/cjs/prefetchPolicy.js +52 -0
  6. package/dist/cjs/request-policy/browser.js +139 -0
  7. package/dist/cjs/request-policy/client.js +234 -0
  8. package/dist/cjs/request-policy/node.js +149 -0
  9. package/dist/cjs/request-policy/policyCore.js +266 -0
  10. package/dist/cjs/request-policy/requestContext.js +137 -0
  11. package/dist/cjs/request-policy/traceparent.js +56 -0
  12. package/dist/cjs/request-policy/transport.js +234 -0
  13. package/dist/cjs/request-policy/types.js +57 -0
  14. package/dist/cjs/routeHydration.js +127 -0
  15. package/dist/cjs/routerPrefetchPolicy.js +153 -0
  16. package/dist/cjs/routerProvider.js +170 -0
  17. package/dist/cjs/routerState.js +167 -0
  18. package/dist/cjs/routerStatePlugin.js +86 -0
  19. package/dist/cjs/routerStateTypes.js +18 -0
  20. package/dist/cjs/runtimeContextProjection.js +112 -0
  21. package/dist/cjs/server-config/bffRuntime.js +18 -0
  22. package/dist/cjs/server-config/index.js +18 -0
  23. package/dist/cjs/server-config/serverTelemetry.js +18 -0
  24. package/dist/cjs/shellStream.js +34 -2
  25. package/dist/cjs/workspaceEvents.js +159 -0
  26. package/dist/esm/contextExtensions.mjs +33 -0
  27. package/dist/esm/headRuntime.mjs +43 -0
  28. package/dist/esm/helmetContext.mjs +13 -0
  29. package/dist/esm/index.mjs +2 -0
  30. package/dist/esm/prefetchPolicy.mjs +11 -0
  31. package/dist/esm/request-policy/browser.mjs +11 -0
  32. package/dist/esm/request-policy/client.mjs +195 -0
  33. package/dist/esm/request-policy/policyCore.mjs +171 -0
  34. package/dist/esm/request-policy/requestContext.mjs +75 -0
  35. package/dist/esm/request-policy/traceparent.mjs +18 -0
  36. package/dist/esm/request-policy/transport.mjs +196 -0
  37. package/dist/esm/request-policy/types.mjs +10 -0
  38. package/dist/esm/routeHydration.mjs +77 -0
  39. package/dist/esm/routerPrefetchPolicy.mjs +115 -0
  40. package/dist/esm/routerProvider.mjs +117 -0
  41. package/dist/esm/routerState.mjs +105 -0
  42. package/dist/esm/routerStatePlugin.mjs +48 -0
  43. package/dist/esm/routerStateTypes.mjs +0 -0
  44. package/dist/esm/runtimeContextProjection.mjs +74 -0
  45. package/dist/esm/server-config/bffRuntime.mjs +0 -0
  46. package/dist/esm/server-config/index.mjs +0 -0
  47. package/dist/esm/server-config/serverTelemetry.mjs +0 -0
  48. package/dist/esm/shellStream.mjs +34 -2
  49. package/dist/esm/workspaceEvents.mjs +66 -0
  50. package/dist/esm-node/contextExtensions.mjs +34 -0
  51. package/dist/esm-node/headRuntime.mjs +44 -0
  52. package/dist/esm-node/helmetContext.mjs +14 -0
  53. package/dist/esm-node/index.mjs +2 -0
  54. package/dist/esm-node/prefetchPolicy.mjs +12 -0
  55. package/dist/esm-node/request-policy/browser.mjs +12 -0
  56. package/dist/esm-node/request-policy/client.mjs +196 -0
  57. package/dist/esm-node/request-policy/node.mjs +22 -0
  58. package/dist/esm-node/request-policy/policyCore.mjs +172 -0
  59. package/dist/esm-node/request-policy/requestContext.mjs +76 -0
  60. package/dist/esm-node/request-policy/traceparent.mjs +19 -0
  61. package/dist/esm-node/request-policy/transport.mjs +197 -0
  62. package/dist/esm-node/request-policy/types.mjs +11 -0
  63. package/dist/esm-node/routeHydration.mjs +78 -0
  64. package/dist/esm-node/routerPrefetchPolicy.mjs +116 -0
  65. package/dist/esm-node/routerProvider.mjs +118 -0
  66. package/dist/esm-node/routerState.mjs +106 -0
  67. package/dist/esm-node/routerStatePlugin.mjs +49 -0
  68. package/dist/esm-node/routerStateTypes.mjs +1 -0
  69. package/dist/esm-node/runtimeContextProjection.mjs +75 -0
  70. package/dist/esm-node/server-config/bffRuntime.mjs +1 -0
  71. package/dist/esm-node/server-config/index.mjs +1 -0
  72. package/dist/esm-node/server-config/serverTelemetry.mjs +1 -0
  73. package/dist/esm-node/shellStream.mjs +34 -2
  74. package/dist/esm-node/workspaceEvents.mjs +67 -0
  75. package/dist/types/contextExtensions.d.ts +34 -0
  76. package/dist/types/headRuntime.d.ts +12 -0
  77. package/dist/types/helmetContext.d.ts +10 -0
  78. package/dist/types/index.d.ts +2 -0
  79. package/dist/types/prefetchPolicy.d.ts +4 -0
  80. package/dist/types/request-policy/browser.d.ts +10 -0
  81. package/dist/types/request-policy/client.d.ts +14 -0
  82. package/dist/types/request-policy/node.d.ts +10 -0
  83. package/dist/types/request-policy/policyCore.d.ts +107 -0
  84. package/dist/types/request-policy/requestContext.d.ts +22 -0
  85. package/dist/types/request-policy/traceparent.d.ts +10 -0
  86. package/dist/types/request-policy/transport.d.ts +12 -0
  87. package/dist/types/request-policy/types.d.ts +144 -0
  88. package/dist/types/routeHydration.d.ts +24 -0
  89. package/dist/types/routerPrefetchPolicy.d.ts +28 -0
  90. package/dist/types/routerProvider.d.ts +77 -0
  91. package/dist/types/routerState.d.ts +12 -0
  92. package/dist/types/routerStatePlugin.d.ts +19 -0
  93. package/dist/types/routerStateTypes.d.ts +40 -0
  94. package/dist/types/runtimeContextProjection.d.ts +30 -0
  95. package/dist/types/server-config/bffRuntime.d.ts +122 -0
  96. package/dist/types/server-config/index.d.ts +2 -0
  97. package/dist/types/server-config/serverTelemetry.d.ts +322 -0
  98. package/dist/types/workspaceEvents.d.ts +62 -0
  99. package/package.json +198 -13
  100. package/rslib.config.mts +17 -2
  101. package/rstest.config.mts +33 -2
  102. package/src/contextExtensions.ts +81 -0
  103. package/src/headRuntime.tsx +77 -0
  104. package/src/helmetContext.ts +31 -0
  105. package/src/index.ts +2 -0
  106. package/src/prefetchPolicy.ts +33 -0
  107. package/src/request-policy/browser.ts +22 -0
  108. package/src/request-policy/client.ts +404 -0
  109. package/src/request-policy/node.ts +41 -0
  110. package/src/request-policy/policyCore.ts +424 -0
  111. package/src/request-policy/requestContext.ts +149 -0
  112. package/src/request-policy/traceparent.ts +43 -0
  113. package/src/request-policy/transport.ts +349 -0
  114. package/src/request-policy/types.ts +229 -0
  115. package/src/routeHydration.ts +208 -0
  116. package/src/routerPrefetchPolicy.ts +195 -0
  117. package/src/routerProvider.ts +340 -0
  118. package/src/routerState.ts +208 -0
  119. package/src/routerStatePlugin.ts +86 -0
  120. package/src/routerStateTypes.ts +46 -0
  121. package/src/runtimeContextProjection.ts +105 -0
  122. package/src/server-config/bffRuntime.ts +136 -0
  123. package/src/server-config/index.ts +2 -0
  124. package/src/server-config/serverTelemetry.ts +337 -0
  125. package/src/shellStream.ts +55 -25
  126. package/src/workspaceEvents.ts +324 -0
  127. package/tests/buildIdentity.test.ts +2 -4
  128. package/tests/contextExtensions.test.ts +71 -0
  129. package/tests/headRuntime.test.tsx +369 -0
  130. package/tests/nativeSafeFailureCompatibility.test.ts +117 -0
  131. package/tests/prefetchPolicy.test.ts +51 -0
  132. package/tests/rendererHead.test.ts +1 -72
  133. package/tests/request-policy/browser-policy.test.ts +608 -0
  134. package/tests/request-policy/client-isolation.test.ts +149 -0
  135. package/tests/request-policy/identityBinding.test.ts +75 -0
  136. package/tests/request-policy/node-policy.test.ts +741 -0
  137. package/tests/request-policy/policyCore.test.ts +250 -0
  138. package/tests/request-policy/requestContext.test.ts +80 -0
  139. package/tests/request-policy/requestFactory.outbound-contract.test.ts +691 -0
  140. package/tests/request-policy/traceparent.test.ts +55 -0
  141. package/tests/request-policy/transport-retry.test.ts +370 -0
  142. package/tests/routeHydration.test.ts +180 -0
  143. package/tests/routerPrefetchPolicy.test.ts +102 -0
  144. package/tests/routerProvider.test.ts +38 -0
  145. package/tests/routerState.test.ts +157 -0
  146. package/tests/routerStatePlugin.test.ts +108 -0
  147. package/tests/runtimeContextProjection.test.ts +135 -0
  148. package/tests/shellStream.test.ts +37 -0
  149. package/tests/workspaceEvents.test.ts +391 -0
  150. package/dist/cjs/rscHtmlStream.js +0 -128
  151. package/dist/esm/rscHtmlStream.mjs +0 -86
  152. package/dist/esm-node/rscHtmlStream.mjs +0 -87
  153. package/dist/types/rscHtmlStream.d.ts +0 -4
  154. package/src/rscHtmlStream.ts +0 -142
  155. package/tests/rscHtmlStream.test.ts +0 -153
@@ -0,0 +1,250 @@
1
+ import {
2
+ attachOperationContextHeaders,
3
+ buildEnvelopeHeaderValue,
4
+ buildOperationContext,
5
+ CrossOriginEnvelopePolicyError,
6
+ deleteHeader,
7
+ findHeaderKey,
8
+ isSecuredRequestId,
9
+ OperationContractViolationError,
10
+ readHeader,
11
+ resolveConfiguredRequest,
12
+ toOrigin,
13
+ validateOperationContract,
14
+ writeHeader,
15
+ } from '../../src/request-policy/policyCore';
16
+
17
+ describe('policyCore (shared browser/node policy module)', () => {
18
+ test('header helpers are case-insensitive and normalize duplicate keys', () => {
19
+ const headers: Record<string, any> = { 'X-Foo': 'a' };
20
+
21
+ expect(findHeaderKey(headers, 'x-foo')).toBe('X-Foo');
22
+ expect(readHeader(headers, 'x-FOO')).toBe('a');
23
+
24
+ writeHeader(headers, 'x-foo', 'b');
25
+ expect(headers).toEqual({ 'x-foo': 'b' });
26
+
27
+ deleteHeader(headers, 'X-FOO');
28
+ expect(headers).toEqual({});
29
+ });
30
+
31
+ test('toOrigin behaves identically for both targets', () => {
32
+ expect(toOrigin('https://example.com/a/b?c=1')).toBe('https://example.com');
33
+ expect(toOrigin('not-a-url')).toBeUndefined();
34
+ });
35
+
36
+ test('isSecuredRequestId treats only "default" as unsecured by default', () => {
37
+ expect(isSecuredRequestId('default')).toBe(false);
38
+ expect(isSecuredRequestId('crm-producer')).toBe(true);
39
+ });
40
+
41
+ test('buildOperationContext prefixes operationId with the requestId', () => {
42
+ const context = buildOperationContext({
43
+ requestId: 'crm',
44
+ method: 'get',
45
+ path: '/hello',
46
+ operationContext: {
47
+ operationId: 'getHello',
48
+ schemaHash: 'abc',
49
+ operationVersion: 2,
50
+ },
51
+ });
52
+
53
+ expect(context.operationId).toBe('crm:getHello');
54
+ expect(context.method).toBe('GET');
55
+ expect(context.schemaHash).toBe('abc');
56
+ expect(context.operationVersion).toBe(2);
57
+ });
58
+
59
+ test('validateOperationContract throws on missing schema hash for secured ids', () => {
60
+ const contextPayload = buildOperationContext({
61
+ requestId: 'crm',
62
+ method: 'GET',
63
+ path: '/hello',
64
+ });
65
+
66
+ expect(() =>
67
+ validateOperationContract({
68
+ requestId: 'crm',
69
+ target: 'browser',
70
+ contextPayload,
71
+ operationContract: undefined,
72
+ }),
73
+ ).toThrow(OperationContractViolationError);
74
+ });
75
+
76
+ test('buildEnvelopeHeaderValue denies cross-origin flows unless allowed', () => {
77
+ expect(() =>
78
+ buildEnvelopeHeaderValue({
79
+ requestId: 'crm',
80
+ target: 'server',
81
+ sourceOrigin: 'https://a.example',
82
+ targetOrigin: 'https://b.example',
83
+ traceContext: undefined,
84
+ allowCrossOriginEnvelope: undefined,
85
+ }),
86
+ ).toThrow(CrossOriginEnvelopePolicyError);
87
+
88
+ const envelope = JSON.parse(
89
+ buildEnvelopeHeaderValue({
90
+ requestId: 'crm',
91
+ target: 'server',
92
+ sourceOrigin: 'https://a.example',
93
+ targetOrigin: 'https://b.example',
94
+ traceContext: { traceId: 't', spanId: 's' },
95
+ allowCrossOriginEnvelope: true,
96
+ }),
97
+ );
98
+ expect(envelope).toMatchObject({
99
+ requestId: 'crm',
100
+ target: 'server',
101
+ sourceOrigin: 'https://a.example',
102
+ targetOrigin: 'https://b.example',
103
+ traceId: 't',
104
+ spanId: 's',
105
+ });
106
+ });
107
+
108
+ test('buildEnvelopeHeaderValue requires cross-origin predicate to return true', () => {
109
+ const allowCrossOriginEnvelope = () => 'yes';
110
+
111
+ expect(() =>
112
+ buildEnvelopeHeaderValue({
113
+ requestId: 'crm',
114
+ target: 'server',
115
+ sourceOrigin: 'https://a.example',
116
+ targetOrigin: 'https://b.example',
117
+ traceContext: undefined,
118
+ allowCrossOriginEnvelope:
119
+ allowCrossOriginEnvelope as unknown as () => boolean,
120
+ }),
121
+ ).toThrow(CrossOriginEnvelopePolicyError);
122
+ });
123
+
124
+ test('attachOperationContextHeaders writes id and detail headers without clobbering caller id', () => {
125
+ const headers: Record<string, any> = {
126
+ 'x-operation-id': 'crm:custom-op',
127
+ };
128
+
129
+ attachOperationContextHeaders({
130
+ headers,
131
+ requestId: 'crm',
132
+ target: 'server',
133
+ method: 'GET',
134
+ path: '/hello',
135
+ operationContext: {
136
+ operationId: 'getHello',
137
+ schemaHash: 'abc',
138
+ operationVersion: 1,
139
+ },
140
+ operationContract: undefined,
141
+ operationContextHeader: 'x-operation-id',
142
+ operationContextDetailHeader: 'x-modernjs-bff-operation-context',
143
+ });
144
+
145
+ expect(headers['x-operation-id']).toBe('crm:custom-op');
146
+ const details = JSON.parse(headers['x-modernjs-bff-operation-context']);
147
+ expect(details).toMatchObject({
148
+ requestId: 'crm',
149
+ operationId: 'crm:getHello',
150
+ schemaHash: 'abc',
151
+ operationVersion: 1,
152
+ });
153
+ });
154
+
155
+ test('resolveConfiguredRequest throws for unconfigured non-default producers', () => {
156
+ const map = new Map<string, string>();
157
+ expect(() => resolveConfiguredRequest(map, 'crm', 'fallback')).toThrow(
158
+ 'Producer client "crm" is not initialized',
159
+ );
160
+ expect(resolveConfiguredRequest(map, 'default', 'fallback')).toBe(
161
+ 'fallback',
162
+ );
163
+ map.set('crm', 'configured');
164
+ expect(resolveConfiguredRequest(map, 'crm', 'fallback')).toBe('configured');
165
+ });
166
+ });
167
+
168
+ describe('createUploader policy headers (node entry)', () => {
169
+ test('attaches envelope and operation-context headers for secured producers', async () => {
170
+ const { configure, createUploader } = await import(
171
+ '@modern-js/runtime-extensions/request-policy/server'
172
+ );
173
+
174
+ const seenInit: Array<{ url: string; init: any }> = [];
175
+ const fakeFetch = (async (url: any, init: any) => {
176
+ seenInit.push({ url: String(url), init });
177
+ return new Response('{}', { status: 200 });
178
+ }) as typeof fetch;
179
+
180
+ configure({
181
+ requestId: 'upload-producer',
182
+ request: fakeFetch,
183
+ setDomain: () => 'http://producer.example',
184
+ });
185
+
186
+ const upload = createUploader({
187
+ path: '/api/upload',
188
+ requestId: 'upload-producer',
189
+ operationContext: {
190
+ operationId: 'upload',
191
+ routePath: '/api/upload',
192
+ method: 'POST',
193
+ schemaHash: 'hash-1',
194
+ operationVersion: 1,
195
+ },
196
+ });
197
+
198
+ const file = new File(['hello'], 'hello.txt', { type: 'text/plain' });
199
+ await upload({ files: { file } });
200
+
201
+ expect(seenInit).toHaveLength(1);
202
+ const { url, init } = seenInit[0]!;
203
+ expect(url).toBe('http://producer.example/api/upload');
204
+ expect(init.method).toBe('POST');
205
+
206
+ const envelope = JSON.parse(init.headers['x-modernjs-bff-envelope']);
207
+ expect(envelope.requestId).toBe('upload-producer');
208
+
209
+ expect(init.headers['x-operation-id']).toBe('upload-producer:upload');
210
+ const details = JSON.parse(
211
+ init.headers['x-modernjs-bff-operation-context'],
212
+ );
213
+ expect(details).toMatchObject({
214
+ requestId: 'upload-producer',
215
+ operationId: 'upload-producer:upload',
216
+ routePath: '/api/upload',
217
+ method: 'POST',
218
+ schemaHash: 'hash-1',
219
+ operationVersion: 1,
220
+ });
221
+ });
222
+
223
+ test('keeps the default uploader free of policy headers', async () => {
224
+ const { createUploader } = await import(
225
+ '@modern-js/runtime-extensions/request-policy/server'
226
+ );
227
+
228
+ const seenInit: Array<{ init: any }> = [];
229
+ const fakeFetch = (async (_url: any, init: any) => {
230
+ seenInit.push({ init });
231
+ return new Response('{}', { status: 200 });
232
+ }) as typeof fetch;
233
+
234
+ // default requestId falls back to the passed fetch only via configure-less
235
+ // path; install a custom request for determinism.
236
+ const { configure } = await import(
237
+ '@modern-js/runtime-extensions/request-policy/server'
238
+ );
239
+ configure({ request: fakeFetch });
240
+
241
+ const upload = createUploader({ path: '/api/upload' });
242
+ const file = new File(['hi'], 'hi.txt', { type: 'text/plain' });
243
+ await upload({ files: { file } });
244
+
245
+ expect(seenInit).toHaveLength(1);
246
+ const headers = seenInit[0]!.init.headers as Record<string, unknown>;
247
+ expect(headers['x-modernjs-bff-envelope']).toBeUndefined();
248
+ expect(headers['x-operation-id']).toBeUndefined();
249
+ });
250
+ });
@@ -0,0 +1,80 @@
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(snapshot.headers).toEqual({
58
+ [BFF_LOCALE_HEADER]: 'cs-CZ',
59
+ [BFF_TRACEPARENT_HEADER]:
60
+ '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01',
61
+ });
62
+ expect(snapshot).toMatchObject({
63
+ locale: 'cs-CZ',
64
+ traceparent: '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01',
65
+ traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
66
+ spanId: 'bbbbbbbbbbbbbbbb',
67
+ operationContext: {
68
+ operationId: 'shell:list',
69
+ routePath: '/effect/recommendations',
70
+ method: 'GET',
71
+ source: 'generated-client',
72
+ scope: { workspace: 'demo' },
73
+ sessionClaims: { role: 'viewer' },
74
+ traceparent: '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01',
75
+ traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
76
+ spanId: 'bbbbbbbbbbbbbbbb',
77
+ },
78
+ });
79
+ });
80
+ });