ehbp 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.
- package/dist/cjs/client.d.ts +51 -0
- package/dist/cjs/client.d.ts.map +1 -0
- package/dist/cjs/client.js +160 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/identity.d.ts +52 -0
- package/dist/cjs/identity.d.ts.map +1 -0
- package/dist/cjs/identity.js +274 -0
- package/dist/cjs/identity.js.map +1 -0
- package/{src/index.ts → dist/cjs/index.d.ts} +2 -4
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +19 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/protocol.d.ts +19 -0
- package/dist/cjs/protocol.d.ts.map +1 -0
- package/dist/cjs/protocol.js +22 -0
- package/dist/cjs/protocol.js.map +1 -0
- package/dist/esm/client.d.ts +51 -0
- package/dist/esm/client.d.ts.map +1 -0
- package/dist/esm/client.js +155 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/example.d.ts +6 -0
- package/dist/esm/example.d.ts.map +1 -0
- package/dist/esm/example.js +115 -0
- package/dist/esm/example.js.map +1 -0
- package/dist/esm/identity.d.ts +52 -0
- package/dist/esm/identity.d.ts.map +1 -0
- package/dist/esm/identity.js +270 -0
- package/dist/esm/identity.js.map +1 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/protocol.d.ts +19 -0
- package/dist/esm/protocol.d.ts.map +1 -0
- package/dist/esm/protocol.js +19 -0
- package/dist/esm/protocol.js.map +1 -0
- package/dist/esm/streaming-test.d.ts +3 -0
- package/dist/esm/streaming-test.d.ts.map +1 -0
- package/dist/esm/streaming-test.js +102 -0
- package/dist/esm/streaming-test.js.map +1 -0
- package/dist/esm/test/client.test.d.ts +2 -0
- package/dist/esm/test/client.test.d.ts.map +1 -0
- package/dist/esm/test/client.test.js +71 -0
- package/dist/esm/test/client.test.js.map +1 -0
- package/dist/esm/test/identity.test.d.ts +2 -0
- package/dist/esm/test/identity.test.d.ts.map +1 -0
- package/dist/esm/test/identity.test.js +39 -0
- package/dist/esm/test/identity.test.js.map +1 -0
- package/dist/esm/test/streaming.test.d.ts +2 -0
- package/dist/esm/test/streaming.test.d.ts.map +1 -0
- package/dist/esm/test/streaming.test.js +71 -0
- package/dist/esm/test/streaming.test.js.map +1 -0
- package/package.json +7 -2
- package/build-browser.js +0 -54
- package/chat.html +0 -285
- package/src/client.ts +0 -181
- package/src/example.ts +0 -126
- package/src/identity.ts +0 -339
- package/src/protocol.ts +0 -19
- package/src/streaming-test.ts +0 -118
- package/src/test/client.test.ts +0 -93
- package/src/test/identity.test.ts +0 -46
- package/src/test/streaming.test.ts +0 -85
- package/test.html +0 -271
- package/tsconfig.cjs.json +0 -8
- package/tsconfig.esm.json +0 -7
- package/tsconfig.json +0 -19
|
@@ -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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ehbp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "JavaScript client for Encrypted HTTP Body Protocol (EHBP)",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -48,5 +48,10 @@
|
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
50
|
"node": ">=20.0.0"
|
|
51
|
-
}
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"README.md",
|
|
55
|
+
"LICENSE"
|
|
56
|
+
]
|
|
52
57
|
}
|
package/build-browser.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Build script to create a browser-compatible bundle
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { build } from 'esbuild';
|
|
8
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
9
|
-
import { join, dirname } from 'path';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
|
|
12
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
const __dirname = dirname(__filename);
|
|
14
|
-
|
|
15
|
-
async function buildBrowser() {
|
|
16
|
-
console.log('Building browser bundle...');
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
// Build the main bundle
|
|
20
|
-
await build({
|
|
21
|
-
entryPoints: ['src/index.ts'],
|
|
22
|
-
bundle: true,
|
|
23
|
-
outfile: 'dist/browser.js',
|
|
24
|
-
format: 'esm',
|
|
25
|
-
target: 'es2020',
|
|
26
|
-
platform: 'browser',
|
|
27
|
-
external: [],
|
|
28
|
-
sourcemap: true,
|
|
29
|
-
minify: false,
|
|
30
|
-
define: {
|
|
31
|
-
'process.env.NODE_ENV': '"production"'
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// Create a simple wrapper that exports everything
|
|
36
|
-
const wrapper = `
|
|
37
|
-
// Browser-compatible wrapper for EHBP client
|
|
38
|
-
export * from './browser.js';
|
|
39
|
-
`;
|
|
40
|
-
|
|
41
|
-
writeFileSync(join(__dirname, 'dist', 'index.js'), wrapper);
|
|
42
|
-
|
|
43
|
-
console.log('Browser bundle created successfully!');
|
|
44
|
-
console.log('Files created:');
|
|
45
|
-
console.log(' - dist/browser.js (main bundle)');
|
|
46
|
-
console.log(' - dist/index.js (wrapper)');
|
|
47
|
-
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.error('Build failed:', error);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
buildBrowser();
|
package/chat.html
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>EHBP Chat Interface</title>
|
|
7
|
-
<style>
|
|
8
|
-
body {
|
|
9
|
-
font-family: Arial, sans-serif;
|
|
10
|
-
max-width: 800px;
|
|
11
|
-
margin: 0 auto;
|
|
12
|
-
padding: 20px;
|
|
13
|
-
background-color: #f5f5f5;
|
|
14
|
-
}
|
|
15
|
-
.container {
|
|
16
|
-
background: white;
|
|
17
|
-
padding: 0;
|
|
18
|
-
height: 95vh;
|
|
19
|
-
display: flex;
|
|
20
|
-
flex-direction: column;
|
|
21
|
-
border-radius: 0;
|
|
22
|
-
}
|
|
23
|
-
h1 {
|
|
24
|
-
color: #333;
|
|
25
|
-
text-align: center;
|
|
26
|
-
margin: 15px 0 10px 0;
|
|
27
|
-
padding: 0 20px;
|
|
28
|
-
}
|
|
29
|
-
.chat-container {
|
|
30
|
-
flex: 1;
|
|
31
|
-
display: flex;
|
|
32
|
-
flex-direction: column;
|
|
33
|
-
overflow: hidden;
|
|
34
|
-
}
|
|
35
|
-
.messages {
|
|
36
|
-
flex: 1;
|
|
37
|
-
padding: 20px;
|
|
38
|
-
overflow-y: auto;
|
|
39
|
-
}
|
|
40
|
-
.message {
|
|
41
|
-
margin-bottom: 12px;
|
|
42
|
-
padding: 8px 12px;
|
|
43
|
-
border-radius: 4px;
|
|
44
|
-
max-width: 80%;
|
|
45
|
-
}
|
|
46
|
-
.message.user {
|
|
47
|
-
background-color: #007bff;
|
|
48
|
-
color: white;
|
|
49
|
-
margin-left: auto;
|
|
50
|
-
text-align: right;
|
|
51
|
-
border: none;
|
|
52
|
-
}
|
|
53
|
-
.message.assistant {
|
|
54
|
-
background-color: #f8f9fa;
|
|
55
|
-
color: #333;
|
|
56
|
-
margin-right: auto;
|
|
57
|
-
border: none;
|
|
58
|
-
}
|
|
59
|
-
.message.system {
|
|
60
|
-
background-color: #fff3cd;
|
|
61
|
-
color: #856404;
|
|
62
|
-
margin: 0 auto;
|
|
63
|
-
text-align: center;
|
|
64
|
-
font-style: italic;
|
|
65
|
-
border: none;
|
|
66
|
-
}
|
|
67
|
-
.input-area {
|
|
68
|
-
display: flex;
|
|
69
|
-
align-items: center;
|
|
70
|
-
padding: 15px 20px;
|
|
71
|
-
gap: 8px;
|
|
72
|
-
border-top: 1px solid #eee;
|
|
73
|
-
background-color: white;
|
|
74
|
-
}
|
|
75
|
-
.input-area input {
|
|
76
|
-
flex: 1;
|
|
77
|
-
padding: 8px 10px;
|
|
78
|
-
border: 1px solid #ddd;
|
|
79
|
-
border-radius: 4px;
|
|
80
|
-
height: 32px;
|
|
81
|
-
font-family: inherit;
|
|
82
|
-
font-size: 13px;
|
|
83
|
-
outline: none;
|
|
84
|
-
box-sizing: border-box;
|
|
85
|
-
}
|
|
86
|
-
.input-area input:focus {
|
|
87
|
-
border-color: #007bff;
|
|
88
|
-
}
|
|
89
|
-
.input-area button {
|
|
90
|
-
padding: 8px 16px;
|
|
91
|
-
background-color: #007bff;
|
|
92
|
-
color: white;
|
|
93
|
-
border: none;
|
|
94
|
-
border-radius: 4px;
|
|
95
|
-
cursor: pointer;
|
|
96
|
-
font-size: 13px;
|
|
97
|
-
font-weight: 500;
|
|
98
|
-
min-width: 60px;
|
|
99
|
-
}
|
|
100
|
-
.input-area button:hover {
|
|
101
|
-
background-color: #0056b3;
|
|
102
|
-
}
|
|
103
|
-
.input-area button:disabled {
|
|
104
|
-
background-color: #6c757d;
|
|
105
|
-
cursor: not-allowed;
|
|
106
|
-
}
|
|
107
|
-
.typing-indicator {
|
|
108
|
-
padding: 10px;
|
|
109
|
-
font-style: italic;
|
|
110
|
-
color: #666;
|
|
111
|
-
display: none;
|
|
112
|
-
}
|
|
113
|
-
</style>
|
|
114
|
-
</head>
|
|
115
|
-
<body>
|
|
116
|
-
<div class="container">
|
|
117
|
-
<h1>EHBP Chat Interface</h1>
|
|
118
|
-
<div class="chat-container">
|
|
119
|
-
<div id="messages" class="messages"></div>
|
|
120
|
-
<div class="typing-indicator" id="typingIndicator">Assistant is typing...</div>
|
|
121
|
-
<div class="input-area">
|
|
122
|
-
<input type="text" id="messageInput" placeholder="Type your message here..." onkeydown="handleKeyDown(event)" />
|
|
123
|
-
<button id="sendBtn" onclick="sendMessage()">Send</button>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
<script type="module">
|
|
129
|
-
import { Identity, createTransport } from './dist/browser.js';
|
|
130
|
-
|
|
131
|
-
const serverUrl = 'http://localhost:8443';
|
|
132
|
-
|
|
133
|
-
let transport = null;
|
|
134
|
-
let clientIdentity = null;
|
|
135
|
-
let conversationHistory = [];
|
|
136
|
-
|
|
137
|
-
async function initializeClient() {
|
|
138
|
-
try {
|
|
139
|
-
clientIdentity = await Identity.generate();
|
|
140
|
-
transport = await createTransport(serverUrl, clientIdentity);
|
|
141
|
-
return true;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
console.error('Failed to connect:', error.message);
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function addMessage(role, content) {
|
|
149
|
-
const messagesContainer = document.getElementById('messages');
|
|
150
|
-
const messageDiv = document.createElement('div');
|
|
151
|
-
messageDiv.className = `message ${role}`;
|
|
152
|
-
messageDiv.textContent = content;
|
|
153
|
-
messagesContainer.appendChild(messageDiv);
|
|
154
|
-
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
155
|
-
|
|
156
|
-
if (role !== 'system') {
|
|
157
|
-
conversationHistory.push({ role, content });
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function showTypingIndicator() {
|
|
162
|
-
document.getElementById('typingIndicator').style.display = 'block';
|
|
163
|
-
const messagesContainer = document.getElementById('messages');
|
|
164
|
-
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function hideTypingIndicator() {
|
|
168
|
-
document.getElementById('typingIndicator').style.display = 'none';
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function sendMessage() {
|
|
172
|
-
const messageInput = document.getElementById('messageInput');
|
|
173
|
-
const sendBtn = document.getElementById('sendBtn');
|
|
174
|
-
const message = messageInput.value.trim();
|
|
175
|
-
|
|
176
|
-
if (!message) return;
|
|
177
|
-
|
|
178
|
-
messageInput.disabled = true;
|
|
179
|
-
sendBtn.disabled = true;
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
if (!transport) {
|
|
183
|
-
const initialized = await initializeClient();
|
|
184
|
-
if (!initialized) return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
addMessage('user', message);
|
|
188
|
-
messageInput.value = '';
|
|
189
|
-
showTypingIndicator();
|
|
190
|
-
|
|
191
|
-
const response = await transport.request(`${serverUrl}/v1/chat/completions`, {
|
|
192
|
-
method: 'POST',
|
|
193
|
-
headers: { 'Content-Type': 'application/json' },
|
|
194
|
-
body: JSON.stringify({
|
|
195
|
-
model: "qwen:0.5b",
|
|
196
|
-
messages: conversationHistory,
|
|
197
|
-
stream: true
|
|
198
|
-
})
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
hideTypingIndicator();
|
|
202
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
203
|
-
await handleStreamingResponse(response);
|
|
204
|
-
} catch (error) {
|
|
205
|
-
hideTypingIndicator();
|
|
206
|
-
addMessage('system', `Error: ${error.message}`);
|
|
207
|
-
console.error('Chat error:', error);
|
|
208
|
-
} finally {
|
|
209
|
-
messageInput.disabled = false;
|
|
210
|
-
sendBtn.disabled = false;
|
|
211
|
-
messageInput.focus();
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async function handleStreamingResponse(response) {
|
|
216
|
-
const reader = response.body?.getReader();
|
|
217
|
-
if (!reader) {
|
|
218
|
-
throw new Error('No readable stream available');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const decoder = new TextDecoder();
|
|
222
|
-
let assistantMessage = '';
|
|
223
|
-
let messageDiv = null;
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
while (true) {
|
|
227
|
-
const { done, value } = await reader.read();
|
|
228
|
-
if (done) break;
|
|
229
|
-
|
|
230
|
-
const text = decoder.decode(value, { stream: true });
|
|
231
|
-
const lines = text.split('\n');
|
|
232
|
-
|
|
233
|
-
for (const line of lines) {
|
|
234
|
-
if (line.trim() === '') continue;
|
|
235
|
-
if (line.startsWith('data: ')) {
|
|
236
|
-
const data = line.slice(6);
|
|
237
|
-
if (data === '[DONE]') continue;
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
const parsed = JSON.parse(data);
|
|
241
|
-
const delta = parsed.choices?.[0]?.delta;
|
|
242
|
-
if (delta?.content) {
|
|
243
|
-
assistantMessage += delta.content;
|
|
244
|
-
if (!messageDiv) {
|
|
245
|
-
const messagesContainer = document.getElementById('messages');
|
|
246
|
-
messageDiv = document.createElement('div');
|
|
247
|
-
messageDiv.className = 'message assistant';
|
|
248
|
-
messagesContainer.appendChild(messageDiv);
|
|
249
|
-
}
|
|
250
|
-
messageDiv.textContent = assistantMessage;
|
|
251
|
-
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
|
|
252
|
-
}
|
|
253
|
-
} catch (parseError) {
|
|
254
|
-
console.warn('Failed to parse streaming data:', parseError);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (assistantMessage) {
|
|
261
|
-
conversationHistory.push({ role: 'assistant', content: assistantMessage });
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
} finally {
|
|
265
|
-
reader.releaseLock();
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
function handleKeyDown(event) {
|
|
271
|
-
if (event.key === 'Enter' && !event.shiftKey) {
|
|
272
|
-
event.preventDefault();
|
|
273
|
-
sendMessage();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
window.sendMessage = sendMessage;
|
|
278
|
-
window.handleKeyDown = handleKeyDown;
|
|
279
|
-
|
|
280
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
281
|
-
document.getElementById('messageInput').focus();
|
|
282
|
-
});
|
|
283
|
-
</script>
|
|
284
|
-
</body>
|
|
285
|
-
</html>
|
package/src/client.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
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
|
-
getServerPublicKey(): CryptoKey {
|
|
49
|
-
return this.serverPublicKey;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get the server public key as hex string
|
|
54
|
-
*/
|
|
55
|
-
async getServerPublicKeyHex(): Promise<string> {
|
|
56
|
-
const exported = await crypto.subtle.exportKey('raw', this.serverPublicKey);
|
|
57
|
-
const keyBytes = new Uint8Array(exported);
|
|
58
|
-
return Array.from(keyBytes)
|
|
59
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
60
|
-
.join('');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Get the client public key
|
|
65
|
-
*/
|
|
66
|
-
getClientPublicKey(): CryptoKey {
|
|
67
|
-
return this.clientIdentity.getPublicKey();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Make an encrypted HTTP request
|
|
72
|
-
*/
|
|
73
|
-
async request(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
74
|
-
// Extract body from init or original request before creating Request object
|
|
75
|
-
let requestBody: BodyInit | null = null;
|
|
76
|
-
|
|
77
|
-
if (input instanceof Request) {
|
|
78
|
-
// If input is a Request, extract its body
|
|
79
|
-
if (input.body) {
|
|
80
|
-
requestBody = await input.arrayBuffer();
|
|
81
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
// If input is URL/string, get body from init
|
|
84
|
-
requestBody = init?.body || null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Create the URL with correct host
|
|
88
|
-
let url: URL;
|
|
89
|
-
let method: string;
|
|
90
|
-
let headers: HeadersInit;
|
|
91
|
-
|
|
92
|
-
if (input instanceof Request) {
|
|
93
|
-
url = new URL(input.url);
|
|
94
|
-
method = input.method;
|
|
95
|
-
headers = input.headers;
|
|
96
|
-
} else {
|
|
97
|
-
url = new URL(input);
|
|
98
|
-
method = init?.method || 'GET';
|
|
99
|
-
headers = init?.headers || {};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
url.host = this.serverHost;
|
|
103
|
-
|
|
104
|
-
let request = new Request(url.toString(), {
|
|
105
|
-
method,
|
|
106
|
-
headers,
|
|
107
|
-
body: requestBody,
|
|
108
|
-
duplex: 'half'
|
|
109
|
-
} as RequestInit);
|
|
110
|
-
|
|
111
|
-
// Encrypt request body if present (check the original requestBody, not request.body)
|
|
112
|
-
if (requestBody !== null && requestBody !== undefined) {
|
|
113
|
-
request = await this.clientIdentity.encryptRequest(request, this.serverPublicKey);
|
|
114
|
-
} else {
|
|
115
|
-
// No body, just set client public key header
|
|
116
|
-
const headers = new Headers(request.headers);
|
|
117
|
-
headers.set(PROTOCOL.CLIENT_PUBLIC_KEY_HEADER, await this.clientIdentity.getPublicKeyHex());
|
|
118
|
-
request = new Request(request.url, {
|
|
119
|
-
method: request.method,
|
|
120
|
-
headers,
|
|
121
|
-
body: null
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Make the request
|
|
126
|
-
const response = await fetch(request);
|
|
127
|
-
|
|
128
|
-
if (!response.ok) {
|
|
129
|
-
console.warn(`Server returned non-OK status: ${response.status}`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Check for encapsulated key header
|
|
133
|
-
const encapKeyHeader = response.headers.get(PROTOCOL.ENCAPSULATED_KEY_HEADER);
|
|
134
|
-
if (!encapKeyHeader) {
|
|
135
|
-
throw new Error(`Missing ${PROTOCOL.ENCAPSULATED_KEY_HEADER} encapsulated key header`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Decode encapsulated key
|
|
139
|
-
const serverEncapKey = new Uint8Array(
|
|
140
|
-
encapKeyHeader.match(/.{2}/g)!.map(byte => parseInt(byte, 16))
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
// Decrypt response
|
|
144
|
-
return await this.clientIdentity.decryptResponse(response, serverEncapKey);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Convenience method for GET requests
|
|
149
|
-
*/
|
|
150
|
-
async get(url: string | URL, init?: RequestInit): Promise<Response> {
|
|
151
|
-
return this.request(url, { ...init, method: 'GET' });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Convenience method for POST requests
|
|
156
|
-
*/
|
|
157
|
-
async post(url: string | URL, body?: BodyInit, init?: RequestInit): Promise<Response> {
|
|
158
|
-
return this.request(url, { ...init, method: 'POST', body });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Convenience method for PUT requests
|
|
163
|
-
*/
|
|
164
|
-
async put(url: string | URL, body?: BodyInit, init?: RequestInit): Promise<Response> {
|
|
165
|
-
return this.request(url, { ...init, method: 'PUT', body });
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Convenience method for DELETE requests
|
|
170
|
-
*/
|
|
171
|
-
async delete(url: string | URL, init?: RequestInit): Promise<Response> {
|
|
172
|
-
return this.request(url, { ...init, method: 'DELETE' });
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Create a new transport instance
|
|
178
|
-
*/
|
|
179
|
-
export async function createTransport(serverURL: string, clientIdentity: Identity): Promise<Transport> {
|
|
180
|
-
return Transport.create(serverURL, clientIdentity);
|
|
181
|
-
}
|