Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions dotty-pi-ext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
"@earendil-works/pi-coding-agent": "^0.74.0"
},
"scripts": {
"test": "npm run test:memory && npm run test:recall && npm run test:remember && npm run test:rememberperson && npm run test:turnlog && npm run test:think && npm run test:play",
"test": "npm run test:memory && npm run test:recall && npm run test:remember && npm run test:rememberperson && npm run test:turnlog && npm run test:think && npm run test:play && npm run test:adminauth",
"test:memory": "node --experimental-strip-types tests/memory_lookup.test.ts",
"test:recall": "node --experimental-strip-types tests/recall_person.test.ts",
"test:remember": "node --experimental-strip-types tests/remember.test.ts",
"test:rememberperson": "node --experimental-strip-types tests/remember_person.test.ts",
"test:turnlog": "node --experimental-strip-types tests/turn_log.test.ts",
"test:think": "node --experimental-strip-types tests/think_hard.test.ts",
"test:play": "node --experimental-strip-types tests/play_song.test.ts"
"test:play": "node --experimental-strip-types tests/play_song.test.ts",
"test:adminauth": "node --experimental-strip-types tests/admin_auth.test.ts"
}
}
15 changes: 10 additions & 5 deletions dotty-pi-ext/src/lib/xiaozhi_admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
// run on the Unraid host networked together, so the default URL is
// localhost — env vars override for dev / tests.
//
// We don't replicate the bridge.py admin auth code path because the
// current admin surface is unauthenticated on the loopback interface
// (see architecture.md threat model). When the surface grows auth this
// is the seam to update.
// Admin auth: when DOTTY_ADMIN_TOKEN is set we attach an X-Admin-Token header
// on every admin request, matching the xiaozhi-server /xiaozhi/admin/*
// middleware. Unset = no header, so this is a no-op until the same token is
// provisioned across all callers + the server. See architecture.md threat model.

const DEFAULT_HOST =
process.env.XIAOZHI_HOST ?? process.env._XIAOZHI_HOST ?? "localhost";
Expand All @@ -17,6 +17,7 @@ const DEFAULT_HTTP_PORT = Number(
const DEFAULT_TIMEOUT_MS = Number(
process.env.XIAOZHI_ADMIN_TIMEOUT_MS ?? "3000",
);
const ADMIN_TOKEN = process.env.DOTTY_ADMIN_TOKEN ?? "";

export interface AdminOptions {
host?: string;
Expand All @@ -38,8 +39,12 @@ async function adminFetch(
const url = buildUrl(path, opts);
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
// Merge the admin token into any caller-supplied headers (e.g. content-type)
// when DOTTY_ADMIN_TOKEN is set; otherwise leave headers untouched.
const headers = new Headers(init.headers);
if (ADMIN_TOKEN) headers.set("X-Admin-Token", ADMIN_TOKEN);
try {
return await fetch(url, { ...init, signal: ac.signal });
return await fetch(url, { ...init, headers, signal: ac.signal });
Comment on lines +42 to +47
} finally {
clearTimeout(timer);
}
Expand Down
39 changes: 39 additions & 0 deletions dotty-pi-ext/tests/admin_auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// admin_auth tests: adminFetch attaches X-Admin-Token from DOTTY_ADMIN_TOKEN
// when set, and preserves caller-supplied headers (content-type). The module
// reads the token at load time, so the env var is set BEFORE a dynamic import
// (static ESM imports are hoisted and would run first).

process.env.DOTTY_ADMIN_TOKEN = "test-token";

const { playAsset } = await import("../src/lib/xiaozhi_admin.ts");

let failures = 0;
function assertEq(label: string, got: unknown, want: unknown): void {
if (got !== want) {
console.error(`FAIL ${label}: got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`);
failures++;
} else {
console.log(`ok - ${label}`);
}
}

const original = globalThis.fetch;
let token: string | null = null;
let contentType: string | null = null;
globalThis.fetch = (async (_url: unknown, init: { headers?: HeadersInit }) => {
const h = new Headers(init?.headers);
token = h.get("X-Admin-Token");
contentType = h.get("content-type");
return new Response(null, { status: 200 });
}) as typeof fetch;

try {
await playAsset("/opt/xiaozhi-esp32-server/config/assets/songs/x.opus");
assertEq("X-Admin-Token sent when DOTTY_ADMIN_TOKEN set", token, "test-token");
assertEq("caller content-type header preserved", contentType, "application/json");
} finally {
globalThis.fetch = original;
}
Comment on lines +30 to +36

if (failures > 0) process.exit(1);
console.log("admin_auth.test.ts passed");
Loading