Skip to content
Draft
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
49 changes: 49 additions & 0 deletions services/companion/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,55 @@ describe("createCompanionApp", () => {
await app.close();
});

it("rejects an empty x-webchain-token header", async () => {
const { app } = await createCompanionApp({
runtime: mockRuntime(),
logger: false,
localToken: "secret",
});

const res = await app.inject({
method: "POST",
url: "/sessions",
headers: { "x-webchain-token": "" },
});

expect(res.statusCode).toBe(401);
await app.close();
});

it("refuses to boot when localToken is empty or whitespace", async () => {
await expect(
createCompanionApp({
runtime: mockRuntime(),
logger: false,
localToken: "",
}),
).rejects.toThrow(/WEBCHAIN_LOCAL_TOKEN must be a non-empty string/);

await expect(
createCompanionApp({
runtime: mockRuntime(),
logger: false,
localToken: " ",
}),
).rejects.toThrow(/WEBCHAIN_LOCAL_TOKEN must be a non-empty string/);
});

it("refuses to boot when WEBCHAIN_LOCAL_TOKEN is empty", async () => {
vi.stubEnv("WEBCHAIN_LOCAL_TOKEN", "");
try {
await expect(
createCompanionApp({
runtime: mockRuntime(),
logger: false,
}),
).rejects.toThrow(/WEBCHAIN_LOCAL_TOKEN must be a non-empty string/);
} finally {
vi.unstubAllEnvs();
}
});

it("creates a session with valid token", async () => {
const runtime = mockRuntime();
const { app } = await createCompanionApp({
Expand Down
40 changes: 35 additions & 5 deletions services/companion/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,43 @@ export type CreateCompanionAppOptions = {
logger?: boolean;
};

const DEFAULT_LOCAL_DEV_TOKEN = "change-me-in-local-dev";

/**
* `??` keeps empty strings from env/options, which previously matched an empty
* `x-webchain-token` header and authenticated as a valid local token.
*
* @example
* resolveCompanionLocalToken("secret")
*/
function resolveCompanionLocalToken(explicitToken: string | undefined): string {
const raw =
explicitToken ??
process.env.WEBCHAIN_LOCAL_TOKEN ??
DEFAULT_LOCAL_DEV_TOKEN;
const token = raw.trim();
if (token.length === 0) {
throw new Error(
`WEBCHAIN_LOCAL_TOKEN must be a non-empty string; received ${JSON.stringify(raw)}, expected a non-empty token`,
);
}
return token;
}

function presentedCompanionToken(
header: string | string[] | undefined,
): string | undefined {
if (typeof header !== "string") {
return undefined;
}
const token = header.trim();
return token.length > 0 ? token : undefined;
}

export async function createCompanionApp(
options: CreateCompanionAppOptions,
): Promise<{ app: FastifyInstance; localToken: string }> {
const localToken =
options.localToken ??
process.env.WEBCHAIN_LOCAL_TOKEN ??
"change-me-in-local-dev";
const localToken = resolveCompanionLocalToken(options.localToken);

const app = Fastify({ logger: options.logger ?? true });

Expand All @@ -141,7 +171,7 @@ export async function createCompanionApp(
return;
}

const token = request.headers["x-webchain-token"];
const token = presentedCompanionToken(request.headers["x-webchain-token"]);

if (token !== localToken) {
return reply.code(401).send({
Expand Down
Loading