@durable-streams/server-conformance-tests 0.1.4 → 0.1.6

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.
@@ -59,7 +59,7 @@ async function fetchSSE(url, opts = {}) {
59
59
  */
60
60
  function parseSSEEvents(sseText) {
61
61
  const events = [];
62
- const normalized = sseText.replace(/\r\n/g, `\n`);
62
+ const normalized = sseText.replace(/\r\n/g, `\n`).replace(/\r/g, `\n`);
63
63
  const eventBlocks = normalized.split(`\n\n`).filter((block) => block.trim());
64
64
  for (const block of eventBlocks) {
65
65
  const lines = block.split(`\n`);
@@ -82,6 +82,7 @@ function parseSSEEvents(sseText) {
82
82
  */
83
83
  function runConformanceTests(options) {
84
84
  const getBaseUrl = () => options.baseUrl;
85
+ const getLongPollTestTimeoutMs = () => (options.longPollTimeoutMs ?? 2e4) + 1e3;
85
86
  describe(`Basic Stream Operations`, () => {
86
87
  test(`should create a stream`, async () => {
87
88
  const streamPath = `/v1/stream/create-test-${Date.now()}`;
@@ -250,13 +251,14 @@ function runConformanceTests(options) {
250
251
  const readPromise = (async () => {
251
252
  const res = await stream.stream({ live: `long-poll` });
252
253
  await new Promise((resolve) => {
253
- const unsubscribe = res.subscribeBytes(async (chunk) => {
254
+ const unsubscribe = res.subscribeBytes((chunk) => {
254
255
  if (chunk.data.length > 0) receivedData.push(new TextDecoder().decode(chunk.data));
255
256
  if (receivedData.length >= 1) {
256
257
  unsubscribe();
257
258
  res.cancel();
258
259
  resolve();
259
260
  }
261
+ return Promise.resolve();
260
262
  });
261
263
  });
262
264
  })();
@@ -264,7 +266,7 @@ function runConformanceTests(options) {
264
266
  await stream.append(`new data`);
265
267
  await readPromise;
266
268
  expect(receivedData).toContain(`new data`);
267
- }, 1e4);
269
+ }, getLongPollTestTimeoutMs());
268
270
  test(`should return immediately if data already exists`, async () => {
269
271
  const streamPath = `/v1/stream/longpoll-immediate-test-${Date.now()}`;
270
272
  const stream = await DurableStream.create({
@@ -299,7 +301,7 @@ function runConformanceTests(options) {
299
301
  method: `PUT`,
300
302
  headers: { "Content-Type": `text/plain` }
301
303
  });
302
- expect([200, 204]).toContain(secondResponse.status);
304
+ expect(secondResponse.status).toBe(200);
303
305
  });
304
306
  test(`should return 409 on PUT with different config`, async () => {
305
307
  const streamPath = `/v1/stream/config-conflict-test-${Date.now()}`;
@@ -324,7 +326,7 @@ function runConformanceTests(options) {
324
326
  headers: { "Content-Type": `text/plain` },
325
327
  body: `hello world`
326
328
  });
327
- expect([200, 204]).toContain(response.status);
329
+ expect(response.status).toBe(204);
328
330
  expect(response.headers.get(STREAM_OFFSET_HEADER)).toBeDefined();
329
331
  });
330
332
  test(`should return 404 on POST to non-existent stream`, async () => {
@@ -507,7 +509,7 @@ function runConformanceTests(options) {
507
509
  },
508
510
  body: `second`
509
511
  });
510
- expect([200, 204]).toContain(response.status);
512
+ expect(response.status).toBe(204);
511
513
  });
512
514
  test(`should reject duplicate seq values`, async () => {
513
515
  const streamPath = `/v1/stream/seq-duplicate-test-${Date.now()}`;
@@ -534,6 +536,140 @@ function runConformanceTests(options) {
534
536
  expect(response.status).toBe(409);
535
537
  });
536
538
  });
539
+ describe(`Browser Security Headers`, () => {
540
+ test(`should include X-Content-Type-Options: nosniff on GET responses`, async () => {
541
+ const streamPath = `/v1/stream/security-get-nosniff-${Date.now()}`;
542
+ await fetch(`${getBaseUrl()}${streamPath}`, {
543
+ method: `PUT`,
544
+ headers: { "Content-Type": `text/plain` },
545
+ body: `test data`
546
+ });
547
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
548
+ expect(response.status).toBe(200);
549
+ expect(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
550
+ });
551
+ test(`should include X-Content-Type-Options: nosniff on PUT responses`, async () => {
552
+ const streamPath = `/v1/stream/security-put-nosniff-${Date.now()}`;
553
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, {
554
+ method: `PUT`,
555
+ headers: { "Content-Type": `text/plain` }
556
+ });
557
+ expect(response.status).toBe(201);
558
+ expect(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
559
+ });
560
+ test(`should include X-Content-Type-Options: nosniff on POST responses`, async () => {
561
+ const streamPath = `/v1/stream/security-post-nosniff-${Date.now()}`;
562
+ await fetch(`${getBaseUrl()}${streamPath}`, {
563
+ method: `PUT`,
564
+ headers: { "Content-Type": `text/plain` }
565
+ });
566
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, {
567
+ method: `POST`,
568
+ headers: { "Content-Type": `text/plain` },
569
+ body: `data`
570
+ });
571
+ expect([200, 204]).toContain(response.status);
572
+ expect(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
573
+ });
574
+ test(`should include X-Content-Type-Options: nosniff on HEAD responses`, async () => {
575
+ const streamPath = `/v1/stream/security-head-nosniff-${Date.now()}`;
576
+ await fetch(`${getBaseUrl()}${streamPath}`, {
577
+ method: `PUT`,
578
+ headers: { "Content-Type": `text/plain` }
579
+ });
580
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
581
+ expect(response.status).toBe(200);
582
+ expect(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
583
+ });
584
+ test(`should include Cross-Origin-Resource-Policy header on GET responses`, async () => {
585
+ const streamPath = `/v1/stream/security-corp-get-${Date.now()}`;
586
+ await fetch(`${getBaseUrl()}${streamPath}`, {
587
+ method: `PUT`,
588
+ headers: { "Content-Type": `application/octet-stream` },
589
+ body: new Uint8Array([
590
+ 1,
591
+ 2,
592
+ 3,
593
+ 4
594
+ ])
595
+ });
596
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
597
+ expect(response.status).toBe(200);
598
+ const corp = response.headers.get(`cross-origin-resource-policy`);
599
+ expect(corp).toBeDefined();
600
+ expect([
601
+ `cross-origin`,
602
+ `same-origin`,
603
+ `same-site`
604
+ ]).toContain(corp);
605
+ });
606
+ test(`should include Cache-Control: no-store on HEAD responses`, async () => {
607
+ const streamPath = `/v1/stream/security-head-cache-${Date.now()}`;
608
+ await fetch(`${getBaseUrl()}${streamPath}`, {
609
+ method: `PUT`,
610
+ headers: { "Content-Type": `text/plain` }
611
+ });
612
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
613
+ expect(response.status).toBe(200);
614
+ const cacheControl = response.headers.get(`cache-control`);
615
+ expect(cacheControl).toBeDefined();
616
+ expect(cacheControl).toContain(`no-store`);
617
+ });
618
+ test(`should include X-Content-Type-Options: nosniff on SSE responses`, async () => {
619
+ const streamPath = `/v1/stream/security-sse-nosniff-${Date.now()}`;
620
+ await fetch(`${getBaseUrl()}${streamPath}`, {
621
+ method: `PUT`,
622
+ headers: { "Content-Type": `application/json` },
623
+ body: JSON.stringify({ test: `data` })
624
+ });
625
+ const headResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
626
+ const offset = headResponse.headers.get(STREAM_OFFSET_HEADER) ?? `-1`;
627
+ const controller = new AbortController();
628
+ const timeoutId = setTimeout(() => controller.abort(), 500);
629
+ try {
630
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=${offset}&live=sse`, {
631
+ method: `GET`,
632
+ signal: controller.signal
633
+ });
634
+ expect(response.status).toBe(200);
635
+ expect(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
636
+ } catch (e) {
637
+ if (!(e instanceof Error && e.name === `AbortError`)) throw e;
638
+ } finally {
639
+ clearTimeout(timeoutId);
640
+ }
641
+ });
642
+ test(`should include X-Content-Type-Options: nosniff on long-poll responses`, async () => {
643
+ const streamPath = `/v1/stream/security-longpoll-nosniff-${Date.now()}`;
644
+ await fetch(`${getBaseUrl()}${streamPath}`, {
645
+ method: `PUT`,
646
+ headers: { "Content-Type": `text/plain` },
647
+ body: `initial data`
648
+ });
649
+ const headResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
650
+ const offset = headResponse.headers.get(STREAM_OFFSET_HEADER) ?? `-1`;
651
+ const controller = new AbortController();
652
+ const timeoutId = setTimeout(() => controller.abort(), 500);
653
+ try {
654
+ const response = await fetch(`${getBaseUrl()}${streamPath}?offset=${offset}&live=long-poll`, {
655
+ method: `GET`,
656
+ signal: controller.signal
657
+ });
658
+ expect([200, 204]).toContain(response.status);
659
+ expect(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
660
+ } catch (e) {
661
+ if (!(e instanceof Error && e.name === `AbortError`)) throw e;
662
+ } finally {
663
+ clearTimeout(timeoutId);
664
+ }
665
+ });
666
+ test(`should include security headers on error responses`, async () => {
667
+ const streamPath = `/v1/stream/security-error-headers-${Date.now()}`;
668
+ const response = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
669
+ expect(response.status).toBe(404);
670
+ expect(response.headers.get(`x-content-type-options`)).toBe(`nosniff`);
671
+ });
672
+ });
537
673
  describe(`TTL and Expiry Validation`, () => {
538
674
  test(`should reject both TTL and Expires-At (400)`, async () => {
539
675
  const streamPath = `/v1/stream/ttl-expires-conflict-test-${Date.now()}`;
@@ -604,7 +740,7 @@ function runConformanceTests(options) {
604
740
  headers: { "Content-Type": `TEXT/PLAIN` },
605
741
  body: `test`
606
742
  });
607
- expect([200, 204]).toContain(response.status);
743
+ expect(response.status).toBe(204);
608
744
  });
609
745
  test(`should allow idempotent create with different case content-type`, async () => {
610
746
  const streamPath = `/v1/stream/case-idempotent-test-${Date.now()}`;
@@ -617,7 +753,7 @@ function runConformanceTests(options) {
617
753
  method: `PUT`,
618
754
  headers: { "Content-Type": `APPLICATION/JSON` }
619
755
  });
620
- expect([200, 204]).toContain(response2.status);
756
+ expect(response2.status).toBe(200);
621
757
  });
622
758
  test(`should accept headers with different casing`, async () => {
623
759
  const streamPath = `/v1/stream/case-header-test-${Date.now()}`;
@@ -633,7 +769,7 @@ function runConformanceTests(options) {
633
769
  },
634
770
  body: `test`
635
771
  });
636
- expect([200, 204]).toContain(response.status);
772
+ expect(response.status).toBe(204);
637
773
  });
638
774
  });
639
775
  describe(`Content-Type Validation`, () => {
@@ -661,7 +797,7 @@ function runConformanceTests(options) {
661
797
  headers: { "Content-Type": `application/json` },
662
798
  body: `{"test": true}`
663
799
  });
664
- expect([200, 204]).toContain(response.status);
800
+ expect(response.status).toBe(204);
665
801
  });
666
802
  test(`should return stream content-type on GET`, async () => {
667
803
  const streamPath = `/v1/stream/content-type-get-test-${Date.now()}`;
@@ -953,7 +1089,8 @@ function runConformanceTests(options) {
953
1089
  expect(response.status).toBe(201);
954
1090
  const location = response.headers.get(`location`);
955
1091
  expect(location).toBeDefined();
956
- expect(location).toBe(`${getBaseUrl()}${streamPath}`);
1092
+ expect(location.endsWith(streamPath)).toBe(true);
1093
+ expect(() => new URL(location)).not.toThrow();
957
1094
  });
958
1095
  test(`should reject missing Content-Type on POST`, async () => {
959
1096
  const streamPath = `/v1/stream/missing-ct-post-test-${Date.now()}`;
@@ -1057,7 +1194,7 @@ function runConformanceTests(options) {
1057
1194
  clearTimeout(timeoutId);
1058
1195
  if (e instanceof Error && e.name !== `AbortError`) throw e;
1059
1196
  }
1060
- }, 1e4);
1197
+ }, getLongPollTestTimeoutMs());
1061
1198
  });
1062
1199
  describe(`TTL and Expiry Edge Cases`, () => {
1063
1200
  test(`should reject TTL with leading zeros`, async () => {
@@ -1157,7 +1294,7 @@ function runConformanceTests(options) {
1157
1294
  "Stream-TTL": `3600`
1158
1295
  }
1159
1296
  });
1160
- expect([200, 204]).toContain(response2.status);
1297
+ expect(response2.status).toBe(200);
1161
1298
  });
1162
1299
  test(`should reject idempotent PUT with different TTL`, async () => {
1163
1300
  const streamPath = `/v1/stream/ttl-conflict-test-${Date.now()}`;
@@ -1261,7 +1398,7 @@ function runConformanceTests(options) {
1261
1398
  headers: { "Content-Type": `text/plain` },
1262
1399
  body: `appended data`
1263
1400
  });
1264
- expect([200, 204]).toContain(postBefore.status);
1401
+ expect(postBefore.status).toBe(204);
1265
1402
  await sleep(1500);
1266
1403
  const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1267
1404
  method: `POST`,
@@ -1321,7 +1458,7 @@ function runConformanceTests(options) {
1321
1458
  headers: { "Content-Type": `text/plain` },
1322
1459
  body: `appended data`
1323
1460
  });
1324
- expect([200, 204]).toContain(postBefore.status);
1461
+ expect(postBefore.status).toBe(204);
1325
1462
  await sleep(1500);
1326
1463
  const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1327
1464
  method: `POST`,
@@ -1744,6 +1881,88 @@ function runConformanceTests(options) {
1744
1881
  expect(received).toContain(`data: line2`);
1745
1882
  expect(received).toContain(`data: line3`);
1746
1883
  });
1884
+ test(`should prevent CRLF injection in payloads - embedded event boundaries become literal data`, async () => {
1885
+ const streamPath = `/v1/stream/sse-crlf-injection-test-${Date.now()}`;
1886
+ const maliciousPayload = `safe content\r\n\r\nevent: control\r\ndata: {"injected":true}\r\n\r\nmore safe content`;
1887
+ await fetch(`${getBaseUrl()}${streamPath}`, {
1888
+ method: `PUT`,
1889
+ headers: { "Content-Type": `text/plain` },
1890
+ body: maliciousPayload
1891
+ });
1892
+ const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=-1&live=sse`, { untilContent: `event: control` });
1893
+ expect(response.status).toBe(200);
1894
+ const events = parseSSEEvents(received);
1895
+ const dataEvents = events.filter((e) => e.type === `data`);
1896
+ const controlEvents = events.filter((e) => e.type === `control`);
1897
+ expect(dataEvents.length).toBe(1);
1898
+ expect(controlEvents.length).toBe(1);
1899
+ const dataContent = dataEvents[0].data;
1900
+ expect(dataContent).toContain(`event: control`);
1901
+ expect(dataContent).toContain(`data: {"injected":true}`);
1902
+ const controlContent = JSON.parse(controlEvents[0].data);
1903
+ expect(controlContent.injected).toBeUndefined();
1904
+ expect(controlContent.streamNextOffset).toBeDefined();
1905
+ });
1906
+ test(`should prevent CRLF injection - LF-only attack vectors`, async () => {
1907
+ const streamPath = `/v1/stream/sse-lf-injection-test-${Date.now()}`;
1908
+ const maliciousPayload = `start\n\nevent: data\ndata: fake-event\n\nend`;
1909
+ await fetch(`${getBaseUrl()}${streamPath}`, {
1910
+ method: `PUT`,
1911
+ headers: { "Content-Type": `text/plain` },
1912
+ body: maliciousPayload
1913
+ });
1914
+ const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=-1&live=sse`, { untilContent: `event: control` });
1915
+ expect(response.status).toBe(200);
1916
+ const events = parseSSEEvents(received);
1917
+ const dataEvents = events.filter((e) => e.type === `data`);
1918
+ expect(dataEvents.length).toBe(1);
1919
+ const dataContent = dataEvents[0].data;
1920
+ expect(dataContent).toContain(`event: data`);
1921
+ expect(dataContent).toContain(`data: fake-event`);
1922
+ });
1923
+ test(`should prevent CRLF injection - carriage return only attack vectors`, async () => {
1924
+ const streamPath = `/v1/stream/sse-cr-injection-test-${Date.now()}`;
1925
+ const maliciousPayload = `start\r\revent: control\rdata: {"cr_injected":true}\r\rend`;
1926
+ await fetch(`${getBaseUrl()}${streamPath}`, {
1927
+ method: `PUT`,
1928
+ headers: { "Content-Type": `text/plain` },
1929
+ body: maliciousPayload
1930
+ });
1931
+ const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=-1&live=sse`, { untilContent: `event: control` });
1932
+ expect(response.status).toBe(200);
1933
+ const events = parseSSEEvents(received);
1934
+ const controlEvents = events.filter((e) => e.type === `control`);
1935
+ expect(controlEvents.length).toBe(1);
1936
+ const controlContent = JSON.parse(controlEvents[0].data);
1937
+ expect(controlContent.cr_injected).toBeUndefined();
1938
+ expect(controlContent.streamNextOffset).toBeDefined();
1939
+ });
1940
+ test(`should handle JSON payloads with embedded newlines safely`, async () => {
1941
+ const streamPath = `/v1/stream/sse-json-newline-test-${Date.now()}`;
1942
+ const jsonPayload = JSON.stringify({
1943
+ message: `line1\nline2\nline3`,
1944
+ attack: `try\r\n\r\nevent: control\r\ndata: {"bad":true}`
1945
+ });
1946
+ await fetch(`${getBaseUrl()}${streamPath}`, {
1947
+ method: `PUT`,
1948
+ headers: { "Content-Type": `application/json` },
1949
+ body: jsonPayload
1950
+ });
1951
+ const { response, received } = await fetchSSE(`${getBaseUrl()}${streamPath}?offset=-1&live=sse`, { untilContent: `event: control` });
1952
+ expect(response.status).toBe(200);
1953
+ const events = parseSSEEvents(received);
1954
+ const dataEvents = events.filter((e) => e.type === `data`);
1955
+ const controlEvents = events.filter((e) => e.type === `control`);
1956
+ expect(dataEvents.length).toBe(1);
1957
+ expect(controlEvents.length).toBe(1);
1958
+ const parsedData = JSON.parse(dataEvents[0].data);
1959
+ expect(Array.isArray(parsedData)).toBe(true);
1960
+ expect(parsedData[0].message).toBe(`line1\nline2\nline3`);
1961
+ expect(parsedData[0].attack).toContain(`event: control`);
1962
+ const controlContent = JSON.parse(controlEvents[0].data);
1963
+ expect(controlContent.bad).toBeUndefined();
1964
+ expect(controlContent.streamNextOffset).toBeDefined();
1965
+ });
1747
1966
  test(`should generate unique, monotonically increasing offsets in SSE mode`, async () => {
1748
1967
  const streamPath = `/v1/stream/sse-monotonic-offset-test-${Date.now()}`;
1749
1968
  await fetch(`${getBaseUrl()}${streamPath}`, {
@@ -2129,7 +2348,7 @@ function runConformanceTests(options) {
2129
2348
  headers: { "Content-Type": `application/octet-stream` },
2130
2349
  body: chunk
2131
2350
  });
2132
- expect([200, 204]).toContain(response.status);
2351
+ expect(response.status).toBe(204);
2133
2352
  }
2134
2353
  const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
2135
2354
  const expected = new Uint8Array(totalLength);
@@ -2232,7 +2451,7 @@ function runConformanceTests(options) {
2232
2451
  headers: { "Content-Type": `application/octet-stream` },
2233
2452
  body: op.data
2234
2453
  });
2235
- expect([200, 204]).toContain(response.status);
2454
+ expect(response.status).toBe(204);
2236
2455
  appendedData.push(...Array.from(op.data));
2237
2456
  const offset = response.headers.get(STREAM_OFFSET_HEADER);
2238
2457
  if (offset) savedOffsets.push(offset);
@@ -2315,7 +2534,7 @@ function runConformanceTests(options) {
2315
2534
  headers: { "Content-Type": `application/octet-stream` },
2316
2535
  body: data
2317
2536
  });
2318
- expect([200, 204]).toContain(appendResponse.status);
2537
+ expect(appendResponse.status).toBe(204);
2319
2538
  const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
2320
2539
  expect(readResponse.status).toBe(200);
2321
2540
  const buffer = await readResponse.arrayBuffer();
@@ -2427,7 +2646,7 @@ function runConformanceTests(options) {
2427
2646
  },
2428
2647
  body: `data-${seq}`
2429
2648
  });
2430
- expect([200, 204]).toContain(response.status);
2649
+ expect(response.status).toBe(204);
2431
2650
  }
2432
2651
  return true;
2433
2652
  }
@@ -2451,7 +2670,7 @@ function runConformanceTests(options) {
2451
2670
  },
2452
2671
  body: `first`
2453
2672
  });
2454
- expect([200, 204]).toContain(response1.status);
2673
+ expect(response1.status).toBe(204);
2455
2674
  const response2 = await fetch(`${getBaseUrl()}${streamPath}`, {
2456
2675
  method: `POST`,
2457
2676
  headers: {
@@ -2466,6 +2685,252 @@ function runConformanceTests(options) {
2466
2685
  ), { numRuns: 25 });
2467
2686
  });
2468
2687
  });
2688
+ describe(`Concurrent Writer Stress Tests`, () => {
2689
+ test(`concurrent writers with sequence numbers - server handles gracefully`, async () => {
2690
+ const streamPath = `/v1/stream/concurrent-seq-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2691
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2692
+ method: `PUT`,
2693
+ headers: { "Content-Type": `text/plain` }
2694
+ });
2695
+ const numWriters = 5;
2696
+ const seqValue = `seq-001`;
2697
+ const writePromises = Array.from({ length: numWriters }, (_, i) => fetch(`${getBaseUrl()}${streamPath}`, {
2698
+ method: `POST`,
2699
+ headers: {
2700
+ "Content-Type": `text/plain`,
2701
+ [STREAM_SEQ_HEADER]: seqValue
2702
+ },
2703
+ body: `writer-${i}`
2704
+ }));
2705
+ const responses = await Promise.all(writePromises);
2706
+ const statuses = responses.map((r) => r.status);
2707
+ for (const status of statuses) expect([
2708
+ 200,
2709
+ 204,
2710
+ 409
2711
+ ]).toContain(status);
2712
+ const successes = statuses.filter((s) => s === 200 || s === 204);
2713
+ expect(successes.length).toBeGreaterThanOrEqual(1);
2714
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
2715
+ const content = await readResponse.text();
2716
+ const matchingWriters = Array.from({ length: numWriters }, (_, i) => content.includes(`writer-${i}`)).filter(Boolean);
2717
+ expect(matchingWriters.length).toBeGreaterThanOrEqual(1);
2718
+ });
2719
+ test(`concurrent writers racing with incrementing seq values`, async () => {
2720
+ await fc.assert(fc.asyncProperty(fc.integer({
2721
+ min: 3,
2722
+ max: 8
2723
+ }), async (numWriters) => {
2724
+ const streamPath = `/v1/stream/concurrent-race-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2725
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2726
+ method: `PUT`,
2727
+ headers: { "Content-Type": `text/plain` }
2728
+ });
2729
+ const writePromises = Array.from({ length: numWriters }, (_, i) => fetch(`${getBaseUrl()}${streamPath}`, {
2730
+ method: `POST`,
2731
+ headers: {
2732
+ "Content-Type": `text/plain`,
2733
+ [STREAM_SEQ_HEADER]: String(i).padStart(4, `0`)
2734
+ },
2735
+ body: `data-${i}`
2736
+ }));
2737
+ const responses = await Promise.all(writePromises);
2738
+ const successIndices = [];
2739
+ for (let i = 0; i < responses.length; i++) {
2740
+ expect([
2741
+ 200,
2742
+ 204,
2743
+ 409
2744
+ ]).toContain(responses[i].status);
2745
+ if (responses[i].status === 200 || responses[i].status === 204) successIndices.push(i);
2746
+ }
2747
+ expect(successIndices.length).toBeGreaterThanOrEqual(1);
2748
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
2749
+ const content = await readResponse.text();
2750
+ for (const i of successIndices) expect(content).toContain(`data-${i}`);
2751
+ return true;
2752
+ }), { numRuns: 10 });
2753
+ });
2754
+ test(`concurrent appends without seq - all data is persisted`, async () => {
2755
+ const streamPath = `/v1/stream/concurrent-no-seq-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2756
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2757
+ method: `PUT`,
2758
+ headers: { "Content-Type": `text/plain` }
2759
+ });
2760
+ const numWriters = 10;
2761
+ const writePromises = Array.from({ length: numWriters }, (_, i) => fetch(`${getBaseUrl()}${streamPath}`, {
2762
+ method: `POST`,
2763
+ headers: { "Content-Type": `text/plain` },
2764
+ body: `concurrent-${i}`
2765
+ }));
2766
+ const responses = await Promise.all(writePromises);
2767
+ for (const response of responses) expect([200, 204]).toContain(response.status);
2768
+ const offsets = responses.map((r) => r.headers.get(STREAM_OFFSET_HEADER));
2769
+ for (const offset of offsets) expect(offset).not.toBeNull();
2770
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`);
2771
+ const content = await readResponse.text();
2772
+ for (let i = 0; i < numWriters; i++) expect(content).toContain(`concurrent-${i}`);
2773
+ });
2774
+ test(`mixed readers and writers - readers see consistent state`, async () => {
2775
+ const streamPath = `/v1/stream/concurrent-rw-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2776
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2777
+ method: `PUT`,
2778
+ headers: { "Content-Type": `text/plain` }
2779
+ });
2780
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2781
+ method: `POST`,
2782
+ headers: { "Content-Type": `text/plain` },
2783
+ body: `initial`
2784
+ });
2785
+ const numOps = 20;
2786
+ const operations = Array.from({ length: numOps }, (_, i) => {
2787
+ if (i % 2 === 0) return fetch(`${getBaseUrl()}${streamPath}`, {
2788
+ method: `POST`,
2789
+ headers: { "Content-Type": `text/plain` },
2790
+ body: `write-${i}`
2791
+ });
2792
+ else return fetch(`${getBaseUrl()}${streamPath}`);
2793
+ });
2794
+ const responses = await Promise.all(operations);
2795
+ responses.forEach((response, i) => {
2796
+ const expectedStatus = i % 2 === 0 ? 204 : 200;
2797
+ expect(response.status).toBe(expectedStatus);
2798
+ });
2799
+ const finalRead = await fetch(`${getBaseUrl()}${streamPath}`);
2800
+ const content = await finalRead.text();
2801
+ expect(content).toContain(`initial`);
2802
+ for (let i = 0; i < numOps; i += 2) expect(content).toContain(`write-${i}`);
2803
+ });
2804
+ });
2805
+ describe(`State Hash Verification`, () => {
2806
+ /**
2807
+ * Simple hash function for content verification.
2808
+ * Uses FNV-1a algorithm for deterministic hashing.
2809
+ */
2810
+ function hashContent(data) {
2811
+ let hash = 2166136261;
2812
+ for (const byte of data) {
2813
+ hash ^= byte;
2814
+ hash = Math.imul(hash, 16777619);
2815
+ hash = hash >>> 0;
2816
+ }
2817
+ return hash.toString(16).padStart(8, `0`);
2818
+ }
2819
+ test(`replay produces identical content hash`, async () => {
2820
+ await fc.assert(fc.asyncProperty(
2821
+ // Generate a sequence of appends
2822
+ fc.array(fc.uint8Array({
2823
+ minLength: 1,
2824
+ maxLength: 100
2825
+ }), {
2826
+ minLength: 1,
2827
+ maxLength: 10
2828
+ }),
2829
+ async (chunks) => {
2830
+ const streamPath1 = `/v1/stream/hash-verify-1-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2831
+ await fetch(`${getBaseUrl()}${streamPath1}`, {
2832
+ method: `PUT`,
2833
+ headers: { "Content-Type": `application/octet-stream` }
2834
+ });
2835
+ for (const chunk of chunks) await fetch(`${getBaseUrl()}${streamPath1}`, {
2836
+ method: `POST`,
2837
+ headers: { "Content-Type": `application/octet-stream` },
2838
+ body: chunk
2839
+ });
2840
+ const response1 = await fetch(`${getBaseUrl()}${streamPath1}`);
2841
+ const data1 = new Uint8Array(await response1.arrayBuffer());
2842
+ const hash1 = hashContent(data1);
2843
+ const streamPath2 = `/v1/stream/hash-verify-2-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2844
+ await fetch(`${getBaseUrl()}${streamPath2}`, {
2845
+ method: `PUT`,
2846
+ headers: { "Content-Type": `application/octet-stream` }
2847
+ });
2848
+ for (const chunk of chunks) await fetch(`${getBaseUrl()}${streamPath2}`, {
2849
+ method: `POST`,
2850
+ headers: { "Content-Type": `application/octet-stream` },
2851
+ body: chunk
2852
+ });
2853
+ const response2 = await fetch(`${getBaseUrl()}${streamPath2}`);
2854
+ const data2 = new Uint8Array(await response2.arrayBuffer());
2855
+ const hash2 = hashContent(data2);
2856
+ expect(hash1).toBe(hash2);
2857
+ expect(data1.length).toBe(data2.length);
2858
+ return true;
2859
+ }
2860
+ ), { numRuns: 15 });
2861
+ });
2862
+ test(`content hash changes with each append`, async () => {
2863
+ const streamPath = `/v1/stream/hash-changes-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2864
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2865
+ method: `PUT`,
2866
+ headers: { "Content-Type": `application/octet-stream` }
2867
+ });
2868
+ const hashes = [];
2869
+ for (let i = 0; i < 5; i++) {
2870
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2871
+ method: `POST`,
2872
+ headers: { "Content-Type": `application/octet-stream` },
2873
+ body: new Uint8Array([
2874
+ i,
2875
+ i + 1,
2876
+ i + 2
2877
+ ])
2878
+ });
2879
+ const response = await fetch(`${getBaseUrl()}${streamPath}`);
2880
+ const data = new Uint8Array(await response.arrayBuffer());
2881
+ hashes.push(hashContent(data));
2882
+ }
2883
+ const uniqueHashes = new Set(hashes);
2884
+ expect(uniqueHashes.size).toBe(5);
2885
+ });
2886
+ test(`empty stream has consistent hash`, async () => {
2887
+ const streamPath1 = `/v1/stream/empty-hash-1-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2888
+ const streamPath2 = `/v1/stream/empty-hash-2-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2889
+ await fetch(`${getBaseUrl()}${streamPath1}`, {
2890
+ method: `PUT`,
2891
+ headers: { "Content-Type": `application/octet-stream` }
2892
+ });
2893
+ await fetch(`${getBaseUrl()}${streamPath2}`, {
2894
+ method: `PUT`,
2895
+ headers: { "Content-Type": `application/octet-stream` }
2896
+ });
2897
+ const response1 = await fetch(`${getBaseUrl()}${streamPath1}`);
2898
+ const response2 = await fetch(`${getBaseUrl()}${streamPath2}`);
2899
+ const data1 = new Uint8Array(await response1.arrayBuffer());
2900
+ const data2 = new Uint8Array(await response2.arrayBuffer());
2901
+ expect(data1.length).toBe(0);
2902
+ expect(data2.length).toBe(0);
2903
+ expect(hashContent(data1)).toBe(hashContent(data2));
2904
+ });
2905
+ test(`deterministic ordering - same data in same order produces same hash`, async () => {
2906
+ await fc.assert(fc.asyncProperty(fc.array(fc.uint8Array({
2907
+ minLength: 1,
2908
+ maxLength: 50
2909
+ }), {
2910
+ minLength: 2,
2911
+ maxLength: 5
2912
+ }), async (chunks) => {
2913
+ const hashes = [];
2914
+ for (let run = 0; run < 2; run++) {
2915
+ const streamPath = `/v1/stream/order-hash-${run}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2916
+ await fetch(`${getBaseUrl()}${streamPath}`, {
2917
+ method: `PUT`,
2918
+ headers: { "Content-Type": `application/octet-stream` }
2919
+ });
2920
+ for (const chunk of chunks) await fetch(`${getBaseUrl()}${streamPath}`, {
2921
+ method: `POST`,
2922
+ headers: { "Content-Type": `application/octet-stream` },
2923
+ body: chunk
2924
+ });
2925
+ const response = await fetch(`${getBaseUrl()}${streamPath}`);
2926
+ const data = new Uint8Array(await response.arrayBuffer());
2927
+ hashes.push(hashContent(data));
2928
+ }
2929
+ expect(hashes[0]).toBe(hashes[1]);
2930
+ return true;
2931
+ }), { numRuns: 10 });
2932
+ });
2933
+ });
2469
2934
  });
2470
2935
  }
2471
2936
 
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const require_src = require('./src-DK3GDgwo.cjs');
2
+ const require_src = require('./src-ChUwq33M.cjs');
3
3
 
4
4
  //#region src/test-runner.ts
5
5
  const baseUrl = process.env.CONFORMANCE_TEST_URL;
@@ -1,4 +1,4 @@
1
- import { runConformanceTests } from "./src-DcbQ_SIQ.js";
1
+ import { runConformanceTests } from "./src-DWkKYD4d.js";
2
2
 
3
3
  //#region src/test-runner.ts
4
4
  const baseUrl = process.env.CONFORMANCE_TEST_URL;