request-iframe 0.0.3 → 0.0.4

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 (53) hide show
  1. package/QUICKSTART.CN.md +35 -8
  2. package/QUICKSTART.md +35 -8
  3. package/README.CN.md +170 -24
  4. package/README.md +230 -19
  5. package/library/__tests__/coverage-branches.test.ts +356 -0
  6. package/library/__tests__/requestIframe.test.ts +1008 -58
  7. package/library/__tests__/stream.test.ts +46 -15
  8. package/library/api/client.d.ts.map +1 -1
  9. package/library/api/client.js +1 -0
  10. package/library/constants/messages.d.ts +2 -0
  11. package/library/constants/messages.d.ts.map +1 -1
  12. package/library/constants/messages.js +2 -0
  13. package/library/core/client-server.d.ts +4 -0
  14. package/library/core/client-server.d.ts.map +1 -1
  15. package/library/core/client-server.js +45 -22
  16. package/library/core/client.d.ts +31 -4
  17. package/library/core/client.d.ts.map +1 -1
  18. package/library/core/client.js +471 -284
  19. package/library/core/request.d.ts +3 -1
  20. package/library/core/request.d.ts.map +1 -1
  21. package/library/core/request.js +2 -1
  22. package/library/core/response.d.ts +26 -4
  23. package/library/core/response.d.ts.map +1 -1
  24. package/library/core/response.js +142 -81
  25. package/library/core/server.d.ts +13 -0
  26. package/library/core/server.d.ts.map +1 -1
  27. package/library/core/server.js +211 -6
  28. package/library/index.d.ts +2 -1
  29. package/library/index.d.ts.map +1 -1
  30. package/library/index.js +32 -3
  31. package/library/message/dispatcher.d.ts.map +1 -1
  32. package/library/message/dispatcher.js +4 -3
  33. package/library/stream/index.d.ts +11 -1
  34. package/library/stream/index.d.ts.map +1 -1
  35. package/library/stream/index.js +21 -3
  36. package/library/stream/types.d.ts +2 -2
  37. package/library/stream/types.d.ts.map +1 -1
  38. package/library/stream/writable-stream.d.ts +1 -1
  39. package/library/stream/writable-stream.d.ts.map +1 -1
  40. package/library/stream/writable-stream.js +8 -10
  41. package/library/types/index.d.ts +26 -4
  42. package/library/types/index.d.ts.map +1 -1
  43. package/library/utils/index.d.ts +14 -0
  44. package/library/utils/index.d.ts.map +1 -1
  45. package/library/utils/index.js +99 -1
  46. package/library/utils/path-match.d.ts +16 -0
  47. package/library/utils/path-match.d.ts.map +1 -1
  48. package/library/utils/path-match.js +65 -0
  49. package/package.json +2 -1
  50. package/react/library/__tests__/index.test.tsx +44 -22
  51. package/react/library/index.d.ts.map +1 -1
  52. package/react/library/index.js +81 -23
  53. package/react/package.json +7 -0
@@ -2,6 +2,7 @@ import { requestIframeClient, clearRequestIframeClientCache } from '../api/clien
2
2
  import { requestIframeServer, clearRequestIframeServerCache } from '../api/server';
3
3
  import { RequestConfig, Response, ErrorResponse, PostMessageData } from '../types';
4
4
  import { HttpHeader, MessageRole, Messages } from '../constants';
5
+ import { IframeWritableStream } from '../stream';
5
6
 
6
7
  /**
7
8
  * Create test iframe
@@ -22,6 +23,18 @@ function cleanupIframe(iframe: HTMLIFrameElement): void {
22
23
  }
23
24
  }
24
25
 
26
+ /**
27
+ * Convert Blob to text (for assertions)
28
+ */
29
+ function blobToText(blob: Blob): Promise<string> {
30
+ return new Promise((resolve, reject) => {
31
+ const reader = new FileReader();
32
+ reader.onload = () => resolve(String(reader.result ?? ''));
33
+ reader.onerror = reject;
34
+ reader.readAsText(blob);
35
+ });
36
+ }
37
+
25
38
  describe('requestIframeClient and requestIframeServer', () => {
26
39
  beforeEach(() => {
27
40
  // Clear all caches
@@ -110,6 +123,251 @@ describe('requestIframeClient and requestIframeServer', () => {
110
123
  cleanupIframe(iframe);
111
124
  });
112
125
 
126
+ it('should return response.data when returnData is true in options', async () => {
127
+ const origin = 'https://example.com';
128
+ const iframe = createTestIframe(origin);
129
+
130
+ const mockContentWindow = {
131
+ postMessage: jest.fn((msg: PostMessageData) => {
132
+ if (msg.type === 'request') {
133
+ window.dispatchEvent(
134
+ new MessageEvent('message', {
135
+ data: {
136
+ __requestIframe__: 1,
137
+ type: 'ack',
138
+ requestId: msg.requestId,
139
+ path: msg.path,
140
+ role: MessageRole.SERVER
141
+ },
142
+ origin
143
+ })
144
+ );
145
+ setTimeout(() => {
146
+ window.dispatchEvent(
147
+ new MessageEvent('message', {
148
+ data: {
149
+ __requestIframe__: 1,
150
+ type: 'response',
151
+ requestId: msg.requestId,
152
+ data: { result: 'success' },
153
+ status: 200,
154
+ statusText: 'OK',
155
+ role: MessageRole.SERVER
156
+ },
157
+ origin
158
+ })
159
+ );
160
+ }, 10);
161
+ }
162
+ })
163
+ };
164
+ Object.defineProperty(iframe, 'contentWindow', {
165
+ value: mockContentWindow,
166
+ writable: true
167
+ });
168
+
169
+ const client = requestIframeClient(iframe);
170
+ const server = requestIframeServer();
171
+
172
+ server.on('test', (req, res) => {
173
+ res.send({ result: 'success' });
174
+ });
175
+
176
+ const data = await client.send('test', { param: 'value' }, { ackTimeout: 1000, returnData: true });
177
+ // Should return data directly, not Response object
178
+ expect(data).toEqual({ result: 'success' });
179
+ expect((data as any).status).toBeUndefined();
180
+ expect((data as any).requestId).toBeUndefined();
181
+
182
+ server.destroy();
183
+ cleanupIframe(iframe);
184
+ });
185
+
186
+ it('should return full Response when returnData is false', async () => {
187
+ const origin = 'https://example.com';
188
+ const iframe = createTestIframe(origin);
189
+
190
+ const mockContentWindow = {
191
+ postMessage: jest.fn((msg: PostMessageData) => {
192
+ if (msg.type === 'request') {
193
+ window.dispatchEvent(
194
+ new MessageEvent('message', {
195
+ data: {
196
+ __requestIframe__: 1,
197
+ type: 'ack',
198
+ requestId: msg.requestId,
199
+ path: msg.path,
200
+ role: MessageRole.SERVER
201
+ },
202
+ origin
203
+ })
204
+ );
205
+ setTimeout(() => {
206
+ window.dispatchEvent(
207
+ new MessageEvent('message', {
208
+ data: {
209
+ __requestIframe__: 1,
210
+ type: 'response',
211
+ requestId: msg.requestId,
212
+ data: { result: 'success' },
213
+ status: 200,
214
+ statusText: 'OK',
215
+ role: MessageRole.SERVER
216
+ },
217
+ origin
218
+ })
219
+ );
220
+ }, 10);
221
+ }
222
+ })
223
+ };
224
+ Object.defineProperty(iframe, 'contentWindow', {
225
+ value: mockContentWindow,
226
+ writable: true
227
+ });
228
+
229
+ const client = requestIframeClient(iframe);
230
+ const server = requestIframeServer();
231
+
232
+ server.on('test', (req, res) => {
233
+ res.send({ result: 'success' });
234
+ });
235
+
236
+ const response = await client.send('test', { param: 'value' }, { ackTimeout: 1000, returnData: false });
237
+ // Should return full Response object
238
+ expect(response).toHaveProperty('data');
239
+ expect(response).toHaveProperty('status');
240
+ expect(response).toHaveProperty('statusText');
241
+ expect(response).toHaveProperty('requestId');
242
+ expect(response.data).toEqual({ result: 'success' });
243
+ expect(response.status).toBe(200);
244
+
245
+ server.destroy();
246
+ cleanupIframe(iframe);
247
+ });
248
+
249
+ it('should use default returnData from RequestIframeClientOptions', async () => {
250
+ const origin = 'https://example.com';
251
+ const iframe = createTestIframe(origin);
252
+
253
+ const mockContentWindow = {
254
+ postMessage: jest.fn((msg: PostMessageData) => {
255
+ if (msg.type === 'request') {
256
+ window.dispatchEvent(
257
+ new MessageEvent('message', {
258
+ data: {
259
+ __requestIframe__: 1,
260
+ type: 'ack',
261
+ requestId: msg.requestId,
262
+ path: msg.path,
263
+ role: MessageRole.SERVER
264
+ },
265
+ origin
266
+ })
267
+ );
268
+ setTimeout(() => {
269
+ window.dispatchEvent(
270
+ new MessageEvent('message', {
271
+ data: {
272
+ __requestIframe__: 1,
273
+ type: 'response',
274
+ requestId: msg.requestId,
275
+ data: { result: 'success' },
276
+ status: 200,
277
+ statusText: 'OK',
278
+ role: MessageRole.SERVER
279
+ },
280
+ origin
281
+ })
282
+ );
283
+ }, 10);
284
+ }
285
+ })
286
+ };
287
+ Object.defineProperty(iframe, 'contentWindow', {
288
+ value: mockContentWindow,
289
+ writable: true
290
+ });
291
+
292
+ // Create client with returnData: true in options
293
+ const client = requestIframeClient(iframe, { returnData: true });
294
+ const server = requestIframeServer();
295
+
296
+ server.on('test', (req, res) => {
297
+ res.send({ result: 'success' });
298
+ });
299
+
300
+ // Should return data directly without specifying returnData in send options
301
+ const data = await client.send('test', { param: 'value' }, { ackTimeout: 1000 });
302
+ expect(data).toEqual({ result: 'success' });
303
+ expect((data as any).status).toBeUndefined();
304
+
305
+ server.destroy();
306
+ cleanupIframe(iframe);
307
+ });
308
+
309
+ it('should allow overriding default returnData in send options', async () => {
310
+ const origin = 'https://example.com';
311
+ const iframe = createTestIframe(origin);
312
+
313
+ const mockContentWindow = {
314
+ postMessage: jest.fn((msg: PostMessageData) => {
315
+ if (msg.type === 'request') {
316
+ window.dispatchEvent(
317
+ new MessageEvent('message', {
318
+ data: {
319
+ __requestIframe__: 1,
320
+ type: 'ack',
321
+ requestId: msg.requestId,
322
+ path: msg.path,
323
+ role: MessageRole.SERVER
324
+ },
325
+ origin
326
+ })
327
+ );
328
+ setTimeout(() => {
329
+ window.dispatchEvent(
330
+ new MessageEvent('message', {
331
+ data: {
332
+ __requestIframe__: 1,
333
+ type: 'response',
334
+ requestId: msg.requestId,
335
+ data: { result: 'success' },
336
+ status: 200,
337
+ statusText: 'OK',
338
+ role: MessageRole.SERVER
339
+ },
340
+ origin
341
+ })
342
+ );
343
+ }, 10);
344
+ }
345
+ })
346
+ };
347
+ Object.defineProperty(iframe, 'contentWindow', {
348
+ value: mockContentWindow,
349
+ writable: true
350
+ });
351
+
352
+ // Create client with returnData: true in options
353
+ const client = requestIframeClient(iframe, { returnData: true });
354
+ const server = requestIframeServer();
355
+
356
+ server.on('test', (req, res) => {
357
+ res.send({ result: 'success' });
358
+ });
359
+
360
+ // Override with returnData: false in send options
361
+ const response = await client.send('test', { param: 'value' }, { ackTimeout: 1000, returnData: false });
362
+ // Should return full Response object despite default being true
363
+ expect(response).toHaveProperty('data');
364
+ expect(response).toHaveProperty('status');
365
+ expect(response.data).toEqual({ result: 'success' });
366
+
367
+ server.destroy();
368
+ cleanupIframe(iframe);
369
+ });
370
+
113
371
  it('should throw error when iframe.contentWindow is unavailable', () => {
114
372
  const iframe = document.createElement('iframe');
115
373
  iframe.src = 'https://example.com/test.html';
@@ -459,70 +717,378 @@ describe('requestIframeClient and requestIframeServer', () => {
459
717
  timeout: 200,
460
718
  asyncTimeout: 5000
461
719
  });
462
- expect(response.data).toEqual({ result: 'async success' });
463
- server.destroy();
464
- cleanupIframe(iframe);
465
- });
466
- });
720
+ expect(response.data).toEqual({ result: 'async success' });
721
+ server.destroy();
722
+ cleanupIframe(iframe);
723
+ });
724
+ });
725
+
726
+ describe('MessageChannel sharing', () => {
727
+ it('should share the same message channel for the same secretKey', () => {
728
+ const iframe1 = createTestIframe('https://example.com');
729
+ const iframe2 = createTestIframe('https://example2.com');
730
+
731
+ const server1 = requestIframeServer({ secretKey: 'demo' });
732
+ const server2 = requestIframeServer({ secretKey: 'demo' });
733
+
734
+ // Server instances are different
735
+ expect(server1).not.toBe(server2);
736
+
737
+ // But they should share the same underlying message channel (verified by secretKey)
738
+ expect(server1.secretKey).toBe(server2.secretKey);
739
+ expect(server1.secretKey).toBe('demo');
740
+
741
+ server1.destroy();
742
+ server2.destroy();
743
+ cleanupIframe(iframe1);
744
+ cleanupIframe(iframe2);
745
+ });
746
+
747
+ it('should have independent message channels for different secretKeys', () => {
748
+ const iframe = createTestIframe('https://example.com');
749
+
750
+ const server1 = requestIframeServer({ secretKey: 'demo1' });
751
+ const server2 = requestIframeServer({ secretKey: 'demo2' });
752
+
753
+ // Verify different server instances
754
+ expect(server1).not.toBe(server2);
755
+
756
+ // secretKeys are different
757
+ expect(server1.secretKey).toBe('demo1');
758
+ expect(server2.secretKey).toBe('demo2');
759
+
760
+ server1.destroy();
761
+ server2.destroy();
762
+ cleanupIframe(iframe);
763
+ });
764
+
765
+ it('should share the same message channel when no secretKey', () => {
766
+ const iframe1 = createTestIframe('https://example.com');
767
+ const iframe2 = createTestIframe('https://example2.com');
768
+
769
+ const server1 = requestIframeServer();
770
+ const server2 = requestIframeServer();
771
+
772
+ // Server instances are different
773
+ expect(server1).not.toBe(server2);
774
+
775
+ // But they should share the same underlying message channel (both have no secretKey)
776
+ expect(server1.secretKey).toBe(server2.secretKey);
777
+ expect(server1.secretKey).toBeUndefined();
778
+
779
+ server1.destroy();
780
+ server2.destroy();
781
+ cleanupIframe(iframe1);
782
+ cleanupIframe(iframe2);
783
+ });
784
+ });
785
+
786
+ describe('secretKey message isolation', () => {
787
+ it('should successfully communicate when client and server use the same secretKey', async () => {
788
+ const origin = 'https://example.com';
789
+ const iframe = createTestIframe(origin);
790
+
791
+ const mockContentWindow = {
792
+ postMessage: jest.fn((msg: PostMessageData) => {
793
+ if (msg.type === 'request') {
794
+ // Verify secretKey is in message
795
+ expect(msg.secretKey).toBe('test-key');
796
+ // Verify path is NOT prefixed with secretKey
797
+ expect(msg.path).toBe('test');
798
+
799
+ // Send ACK first
800
+ window.dispatchEvent(
801
+ new MessageEvent('message', {
802
+ data: {
803
+ __requestIframe__: 1,
804
+ type: 'ack',
805
+ requestId: msg.requestId,
806
+ path: msg.path,
807
+ secretKey: 'test-key',
808
+ role: MessageRole.SERVER
809
+ },
810
+ origin
811
+ })
812
+ );
813
+ // Then send response
814
+ setTimeout(() => {
815
+ window.dispatchEvent(
816
+ new MessageEvent('message', {
817
+ data: {
818
+ __requestIframe__: 1,
819
+ type: 'response',
820
+ requestId: msg.requestId,
821
+ data: { result: 'success' },
822
+ status: 200,
823
+ statusText: 'OK',
824
+ secretKey: 'test-key',
825
+ role: MessageRole.SERVER
826
+ },
827
+ origin
828
+ })
829
+ );
830
+ }, 10);
831
+ }
832
+ })
833
+ };
834
+ Object.defineProperty(iframe, 'contentWindow', {
835
+ value: mockContentWindow,
836
+ writable: true
837
+ });
838
+
839
+ const client = requestIframeClient(iframe, { secretKey: 'test-key' });
840
+ const server = requestIframeServer({ secretKey: 'test-key' });
841
+
842
+ server.on('test', (req, res) => {
843
+ res.send({ result: 'success' });
844
+ });
845
+
846
+ const response = await client.send('test', { param: 'value' }, { ackTimeout: 1000 });
847
+ expect(response.data).toEqual({ result: 'success' });
848
+ expect(response.status).toBe(200);
849
+ expect(mockContentWindow.postMessage).toHaveBeenCalled();
850
+
851
+ // Verify the sent message has secretKey
852
+ const sentMessage = (mockContentWindow.postMessage as jest.Mock).mock.calls[0][0];
853
+ expect(sentMessage.secretKey).toBe('test-key');
854
+ expect(sentMessage.path).toBe('test'); // Path should NOT be prefixed
855
+
856
+ server.destroy();
857
+ cleanupIframe(iframe);
858
+ });
859
+
860
+ it('should NOT communicate when client and server use different secretKeys', async () => {
861
+ const origin = 'https://example.com';
862
+ const iframe = createTestIframe(origin);
863
+
864
+ const mockContentWindow = {
865
+ postMessage: jest.fn()
866
+ };
867
+ Object.defineProperty(iframe, 'contentWindow', {
868
+ value: mockContentWindow,
869
+ writable: true
870
+ });
871
+
872
+ const client = requestIframeClient(iframe, { secretKey: 'client-key' });
873
+ const server = requestIframeServer({ secretKey: 'server-key' });
874
+
875
+ server.on('test', (req, res) => {
876
+ res.send({ result: 'success' });
877
+ });
878
+
879
+ // Request should timeout because server won't respond (different secretKey)
880
+ await expect(
881
+ client.send('test', { param: 'value' }, { ackTimeout: 100 })
882
+ ).rejects.toMatchObject({
883
+ code: 'ACK_TIMEOUT'
884
+ });
885
+
886
+ server.destroy();
887
+ cleanupIframe(iframe);
888
+ });
889
+
890
+ it('should NOT communicate when client has secretKey but server does not', async () => {
891
+ const origin = 'https://example.com';
892
+ const iframe = createTestIframe(origin);
893
+
894
+ const mockContentWindow = {
895
+ postMessage: jest.fn()
896
+ };
897
+ Object.defineProperty(iframe, 'contentWindow', {
898
+ value: mockContentWindow,
899
+ writable: true
900
+ });
901
+
902
+ const client = requestIframeClient(iframe, { secretKey: 'client-key' });
903
+ const server = requestIframeServer(); // No secretKey
904
+
905
+ server.on('test', (req, res) => {
906
+ res.send({ result: 'success' });
907
+ });
908
+
909
+ // Request should timeout because server won't respond (different secretKey)
910
+ await expect(
911
+ client.send('test', { param: 'value' }, { ackTimeout: 100 })
912
+ ).rejects.toMatchObject({
913
+ code: 'ACK_TIMEOUT'
914
+ });
915
+
916
+ server.destroy();
917
+ cleanupIframe(iframe);
918
+ });
919
+
920
+ it('should NOT communicate when client has no secretKey but server has secretKey', async () => {
921
+ const origin = 'https://example.com';
922
+ const iframe = createTestIframe(origin);
923
+
924
+ const mockContentWindow = {
925
+ postMessage: jest.fn()
926
+ };
927
+ Object.defineProperty(iframe, 'contentWindow', {
928
+ value: mockContentWindow,
929
+ writable: true
930
+ });
931
+
932
+ const client = requestIframeClient(iframe); // No secretKey
933
+ const server = requestIframeServer({ secretKey: 'server-key' });
934
+
935
+ server.on('test', (req, res) => {
936
+ res.send({ result: 'success' });
937
+ });
938
+
939
+ // Request should timeout because server won't respond (different secretKey)
940
+ await expect(
941
+ client.send('test', { param: 'value' }, { ackTimeout: 100 })
942
+ ).rejects.toMatchObject({
943
+ code: 'ACK_TIMEOUT'
944
+ });
945
+
946
+ server.destroy();
947
+ cleanupIframe(iframe);
948
+ });
949
+
950
+ it('should successfully communicate when both client and server have no secretKey', async () => {
951
+ const origin = 'https://example.com';
952
+ const iframe = createTestIframe(origin);
953
+
954
+ const mockContentWindow = {
955
+ postMessage: jest.fn((msg: PostMessageData) => {
956
+ if (msg.type === 'request') {
957
+ // Verify secretKey is NOT in message
958
+ expect(msg.secretKey).toBeUndefined();
959
+ // Verify path is NOT prefixed
960
+ expect(msg.path).toBe('test');
961
+
962
+ // Send ACK first
963
+ window.dispatchEvent(
964
+ new MessageEvent('message', {
965
+ data: {
966
+ __requestIframe__: 1,
967
+ type: 'ack',
968
+ requestId: msg.requestId,
969
+ path: msg.path,
970
+ role: MessageRole.SERVER
971
+ },
972
+ origin
973
+ })
974
+ );
975
+ // Then send response
976
+ setTimeout(() => {
977
+ window.dispatchEvent(
978
+ new MessageEvent('message', {
979
+ data: {
980
+ __requestIframe__: 1,
981
+ type: 'response',
982
+ requestId: msg.requestId,
983
+ data: { result: 'success' },
984
+ status: 200,
985
+ statusText: 'OK',
986
+ role: MessageRole.SERVER
987
+ },
988
+ origin
989
+ })
990
+ );
991
+ }, 10);
992
+ }
993
+ })
994
+ };
995
+ Object.defineProperty(iframe, 'contentWindow', {
996
+ value: mockContentWindow,
997
+ writable: true
998
+ });
999
+
1000
+ const client = requestIframeClient(iframe); // No secretKey
1001
+ const server = requestIframeServer(); // No secretKey
1002
+
1003
+ server.on('test', (req, res) => {
1004
+ res.send({ result: 'success' });
1005
+ });
1006
+
1007
+ const response = await client.send('test', { param: 'value' }, { ackTimeout: 1000 });
1008
+ expect(response.data).toEqual({ result: 'success' });
1009
+ expect(response.status).toBe(200);
1010
+ expect(mockContentWindow.postMessage).toHaveBeenCalled();
1011
+
1012
+ // Verify the sent message has no secretKey
1013
+ const sentMessage = (mockContentWindow.postMessage as jest.Mock).mock.calls[0][0];
1014
+ expect(sentMessage.secretKey).toBeUndefined();
1015
+ expect(sentMessage.path).toBe('test'); // Path should NOT be prefixed
1016
+
1017
+ server.destroy();
1018
+ cleanupIframe(iframe);
1019
+ });
1020
+
1021
+ it('should handle path correctly with secretKey (path should not be prefixed)', async () => {
1022
+ const origin = 'https://example.com';
1023
+ const iframe = createTestIframe(origin);
1024
+
1025
+ const mockContentWindow = {
1026
+ postMessage: jest.fn((msg: PostMessageData) => {
1027
+ if (msg.type === 'request') {
1028
+ // Verify path is NOT prefixed with secretKey
1029
+ expect(msg.path).toBe('api/users');
1030
+ expect(msg.secretKey).toBe('my-app');
1031
+
1032
+ // Send ACK first
1033
+ window.dispatchEvent(
1034
+ new MessageEvent('message', {
1035
+ data: {
1036
+ __requestIframe__: 1,
1037
+ type: 'ack',
1038
+ requestId: msg.requestId,
1039
+ path: msg.path,
1040
+ secretKey: 'my-app',
1041
+ role: MessageRole.SERVER
1042
+ },
1043
+ origin
1044
+ })
1045
+ );
1046
+ // Then send response
1047
+ setTimeout(() => {
1048
+ window.dispatchEvent(
1049
+ new MessageEvent('message', {
1050
+ data: {
1051
+ __requestIframe__: 1,
1052
+ type: 'response',
1053
+ requestId: msg.requestId,
1054
+ data: { users: [] },
1055
+ status: 200,
1056
+ statusText: 'OK',
1057
+ secretKey: 'my-app',
1058
+ role: MessageRole.SERVER
1059
+ },
1060
+ origin
1061
+ })
1062
+ );
1063
+ }, 10);
1064
+ }
1065
+ })
1066
+ };
1067
+ Object.defineProperty(iframe, 'contentWindow', {
1068
+ value: mockContentWindow,
1069
+ writable: true
1070
+ });
467
1071
 
468
- describe('MessageChannel sharing', () => {
469
- it('should share the same message channel for the same secretKey', () => {
470
- const iframe1 = createTestIframe('https://example.com');
471
- const iframe2 = createTestIframe('https://example2.com');
1072
+ const client = requestIframeClient(iframe, { secretKey: 'my-app' });
1073
+ const server = requestIframeServer({ secretKey: 'my-app' });
472
1074
 
473
- const server1 = requestIframeServer({ secretKey: 'demo' });
474
- const server2 = requestIframeServer({ secretKey: 'demo' });
1075
+ // Server registers handler with original path (not prefixed)
1076
+ server.on('api/users', (req, res) => {
1077
+ res.send({ users: [] });
1078
+ });
475
1079
 
476
- // Server instances are different
477
- expect(server1).not.toBe(server2);
1080
+ // Client sends request with original path (not prefixed)
1081
+ const response = await client.send('api/users', undefined, { ackTimeout: 1000 });
1082
+ expect(response.data).toEqual({ users: [] });
478
1083
 
479
- // But they should share the same underlying message channel (verified by secretKey)
480
- expect(server1.secretKey).toBe(server2.secretKey);
481
- expect(server1.secretKey).toBe('demo');
482
-
483
- server1.destroy();
484
- server2.destroy();
485
- cleanupIframe(iframe1);
486
- cleanupIframe(iframe2);
487
- });
488
-
489
- it('should have independent message channels for different secretKeys', () => {
490
- const iframe = createTestIframe('https://example.com');
491
-
492
- const server1 = requestIframeServer({ secretKey: 'demo1' });
493
- const server2 = requestIframeServer({ secretKey: 'demo2' });
494
-
495
- // Verify different server instances
496
- expect(server1).not.toBe(server2);
1084
+ // Verify path in sent message is NOT prefixed
1085
+ const sentMessage = (mockContentWindow.postMessage as jest.Mock).mock.calls[0][0];
1086
+ expect(sentMessage.path).toBe('api/users');
1087
+ expect(sentMessage.secretKey).toBe('my-app');
497
1088
 
498
- // secretKeys are different
499
- expect(server1.secretKey).toBe('demo1');
500
- expect(server2.secretKey).toBe('demo2');
501
-
502
- server1.destroy();
503
- server2.destroy();
1089
+ server.destroy();
504
1090
  cleanupIframe(iframe);
505
1091
  });
506
-
507
- it('should share the same message channel when no secretKey', () => {
508
- const iframe1 = createTestIframe('https://example.com');
509
- const iframe2 = createTestIframe('https://example2.com');
510
-
511
- const server1 = requestIframeServer();
512
- const server2 = requestIframeServer();
513
-
514
- // Server instances are different
515
- expect(server1).not.toBe(server2);
516
-
517
- // But they should share the same underlying message channel (both have no secretKey)
518
- expect(server1.secretKey).toBe(server2.secretKey);
519
- expect(server1.secretKey).toBeUndefined();
520
-
521
- server1.destroy();
522
- server2.destroy();
523
- cleanupIframe(iframe1);
524
- cleanupIframe(iframe2);
525
- });
526
1092
  });
527
1093
 
528
1094
  describe('Middleware', () => {
@@ -701,7 +1267,7 @@ describe('requestIframeClient and requestIframeServer', () => {
701
1267
  });
702
1268
 
703
1269
  describe('sendFile', () => {
704
- it('should support sending file (base64 encoded)', async () => {
1270
+ it('should support sending file (stream)', async () => {
705
1271
  const origin = 'https://example.com';
706
1272
  const iframe = createTestIframe(origin);
707
1273
 
@@ -1070,6 +1636,157 @@ describe('requestIframeClient and requestIframeServer', () => {
1070
1636
  }, 20000);
1071
1637
  });
1072
1638
 
1639
+ describe('Path parameters', () => {
1640
+ it('should extract path parameters from route pattern', async () => {
1641
+ const origin = 'https://example.com';
1642
+ const iframe = createTestIframe(origin);
1643
+
1644
+ const mockContentWindow: any = {
1645
+ postMessage: jest.fn((msg: PostMessageData) => {
1646
+ window.dispatchEvent(
1647
+ new MessageEvent('message', {
1648
+ data: msg,
1649
+ origin,
1650
+ source: mockContentWindow as any
1651
+ })
1652
+ );
1653
+ })
1654
+ };
1655
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
1656
+
1657
+ const client = requestIframeClient(iframe);
1658
+ const server = requestIframeServer();
1659
+
1660
+ server.on('/api/users/:id', (req, res) => {
1661
+ expect(req.params.id).toBe('123');
1662
+ expect(req.path).toBe('/api/users/123');
1663
+ res.send({ userId: req.params.id });
1664
+ });
1665
+
1666
+ const resp = await client.send<any>('/api/users/123');
1667
+ expect((resp as any).data.userId).toBe('123');
1668
+
1669
+ client.destroy();
1670
+ server.destroy();
1671
+ cleanupIframe(iframe);
1672
+ });
1673
+
1674
+ it('should extract multiple path parameters', async () => {
1675
+ const origin = 'https://example.com';
1676
+ const iframe = createTestIframe(origin);
1677
+
1678
+ const mockContentWindow: any = {
1679
+ postMessage: jest.fn((msg: PostMessageData) => {
1680
+ window.dispatchEvent(
1681
+ new MessageEvent('message', {
1682
+ data: msg,
1683
+ origin,
1684
+ source: mockContentWindow as any
1685
+ })
1686
+ );
1687
+ })
1688
+ };
1689
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
1690
+
1691
+ const client = requestIframeClient(iframe);
1692
+ const server = requestIframeServer();
1693
+
1694
+ server.on('/api/users/:userId/posts/:postId', (req, res) => {
1695
+ expect(req.params.userId).toBe('456');
1696
+ expect(req.params.postId).toBe('789');
1697
+ res.send({ userId: req.params.userId, postId: req.params.postId });
1698
+ });
1699
+
1700
+ const resp = await client.send<any>('/api/users/456/posts/789');
1701
+ expect((resp as any).data.userId).toBe('456');
1702
+ expect((resp as any).data.postId).toBe('789');
1703
+
1704
+ client.destroy();
1705
+ server.destroy();
1706
+ cleanupIframe(iframe);
1707
+ });
1708
+
1709
+ it('should return empty params for exact path match', async () => {
1710
+ const origin = 'https://example.com';
1711
+ const iframe = createTestIframe(origin);
1712
+
1713
+ const mockContentWindow: any = {
1714
+ postMessage: jest.fn((msg: PostMessageData) => {
1715
+ window.dispatchEvent(
1716
+ new MessageEvent('message', {
1717
+ data: msg,
1718
+ origin,
1719
+ source: mockContentWindow as any
1720
+ })
1721
+ );
1722
+ })
1723
+ };
1724
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
1725
+
1726
+ const client = requestIframeClient(iframe);
1727
+ const server = requestIframeServer();
1728
+
1729
+ server.on('/api/users', (req, res) => {
1730
+ expect(req.params).toEqual({});
1731
+ expect(req.path).toBe('/api/users');
1732
+ res.send({ success: true });
1733
+ });
1734
+
1735
+ const resp = await client.send<any>('/api/users');
1736
+ expect((resp as any).data.success).toBe(true);
1737
+
1738
+ client.destroy();
1739
+ server.destroy();
1740
+ cleanupIframe(iframe);
1741
+ });
1742
+
1743
+ it('should work with stream requests', async () => {
1744
+ const origin = 'https://example.com';
1745
+ const iframe = createTestIframe(origin);
1746
+
1747
+ const mockContentWindow: any = {
1748
+ postMessage: jest.fn((msg: PostMessageData) => {
1749
+ window.dispatchEvent(
1750
+ new MessageEvent('message', {
1751
+ data: msg,
1752
+ origin,
1753
+ source: mockContentWindow as any
1754
+ })
1755
+ );
1756
+ })
1757
+ };
1758
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
1759
+
1760
+ const client = requestIframeClient(iframe);
1761
+ const server = requestIframeServer();
1762
+
1763
+ server.on('/api/upload/:fileId', async (req, res) => {
1764
+ expect(req.params.fileId).toBe('file-123');
1765
+ expect(req.stream).toBeDefined();
1766
+ const chunks: any[] = [];
1767
+ for await (const chunk of req.stream as any) {
1768
+ chunks.push(chunk);
1769
+ }
1770
+ res.send({ fileId: req.params.fileId, chunks });
1771
+ });
1772
+
1773
+ const stream = new IframeWritableStream({
1774
+ iterator: async function* () {
1775
+ yield 'chunk1';
1776
+ yield 'chunk2';
1777
+ }
1778
+ });
1779
+
1780
+ const resp = await client.sendStream<any>('/api/upload/file-123', stream);
1781
+ expect((resp as any).data.fileId).toBe('file-123');
1782
+ expect((resp as any).data.chunks).toEqual(['chunk1', 'chunk2']);
1783
+
1784
+ client.destroy();
1785
+ server.destroy();
1786
+ cleanupIframe(iframe);
1787
+ });
1788
+ });
1789
+
1073
1790
  describe('server.map', () => {
1074
1791
  it('should register multiple event handlers at once', async () => {
1075
1792
  const origin = 'https://example.com';
@@ -2133,6 +2850,239 @@ describe('requestIframeClient and requestIframeServer', () => {
2133
2850
  });
2134
2851
  });
2135
2852
 
2853
+ describe('client send various body types', () => {
2854
+ it('should send plain object and server receives JSON + Content-Type', async () => {
2855
+ const origin = 'https://example.com';
2856
+ const iframe = createTestIframe(origin);
2857
+
2858
+ const mockContentWindow: any = {
2859
+ postMessage: jest.fn((msg: PostMessageData) => {
2860
+ window.dispatchEvent(
2861
+ new MessageEvent('message', {
2862
+ data: msg,
2863
+ origin,
2864
+ source: mockContentWindow as any
2865
+ })
2866
+ );
2867
+ })
2868
+ };
2869
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2870
+
2871
+ const client = requestIframeClient(iframe);
2872
+ const server = requestIframeServer();
2873
+
2874
+ server.on('echoObject', (req, res) => {
2875
+ expect(req.headers[HttpHeader.CONTENT_TYPE]).toBe('application/json');
2876
+ res.send({ ok: true, received: req.body });
2877
+ });
2878
+
2879
+ const resp = await client.send<any>('echoObject', { a: 1 });
2880
+ expect((resp as any).data.ok).toBe(true);
2881
+ expect((resp as any).data.received).toEqual({ a: 1 });
2882
+
2883
+ client.destroy();
2884
+ server.destroy();
2885
+ cleanupIframe(iframe);
2886
+ });
2887
+
2888
+ it('should send string and server receives text/plain Content-Type', async () => {
2889
+ const origin = 'https://example.com';
2890
+ const iframe = createTestIframe(origin);
2891
+
2892
+ const mockContentWindow: any = {
2893
+ postMessage: jest.fn((msg: PostMessageData) => {
2894
+ window.dispatchEvent(
2895
+ new MessageEvent('message', {
2896
+ data: msg,
2897
+ origin,
2898
+ source: mockContentWindow as any
2899
+ })
2900
+ );
2901
+ })
2902
+ };
2903
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2904
+
2905
+ const client = requestIframeClient(iframe);
2906
+ const server = requestIframeServer();
2907
+
2908
+ server.on('echoText', (req, res) => {
2909
+ expect(req.headers[HttpHeader.CONTENT_TYPE]).toContain('text/plain');
2910
+ res.send({ received: req.body, type: typeof req.body });
2911
+ });
2912
+
2913
+ const resp = await client.send<any>('echoText', 'hello');
2914
+ expect((resp as any).data.received).toBe('hello');
2915
+ expect((resp as any).data.type).toBe('string');
2916
+
2917
+ client.destroy();
2918
+ server.destroy();
2919
+ cleanupIframe(iframe);
2920
+ });
2921
+
2922
+ it('should send URLSearchParams and server receives correct Content-Type', async () => {
2923
+ const origin = 'https://example.com';
2924
+ const iframe = createTestIframe(origin);
2925
+
2926
+ const mockContentWindow: any = {
2927
+ postMessage: jest.fn((msg: PostMessageData) => {
2928
+ window.dispatchEvent(
2929
+ new MessageEvent('message', {
2930
+ data: msg,
2931
+ origin,
2932
+ source: mockContentWindow as any
2933
+ })
2934
+ );
2935
+ })
2936
+ };
2937
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2938
+
2939
+ const client = requestIframeClient(iframe);
2940
+ const server = requestIframeServer();
2941
+
2942
+ server.on('echoParams', (req, res) => {
2943
+ expect(req.headers[HttpHeader.CONTENT_TYPE]).toBe('application/x-www-form-urlencoded');
2944
+ // URLSearchParams should be structured-cloneable in modern browsers
2945
+ const value = req.body?.toString?.() ?? String(req.body);
2946
+ res.send({ received: value });
2947
+ });
2948
+
2949
+ const params = new URLSearchParams({ a: '1', b: '2' });
2950
+ const resp = await client.send<any>('echoParams', params as any);
2951
+ expect((resp as any).data.received).toContain('a=1');
2952
+ expect((resp as any).data.received).toContain('b=2');
2953
+
2954
+ client.destroy();
2955
+ server.destroy();
2956
+ cleanupIframe(iframe);
2957
+ });
2958
+
2959
+ it('should auto-dispatch File/Blob body to client.sendFile and server receives file via stream (autoResolve)', async () => {
2960
+ const origin = 'https://example.com';
2961
+ const iframe = createTestIframe(origin);
2962
+
2963
+ const mockContentWindow: any = {
2964
+ postMessage: jest.fn((msg: PostMessageData) => {
2965
+ window.dispatchEvent(
2966
+ new MessageEvent('message', {
2967
+ data: msg,
2968
+ origin,
2969
+ source: mockContentWindow as any
2970
+ })
2971
+ );
2972
+ })
2973
+ };
2974
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2975
+
2976
+ const client = requestIframeClient(iframe);
2977
+ const server = requestIframeServer();
2978
+
2979
+ server.on('uploadFile', async (req, res) => {
2980
+ expect(req.body).toBeDefined();
2981
+ const blob = req.body as Blob;
2982
+ const text = await blobToText(blob);
2983
+ res.send({ ok: true, text });
2984
+ });
2985
+
2986
+ const blob = new Blob(['Hello Upload'], { type: 'text/plain' });
2987
+ const resp = await client.send<any>('uploadFile', blob);
2988
+ expect((resp as any).data.ok).toBe(true);
2989
+ expect((resp as any).data.text).toBe('Hello Upload');
2990
+
2991
+ client.destroy();
2992
+ server.destroy();
2993
+ cleanupIframe(iframe);
2994
+ });
2995
+
2996
+ it('should send stream from client to server and server receives req.stream', async () => {
2997
+ const origin = 'https://example.com';
2998
+ const iframe = createTestIframe(origin);
2999
+
3000
+ const mockContentWindow: any = {
3001
+ postMessage: jest.fn((msg: PostMessageData) => {
3002
+ window.dispatchEvent(
3003
+ new MessageEvent('message', {
3004
+ data: msg,
3005
+ origin,
3006
+ source: mockContentWindow as any
3007
+ })
3008
+ );
3009
+ })
3010
+ };
3011
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
3012
+
3013
+ const client = requestIframeClient(iframe);
3014
+ const server = requestIframeServer();
3015
+
3016
+ server.on('uploadStream', async (req, res) => {
3017
+ expect(req.stream).toBeDefined();
3018
+ const chunks: any[] = [];
3019
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3020
+ for await (const chunk of req.stream as any) {
3021
+ chunks.push(chunk);
3022
+ }
3023
+ res.send({ chunks });
3024
+ });
3025
+
3026
+ const stream = new IframeWritableStream({
3027
+ iterator: async function* () {
3028
+ yield 'c1';
3029
+ yield 'c2';
3030
+ yield 'c3';
3031
+ }
3032
+ });
3033
+
3034
+ const resp = await client.sendStream<any>('uploadStream', stream);
3035
+ expect((resp as any).data.chunks).toEqual(['c1', 'c2', 'c3']);
3036
+
3037
+ client.destroy();
3038
+ server.destroy();
3039
+ cleanupIframe(iframe);
3040
+ });
3041
+
3042
+ it('should support client.sendFile with autoResolve (server receives File/Blob in req.body)', async () => {
3043
+ const origin = 'https://example.com';
3044
+ const iframe = createTestIframe(origin);
3045
+
3046
+ const mockContentWindow: any = {
3047
+ postMessage: jest.fn((msg: PostMessageData) => {
3048
+ window.dispatchEvent(
3049
+ new MessageEvent('message', {
3050
+ data: msg,
3051
+ origin,
3052
+ source: mockContentWindow as any
3053
+ })
3054
+ );
3055
+ })
3056
+ };
3057
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
3058
+
3059
+ const client = requestIframeClient(iframe);
3060
+ const server = requestIframeServer();
3061
+
3062
+ server.on('uploadFileStream', async (req, res) => {
3063
+ // autoResolve: server should get File/Blob directly
3064
+ expect(req.body).toBeDefined();
3065
+ expect(req.stream).toBeUndefined();
3066
+ const blob = req.body as Blob;
3067
+ const text = await blobToText(blob);
3068
+ res.send({ ok: true, text });
3069
+ });
3070
+
3071
+ const blob = new Blob(['Hello Upload Stream'], { type: 'text/plain' });
3072
+ const resp = await client.sendFile<any>('uploadFileStream', blob, {
3073
+ autoResolve: true,
3074
+ mimeType: 'text/plain',
3075
+ fileName: 'upload.txt'
3076
+ });
3077
+ expect((resp as any).data.ok).toBe(true);
3078
+ expect((resp as any).data.text).toBe('Hello Upload Stream');
3079
+
3080
+ client.destroy();
3081
+ server.destroy();
3082
+ cleanupIframe(iframe);
3083
+ });
3084
+ });
3085
+
2136
3086
  describe('Stream response', () => {
2137
3087
  it('should support sendStream', async () => {
2138
3088
  const origin = 'https://example.com';