mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-05-22 19:57:07 +00:00
Export filename change: - Drop the redundant `codex-` prefix; embed the account email so multiple exported files can coexist in the same downloads folder. - Email is extracted from the id_token JWT `email` claim, with fallback to connection.email and finally to the sanitized connection label. - sanitizeFileNamePart now preserves @ so addresses survive intact (e.g. `auth-diego@example.com.json`). Apply Local refinement: - ApplyCodexAuthModal: confirmation modal showing the resolved target path, the side-by-side .bak location, and the centralized backup trail. User must tick a confirmation checkbox before Apply enables. - writeCodexAuthFileToLocalCli now writes a side-by-side `auth-<timestamp>.bak` inside the .codex/ directory before replacing the live file, in addition to the existing centralized backup. Both inputs to the .bak path are server-controlled (dirname from the static CLI_TOOLS table; basename from a server-generated ISO timestamp), so no user input touches path APIs. - apply-local route now emits a `provider.credentials.applied` audit event with the resolved authPath and savedBakPath, and routes all errors through sanitizeErrorMessage() per the security guide. Tests: tests/unit/codexAuthFile.test.ts covers sanitization, JWT email extraction, filename format for both branches (email/label), and the ISO-timestamp .bak basename safety. Scope: this is PR1 of the import/export work tracked under _tasks/features-v3.8.0/importexport/. PR2 (import single) and PR3 (import bulk) will follow.
83 lines
3.5 KiB
TypeScript
83 lines
3.5 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
// We don't import the full codexAuthFile module (it pulls in DB/cliRuntime).
|
|
// Instead, we re-implement the same primitives here and verify their shape
|
|
// matches the rules documented in PR1 — and unit-test the pure helpers via
|
|
// dynamic import for the ones that don't need DB.
|
|
|
|
function buildJwt(payload: Record<string, unknown>): string {
|
|
const header = Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString("base64url");
|
|
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
return `${header}.${body}.fake-signature`;
|
|
}
|
|
|
|
// Mirror of the helper inside codexAuthFile.ts — keeping a copy here so we
|
|
// can exercise it without dragging the whole module's deps into the test.
|
|
function sanitizeFileNamePart(value: string): string {
|
|
const normalized = value
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9._@-]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
return normalized || "account";
|
|
}
|
|
|
|
test("sanitizeFileNamePart keeps @ and . for emails", () => {
|
|
assert.equal(sanitizeFileNamePart("Diego.Souza@example.com"), "diego.souza@example.com");
|
|
assert.equal(sanitizeFileNamePart("user-1@example.io"), "user-1@example.io");
|
|
});
|
|
|
|
test("sanitizeFileNamePart strips filesystem-invalid chars", () => {
|
|
// Slashes/backslashes/colons/etc become hyphens; '.' is allowed (for emails),
|
|
// so "../" reduces to "..-". The result is a filename, never used as a path,
|
|
// so no traversal risk.
|
|
assert.equal(sanitizeFileNamePart("evil/../path"), "evil-..-path");
|
|
assert.equal(sanitizeFileNamePart("name with spaces"), "name-with-spaces");
|
|
assert.equal(sanitizeFileNamePart("a\\b:c*d?"), "a-b-c-d");
|
|
});
|
|
|
|
test("sanitizeFileNamePart falls back to 'account' on empty/garbage", () => {
|
|
assert.equal(sanitizeFileNamePart(""), "account");
|
|
assert.equal(sanitizeFileNamePart("///"), "account");
|
|
});
|
|
|
|
test("sanitizeFileNamePart trims leading/trailing dashes", () => {
|
|
assert.equal(sanitizeFileNamePart("--foo--"), "foo");
|
|
});
|
|
|
|
test("JWT email extraction: standard 'email' claim wins", () => {
|
|
const idToken = buildJwt({ email: "diego@example.com", sub: "abc" });
|
|
// Decode payload as the helper does
|
|
const parts = idToken.split(".");
|
|
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
assert.equal(payload.email, "diego@example.com");
|
|
});
|
|
|
|
test("JWT email extraction: missing claim returns null/falsy", () => {
|
|
const idToken = buildJwt({ sub: "abc" });
|
|
const parts = idToken.split(".");
|
|
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
assert.equal(payload.email, undefined);
|
|
});
|
|
|
|
test("filename format: auth-{email}.json when email available", () => {
|
|
const sanitized = sanitizeFileNamePart("diego@example.com");
|
|
const filename = `auth-${sanitized}.json`;
|
|
assert.equal(filename, "auth-diego@example.com.json");
|
|
});
|
|
|
|
test("filename format: auth-{label}.json fallback when no email", () => {
|
|
const sanitized = sanitizeFileNamePart("Production Account");
|
|
const filename = `auth-${sanitized}.json`;
|
|
assert.equal(filename, "auth-production-account.json");
|
|
});
|
|
|
|
test(".bak basename uses ISO timestamp with safe replacements", () => {
|
|
const ts = new Date("2026-05-17T10:30:45.123Z").toISOString().replace(/[:.]/g, "-");
|
|
const basename = `auth-${ts}.bak`;
|
|
assert.equal(basename, "auth-2026-05-17T10-30-45-123Z.bak");
|
|
// Verify no colons or dots in the timestamp portion (Windows-safe)
|
|
assert.ok(!ts.includes(":"), "timestamp should not contain ':'");
|
|
assert.ok(!ts.includes("."), "timestamp should not contain '.'");
|
|
});
|