ehbp 0.0.1

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 (50) hide show
  1. package/build-browser.js +54 -0
  2. package/chat.html +285 -0
  3. package/dist/client.d.ts +47 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +145 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/example.d.ts +6 -0
  8. package/dist/example.d.ts.map +1 -0
  9. package/dist/example.js +115 -0
  10. package/dist/example.js.map +1 -0
  11. package/dist/identity.d.ts +52 -0
  12. package/dist/identity.d.ts.map +1 -0
  13. package/dist/identity.js +270 -0
  14. package/dist/identity.js.map +1 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +11 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/protocol.d.ts +19 -0
  20. package/dist/protocol.d.ts.map +1 -0
  21. package/dist/protocol.js +19 -0
  22. package/dist/protocol.js.map +1 -0
  23. package/dist/streaming-test.d.ts +3 -0
  24. package/dist/streaming-test.d.ts.map +1 -0
  25. package/dist/streaming-test.js +102 -0
  26. package/dist/streaming-test.js.map +1 -0
  27. package/dist/test/client.test.d.ts +2 -0
  28. package/dist/test/client.test.d.ts.map +1 -0
  29. package/dist/test/client.test.js +70 -0
  30. package/dist/test/client.test.js.map +1 -0
  31. package/dist/test/identity.test.d.ts +2 -0
  32. package/dist/test/identity.test.d.ts.map +1 -0
  33. package/dist/test/identity.test.js +39 -0
  34. package/dist/test/identity.test.js.map +1 -0
  35. package/dist/test/streaming.test.d.ts +2 -0
  36. package/dist/test/streaming.test.d.ts.map +1 -0
  37. package/dist/test/streaming.test.js +71 -0
  38. package/dist/test/streaming.test.js.map +1 -0
  39. package/package.json +42 -0
  40. package/src/client.ts +170 -0
  41. package/src/example.ts +126 -0
  42. package/src/identity.ts +339 -0
  43. package/src/index.ts +14 -0
  44. package/src/protocol.ts +19 -0
  45. package/src/streaming-test.ts +118 -0
  46. package/src/test/client.test.ts +91 -0
  47. package/src/test/identity.test.ts +46 -0
  48. package/src/test/streaming.test.ts +85 -0
  49. package/test.html +271 -0
  50. package/tsconfig.json +19 -0
@@ -0,0 +1,39 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { Identity } from '../identity.js';
4
+ describe('Identity', () => {
5
+ it('should generate a new identity', async () => {
6
+ const identity = await Identity.generate();
7
+ assert(identity.getPublicKey() instanceof CryptoKey, 'Public key should be a CryptoKey');
8
+ assert(identity.getPrivateKey() instanceof CryptoKey, 'Private key should be a CryptoKey');
9
+ const publicKeyHex = await identity.getPublicKeyHex();
10
+ assert(publicKeyHex.length > 0, 'Public key hex should not be empty');
11
+ });
12
+ it('should serialize and deserialize identity', async () => {
13
+ const original = await Identity.generate();
14
+ const json = await original.toJSON();
15
+ const restored = await Identity.fromJSON(json);
16
+ const originalHex = await original.getPublicKeyHex();
17
+ const restoredHex = await restored.getPublicKeyHex();
18
+ assert(originalHex === restoredHex, 'Public keys should match');
19
+ assert(original.getPrivateKey() instanceof CryptoKey, 'Private key should be a CryptoKey');
20
+ assert(restored.getPrivateKey() instanceof CryptoKey, 'Private key should be a CryptoKey');
21
+ });
22
+ it('should marshal configuration', async () => {
23
+ const identity = await Identity.generate();
24
+ const config = await identity.marshalConfig();
25
+ assert(config.length > 0, 'Config should not be empty');
26
+ assert(config[0] === 0, 'Key ID should be 0');
27
+ assert(config[1] === 0x00, 'KEM ID high byte should be 0x00');
28
+ assert(config[2] === 0x20, 'KEM ID low byte should be 0x20');
29
+ });
30
+ it('should unmarshal public configuration', async () => {
31
+ const identity = await Identity.generate();
32
+ const config = await identity.marshalConfig();
33
+ const restored = await Identity.unmarshalPublicConfig(config);
34
+ const originalHex = await identity.getPublicKeyHex();
35
+ const restoredHex = await restored.getPublicKeyHex();
36
+ assert(restoredHex === originalHex, 'Public keys should match');
37
+ });
38
+ });
39
+ //# sourceMappingURL=identity.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.test.js","sourceRoot":"","sources":["../../src/test/identity.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,gCAAgC,EAAE,KAAK,IAAI,EAAE;QAC9C,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC;QAE3C,MAAM,CAAC,QAAQ,CAAC,YAAY,EAAE,YAAY,SAAS,EAAE,kCAAkC,CAAC,CAAC;QACzF,MAAM,CAAC,QAAQ,CAAC,aAAa,EAAE,YAAY,SAAS,EAAE,mCAAmC,CAAC,CAAC;QAC3F,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,eAAe,EAAE,CAAC;QACtD,MAAM,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,oCAAoC,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,KAAK,IAAI,EAAE;QACzD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAE/C,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,eAAe,EAAE,CAAC;QACrD,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,eAAe,EAAE,CAAC;QACrD,MAAM,CAAC,WAAW,KAAK,WAAW,EAAE,0BAA0B,CAAC,CAAC;QAChE,MAAM,CAAC,QAAQ,CAAC,aAAa,EAAE,YAAY,SAAS,EAAE,mCAAmC,CAAC,CAAC;QAC3F,MAAM,CAAC,QAAQ,CAAC,aAAa,EAAE,YAAY,SAAS,EAAE,mCAAmC,CAAC,CAAC;IAC7F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,KAAK,IAAI,EAAE;QAC5C,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,aAAa,EAAE,CAAC;QAE9C,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,4BAA4B,CAAC,CAAC;QACxD,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,oBAAoB,CAAC,CAAC;QAC9C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,iCAAiC,CAAC,CAAC;QAC9D,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,gCAAgC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,aAAa,EAAE,CAAC;QAC9C,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC;QAE9D,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,eAAe,EAAE,CAAC;QACrD,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,eAAe,EAAE,CAAC;QACrD,MAAM,CAAC,WAAW,KAAK,WAAW,EAAE,0BAA0B,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=streaming.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming.test.d.ts","sourceRoot":"","sources":["../../src/test/streaming.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,71 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ describe('Streaming', () => {
4
+ it('should handle streaming responses', async () => {
5
+ // Create a mock streaming response
6
+ const mockStreamData = 'Number: 1\nNumber: 2\nNumber: 3\n';
7
+ const mockResponse = new Response(mockStreamData, {
8
+ status: 200,
9
+ headers: {
10
+ 'Content-Type': 'text/plain',
11
+ 'Ehbp-Encapsulated-Key': 'abcd1234' // Mock encapsulated key
12
+ }
13
+ });
14
+ // Test that we can read from a stream
15
+ const reader = mockResponse.body?.getReader();
16
+ assert(reader, 'Response should have a readable stream');
17
+ const decoder = new TextDecoder();
18
+ let receivedData = '';
19
+ while (true) {
20
+ const { done, value } = await reader.read();
21
+ if (done)
22
+ break;
23
+ const text = decoder.decode(value, { stream: true });
24
+ receivedData += text;
25
+ }
26
+ assert(receivedData === mockStreamData, 'Should receive all stream data');
27
+ });
28
+ it('should handle empty streams', async () => {
29
+ const emptyResponse = new Response('', {
30
+ status: 200,
31
+ headers: {
32
+ 'Ehbp-Encapsulated-Key': 'abcd1234'
33
+ }
34
+ });
35
+ const reader = emptyResponse.body?.getReader();
36
+ assert(reader, 'Response should have a readable stream');
37
+ const { done } = await reader.read();
38
+ assert(done, 'Empty stream should be done immediately');
39
+ });
40
+ it('should handle chunked data correctly', async () => {
41
+ // Simulate chunked data
42
+ const chunks = ['Hello', ' ', 'World', '!'];
43
+ const stream = new ReadableStream({
44
+ start(controller) {
45
+ chunks.forEach(chunk => {
46
+ controller.enqueue(new TextEncoder().encode(chunk));
47
+ });
48
+ controller.close();
49
+ }
50
+ });
51
+ const response = new Response(stream, {
52
+ status: 200,
53
+ headers: {
54
+ 'Ehbp-Encapsulated-Key': 'abcd1234'
55
+ }
56
+ });
57
+ const reader = response.body?.getReader();
58
+ assert(reader, 'Response should have a readable stream');
59
+ const decoder = new TextDecoder();
60
+ let receivedData = '';
61
+ while (true) {
62
+ const { done, value } = await reader.read();
63
+ if (done)
64
+ break;
65
+ const text = decoder.decode(value, { stream: true });
66
+ receivedData += text;
67
+ }
68
+ assert(receivedData === 'Hello World!', 'Should receive all chunks correctly');
69
+ });
70
+ });
71
+ //# sourceMappingURL=streaming.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming.test.js","sourceRoot":"","sources":["../../src/test/streaming.test.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "ehbp",
3
+ "version": "0.0.1",
4
+ "description": "JavaScript client for Encrypted HTTP Body Protocol (EHBP)",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "build:browser": "node build-browser.js",
10
+ "build:all": "npm run build && npm run build:browser",
11
+ "dev": "tsc --watch",
12
+ "test": "node --test dist/test/*.test.js",
13
+ "example": "node dist/example.js",
14
+ "streaming": "node dist/streaming-test.js",
15
+ "serve": "node serve.js"
16
+ },
17
+ "keywords": [
18
+ "hpke",
19
+ "encryption",
20
+ "http",
21
+ "security",
22
+ "ehbp"
23
+ ],
24
+ "author": "Tinfoil",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/tinfoilsh/encrypted-http-body-protocol.git"
28
+ },
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@hpke/core": "^1.7.4",
32
+ "@hpke/dhkem-x25519": "^1.6.4"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.0.0",
36
+ "esbuild": "^0.25.9",
37
+ "typescript": "^5.0.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=20.0.0"
41
+ }
42
+ }
package/src/client.ts ADDED
@@ -0,0 +1,170 @@
1
+ import { Identity } from './identity.js';
2
+ import { PROTOCOL } from './protocol.js';
3
+
4
+ /**
5
+ * HTTP transport for EHBP
6
+ */
7
+ export class Transport {
8
+ private clientIdentity: Identity;
9
+ private serverHost: string;
10
+ private serverPublicKey: CryptoKey;
11
+
12
+ constructor(clientIdentity: Identity, serverHost: string, serverPublicKey: CryptoKey) {
13
+ this.clientIdentity = clientIdentity;
14
+ this.serverHost = serverHost;
15
+ this.serverPublicKey = serverPublicKey;
16
+ }
17
+
18
+ /**
19
+ * Create a new transport by fetching server public key
20
+ */
21
+ static async create(serverURL: string, clientIdentity: Identity): Promise<Transport> {
22
+ const url = new URL(serverURL);
23
+ const serverHost = url.host;
24
+
25
+ // Fetch server public key
26
+ const keysURL = new URL(PROTOCOL.KEYS_PATH, serverURL);
27
+ const response = await fetch(keysURL.toString());
28
+
29
+ if (!response.ok) {
30
+ throw new Error(`Failed to get server public key: ${response.status}`);
31
+ }
32
+
33
+ const contentType = response.headers.get('content-type');
34
+ if (contentType !== PROTOCOL.KEYS_MEDIA_TYPE) {
35
+ throw new Error(`Invalid content type: ${contentType}`);
36
+ }
37
+
38
+ const keysData = new Uint8Array(await response.arrayBuffer());
39
+ const serverIdentity = await Identity.unmarshalPublicConfig(keysData);
40
+ const serverPublicKey = serverIdentity.getPublicKey();
41
+
42
+ return new Transport(clientIdentity, serverHost, serverPublicKey);
43
+ }
44
+
45
+ /**
46
+ * Get the server public key
47
+ */
48
+ async getServerPublicKey(): Promise<CryptoKey> {
49
+ return this.serverPublicKey;
50
+ }
51
+
52
+ /**
53
+ * Get the client public key
54
+ */
55
+ async getClientPublicKey(): Promise<CryptoKey> {
56
+ return this.clientIdentity.getPublicKey();
57
+ }
58
+
59
+ /**
60
+ * Make an encrypted HTTP request
61
+ */
62
+ async request(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
63
+ // Extract body from init or original request before creating Request object
64
+ let requestBody: BodyInit | null = null;
65
+
66
+ if (input instanceof Request) {
67
+ // If input is a Request, extract its body
68
+ if (input.body) {
69
+ requestBody = await input.arrayBuffer();
70
+ }
71
+ } else {
72
+ // If input is URL/string, get body from init
73
+ requestBody = init?.body || null;
74
+ }
75
+
76
+ // Create the URL with correct host
77
+ let url: URL;
78
+ let method: string;
79
+ let headers: HeadersInit;
80
+
81
+ if (input instanceof Request) {
82
+ url = new URL(input.url);
83
+ method = input.method;
84
+ headers = input.headers;
85
+ } else {
86
+ url = new URL(input);
87
+ method = init?.method || 'GET';
88
+ headers = init?.headers || {};
89
+ }
90
+
91
+ url.host = this.serverHost;
92
+
93
+ let request = new Request(url.toString(), {
94
+ method,
95
+ headers,
96
+ body: requestBody,
97
+ duplex: 'half'
98
+ } as RequestInit);
99
+
100
+ // Encrypt request body if present (check the original requestBody, not request.body)
101
+ if (requestBody !== null && requestBody !== undefined) {
102
+ request = await this.clientIdentity.encryptRequest(request, this.serverPublicKey);
103
+ } else {
104
+ // No body, just set client public key header
105
+ const headers = new Headers(request.headers);
106
+ headers.set(PROTOCOL.CLIENT_PUBLIC_KEY_HEADER, await this.clientIdentity.getPublicKeyHex());
107
+ request = new Request(request.url, {
108
+ method: request.method,
109
+ headers,
110
+ body: null
111
+ });
112
+ }
113
+
114
+ // Make the request
115
+ const response = await fetch(request);
116
+
117
+ if (!response.ok) {
118
+ console.warn(`Server returned non-OK status: ${response.status}`);
119
+ }
120
+
121
+ // Check for encapsulated key header
122
+ const encapKeyHeader = response.headers.get(PROTOCOL.ENCAPSULATED_KEY_HEADER);
123
+ if (!encapKeyHeader) {
124
+ throw new Error(`Missing ${PROTOCOL.ENCAPSULATED_KEY_HEADER} encapsulated key header`);
125
+ }
126
+
127
+ // Decode encapsulated key
128
+ const serverEncapKey = new Uint8Array(
129
+ encapKeyHeader.match(/.{2}/g)!.map(byte => parseInt(byte, 16))
130
+ );
131
+
132
+ // Decrypt response
133
+ return await this.clientIdentity.decryptResponse(response, serverEncapKey);
134
+ }
135
+
136
+ /**
137
+ * Convenience method for GET requests
138
+ */
139
+ async get(url: string | URL, init?: RequestInit): Promise<Response> {
140
+ return this.request(url, { ...init, method: 'GET' });
141
+ }
142
+
143
+ /**
144
+ * Convenience method for POST requests
145
+ */
146
+ async post(url: string | URL, body?: BodyInit, init?: RequestInit): Promise<Response> {
147
+ return this.request(url, { ...init, method: 'POST', body });
148
+ }
149
+
150
+ /**
151
+ * Convenience method for PUT requests
152
+ */
153
+ async put(url: string | URL, body?: BodyInit, init?: RequestInit): Promise<Response> {
154
+ return this.request(url, { ...init, method: 'PUT', body });
155
+ }
156
+
157
+ /**
158
+ * Convenience method for DELETE requests
159
+ */
160
+ async delete(url: string | URL, init?: RequestInit): Promise<Response> {
161
+ return this.request(url, { ...init, method: 'DELETE' });
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Create a new transport instance
167
+ */
168
+ export async function createTransport(serverURL: string, clientIdentity: Identity): Promise<Transport> {
169
+ return Transport.create(serverURL, clientIdentity);
170
+ }
package/src/example.ts ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Example usage of the EHBP JavaScript client
5
+ */
6
+
7
+ import { Identity, createTransport } from './index.js';
8
+
9
+ async function main() {
10
+ console.log('EHBP JavaScript Client Example');
11
+ console.log('==============================');
12
+
13
+ try {
14
+ // Create client identity
15
+ console.log('Creating client identity...');
16
+ const clientIdentity = await Identity.generate();
17
+ console.log('Client public key:', await clientIdentity.getPublicKeyHex());
18
+
19
+ // Create transport (this will fetch server public key)
20
+ console.log('Creating transport...');
21
+ const serverURL = 'http://localhost:8080'; // Adjust as needed
22
+ const transport = await createTransport(serverURL, clientIdentity);
23
+ console.log('Transport created successfully');
24
+
25
+ // Example 1: GET request to secure endpoint
26
+ console.log('\n--- GET Request ---');
27
+ try {
28
+ const getResponse = await transport.get(`${serverURL}/secure`);
29
+ console.log('GET Response status:', getResponse.status);
30
+ if (getResponse.ok) {
31
+ const getData = await getResponse.text();
32
+ console.log('GET Response:', getData);
33
+ } else {
34
+ console.log('GET Request failed with status:', getResponse.status);
35
+ }
36
+ } catch (error) {
37
+ console.log('GET Request failed:', error instanceof Error ? error.message : String(error));
38
+ }
39
+
40
+ // Example 2: POST request with JSON data
41
+ console.log('\n--- POST Request ---');
42
+ try {
43
+ const postData = { message: 'Hello from JavaScript client!', timestamp: new Date().toISOString() };
44
+ const postResponse = await transport.post(
45
+ `${serverURL}/secure`,
46
+ JSON.stringify(postData),
47
+ { headers: { 'Content-Type': 'application/json' } }
48
+ );
49
+ console.log('POST Response status:', postResponse.status);
50
+ if (postResponse.ok) {
51
+ const responseData = await postResponse.text();
52
+ console.log('POST Response:', responseData);
53
+ } else {
54
+ console.log('POST Request failed with status:', postResponse.status);
55
+ }
56
+ } catch (error) {
57
+ console.log('POST Request failed:', error instanceof Error ? error.message : String(error));
58
+ }
59
+
60
+ // Example 3: PUT request
61
+ console.log('\n--- PUT Request ---');
62
+ try {
63
+ const putData = { id: 1, name: 'Updated Item' };
64
+ const putResponse = await transport.put(
65
+ `${serverURL}/secure`,
66
+ JSON.stringify(putData),
67
+ { headers: { 'Content-Type': 'application/json' } }
68
+ );
69
+ console.log('PUT Response status:', putResponse.status);
70
+ if (putResponse.ok) {
71
+ const putResponseData = await putResponse.text();
72
+ console.log('PUT Response:', putResponseData);
73
+ } else {
74
+ console.log('PUT Request failed with status:', putResponse.status);
75
+ }
76
+ } catch (error) {
77
+ console.log('PUT Request failed:', error instanceof Error ? error.message : String(error));
78
+ }
79
+
80
+ // Example 4: Streaming request
81
+ console.log('\n--- Streaming Request ---');
82
+ try {
83
+ const streamResponse = await transport.get(`${serverURL}/stream`);
84
+ console.log('Stream Response status:', streamResponse.status);
85
+ if (streamResponse.ok) {
86
+ console.log('Streaming response (should show numbers 1-20):');
87
+ const reader = streamResponse.body?.getReader();
88
+ if (reader) {
89
+ const decoder = new TextDecoder();
90
+ let chunkCount = 0;
91
+
92
+ while (true) {
93
+ const { done, value } = await reader.read();
94
+ if (done) break;
95
+
96
+ const text = decoder.decode(value, { stream: true });
97
+ process.stdout.write(text);
98
+ chunkCount++;
99
+ }
100
+
101
+ console.log(`\nStream completed with ${chunkCount} chunks`);
102
+ } else {
103
+ console.log('No readable stream available');
104
+ }
105
+ } else {
106
+ console.log('Stream Request failed with status:', streamResponse.status);
107
+ }
108
+ } catch (error) {
109
+ console.log('Stream Request failed:', error instanceof Error ? error.message : String(error));
110
+ }
111
+
112
+ console.log('\nExample completed successfully!');
113
+ console.log('\nTo test with a real server:');
114
+ console.log('1. Start the Go server: go run pkg/server/main.go');
115
+ console.log('2. Run this example: npm run example');
116
+
117
+ } catch (error) {
118
+ console.error('Error:', error);
119
+ process.exit(1);
120
+ }
121
+ }
122
+
123
+ // Run the example
124
+ if (import.meta.url === `file://${process.argv[1]}`) {
125
+ main().catch(console.error);
126
+ }