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: 5 additions & 0 deletions .changeset/file-secrets-data-dir.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@executor-js/plugin-file-secrets": patch
---

`fileSecretsPlugin()` now stores `auth.json` under `EXECUTOR_DATA_DIR` when that variable is set (an explicit `directory` option still wins; the XDG location remains the fallback when it is unset). Existing secrets in the legacy XDG location are migrated automatically on first use. This keeps all daemon state under one directory, so persisting `EXECUTOR_DATA_DIR` alone preserves credentials across environment recreation.
115 changes: 115 additions & 0 deletions packages/plugins/file-secrets/src/data-dir.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "@effect/vitest";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Effect } from "effect";

import {
AuthTemplateSlug,
ConnectionName,
IntegrationSlug,
ProviderKey,
definePlugin,
} from "@executor-js/sdk";
import { makeTestWorkspaceHarness } from "@executor-js/sdk/testing";

import { fileSecretsPlugin } from "./index";

const INTEGRATION = IntegrationSlug.make("durable-secrets");
const CONNECTION = ConnectionName.make("main");
const TEMPLATE = AuthTemplateSlug.make("apiKey");
const CREDENTIAL = "secret-token";

const connectionFixturePlugin = definePlugin(() => ({
id: "connectionFixture" as const,
storage: () => ({}),
resolveTools: () => Effect.succeed({ tools: [] }),
extension: (ctx) => ({
registerIntegration: () =>
ctx.core.integrations.register({
slug: INTEGRATION,
description: "Durable secrets test integration",
config: {},
}),
resolveCredential: () =>
ctx.connections.resolveValue({
owner: "org",
integration: INTEGRATION,
name: CONNECTION,
}),
}),
}))();

const plugins = () => [fileSecretsPlugin(), connectionFixturePlugin] as const;

describe("file secrets data directory", () => {
let workDir: string;
let dataDir: string;
let firstSandboxDataHome: string;
let recreatedSandboxDataHome: string;

beforeEach(() => {
workDir = mkdtempSync(join(tmpdir(), "executor-file-secrets-data-dir-"));
dataDir = join(workDir, "executor-data");
firstSandboxDataHome = join(workDir, "sandbox-a-xdg");
recreatedSandboxDataHome = join(workDir, "sandbox-b-xdg");
mkdirSync(dataDir, { recursive: true });
mkdirSync(firstSandboxDataHome, { recursive: true });
mkdirSync(recreatedSandboxDataHome, { recursive: true });
vi.stubEnv("EXECUTOR_DATA_DIR", dataDir);
});

afterEach(() => {
vi.unstubAllEnvs();
rmSync(workDir, { recursive: true, force: true });
});

it.effect("keeps credentials when only EXECUTOR_DATA_DIR survives sandbox recreation", () =>
Effect.gen(function* () {
vi.stubEnv("XDG_DATA_HOME", firstSandboxDataHome);
const firstAuthPath = yield* Effect.scoped(
Effect.gen(function* () {
const first = yield* makeTestWorkspaceHarness({ dataDir, plugins: plugins() });
yield* first.executor.connectionFixture.registerIntegration();
const connection = yield* first.executor.connections.create({
owner: "org",
name: CONNECTION,
integration: INTEGRATION,
template: TEMPLATE,
value: CREDENTIAL,
});

expect(connection.provider).toBe(ProviderKey.make("file"));
const authPath = first.executor.fileSecrets.filePath;
expect(authPath).toBe(join(dataDir, "auth.json"));
expect(existsSync(authPath)).toBe(true);
expect(readFileSync(authPath, "utf8")).toContain(
'"connection:org:durable-secrets:main:token": "secret-token"',
);
expect(existsSync(join(dataDir, "test.db"))).toBe(true);
return authPath;
}),
);

vi.stubEnv("XDG_DATA_HOME", recreatedSandboxDataHome);
yield* Effect.scoped(
Effect.gen(function* () {
const recreated = yield* makeTestWorkspaceHarness({ dataDir, plugins: plugins() });
const connections = yield* recreated.executor.connections.list({
integration: INTEGRATION,
});
expect(connections.map((connection) => String(connection.name))).toEqual(["main"]);

const resolved = yield* recreated.executor.connectionFixture.resolveCredential();

// Regression: auth.json follows XDG_DATA_HOME instead of EXECUTOR_DATA_DIR.
// After the fix it must live with test.db under dataDir and survive this home swap.
expect(
resolved,
`credential persisted in ${firstAuthPath} was not available after recreating the sandbox with ${dataDir}`,
).toBe(CREDENTIAL);
}),
);
}),
);
});
208 changes: 208 additions & 0 deletions packages/plugins/file-secrets/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "@effect/vitest";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { Effect, Predicate, Result } from "effect";

import { ProviderKey } from "@executor-js/sdk";
import { makeTestWorkspaceHarness } from "@executor-js/sdk/testing";

import { fileSecretsPlugin } from "./index";

const FILE_PROVIDER = ProviderKey.make("file");

const inspectPlugin = (plugin: ReturnType<typeof fileSecretsPlugin>) =>
Effect.scoped(
Effect.gen(function* () {
const workspace = yield* makeTestWorkspaceHarness({ plugins: [plugin] as const });
const items = yield* workspace.executor.providers.items(FILE_PROVIDER);
return {
filePath: workspace.executor.fileSecrets.filePath,
itemIds: items.map((item) => String(item.id)),
};
}),
);

const writeAuthFile = (filePath: string, contents: string, mode = 0o600): void => {
mkdirSync(dirname(filePath), { recursive: true });
writeFileSync(filePath, contents, { mode });
};

describe("file secrets auth location", () => {
let workDir: string;
let dataDir: string;
let otherDataDir: string;
let xdgDataHome: string;
let overrideDir: string;
let legacyFilePath: string;

beforeEach(() => {
workDir = mkdtempSync(join(tmpdir(), "executor-file-secrets-location-"));
dataDir = join(workDir, "data");
otherDataDir = join(workDir, "other-data");
xdgDataHome = join(workDir, "xdg");
overrideDir = join(workDir, "override");
legacyFilePath = join(xdgDataHome, "executor", "auth.json");
vi.stubEnv("XDG_DATA_HOME", xdgDataHome);
});

afterEach(() => {
vi.unstubAllEnvs();
rmSync(workDir, { recursive: true, force: true });
});

it.effect("uses auth.json directly under EXECUTOR_DATA_DIR resolved at construction", () =>
Effect.gen(function* () {
vi.stubEnv("EXECUTOR_DATA_DIR", dataDir);
const plugin = fileSecretsPlugin();
vi.stubEnv("EXECUTOR_DATA_DIR", otherDataDir);

const inspected = yield* inspectPlugin(plugin);

expect(inspected.filePath).toBe(join(dataDir, "auth.json"));
}),
);

it.effect("keeps the XDG location when EXECUTOR_DATA_DIR is unset", () =>
Effect.gen(function* () {
vi.stubEnv("EXECUTOR_DATA_DIR", "");

const inspected = yield* inspectPlugin(fileSecretsPlugin());

expect(inspected.filePath).toBe(legacyFilePath);
}),
);

it.effect("gives an explicit directory precedence without migration", () =>
Effect.gen(function* () {
vi.stubEnv("EXECUTOR_DATA_DIR", dataDir);
writeAuthFile(legacyFilePath, '{"legacy":"legacy-secret"}');

const inspected = yield* inspectPlugin(fileSecretsPlugin({ directory: overrideDir }));

expect(inspected.filePath).toBe(join(overrideDir, "auth.json"));
expect(inspected.itemIds).toEqual([]);
expect(existsSync(join(dataDir, "auth.json"))).toBe(false);
expect(readFileSync(legacyFilePath, "utf8")).toBe('{"legacy":"legacy-secret"}');
}),
);

it.effect("copies a valid legacy XDG file once and preserves 0600 permissions", () =>
Effect.gen(function* () {
vi.stubEnv("EXECUTOR_DATA_DIR", dataDir);
const legacyContents = '{"legacy-token":"legacy-secret"}';
writeAuthFile(legacyFilePath, legacyContents, 0o644);

const inspected = yield* inspectPlugin(fileSecretsPlugin());
const migratedFilePath = join(dataDir, "auth.json");

expect(inspected.filePath).toBe(migratedFilePath);
expect(inspected.itemIds).toEqual(["legacy-token"]);
expect(readFileSync(migratedFilePath, "utf8")).toContain('"legacy-token": "legacy-secret"');
expect(statSync(migratedFilePath).mode & 0o777).toBe(0o600);
expect(readFileSync(legacyFilePath, "utf8")).toBe(legacyContents);
}),
);

it.effect("uses an existing data-dir file without merging the legacy file", () =>
Effect.gen(function* () {
vi.stubEnv("EXECUTOR_DATA_DIR", dataDir);
const activeFilePath = join(dataDir, "auth.json");
const activeContents = '{"active-token":"active-secret"}';
const legacyContents = '{"legacy-token":"legacy-secret"}';
writeAuthFile(activeFilePath, activeContents);
writeAuthFile(legacyFilePath, legacyContents);

const inspected = yield* inspectPlugin(fileSecretsPlugin());

expect(inspected.itemIds).toEqual(["active-token"]);
expect(readFileSync(activeFilePath, "utf8")).toBe(activeContents);
expect(readFileSync(legacyFilePath, "utf8")).toBe(legacyContents);
}),
);

it.effect("leaves the new store empty when the legacy file is corrupt", () =>
Effect.scoped(
Effect.gen(function* () {
vi.stubEnv("EXECUTOR_DATA_DIR", dataDir);
writeAuthFile(legacyFilePath, "not-json");
const workspace = yield* makeTestWorkspaceHarness({
plugins: [fileSecretsPlugin()] as const,
});

const initial = yield* workspace.executor.providers.items(FILE_PROVIDER);
expect(initial).toEqual([]);
expect(existsSync(join(dataDir, "auth.json"))).toBe(false);
expect(readFileSync(legacyFilePath, "utf8")).toBe("not-json");

writeAuthFile(legacyFilePath, '{"repaired-token":"repaired-secret"}');
const afterRepair = yield* workspace.executor.providers.items(FILE_PROVIDER);
expect(afterRepair).toEqual([]);
expect(existsSync(join(dataDir, "auth.json"))).toBe(false);
}),
),
);

it.effect("retries migration after a legacy read I/O failure", () =>
Effect.scoped(
Effect.gen(function* () {
vi.stubEnv("EXECUTOR_DATA_DIR", dataDir);
mkdirSync(legacyFilePath, { recursive: true });
const workspace = yield* makeTestWorkspaceHarness({
plugins: [fileSecretsPlugin()] as const,
});

const failed = yield* Effect.result(workspace.executor.providers.items(FILE_PROVIDER));
expect(Result.isFailure(failed)).toBe(true);
if (!Result.isFailure(failed)) return;
expect(Predicate.isTagged("StorageError")(failed.failure)).toBe(true);

rmSync(legacyFilePath, { recursive: true, force: true });
writeAuthFile(legacyFilePath, '{"recovered-token":"recovered-secret"}');

const recovered = yield* workspace.executor.providers.items(FILE_PROVIDER);
expect(recovered.map((item) => String(item.id))).toEqual(["recovered-token"]);
expect(readFileSync(join(dataDir, "auth.json"), "utf8")).toContain(
'"recovered-token": "recovered-secret"',
);
}),
),
);

it.effect("shares one migration across concurrent first provider operations", () =>
Effect.scoped(
Effect.gen(function* () {
vi.stubEnv("EXECUTOR_DATA_DIR", dataDir);
const legacyContents = '{"legacy-token":"legacy-secret"}';
writeAuthFile(legacyFilePath, legacyContents);
const workspace = yield* makeTestWorkspaceHarness({
plugins: [fileSecretsPlugin()] as const,
});

const results = yield* Effect.all(
[
workspace.executor.providers.items(FILE_PROVIDER),
workspace.executor.providers.items(FILE_PROVIDER),
],
{ concurrency: "unbounded" },
);

expect(results.map((items) => items.map((item) => String(item.id)))).toEqual([
["legacy-token"],
["legacy-token"],
]);
expect(readdirSync(dataDir)).toEqual(["auth.json"]);
expect(readFileSync(legacyFilePath, "utf8")).toBe(legacyContents);
}),
),
);
});
Loading
Loading