feat: Add password reset functionality with request and reset forms
feat: Implement price list import feature with preview and apply options feat: Create price rules management page with CRUD operations feat: Develop quotes management page with itemized quotes and status tracking feat: Introduce organization registration page for new users feat: Build suppliers management page with detailed supplier information feat: Create users management page for inviting and managing roles chore: Add TypeScript configuration for improved type checking chore: Set up Vite configuration for development server and API proxy chore: Add Vite environment type definitions for better TypeScript support
This commit is contained in:
139
scripts/ws-smoke-test.mjs
Normal file
139
scripts/ws-smoke-test.mjs
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
const wsUrl = process.argv[2] ?? process.env.WS_URL ?? "ws://localhost:8080/ws";
|
||||
const protocolVersion = 1;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
return Buffer.from(bytes).toString("base64");
|
||||
}
|
||||
|
||||
function base64ToBytes(value) {
|
||||
return Buffer.from(value, "base64");
|
||||
}
|
||||
|
||||
async function createSession() {
|
||||
const key = await webcrypto.subtle.generateKey(
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
true,
|
||||
["encrypt", "decrypt"],
|
||||
);
|
||||
const rawKey = await webcrypto.subtle.exportKey("raw", key);
|
||||
|
||||
return {
|
||||
key,
|
||||
keyId: webcrypto.randomUUID(),
|
||||
exportedKey: bytesToBase64(new Uint8Array(rawKey)),
|
||||
};
|
||||
}
|
||||
|
||||
async function encryptMessage(session, message) {
|
||||
const nonce = webcrypto.getRandomValues(new Uint8Array(12));
|
||||
const plaintext = encoder.encode(JSON.stringify(message));
|
||||
const ciphertext = await webcrypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv: nonce },
|
||||
session.key,
|
||||
plaintext,
|
||||
);
|
||||
|
||||
return {
|
||||
enc: `aes-256-gcm-v${protocolVersion}`,
|
||||
key_id: session.keyId,
|
||||
nonce: bytesToBase64(nonce),
|
||||
ciphertext: bytesToBase64(new Uint8Array(ciphertext)),
|
||||
};
|
||||
}
|
||||
|
||||
async function decryptMessage(session, envelope) {
|
||||
const plaintext = await webcrypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv: base64ToBytes(envelope.nonce) },
|
||||
session.key,
|
||||
base64ToBytes(envelope.ciphertext),
|
||||
);
|
||||
|
||||
return JSON.parse(decoder.decode(plaintext));
|
||||
}
|
||||
|
||||
function waitForOpen(socket) {
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.addEventListener("open", resolve, { once: true });
|
||||
socket.addEventListener("error", reject, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
function waitForMessage(socket, timeoutMs = 5000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
socket.removeEventListener("message", onMessage);
|
||||
reject(new Error(`timeout after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
function onMessage(event) {
|
||||
clearTimeout(timeout);
|
||||
resolve(JSON.parse(event.data));
|
||||
}
|
||||
|
||||
socket.addEventListener("message", onMessage, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const session = await createSession();
|
||||
const socket = new WebSocket(wsUrl);
|
||||
|
||||
console.log(`connecting ${wsUrl}`);
|
||||
await waitForOpen(socket);
|
||||
console.log("socket open");
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
type: "hello",
|
||||
payload: {
|
||||
protocol_version: protocolVersion,
|
||||
key_id: session.keyId,
|
||||
session_key: session.exportedKey,
|
||||
},
|
||||
}));
|
||||
|
||||
const ack = await waitForMessage(socket);
|
||||
if (ack.type !== "hello_ack") {
|
||||
throw new Error(`expected hello_ack, got ${JSON.stringify(ack)}`);
|
||||
}
|
||||
console.log(`hello_ack protocol=${ack.payload.protocol_version} key=${ack.payload.key_id}`);
|
||||
|
||||
const firstEncrypted = await waitForMessage(socket);
|
||||
if (firstEncrypted.type !== "encrypted") {
|
||||
throw new Error(`expected encrypted snapshot, got ${JSON.stringify(firstEncrypted)}`);
|
||||
}
|
||||
const snapshot = await decryptMessage(session, firstEncrypted.payload);
|
||||
console.log(`decrypted first server message: ${snapshot.type}`);
|
||||
|
||||
const subscribe = await encryptMessage(session, {
|
||||
type: "subscribe",
|
||||
payload: { topic: "records" },
|
||||
});
|
||||
socket.send(JSON.stringify({ type: "encrypted", payload: subscribe }));
|
||||
console.log("sent encrypted subscribe");
|
||||
|
||||
const ping = await encryptMessage(session, { type: "ping" });
|
||||
socket.send(JSON.stringify({ type: "encrypted", payload: ping }));
|
||||
|
||||
const pongEnvelope = await waitForMessage(socket);
|
||||
if (pongEnvelope.type !== "encrypted") {
|
||||
throw new Error(`expected encrypted pong, got ${JSON.stringify(pongEnvelope)}`);
|
||||
}
|
||||
const pong = await decryptMessage(session, pongEnvelope.payload);
|
||||
console.log(`decrypted ping response: ${pong.type}`);
|
||||
|
||||
socket.close();
|
||||
console.log("communication smoke test ok");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user