Add GoogleTokenStore for workspace-local refresh token
This commit is contained in:
56
src/tools/google-token-store.test.ts
Normal file
56
src/tools/google-token-store.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { createGoogleTokenStore } from "./google-token-store.js";
|
||||
|
||||
describe("createGoogleTokenStore", () => {
|
||||
let dir: string;
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(path.join(tmpdir(), "vs-token-"));
|
||||
});
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("read returns null when the file does not exist", () => {
|
||||
const store = createGoogleTokenStore(dir);
|
||||
expect(store.read()).toBeNull();
|
||||
});
|
||||
|
||||
it("read returns null when the file is empty, malformed, or missing the key", () => {
|
||||
const store = createGoogleTokenStore(dir);
|
||||
const p = path.join(dir, ".secretary", "google-token.json");
|
||||
mkdirSync(path.dirname(p), { recursive: true });
|
||||
// Empty
|
||||
writeFileSync(p, "", { mode: 0o600 });
|
||||
expect(store.read()).toBeNull();
|
||||
// Malformed
|
||||
writeFileSync(p, "{not-json", { mode: 0o600 });
|
||||
expect(store.read()).toBeNull();
|
||||
// Right shape but no key
|
||||
writeFileSync(p, JSON.stringify({ other: "x" }), { mode: 0o600 });
|
||||
expect(store.read()).toBeNull();
|
||||
});
|
||||
|
||||
it("write creates the .secretary directory if it does not exist and round-trips through read", () => {
|
||||
const store = createGoogleTokenStore(dir);
|
||||
store.write("refresh-token-abc");
|
||||
expect(store.read()).toBe("refresh-token-abc");
|
||||
});
|
||||
|
||||
it("write replaces a previous token", () => {
|
||||
const store = createGoogleTokenStore(dir);
|
||||
store.write("first");
|
||||
store.write("second");
|
||||
expect(store.read()).toBe("second");
|
||||
});
|
||||
|
||||
it("write applies mode 0600 on POSIX", () => {
|
||||
if (process.platform === "win32") return;
|
||||
const store = createGoogleTokenStore(dir);
|
||||
store.write("t");
|
||||
const mode = statSync(path.join(dir, ".secretary", "google-token.json")).mode & 0o777;
|
||||
expect(mode).toBe(0o600);
|
||||
});
|
||||
});
|
||||
31
src/tools/google-token-store.ts
Normal file
31
src/tools/google-token-store.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export interface GoogleTokenStore {
|
||||
read(): string | null;
|
||||
write(refreshToken: string): void;
|
||||
}
|
||||
|
||||
export function createGoogleTokenStore(workspaceDir: string): GoogleTokenStore {
|
||||
const dir = path.join(workspaceDir, ".secretary");
|
||||
const file = path.join(dir, "google-token.json");
|
||||
return {
|
||||
read() {
|
||||
if (!existsSync(file)) return null;
|
||||
try {
|
||||
const raw = readFileSync(file, "utf8");
|
||||
if (!raw.trim()) return null;
|
||||
const parsed = JSON.parse(raw) as { refresh_token?: unknown };
|
||||
return typeof parsed.refresh_token === "string" && parsed.refresh_token
|
||||
? parsed.refresh_token
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
write(refreshToken: string) {
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(file, JSON.stringify({ refresh_token: refreshToken }), { mode: 0o600 });
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user