quickwin 1.3.0 → 2.0.0

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 (143) hide show
  1. package/README.en.md +1 -2
  2. package/README.md +19 -23
  3. package/examples/PdfCanvas.js +8 -37
  4. package/examples/PdfCanvas.tsx +11 -41
  5. package/examples/exec_server.js +20054 -1423
  6. package/examples/exec_server.tsx +489 -0
  7. package/examples/exec_server_worker.js +383 -33
  8. package/examples/exec_server_worker.ts +408 -30
  9. package/examples/pdf_preview2.js +25 -82
  10. package/examples/pdf_preview2.ts +27 -84
  11. package/examples/pdf_viewer.js +1318 -1178
  12. package/examples/pdf_viewer.tsx +16 -67
  13. package/examples/setres.js +22 -47
  14. package/examples/setres.ts +23 -57
  15. package/examples/test_gallery.js +1313 -1074
  16. package/examples/test_react_lazy.js +1284 -1056
  17. package/examples/test_react_listbox.js +1283 -1055
  18. package/examples/test_root_window.js +1279 -1051
  19. package/examples/test_tab.js +1284 -1056
  20. package/lib/abort.js +70 -0
  21. package/lib/abort.ts +69 -0
  22. package/lib/certs.js +214 -0
  23. package/lib/certs.ts +224 -0
  24. package/lib/exec-log.js +15 -0
  25. package/lib/exec-log.ts +30 -0
  26. package/lib/fetch.js +37 -4
  27. package/lib/fetch.ts +35 -4
  28. package/lib/ffi/bind.js +352 -0
  29. package/lib/ffi/bind.ts +442 -0
  30. package/lib/ffi/ctype.js +168 -0
  31. package/lib/ffi/ctype.ts +331 -0
  32. package/lib/ffi/index.js +6 -0
  33. package/lib/ffi/index.ts +6 -0
  34. package/lib/ffi/struct.js +412 -0
  35. package/lib/ffi/struct.ts +664 -0
  36. package/lib/http-common.js +43 -5
  37. package/lib/http-common.ts +48 -6
  38. package/lib/http-server.js +187 -29
  39. package/lib/http-server.ts +177 -26
  40. package/lib/react-qw/components/DateTimePicker.js +18 -37
  41. package/lib/react-qw/components/Input.js +3 -10
  42. package/lib/react-qw/components/Link.js +6 -19
  43. package/lib/react-qw/components/ListView.js +49 -154
  44. package/lib/react-qw/components/PathPicker.js +28 -88
  45. package/lib/react-qw/components/ScrollView.js +15 -8
  46. package/lib/react-qw/components/Slider.js +2 -1
  47. package/lib/react-qw/components/Tab.js +6 -22
  48. package/lib/react-qw/components/Tooltip.js +10 -21
  49. package/lib/react-qw/components/TreeView.js +8 -31
  50. package/lib/react-qw/index.js +4 -2
  51. package/lib/react-qw/index.ts +4 -2
  52. package/lib/react-qw/nmhdr.js +3 -24
  53. package/lib/react-qw/nmhdr.ts +5 -30
  54. package/lib/react-qw/props.js +7 -2
  55. package/lib/react-qw/props.ts +7 -3
  56. package/lib/react-qw/reconciler.js +23 -13
  57. package/lib/react-qw/reconciler.ts +22 -13
  58. package/lib/text-measure.js +18 -23
  59. package/lib/text-measure.ts +20 -25
  60. package/lib/websocket.js +12 -2
  61. package/lib/websocket.ts +15 -2
  62. package/lib/windows/comctl32.js +18 -0
  63. package/lib/windows/comctl32.ts +20 -0
  64. package/lib/windows/comdlg32.js +7 -0
  65. package/lib/windows/comdlg32.ts +9 -0
  66. package/lib/windows/gdi32.js +90 -0
  67. package/lib/windows/gdi32.ts +97 -0
  68. package/lib/windows/kernel32.js +66 -0
  69. package/lib/windows/kernel32.ts +76 -0
  70. package/lib/windows/ntdll.js +8 -0
  71. package/lib/windows/ntdll.ts +10 -0
  72. package/lib/windows/ole32.js +7 -0
  73. package/lib/windows/ole32.ts +9 -0
  74. package/lib/windows/shell32.js +11 -0
  75. package/lib/windows/shell32.ts +14 -0
  76. package/lib/windows/structs.js +384 -0
  77. package/lib/windows/structs.ts +422 -0
  78. package/lib/windows/user32.js +469 -0
  79. package/lib/windows/user32.ts +502 -0
  80. package/package.json +1 -1
  81. package/quickwin.d.ts +22 -37
  82. package/quickwin_const.d.ts +1 -0
  83. package/qwin-nowasm-x86.exe +0 -0
  84. package/qwin-nowasm.exe +0 -0
  85. package/qwin-x86.exe +0 -0
  86. package/qwin.exe +0 -0
  87. package/test/bench_ffi_callraw.js +236 -0
  88. package/test/bench_ffi_callraw.ts +236 -0
  89. package/test/bench_ffi_overhead.js +139 -0
  90. package/test/bench_ffi_overhead.ts +141 -0
  91. package/test/bench_sendmessage.js +104 -0
  92. package/test/bench_sendmessage.ts +100 -0
  93. package/test/bench_sendmessage_nonnull.js +187 -0
  94. package/test/bench_sendmessage_nonnull.ts +188 -0
  95. package/test/run.js +12 -4
  96. package/test/run.ts +13 -5
  97. package/test/test_basic_fetch.js +8 -0
  98. package/test/test_basic_fetch.ts +9 -0
  99. package/test/test_ffi.js +25 -43
  100. package/test/test_ffi.ts +33 -52
  101. package/test/test_ffi_abi.js +235 -0
  102. package/test/test_ffi_abi.ts +218 -0
  103. package/test/test_ffi_bind.js +538 -8
  104. package/test/test_ffi_bind.ts +559 -8
  105. package/test/test_ffi_struct.js +632 -88
  106. package/test/test_ffi_struct.ts +666 -91
  107. package/test/test_ffi_struct_layout.js +495 -0
  108. package/test/test_ffi_struct_layout.ts +543 -0
  109. package/test/test_ffi_user32.js +103 -0
  110. package/test/test_ffi_user32.ts +113 -0
  111. package/test/test_ffi_windows.js +101 -0
  112. package/test/test_ffi_windows.ts +113 -0
  113. package/test/test_helper.js +8 -1
  114. package/test/test_helper.ts +9 -1
  115. package/test/test_http_import.js +18 -0
  116. package/test/test_http_import.ts +19 -0
  117. package/test/test_http_server.js +73 -1
  118. package/test/test_http_server.ts +75 -1
  119. package/test/test_ipv6.js +2 -2
  120. package/test/test_ipv6.ts +2 -2
  121. package/test/test_net_fetch.js +70 -8
  122. package/test/test_net_fetch.ts +69 -7
  123. package/test/test_net_websocket.js +1 -1
  124. package/test/test_net_websocket.ts +1 -1
  125. package/test/test_react_complex.js +1294 -1066
  126. package/test/test_react_counter.js +1284 -1056
  127. package/test/test_react_flex.js +1304 -1069
  128. package/test/test_react_hidden.js +1287 -1059
  129. package/test/test_react_listview_link.js +1284 -1056
  130. package/test/test_react_notify.js +18534 -0
  131. package/test/test_react_render.js +1284 -1056
  132. package/test/test_wolfssl_read.js +3 -2
  133. package/test/test_wolfssl_read.ts +3 -1
  134. package/test/test_worker_net.js +35 -17
  135. package/test/test_worker_net.ts +27 -15
  136. package/tsconfig.json +1 -1
  137. package/examples/exec_server.ts +0 -80
  138. package/lib/ffi-bind.js +0 -85
  139. package/lib/ffi-bind.ts +0 -170
  140. package/lib/ffi-struct.js +0 -224
  141. package/lib/ffi-struct.ts +0 -239
  142. package/lib/win-common-structs.js +0 -39
  143. package/lib/win-common-structs.ts +0 -41
@@ -4,6 +4,7 @@ import * as os from 'os'
4
4
  import * as sock from 'sock'
5
5
  import { Tester } from './test_helper.js'
6
6
  import { createServer } from '../lib/http-server.js'
7
+ import '../lib/fetch.js'
7
8
 
8
9
  // ── Minimal raw HTTP client for testing ──
9
10
 
@@ -69,6 +70,37 @@ export const suite = {
69
70
  return new Response(req.method + ':' + await req.text(), { headers: { 'Content-Type': 'text/plain' } })
70
71
  } else if (path === '/json') {
71
72
  return new Response(JSON.stringify({ method: req.method, q: Object.fromEntries(url.searchParams) }), { headers: { 'Content-Type': 'application/json' } })
73
+ } else if (path === '/stream') {
74
+ // length unknown: server must send chunked frames
75
+ return new Response(new ReadableStream({
76
+ start(control) {
77
+ control.enqueue(new TextEncoder().encode('chunkA/'))
78
+ control.enqueue(new TextEncoder().encode('chunkB'))
79
+ control.close()
80
+ }
81
+ }), { headers: { 'Content-Type': 'text/plain' } })
82
+ } else if (path === '/file') {
83
+ // length known: server streams raw bytes with Content-Length
84
+ return new Response(new ReadableStream({
85
+ start(control) {
86
+ control.enqueue(new TextEncoder().encode('chunkA/'))
87
+ control.enqueue(new TextEncoder().encode('chunkB'))
88
+ control.close()
89
+ }
90
+ }), { headers: { 'Content-Type': 'text/plain', 'Content-Length': '13' } })
91
+ } else if (path === '/stream-trailer') {
92
+ // chunked stream with trailing headers (Node addTrailers
93
+ // semantics: `Trailer` declaration + frames, then the footer)
94
+ return new Response(new ReadableStream({
95
+ start(control) {
96
+ control.enqueue(new TextEncoder().encode('chunkA/'))
97
+ control.enqueue(new TextEncoder().encode('chunkB'))
98
+ control.close()
99
+ }
100
+ }), {
101
+ headers: { 'Content-Type': 'text/plain', 'Trailer': 'X-Exit-Code' },
102
+ trailers: Promise.resolve({ 'X-Exit-Code': '3' })
103
+ })
72
104
  } else {
73
105
  return new Response('not found', { status: 404 })
74
106
  }
@@ -87,6 +119,48 @@ export const suite = {
87
119
  t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0)
88
120
  t.checkTrue('body = hello world', r.endsWith('hello world'))
89
121
  t.checkTrue('content-type text', r.toLowerCase().indexOf('content-type: text/plain') >= 0)
122
+ t.checkTrue('static body gets content-length', r.toLowerCase().indexOf('content-length: 11') >= 0)
123
+
124
+ // ── streaming chunked response (length unknown) ──
125
+ t.section('streaming chunked response')
126
+ r = await httpSend('127.0.0.1', port, 'GET /stream HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
127
+ t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0)
128
+ t.checkTrue('transfer-encoding chunked', r.toLowerCase().indexOf('transfer-encoding: chunked') >= 0)
129
+ t.checkTrue('no content-length', r.toLowerCase().indexOf('content-length:') < 0)
130
+ t.checkTrue('chunk frames + terminator', r.indexOf('7\r\nchunkA/\r\n6\r\nchunkB\r\n0\r\n\r\n') >= 0)
131
+
132
+ // ── streaming raw response with known length ──
133
+ t.section('streaming known-length response')
134
+ r = await httpSend('127.0.0.1', port, 'GET /file HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
135
+ t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0)
136
+ t.checkTrue('content-length 13', r.toLowerCase().indexOf('content-length: 13') >= 0)
137
+ t.checkTrue('no chunked encoding', r.toLowerCase().indexOf('transfer-encoding: chunked') < 0)
138
+ t.checkTrue('streamed raw body', r.includes('chunkA/chunkB'))
139
+ t.checkTrue('no chunk frame headers', r.indexOf('7\r\nchunkA/') < 0)
140
+
141
+ // ── HEAD of a streamed body: headers only, empty body ──
142
+ t.section('HEAD streaming response')
143
+ r = await httpSend('127.0.0.1', port, 'HEAD /stream HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
144
+ t.checkTrue('HEAD 200', r.indexOf('HTTP/1.1 200') === 0)
145
+ t.checkTrue('HEAD content-length 13', r.toLowerCase().indexOf('content-length: 13') >= 0)
146
+ const headBodyIdx = r.indexOf('\r\n\r\n')
147
+ t.checkTrue('HEAD empty body', r.slice(headBodyIdx + 4).length === 0)
148
+
149
+ // ── streaming chunked response with trailing headers ──
150
+ t.section('streaming trailer response')
151
+ r = await httpSend('127.0.0.1', port, 'GET /stream-trailer HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
152
+ t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0)
153
+ t.checkTrue('trailer declared in head', r.toLowerCase().indexOf('trailer: x-exit-code') >= 0)
154
+ t.checkTrue('terminator carries trailer', r.indexOf('0\r\nX-Exit-Code: 3\r\n\r\n') >= 0)
155
+ t.checkTrue('trailer footer after body', r.indexOf('7\r\nchunkA/\r\n6\r\nchunkB\r\n0\r\nX-Exit-Code: 3\r\n\r\n') >= 0)
156
+
157
+ // ── fetch client consumes chunked response with trailer footer ──
158
+ t.section('fetch consumes trailer response')
159
+ {
160
+ const rr = await fetch(`http://127.0.0.1:${port}/stream-trailer`)
161
+ t.checkTrue('status 200', rr.status === 200)
162
+ t.checkTrue('body decoded without trailer', await rr.text() === 'chunkA/chunkB')
163
+ }
90
164
 
91
165
  // ── POST with body ──
92
166
  t.section('POST with body')
@@ -124,7 +198,7 @@ export const suite = {
124
198
 
125
199
  // ── server received expected requests ──
126
200
  t.section('request log')
127
- t.check('total requests', 7, requests.length)
201
+ t.check('total requests', 12, requests.length)
128
202
  t.checkTrue('saw GET /', requests.indexOf('GET /') >= 0)
129
203
  t.checkTrue('saw POST /echo', requests.indexOf('POST /echo') >= 0)
130
204
 
package/test/test_ipv6.js CHANGED
@@ -79,7 +79,7 @@ export const suite = {
79
79
  // ── 4. fetch HTTPS IPv6 ──
80
80
  t.section('fetch https://[::1]');
81
81
  {
82
- const r = await fetch(`https://[::1]:${HTTPS_PORT}/`, { timeout: 5000 });
82
+ const r = await fetch(`https://[::1]:${HTTPS_PORT}/`, { timeout: 5000, rejectUnauthorized: false });
83
83
  assert('fetch IPv6 HTTPS status 200', r.status === 200);
84
84
  const body = await r.text();
85
85
  assert('fetch IPv6 HTTPS body', body === 'hello from test server');
@@ -110,7 +110,7 @@ export const suite = {
110
110
  // ── 6. WebSocket WSS IPv6 ──
111
111
  t.section('wss://[::1] echo');
112
112
  await new Promise((resolve, reject) => {
113
- const ws = new WebSocket(`wss://[::1]:${HTTPS_PORT}/`);
113
+ const ws = new WebSocket(`wss://[::1]:${HTTPS_PORT}/`, { rejectUnauthorized: false });
114
114
  const timeoutId = os.setTimeout(() => reject(new Error('WSS IPv6 timeout')), 10000);
115
115
  ws.onopen = () => {
116
116
  t.ok++;
package/test/test_ipv6.ts CHANGED
@@ -70,7 +70,7 @@ export const suite = {
70
70
  // ── 4. fetch HTTPS IPv6 ──
71
71
  t.section('fetch https://[::1]')
72
72
  {
73
- const r = await fetch(`https://[::1]:${HTTPS_PORT}/`, { timeout: 5000 })
73
+ const r = await fetch(`https://[::1]:${HTTPS_PORT}/`, { timeout: 5000, rejectUnauthorized: false })
74
74
  assert('fetch IPv6 HTTPS status 200', r.status === 200)
75
75
  const body = await r.text()
76
76
  assert('fetch IPv6 HTTPS body', body === 'hello from test server')
@@ -104,7 +104,7 @@ export const suite = {
104
104
  // ── 6. WebSocket WSS IPv6 ──
105
105
  t.section('wss://[::1] echo')
106
106
  await new Promise<void>((resolve, reject) => {
107
- const ws = new WebSocket(`wss://[::1]:${HTTPS_PORT}/`)
107
+ const ws = new WebSocket(`wss://[::1]:${HTTPS_PORT}/`, { rejectUnauthorized: false })
108
108
  const timeoutId = os.setTimeout(() => reject(new Error('WSS IPv6 timeout')), 10000)
109
109
 
110
110
  ws.onopen = () => {
@@ -14,13 +14,24 @@ export const suite = {
14
14
  std.printf(' FAIL: %s\n', name);
15
15
  }
16
16
  }
17
+ // Retry: public-site reachability (baidu/jsdelivr/esm.sh) depends on
18
+ // CI network conditions, and a single dropped connection must not be
19
+ // reported as a certificate regression. The last error is printed so a
20
+ // real failure is still diagnosable.
17
21
  async function safeFetch(url, init) {
18
- try {
19
- return await fetch(url, init);
20
- }
21
- catch {
22
- return null;
22
+ let last = 'unknown';
23
+ for (let attempt = 0; attempt < 3; attempt++) {
24
+ try {
25
+ return await fetch(url, init);
26
+ }
27
+ catch (e) {
28
+ last = String(e?.message || e);
29
+ if (attempt < 2)
30
+ os.sleep(500);
31
+ }
23
32
  }
33
+ std.printf(' fetch %s failed after 3 attempts: %s\n', String(url), last);
34
+ return null;
24
35
  }
25
36
  const BASE = 'http://localhost:18923';
26
37
  // ── Plain HTTP GET (regression: slot 0 + !s bug) ──
@@ -247,7 +258,7 @@ export const suite = {
247
258
  }
248
259
  // ── HTTPS fetch ──
249
260
  t.section('HTTPS fetch');
250
- const r11 = await safeFetch('https://localhost:18924/', { timeout: 5000 });
261
+ const r11 = await safeFetch('https://localhost:18924/', { timeout: 5000, rejectUnauthorized: false });
251
262
  if (r11) {
252
263
  assert('https status 200', r11.status === 200);
253
264
  const body = await r11.text();
@@ -256,6 +267,57 @@ export const suite = {
256
267
  else {
257
268
  assert('https endpoint reachable', false);
258
269
  }
270
+ // ── GlobalSign R3 root chain (regression: ASN_SIG_CONFIRM_E -155) ──
271
+ // baidu.com and jsdelivr terminate in GlobalSign's 2018 re-keyed R3,
272
+ // which reuses the self-signed R3's subject-key-identifier with a
273
+ // different public key. Shipping the self-signed R3 in lib/certs.ts
274
+ // made wolfSSL match the wrong key and fail the handshake with -155.
275
+ // Both are public CDNs: a 500/429 or a dropped connection then counts
276
+ // as a retried attempt, not an immediate failure — only a persistent
277
+ // failure (cert regression or long-lived outage) should fail CI.
278
+ t.section('GlobalSign R3 root chain');
279
+ {
280
+ async function fetchR3(url) {
281
+ const init = { timeout: 10000 };
282
+ for (let attempt = 0; attempt < 3; attempt++) {
283
+ try {
284
+ const r = await fetch(url, init);
285
+ const body = await r.text();
286
+ if (r.status === 200 && body.length > 0)
287
+ return body;
288
+ std.printf(' fetch %s status=%s len=%d (attempt %d)\n', String(url), String(r.status), body.length, attempt);
289
+ }
290
+ catch (e) {
291
+ std.printf(' fetch %s error: %s (attempt %d)\n', String(url), String(e?.message || e), attempt);
292
+ }
293
+ os.sleep(1000);
294
+ }
295
+ return null;
296
+ }
297
+ const bb = await fetchR3('https://www.baidu.com/');
298
+ if (bb) {
299
+ assert('baidu (GlobalSign R3) status', true);
300
+ assert('baidu (GlobalSign R3) body non-empty', bb.length > 0);
301
+ }
302
+ else {
303
+ assert('baidu (GlobalSign R3) reachable', false);
304
+ }
305
+ const JSDELIVR_HOSTS = ['cdn.jsdelivr.net', 'fastly.jsdelivr.net', 'gcore.jsdelivr.net'];
306
+ for (const host of JSDELIVR_HOSTS) {
307
+ const bj = await fetchR3('https://' + host + '/npm/left-pad@1.3.0');
308
+ if (bj) {
309
+ assert('jsdelivr (GlobalSign R3) status', true);
310
+ assert('jsdelivr (GlobalSign R3) body non-empty', bj.length > 0);
311
+ break;
312
+ }
313
+ if (host !== JSDELIVR_HOSTS[JSDELIVR_HOSTS.length - 1]) {
314
+ std.printf(' jsdelivr host %s failed, trying next\n', host);
315
+ }
316
+ else {
317
+ assert('jsdelivr (GlobalSign R3) reachable', false);
318
+ }
319
+ }
320
+ }
259
321
  // ── large HTTPS body (wolfSSL_read null regression) ──
260
322
  // Pre-fix: wolfSSL_read returned number -1 on WANT_READ →
261
323
  // receivedBytes became NaN → stream never closed → hang.
@@ -264,7 +326,7 @@ export const suite = {
264
326
  const LARGE = 200000;
265
327
  try {
266
328
  const r = await Promise.race([
267
- fetch(`https://localhost:18924/large/${LARGE}`, { timeout: 15000 }),
329
+ fetch(`https://localhost:18924/large/${LARGE}`, { timeout: 15000, rejectUnauthorized: false }),
268
330
  new Promise((_, rej) => os.setTimeout(() => rej(new Error('Body hang watchdog 15s')), 15000))
269
331
  ]);
270
332
  assert('large HTTPS status 200', r.status === 200);
@@ -304,7 +366,7 @@ export const suite = {
304
366
  let rejected = false;
305
367
  let msg = '';
306
368
  try {
307
- const r = await fetch('https://localhost:18924/stall', { timeout: 2000 });
369
+ const r = await fetch('https://localhost:18924/stall', { timeout: 2000, rejectUnauthorized: false });
308
370
  await Promise.race([
309
371
  r.arrayBuffer(),
310
372
  new Promise((_, rej) => os.setTimeout(() => rej(new Error('Body timeout')), 8000))
@@ -11,12 +11,22 @@ export const suite = {
11
11
  else { t.fail++; std.printf(' FAIL: %s\n', name) }
12
12
  }
13
13
 
14
+ // Retry: public-site reachability (baidu/jsdelivr/esm.sh) depends on
15
+ // CI network conditions, and a single dropped connection must not be
16
+ // reported as a certificate regression. The last error is printed so a
17
+ // real failure is still diagnosable.
14
18
  async function safeFetch(url: any, init?: RequestInit) {
15
- try {
16
- return await fetch(url, init);
17
- } catch {
18
- return null;
19
+ let last = 'unknown'
20
+ for (let attempt = 0; attempt < 3; attempt++) {
21
+ try {
22
+ return await fetch(url, init)
23
+ } catch (e) {
24
+ last = String((e as Error)?.message || e)
25
+ if (attempt < 2) os.sleep(500)
26
+ }
19
27
  }
28
+ std.printf(' fetch %s failed after 3 attempts: %s\n', String(url), last)
29
+ return null
20
30
  }
21
31
  const BASE = 'http://localhost:18923'
22
32
 
@@ -227,7 +237,7 @@ export const suite = {
227
237
 
228
238
  // ── HTTPS fetch ──
229
239
  t.section('HTTPS fetch')
230
- const r11 = await safeFetch('https://localhost:18924/', { timeout: 5000 })
240
+ const r11 = await safeFetch('https://localhost:18924/', { timeout: 5000, rejectUnauthorized: false })
231
241
  if (r11) {
232
242
  assert('https status 200', r11.status === 200)
233
243
  const body = await r11.text()
@@ -236,6 +246,58 @@ export const suite = {
236
246
  assert('https endpoint reachable', false)
237
247
  }
238
248
 
249
+ // ── GlobalSign R3 root chain (regression: ASN_SIG_CONFIRM_E -155) ──
250
+ // baidu.com and jsdelivr terminate in GlobalSign's 2018 re-keyed R3,
251
+ // which reuses the self-signed R3's subject-key-identifier with a
252
+ // different public key. Shipping the self-signed R3 in lib/certs.ts
253
+ // made wolfSSL match the wrong key and fail the handshake with -155.
254
+ // Both are public CDNs: a 500/429 or a dropped connection then counts
255
+ // as a retried attempt, not an immediate failure — only a persistent
256
+ // failure (cert regression or long-lived outage) should fail CI.
257
+ t.section('GlobalSign R3 root chain')
258
+ {
259
+ async function fetchR3(url: any): Promise<string | null> {
260
+ const init = { timeout: 10000 }
261
+ for (let attempt = 0; attempt < 3; attempt++) {
262
+ try {
263
+ const r = await fetch(url, init)
264
+ const body = await r.text()
265
+ if (r.status === 200 && body.length > 0) return body
266
+ std.printf(' fetch %s status=%s len=%d (attempt %d)\n',
267
+ String(url), String(r.status), body.length, attempt)
268
+ } catch (e) {
269
+ std.printf(' fetch %s error: %s (attempt %d)\n',
270
+ String(url), String((e as Error)?.message || e), attempt)
271
+ }
272
+ os.sleep(1000)
273
+ }
274
+ return null
275
+ }
276
+
277
+ const bb = await fetchR3('https://www.baidu.com/')
278
+ if (bb) {
279
+ assert('baidu (GlobalSign R3) status', true)
280
+ assert('baidu (GlobalSign R3) body non-empty', bb.length > 0)
281
+ } else {
282
+ assert('baidu (GlobalSign R3) reachable', false)
283
+ }
284
+
285
+ const JSDELIVR_HOSTS = ['cdn.jsdelivr.net', 'fastly.jsdelivr.net', 'gcore.jsdelivr.net']
286
+ for (const host of JSDELIVR_HOSTS) {
287
+ const bj = await fetchR3('https://' + host + '/npm/left-pad@1.3.0')
288
+ if (bj) {
289
+ assert('jsdelivr (GlobalSign R3) status', true)
290
+ assert('jsdelivr (GlobalSign R3) body non-empty', bj.length > 0)
291
+ break
292
+ }
293
+ if (host !== JSDELIVR_HOSTS[JSDELIVR_HOSTS.length - 1]) {
294
+ std.printf(' jsdelivr host %s failed, trying next\n', host)
295
+ } else {
296
+ assert('jsdelivr (GlobalSign R3) reachable', false)
297
+ }
298
+ }
299
+ }
300
+
239
301
  // ── large HTTPS body (wolfSSL_read null regression) ──
240
302
  // Pre-fix: wolfSSL_read returned number -1 on WANT_READ →
241
303
  // receivedBytes became NaN → stream never closed → hang.
@@ -244,7 +306,7 @@ export const suite = {
244
306
  const LARGE = 200000
245
307
  try {
246
308
  const r = await Promise.race([
247
- fetch(`https://localhost:18924/large/${LARGE}`, { timeout: 15000 }),
309
+ fetch(`https://localhost:18924/large/${LARGE}`, { timeout: 15000, rejectUnauthorized: false }),
248
310
  new Promise<never>((_, rej) =>
249
311
  os.setTimeout(() => rej(new Error('Body hang watchdog 15s')), 15000)
250
312
  )
@@ -292,7 +354,7 @@ export const suite = {
292
354
  let rejected = false
293
355
  let msg = ''
294
356
  try {
295
- const r = await fetch('https://localhost:18924/stall', { timeout: 2000 })
357
+ const r = await fetch('https://localhost:18924/stall', { timeout: 2000, rejectUnauthorized: false })
296
358
  await Promise.race([
297
359
  r.arrayBuffer(),
298
360
  new Promise<never>((_, rej) =>
@@ -158,7 +158,7 @@ export const suite = {
158
158
  t.section('wss basic text message');
159
159
  const p7 = waitForTest();
160
160
  let opened7 = false, received7 = false, closed7 = false;
161
- const ws7 = new WebSocket("wss://localhost:18924");
161
+ const ws7 = new WebSocket("wss://localhost:18924", { rejectUnauthorized: false });
162
162
  ws7.onopen = () => { opened7 = true; ws7.send("Hello WSS!"); };
163
163
  ws7.onmessage = () => { received7 = true; ws7.close(1000, "done"); };
164
164
  ws7.onclose = (e) => {
@@ -145,7 +145,7 @@ export const suite = {
145
145
  t.section('wss basic text message')
146
146
  const p7 = waitForTest()
147
147
  let opened7 = false, received7 = false, closed7 = false
148
- const ws7 = new WebSocket("wss://localhost:18924")
148
+ const ws7 = new WebSocket("wss://localhost:18924", { rejectUnauthorized: false })
149
149
  ws7.onopen = () => { opened7 = true; ws7.send("Hello WSS!") }
150
150
  ws7.onmessage = () => { received7 = true; ws7.close(1000, "done") }
151
151
  ws7.onclose = (e) => {