dev/2026-04-08 #1

Merged
hzhang merged 29 commits from dev/2026-04-08 into main 2026-04-13 09:34:22 +00:00
Showing only changes of commit 0717b204f1 - Show all commits

View File

@@ -4,8 +4,7 @@ import {
buildAuthRequest,
decodeBuiltin,
encodeBuiltin,
createAuthRequestSigningInput,
YONEXUS_PROTOCOL_VERSION
createAuthRequestSigningInput
} from "../../Yonexus.Protocol/src/index.js";
import { createYonexusServerRuntime } from "../plugin/core/runtime.js";
import type { ClientRecord } from "../plugin/core/persistence.js";
@@ -71,6 +70,42 @@ function createMockTransport() {
return { transport, sent };
}
async function buildSignedAuthRequest(options: {
identifier: string;
secret: string;
privateKey: string;
publicKey: string;
nonce: string;
proofTimestamp: number;
requestId?: string;
signatureOverride?: string;
publicKeyOverride?: string;
}) {
const signature =
options.signatureOverride ??
(await signMessage(
options.privateKey,
createAuthRequestSigningInput({
secret: options.secret,
nonce: options.nonce,
proofTimestamp: options.proofTimestamp
})
));
return encodeBuiltin(
buildAuthRequest(
{
identifier: options.identifier,
nonce: options.nonce,
proofTimestamp: options.proofTimestamp,
signature,
publicKey: options.publicKeyOverride ?? options.publicKey
},
{ requestId: options.requestId, timestamp: options.proofTimestamp }
)
);
}
describe("YNX-1105c: Auth Failure Paths", () => {
let now = 1_710_000_000;
@@ -83,6 +118,282 @@ describe("YNX-1105c: Auth Failure Paths", () => {
vi.useRealTimers();
});
it("AF-01: unknown identifier returns auth_failed(unknown_identifier)", async () => {
const keyPair = await generateKeyPair();
const store = createMockStore([]);
const { transport, sent } = createMockTransport();
const runtime = createYonexusServerRuntime({
config: {
followerIdentifiers: ["client-a"],
notifyBotToken: "stub-token",
adminUserId: "admin-user",
listenHost: "127.0.0.1",
listenPort: 8787
},
store,
transport,
now: () => now
});
await runtime.start();
const connection = createConnection("rogue-client");
await runtime.handleMessage(
connection,
await buildSignedAuthRequest({
identifier: "rogue-client",
secret: "shared-secret",
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey.trim(),
nonce: "NONCE1234567890123456789",
proofTimestamp: now,
requestId: "req-auth-unknown"
})
);
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
expect(lastMessage.type).toBe("auth_failed");
expect(lastMessage.payload).toMatchObject({
identifier: "rogue-client",
reason: "unknown_identifier"
});
});
it("AF-02: auth before pairing returns auth_failed(not_paired)", async () => {
const keyPair = await generateKeyPair();
const store = createMockStore([
{
identifier: "client-a",
pairingStatus: "unpaired",
status: "offline",
recentNonces: [],
recentHandshakeAttempts: [],
createdAt: now - 10,
updatedAt: now - 10
}
]);
const { transport, sent } = createMockTransport();
const runtime = createYonexusServerRuntime({
config: {
followerIdentifiers: ["client-a"],
notifyBotToken: "stub-token",
adminUserId: "admin-user",
listenHost: "127.0.0.1",
listenPort: 8787
},
store,
transport,
now: () => now
});
await runtime.start();
const connection = createConnection("client-a");
runtime.state.registry.sessions.set("client-a", {
identifier: "client-a",
socket: connection.ws,
isAuthenticated: false,
connectedAt: now,
lastActivityAt: now,
publicKey: keyPair.publicKey.trim()
});
await runtime.handleMessage(
connection,
await buildSignedAuthRequest({
identifier: "client-a",
secret: "shared-secret",
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey.trim(),
nonce: "NONCE1234567890123456789",
proofTimestamp: now,
requestId: "req-auth-not-paired"
})
);
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
expect(lastMessage.type).toBe("auth_failed");
expect(lastMessage.payload).toMatchObject({ identifier: "client-a", reason: "not_paired" });
});
it("AF-03: invalid signature returns auth_failed(invalid_signature)", async () => {
const keyPair = await generateKeyPair();
const wrongKeyPair = await generateKeyPair();
const store = createMockStore([
{
identifier: "client-a",
pairingStatus: "paired",
publicKey: keyPair.publicKey.trim(),
secret: "shared-secret",
status: "offline",
recentNonces: [],
recentHandshakeAttempts: [],
createdAt: now - 10,
updatedAt: now - 10
}
]);
const { transport, sent } = createMockTransport();
const runtime = createYonexusServerRuntime({
config: {
followerIdentifiers: ["client-a"],
notifyBotToken: "stub-token",
adminUserId: "admin-user",
listenHost: "127.0.0.1",
listenPort: 8787
},
store,
transport,
now: () => now
});
await runtime.start();
const connection = createConnection("client-a");
runtime.state.registry.sessions.set("client-a", {
identifier: "client-a",
socket: connection.ws,
isAuthenticated: false,
connectedAt: now,
lastActivityAt: now,
publicKey: keyPair.publicKey.trim()
});
await runtime.handleMessage(
connection,
await buildSignedAuthRequest({
identifier: "client-a",
secret: "shared-secret",
privateKey: wrongKeyPair.privateKey,
publicKey: keyPair.publicKey.trim(),
nonce: "NONCE1234567890123456789",
proofTimestamp: now,
requestId: "req-auth-invalid-signature"
})
);
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
expect(lastMessage.type).toBe("auth_failed");
expect(lastMessage.payload).toMatchObject({ identifier: "client-a", reason: "invalid_signature" });
});
it("AF-05: stale timestamp returns auth_failed(stale_timestamp)", async () => {
const keyPair = await generateKeyPair();
const store = createMockStore([
{
identifier: "client-a",
pairingStatus: "paired",
publicKey: keyPair.publicKey.trim(),
secret: "shared-secret",
status: "offline",
recentNonces: [],
recentHandshakeAttempts: [],
createdAt: now - 20,
updatedAt: now - 20
}
]);
const { transport, sent } = createMockTransport();
const runtime = createYonexusServerRuntime({
config: {
followerIdentifiers: ["client-a"],
notifyBotToken: "stub-token",
adminUserId: "admin-user",
listenHost: "127.0.0.1",
listenPort: 8787
},
store,
transport,
now: () => now
});
await runtime.start();
const connection = createConnection("client-a");
runtime.state.registry.sessions.set("client-a", {
identifier: "client-a",
socket: connection.ws,
isAuthenticated: false,
connectedAt: now,
lastActivityAt: now,
publicKey: keyPair.publicKey.trim()
});
await runtime.handleMessage(
connection,
await buildSignedAuthRequest({
identifier: "client-a",
secret: "shared-secret",
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey.trim(),
nonce: "NONCE1234567890123456789",
proofTimestamp: now - 11,
requestId: "req-auth-stale"
})
);
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
expect(lastMessage.type).toBe("auth_failed");
expect(lastMessage.payload).toMatchObject({ identifier: "client-a", reason: "stale_timestamp" });
});
it("AF-06: future timestamp returns auth_failed(future_timestamp)", async () => {
const keyPair = await generateKeyPair();
const store = createMockStore([
{
identifier: "client-a",
pairingStatus: "paired",
publicKey: keyPair.publicKey.trim(),
secret: "shared-secret",
status: "offline",
recentNonces: [],
recentHandshakeAttempts: [],
createdAt: now - 20,
updatedAt: now - 20
}
]);
const { transport, sent } = createMockTransport();
const runtime = createYonexusServerRuntime({
config: {
followerIdentifiers: ["client-a"],
notifyBotToken: "stub-token",
adminUserId: "admin-user",
listenHost: "127.0.0.1",
listenPort: 8787
},
store,
transport,
now: () => now
});
await runtime.start();
const connection = createConnection("client-a");
runtime.state.registry.sessions.set("client-a", {
identifier: "client-a",
socket: connection.ws,
isAuthenticated: false,
connectedAt: now,
lastActivityAt: now,
publicKey: keyPair.publicKey.trim()
});
await runtime.handleMessage(
connection,
await buildSignedAuthRequest({
identifier: "client-a",
secret: "shared-secret",
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey.trim(),
nonce: "NONCE1234567890123456789",
proofTimestamp: now + 11,
requestId: "req-auth-future"
})
);
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
expect(lastMessage.type).toBe("auth_failed");
expect(lastMessage.payload).toMatchObject({ identifier: "client-a", reason: "future_timestamp" });
});
it("AF-07: nonce collision triggers re_pair_required", async () => {
const keyPair = await generateKeyPair();
const store = createMockStore([
@@ -125,52 +436,33 @@ describe("YNX-1105c: Auth Failure Paths", () => {
});
const nonce = "NONCE1234567890123456789";
const signingInput = createAuthRequestSigningInput({
secret: "shared-secret",
nonce,
proofTimestamp: now
});
const signature = await signMessage(keyPair.privateKey, signingInput);
await runtime.handleMessage(
connection,
encodeBuiltin(
buildAuthRequest(
{
identifier: "client-a",
nonce,
proofTimestamp: now,
signature,
publicKey: keyPair.publicKey.trim()
},
{ requestId: "req-auth-1", timestamp: now }
)
)
await buildSignedAuthRequest({
identifier: "client-a",
secret: "shared-secret",
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey.trim(),
nonce,
proofTimestamp: now,
requestId: "req-auth-1"
})
);
// Second request with same nonce triggers re-pair
now += 1;
const signingInput2 = createAuthRequestSigningInput({
secret: "shared-secret",
nonce,
proofTimestamp: now
});
const signature2 = await signMessage(keyPair.privateKey, signingInput2);
await runtime.handleMessage(
connection,
encodeBuiltin(
buildAuthRequest(
{
identifier: "client-a",
nonce,
proofTimestamp: now,
signature: signature2,
publicKey: keyPair.publicKey.trim()
},
{ requestId: "req-auth-2", timestamp: now }
)
)
await buildSignedAuthRequest({
identifier: "client-a",
secret: "shared-secret",
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey.trim(),
nonce,
proofTimestamp: now,
requestId: "req-auth-2"
})
);
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
@@ -223,28 +515,17 @@ describe("YNX-1105c: Auth Failure Paths", () => {
publicKey: keyPair.publicKey.trim()
});
const nonce = "NONCE987654321098765432";
const signingInput = createAuthRequestSigningInput({
secret: "shared-secret",
nonce,
proofTimestamp: now
});
const signature = await signMessage(keyPair.privateKey, signingInput);
await runtime.handleMessage(
connection,
encodeBuiltin(
buildAuthRequest(
{
identifier: "client-a",
nonce,
proofTimestamp: now,
signature,
publicKey: keyPair.publicKey.trim()
},
{ requestId: "req-auth", timestamp: now }
)
)
await buildSignedAuthRequest({
identifier: "client-a",
secret: "shared-secret",
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey.trim(),
nonce: "NONCE9876543210987654321",
proofTimestamp: now,
requestId: "req-auth-rate-limit"
})
);
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
@@ -255,4 +536,170 @@ describe("YNX-1105c: Auth Failure Paths", () => {
expect(record?.secret).toBeUndefined();
expect(record?.pairingStatus).toBe("revoked");
});
it("AF-09: wrong public key returns auth_failed(invalid_signature)", async () => {
const keyPair = await generateKeyPair();
const rotatedKeyPair = await generateKeyPair();
const store = createMockStore([
{
identifier: "client-a",
pairingStatus: "paired",
publicKey: keyPair.publicKey.trim(),
secret: "shared-secret",
status: "offline",
recentNonces: [],
recentHandshakeAttempts: [],
createdAt: now - 10,
updatedAt: now - 10
}
]);
const { transport, sent } = createMockTransport();
const runtime = createYonexusServerRuntime({
config: {
followerIdentifiers: ["client-a"],
notifyBotToken: "stub-token",
adminUserId: "admin-user",
listenHost: "127.0.0.1",
listenPort: 8787
},
store,
transport,
now: () => now
});
await runtime.start();
const connection = createConnection("client-a");
runtime.state.registry.sessions.set("client-a", {
identifier: "client-a",
socket: connection.ws,
isAuthenticated: false,
connectedAt: now,
lastActivityAt: now,
publicKey: keyPair.publicKey.trim()
});
await runtime.handleMessage(
connection,
await buildSignedAuthRequest({
identifier: "client-a",
secret: "shared-secret",
privateKey: rotatedKeyPair.privateKey,
publicKey: keyPair.publicKey.trim(),
publicKeyOverride: rotatedKeyPair.publicKey.trim(),
nonce: "NONCE1234567890123456789",
proofTimestamp: now,
requestId: "req-auth-wrong-public-key"
})
);
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
expect(lastMessage.type).toBe("auth_failed");
expect(lastMessage.payload).toMatchObject({ identifier: "client-a", reason: "invalid_signature" });
});
it("AF-10: malformed auth_request payload returns protocol error", async () => {
const store = createMockStore([]);
const { transport, sent } = createMockTransport();
const runtime = createYonexusServerRuntime({
config: {
followerIdentifiers: ["client-a"],
notifyBotToken: "stub-token",
adminUserId: "admin-user",
listenHost: "127.0.0.1",
listenPort: 8787
},
store,
transport,
now: () => now
});
await runtime.start();
const connection = createConnection("client-a");
await runtime.handleMessage(
connection,
encodeBuiltin({
type: "auth_request",
requestId: "req-auth-malformed",
timestamp: now
})
);
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
expect(lastMessage.type).toBe("error");
expect(lastMessage.payload).toMatchObject({
code: "MALFORMED_MESSAGE",
message: "auth_request payload is required"
});
});
it("AF-11: tampered signature returns auth_failed(invalid_signature)", async () => {
const keyPair = await generateKeyPair();
const store = createMockStore([
{
identifier: "client-a",
pairingStatus: "paired",
publicKey: keyPair.publicKey.trim(),
secret: "shared-secret",
status: "offline",
recentNonces: [],
recentHandshakeAttempts: [],
createdAt: now - 10,
updatedAt: now - 10
}
]);
const { transport, sent } = createMockTransport();
const runtime = createYonexusServerRuntime({
config: {
followerIdentifiers: ["client-a"],
notifyBotToken: "stub-token",
adminUserId: "admin-user",
listenHost: "127.0.0.1",
listenPort: 8787
},
store,
transport,
now: () => now
});
await runtime.start();
const connection = createConnection("client-a");
runtime.state.registry.sessions.set("client-a", {
identifier: "client-a",
socket: connection.ws,
isAuthenticated: false,
connectedAt: now,
lastActivityAt: now,
publicKey: keyPair.publicKey.trim()
});
const validMessage = decodeBuiltin(
await buildSignedAuthRequest({
identifier: "client-a",
secret: "shared-secret",
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey.trim(),
nonce: "NONCE1234567890123456789",
proofTimestamp: now,
requestId: "req-auth-tampered"
})
);
await runtime.handleMessage(
connection,
encodeBuiltin({
...validMessage,
payload: {
...validMessage.payload,
signature: `A${String(validMessage.payload?.signature).slice(1)}`
}
})
);
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
expect(lastMessage.type).toBe("auth_failed");
expect(lastMessage.payload).toMatchObject({ identifier: "client-a", reason: "invalid_signature" });
});
});