Compare commits
1 Commits
4d8c787dbc
...
fbe1457ab6
| Author | SHA1 | Date | |
|---|---|---|---|
| fbe1457ab6 |
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "yonexus-protocol",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Yonexus shared communication protocol",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vitest": "^1.0.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/",
|
||||||
|
"src/",
|
||||||
|
"PROTOCOL.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
237
tests/codec.test.ts
Normal file
237
tests/codec.test.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
:
|
||||||
|
import { describe, it, expect, beforeEach } from "vitest";
|
||||||
|
import {
|
||||||
|
encodeBuiltin,
|
||||||
|
decodeBuiltin,
|
||||||
|
parseRuleMessage,
|
||||||
|
encodeRuleMessage,
|
||||||
|
parseRewrittenRuleMessage,
|
||||||
|
encodeRewrittenRuleMessage,
|
||||||
|
isBuiltinMessage,
|
||||||
|
CodecError
|
||||||
|
} from "../src/codec.js";
|
||||||
|
import type { BuiltinEnvelope, HelloPayload, HelloAckPayload } from "../src/types.js";
|
||||||
|
|
||||||
|
describe("Protocol Codec", () => {
|
||||||
|
describe("encodeBuiltin / decodeBuiltin", () => {
|
||||||
|
it("should encode and decode a hello message", () => {
|
||||||
|
const envelope: BuiltinEnvelope<"hello", HelloPayload> = {
|
||||||
|
type: "hello",
|
||||||
|
requestId: "req_001",
|
||||||
|
timestamp: 1711886400,
|
||||||
|
payload: {
|
||||||
|
identifier: "client-a",
|
||||||
|
hasSecret: true,
|
||||||
|
hasKeyPair: true,
|
||||||
|
publicKey: "pk_test",
|
||||||
|
protocolVersion: "1"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const encoded = encodeBuiltin(envelope);
|
||||||
|
expect(encoded).toBe(
|
||||||
|
'builtin::{"type":"hello","requestId":"req_001","timestamp":1711886400,"payload":{"identifier":"client-a","hasSecret":true,"hasKeyPair":true,"publicKey":"pk_test","protocolVersion":"1"}}'
|
||||||
|
);
|
||||||
|
|
||||||
|
const decoded = decodeBuiltin(encoded);
|
||||||
|
expect(decoded).toEqual(envelope);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should encode and decode a hello_ack message", () => {
|
||||||
|
const envelope: BuiltinEnvelope<"hello_ack", HelloAckPayload> = {
|
||||||
|
type: "hello_ack",
|
||||||
|
requestId: "req_001",
|
||||||
|
timestamp: 1711886401,
|
||||||
|
payload: {
|
||||||
|
identifier: "client-a",
|
||||||
|
nextAction: "pair_required"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const encoded = encodeBuiltin(envelope);
|
||||||
|
const decoded = decodeBuiltin(encoded);
|
||||||
|
expect(decoded).toEqual(envelope);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle envelope without optional fields", () => {
|
||||||
|
const envelope: BuiltinEnvelope = {
|
||||||
|
type: "heartbeat"
|
||||||
|
};
|
||||||
|
|
||||||
|
const encoded = encodeBuiltin(envelope);
|
||||||
|
expect(encoded).toBe('builtin::{"type":"heartbeat"}');
|
||||||
|
|
||||||
|
const decoded = decodeBuiltin(encoded);
|
||||||
|
expect(decoded).toEqual(envelope);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw CodecError for malformed builtin message", () => {
|
||||||
|
expect(() => decodeBuiltin("invalid")).toThrow(CodecError);
|
||||||
|
expect(() => decodeBuiltin("builtin::")).toThrow(CodecError);
|
||||||
|
expect(() => decodeBuiltin("builtin::invalid json")).toThrow(CodecError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw CodecError for non-builtin message prefix", () => {
|
||||||
|
expect(() => decodeBuiltin("hello::{\"type\":\"test\"}")).toThrow(CodecError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isBuiltinMessage", () => {
|
||||||
|
it("should return true for builtin messages", () => {
|
||||||
|
expect(isBuiltinMessage('builtin::{"type":"hello"}')).toBe(true);
|
||||||
|
expect(isBuiltinMessage("builtin::{}")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for non-builtin messages", () => {
|
||||||
|
expect(isBuiltinMessage("hello::world")).toBe(false);
|
||||||
|
expect(isBuiltinMessage("rule::content")).toBe(false);
|
||||||
|
expect(isBuiltinMessage("")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseRuleMessage", () => {
|
||||||
|
it("should parse simple rule message", () => {
|
||||||
|
const result = parseRuleMessage("chat_sync::{\"body\":\"hello\"}");
|
||||||
|
expect(result).toEqual({
|
||||||
|
rule: "chat_sync",
|
||||||
|
content: '{"body":"hello"}'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse rule message with content containing ::", () => {
|
||||||
|
const result = parseRuleMessage("chat_sync::sender::extra::{\"body\":\"hello::world\"}");
|
||||||
|
expect(result).toEqual({
|
||||||
|
rule: "chat_sync",
|
||||||
|
content: 'sender::extra::{"body":"hello::world"}'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw CodecError for malformed rule message", () => {
|
||||||
|
expect(() => parseRuleMessage("invalid")).toThrow(CodecError);
|
||||||
|
expect(() => parseRuleMessage("")).toThrow(CodecError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject builtin as rule identifier", () => {
|
||||||
|
expect(() => parseRuleMessage('builtin::{"type":"hello"}')).toThrow(CodecError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encodeRuleMessage", () => {
|
||||||
|
it("should encode rule message", () => {
|
||||||
|
const encoded = encodeRuleMessage("chat_sync", '{"body":"hello"}');
|
||||||
|
expect(encoded).toBe('chat_sync::{"body":"hello"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject builtin as rule", () => {
|
||||||
|
expect(() => encodeRuleMessage("builtin", "content")).toThrow(CodecError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseRewrittenRuleMessage", () => {
|
||||||
|
it("should parse rewritten server-side message", () => {
|
||||||
|
const result = parseRewrittenRuleMessage("chat_sync::client-a::{\"body\":\"hello\"}");
|
||||||
|
expect(result).toEqual({
|
||||||
|
rule: "chat_sync",
|
||||||
|
sender: "client-a",
|
||||||
|
content: '{"body":"hello"}'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle content with :: delimiters", () => {
|
||||||
|
const result = parseRewrittenRuleMessage("chat_sync::client-a::extra::{\"body\":\"hello::world\"}");
|
||||||
|
expect(result).toEqual({
|
||||||
|
rule: "chat_sync",
|
||||||
|
sender: "client-a",
|
||||||
|
content: 'extra::{"body":"hello::world"}'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw CodecError for malformed rewritten message", () => {
|
||||||
|
expect(() => parseRewrittenRuleMessage("rule::content")).toThrow(CodecError);
|
||||||
|
expect(() => parseRewrittenRuleMessage("invalid")).toThrow(CodecError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encodeRewrittenRuleMessage", () => {
|
||||||
|
it("should encode rewritten message", () => {
|
||||||
|
const encoded = encodeRewrittenRuleMessage("chat_sync", "client-a", '{"body":"hello"}');
|
||||||
|
expect(encoded).toBe('chat_sync::client-a::{"body":"hello"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject builtin as rule", () => {
|
||||||
|
expect(() => encodeRewrittenRuleMessage("builtin", "sender", "content")).toThrow(CodecError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Protocol Message Examples", () => {
|
||||||
|
describe("Hello Flow", () => {
|
||||||
|
it("should demonstrate complete hello flow", () => {
|
||||||
|
// Client sends hello
|
||||||
|
const hello: BuiltinEnvelope<"hello", HelloPayload> = {
|
||||||
|
type: "hello",
|
||||||
|
requestId: "req_001",
|
||||||
|
timestamp: Math.floor(Date.now() / 1000),
|
||||||
|
payload: {
|
||||||
|
identifier: "client-a",
|
||||||
|
hasSecret: false,
|
||||||
|
hasKeyPair: true,
|
||||||
|
publicKey: "ed25519_pk_test123",
|
||||||
|
protocolVersion: "1"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const helloWire = encodeBuiltin(hello);
|
||||||
|
expect(isBuiltinMessage(helloWire)).toBe(true);
|
||||||
|
|
||||||
|
// Server parses hello
|
||||||
|
const parsedHello = decodeBuiltin(helloWire);
|
||||||
|
expect(parsedHello.type).toBe("hello");
|
||||||
|
expect((parsedHello.payload as HelloPayload).identifier).toBe("client-a");
|
||||||
|
|
||||||
|
// Server responds with hello_ack
|
||||||
|
const helloAck: BuiltinEnvelope<"hello_ack", HelloAckPayload> = {
|
||||||
|
type: "hello_ack",
|
||||||
|
requestId: hello.requestId,
|
||||||
|
timestamp: Math.floor(Date.now() / 1000),
|
||||||
|
payload: {
|
||||||
|
identifier: "client-a",
|
||||||
|
nextAction: "pair_required"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ackWire = encodeBuiltin(helloAck);
|
||||||
|
const parsedAck = decodeBuiltin(ackWire);
|
||||||
|
expect(parsedAck.type).toBe("hello_ack");
|
||||||
|
expect((parsedAck.payload as HelloAckPayload).nextAction).toBe("pair_required");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Rule Message Flow", () => {
|
||||||
|
it("should demonstrate client to server message", () => {
|
||||||
|
const message = encodeRuleMessage("chat_sync", '{"conversationId":"abc","body":"hello"}');
|
||||||
|
expect(message).toBe('chat_sync::{"conversationId":"abc","body":"hello"}');
|
||||||
|
|
||||||
|
// Server parses
|
||||||
|
const parsed = parseRuleMessage(message);
|
||||||
|
expect(parsed.rule).toBe("chat_sync");
|
||||||
|
expect(parsed.content).toBe('{"conversationId":"abc","body":"hello"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should demonstrate server-side rewriting", () => {
|
||||||
|
// Server rewrites incoming message
|
||||||
|
const rewritten = encodeRewrittenRuleMessage(
|
||||||
|
"chat_sync",
|
||||||
|
"client-a",
|
||||||
|
'{"conversationId":"abc","body":"hello"}'
|
||||||
|
);
|
||||||
|
expect(rewritten).toBe('chat_sync::client-a::{"conversationId":"abc","body":"hello"}');
|
||||||
|
|
||||||
|
// Server-side handler parses
|
||||||
|
const parsed = parseRewrittenRuleMessage(rewritten);
|
||||||
|
expect(parsed.rule).toBe("chat_sync");
|
||||||
|
expect(parsed.sender).toBe("client-a");
|
||||||
|
expect(parsed.content).toBe('{"conversationId":"abc","body":"hello"}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
|
}
|
||||||
8
vitest.config.ts
Normal file
8
vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: "node"
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user