Compare commits

...

4 Commits

Author SHA1 Message Date
nav
ccdf167daf feat: add crypto module (Ed25519 key generation, sign, verify)
Moves verifySignature, signMessage, generateKeyPair and utility functions
from Yonexus.Client into Protocol so Server no longer depends on Client
at build time or runtime.
2026-04-16 10:36:55 +00:00
h z
d2a16bcb02 Merge pull request 'dev/2026-04-08' (#1) from dev/2026-04-08 into main
Reviewed-on: #1
2026-04-13 09:33:41 +00:00
nav
2611304084 chore: align protocol validation tooling 2026-04-09 05:03:03 +00:00
nav
8744a771a2 Tighten protocol typings for strict consumers 2026-04-09 04:37:59 +00:00
6 changed files with 1894 additions and 17 deletions

1826
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"check": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
},

View File

@@ -128,7 +128,7 @@ export function decodeBuiltin(raw: string): BuiltinEnvelope {
throw new CodecError("Missing or invalid 'type' field in envelope");
}
return envelope as BuiltinEnvelope;
return envelope as unknown as BuiltinEnvelope;
} catch (cause) {
if (cause instanceof CodecError) {
throw cause;

49
src/crypto.ts Normal file
View File

@@ -0,0 +1,49 @@
import { randomBytes, generateKeyPair as _generateKeyPair, sign, verify } from "node:crypto";
export interface KeyPair {
readonly privateKey: string;
readonly publicKey: string;
readonly algorithm: "Ed25519";
}
export async function generateKeyPair(): Promise<KeyPair> {
const { publicKey, privateKey } = await new Promise<{ publicKey: string; privateKey: string }>((resolve, reject) => {
_generateKeyPair(
"ed25519",
{
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs8", format: "pem" },
},
(err, pubKey, privKey) => (err ? reject(err) : resolve({ publicKey: pubKey, privateKey: privKey }))
);
});
return { privateKey, publicKey, algorithm: "Ed25519" };
}
export async function signMessage(privateKeyPem: string, message: Buffer | string): Promise<string> {
return sign(null, typeof message === "string" ? Buffer.from(message) : message, privateKeyPem).toString("base64");
}
export async function verifySignature(publicKeyPem: string, message: Buffer | string, signature: string): Promise<boolean> {
try {
return verify(null, typeof message === "string" ? Buffer.from(message) : message, publicKeyPem, Buffer.from(signature, "base64"));
} catch {
return false;
}
}
export function generatePairingCode(): string {
const bytes = randomBytes(8);
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let code = "";
for (let i = 0; i < 12; i++) code += chars[bytes[i % bytes.length] % chars.length];
return `${code.slice(0, 4)}-${code.slice(4, 8)}-${code.slice(8, 12)}`;
}
export function generateSecret(): string {
return randomBytes(32).toString("base64url");
}
export function generateNonce(): string {
return randomBytes(18).toString("base64url").slice(0, 24);
}

View File

@@ -2,3 +2,4 @@ export * from "./types.js";
export * from "./codec.js";
export * from "./errors.js";
export * from "./auth.js";
export * from "./crypto.js";

View File

@@ -22,7 +22,7 @@ export type BuiltinMessageType = (typeof builtinMessageTypes)[number];
export interface BuiltinEnvelope<
TType extends BuiltinMessageType = BuiltinMessageType,
TPayload extends Record<string, unknown> = Record<string, unknown>
TPayload = Record<string, unknown>
> {
type: TType;
requestId?: string;
@@ -30,7 +30,7 @@ export interface BuiltinEnvelope<
payload?: TPayload;
}
export interface HelloPayload {
export interface HelloPayload extends Record<string, unknown> {
identifier: string;
hasSecret: boolean;
hasKeyPair: boolean;
@@ -44,14 +44,14 @@ export type HelloAckNextAction =
| "rejected"
| "waiting_pair_confirm";
export interface HelloAckPayload {
export interface HelloAckPayload extends Record<string, unknown> {
identifier: string;
nextAction: HelloAckNextAction;
}
export type AdminNotificationStatus = "sent" | "failed";
export interface PairRequestPayload {
export interface PairRequestPayload extends Record<string, unknown> {
identifier: string;
expiresAt: number;
ttlSeconds: number;
@@ -59,12 +59,12 @@ export interface PairRequestPayload {
codeDelivery: "out_of_band";
}
export interface PairConfirmPayload {
export interface PairConfirmPayload extends Record<string, unknown> {
identifier: string;
pairingCode: string;
}
export interface PairSuccessPayload {
export interface PairSuccessPayload extends Record<string, unknown> {
identifier: string;
secret: string;
pairedAt: number;
@@ -77,12 +77,12 @@ export type PairFailedReason =
| "admin_notification_failed"
| "internal_error";
export interface PairFailedPayload {
export interface PairFailedPayload extends Record<string, unknown> {
identifier: string;
reason: PairFailedReason;
}
export interface AuthRequestPayload {
export interface AuthRequestPayload extends Record<string, unknown> {
identifier: string;
nonce: string;
proofTimestamp: number;
@@ -90,7 +90,7 @@ export interface AuthRequestPayload {
publicKey?: string;
}
export interface AuthSuccessPayload {
export interface AuthSuccessPayload extends Record<string, unknown> {
identifier: string;
authenticatedAt: number;
status: "online";
@@ -107,33 +107,33 @@ export type AuthFailedReason =
| "rate_limited"
| "re_pair_required";
export interface AuthFailedPayload {
export interface AuthFailedPayload extends Record<string, unknown> {
identifier: string;
reason: AuthFailedReason;
}
export interface RePairRequiredPayload {
export interface RePairRequiredPayload extends Record<string, unknown> {
identifier: string;
reason: "nonce_collision" | "rate_limited" | "trust_revoked";
}
export interface HeartbeatPayload {
export interface HeartbeatPayload extends Record<string, unknown> {
identifier: string;
status: "alive";
}
export interface HeartbeatAckPayload {
export interface HeartbeatAckPayload extends Record<string, unknown> {
identifier: string;
status: "online" | "unstable" | "offline";
}
export interface StatusUpdatePayload {
export interface StatusUpdatePayload extends Record<string, unknown> {
identifier: string;
status: "online" | "unstable" | "offline";
reason: string;
}
export interface DisconnectNoticePayload {
export interface DisconnectNoticePayload extends Record<string, unknown> {
identifier: string;
reason: string;
}
@@ -152,7 +152,7 @@ export type ProtocolErrorCode =
| "CLIENT_OFFLINE"
| "INTERNAL_ERROR";
export interface ErrorPayload {
export interface ErrorPayload extends Record<string, unknown> {
code: ProtocolErrorCode;
message?: string;
details?: Record<string, unknown>;