fix(credentials): one vault per user, in the config dir (#119)

The credential store resolved its base directory against process.cwd().
Running the CLI from inside a git checkout wrote `.logicsrc/credentials`
into that repo's working tree — a directory containing `vault/`, the one
place raw credential values touch disk — untracked, unignored, and one
`git add -A` from being committed. Two such directories were sitting in
unrelated repos on the machine this was found on.

A per-directory store is also the wrong shape for what the store is for.
It is the record of what was rotated and what the prior values were, and
a record that forks per project folder is several records that disagree.
There is one user, one identity, one vault.

Everything now hangs off a single logicsrcHome(): $LOGICSRC_HOME, else
$XDG_CONFIG_HOME/logicsrc, else ~/.config/logicsrc. The credential store,
the identity and the CLI config all read it rather than each deriving
their own answer — three separate derivations is how the vault ended up
somewhere the config never was.

~/.logicsrc is migrated rather than abandoned. It holds the X25519 secret
key, and losing that loses access to every team vault the member was ever
given, so it is moved on first use; a move that fails says so on stderr
instead of leaving someone silently logged out with a key still on disk
somewhere they were not told about. If the new directory already exists
it wins and the old one is left untouched, because two directories both
claiming to be the identity is how a login writes one and a read finds
the other.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-07-31 22:53:17 -07:00 committed by GitHub
parent 87266bb815
commit 36236eb1a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 223 additions and 27 deletions

View file

@ -3,7 +3,7 @@
Logicsrc stores user config at: Logicsrc stores user config at:
```text ```text
$HOME/.logicsrc/config.json $HOME/.config/logicsrc/config.json
``` ```
Read and write values with dot paths: Read and write values with dot paths:

View file

@ -21,7 +21,7 @@ logicsrc credentials inspect --provider env --path .env
logicsrc credentials diff --from env --from-path .env --to railway \ logicsrc credentials diff --from env --from-path .env --to railway \
--to-project <projectId> --to-config <environmentId> --to-project <projectId> --to-config <environmentId>
# Build a plan (stored under .logicsrc/credentials), then dry-run, then apply # Build a plan (stored under ~/.config/logicsrc/credentials), then dry-run, then apply
logicsrc credentials plan --from env --from-path .env --to doppler \ logicsrc credentials plan --from env --from-path .env --to doppler \
--to-project <project> --to-config <config> --to-project <project> --to-config <config>
logicsrc credentials sync --plan <planId> # dry-run (no writes) logicsrc credentials sync --plan <planId> # dry-run (no writes)
@ -40,8 +40,9 @@ Implementation notes:
- `github-secrets` is write-only for values (GitHub never returns secret values), so - `github-secrets` is write-only for values (GitHub never returns secret values), so
it cannot be a sync source or a value-restoring rollback target. Secret writes are it cannot be a sync source or a value-restoring rollback target. Secret writes are
libsodium sealed-box encrypted against the repo/org/environment public key. libsodium sealed-box encrypted against the repo/org/environment public key.
- Rollback captures the target's prior values into a 0600 vault under `.logicsrc/` - Rollback captures the target's prior values into a 0600 vault under
(gitignored) — the only place raw values touch disk. Plans, runs, and audit records `~/.config/logicsrc/` — outside any project, so there is nothing to gitignore
and nothing lands in a repo. The only place raw values touch disk. Plans, runs, and audit records
contain fingerprints only. contain fingerprints only.
Credential Sharing is a LogicSRC OpenSpec for portable, auditable secret synchronization across local files and infrastructure providers. It is intended to replace closed, proprietary credential-sharing workflows with a provider-neutral contract. Credential Sharing is a LogicSRC OpenSpec for portable, auditable secret synchronization across local files and infrastructure providers. It is intended to replace closed, proprietary credential-sharing workflows with a provider-neutral contract.
@ -195,7 +196,7 @@ relay for secret values**. It stores only:
Plaintext secret values and the raw DEK never leave a member's machine. Granting a Plaintext secret values and the raw DEK never leave a member's machine. Granting a
teammate access = an existing member unwraps the DEK with their private key and teammate access = an existing member unwraps the DEK with their private key and
re-wraps (seals) it to the new member's public key. The private key lives only in re-wraps (seals) it to the new member's public key. The private key lives only in
`~/.logicsrc/identity.json` (mode 0600) and is never uploaded. `~/.config/logicsrc/identity.json` (mode 0600) and is never uploaded.
### CLI ### CLI
@ -275,7 +276,7 @@ Safety properties, all enforced rather than documented:
It talks to the hosted credentials app by default. Point it elsewhere (local dev, It talks to the hosted credentials app by default. Point it elsewhere (local dev,
self-hosted) with `LOGICSRC_API=http://localhost:8080 logicsrc login` or self-hosted) with `LOGICSRC_API=http://localhost:8080 logicsrc login` or
`logicsrc login --api-url …`; the chosen origin is remembered in `logicsrc login --api-url …`; the chosen origin is remembered in
`~/.logicsrc/identity.json` once login succeeds. `~/.config/logicsrc/identity.json` once login succeeds.
Because `team` is a normal provider, the generic sync surface works too — e.g. Because `team` is a normal provider, the generic sync surface works too — e.g.
`logicsrc credentials plan --from env --from-path .env --to team --to-project acme `logicsrc credentials plan --from env --from-path .env --to team --to-project acme

View file

@ -1,6 +1,6 @@
{ {
"name": "@logicsrc/cli", "name": "@logicsrc/cli",
"version": "0.1.0", "version": "0.1.1",
"description": "LogicSRC OpenSpec CLI.", "description": "LogicSRC OpenSpec CLI.",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View file

@ -1,6 +1,6 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { homedir } from "node:os"; import { logicsrcHome } from "@logicsrc/plugin-credential-sharing";
export type JsonObject = Record<string, unknown>; export type JsonObject = Record<string, unknown>;
@ -21,8 +21,14 @@ export const defaultConfig: JsonObject = {
} }
}; };
/**
* The same one directory the identity and the vault use.
*
* Shared rather than re-derived: three copies of "where does logicsrc keep
* things" is how the vault ended up somewhere the config never was.
*/
export function configPath() { export function configPath() {
return join(homedir(), ".logicsrc", "config.json"); return join(logicsrcHome(), "config.json");
} }
export function readConfig() { export function readConfig() {

View file

@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { evaluateAccountPolicy, scoreAccountActionRisk } from "@logicsrc/account-core"; import { evaluateAccountPolicy, scoreAccountActionRisk } from "@logicsrc/account-core";
import { Command } from "commander"; import { Command } from "commander";
import { createCredentialEngine, listCredentialProviders, type CredentialEndpoint } from "@logicsrc/plugin-credential-sharing"; import { createCredentialEngine, listCredentialProviders, logicsrcHome, type CredentialEndpoint } from "@logicsrc/plugin-credential-sharing";
import { listEmailAccountProviders } from "@logicsrc/plugin-email-accounts"; import { listEmailAccountProviders } from "@logicsrc/plugin-email-accounts";
import { discoverFeeds, listFeedProviders, probeSite, renderDiscoveryOutput, validateFeed, type FeedKind, type FeedOutputFormat } from "@logicsrc/plugin-feed-discovery"; import { discoverFeeds, listFeedProviders, probeSite, renderDiscoveryOutput, validateFeed, type FeedKind, type FeedOutputFormat } from "@logicsrc/plugin-feed-discovery";
import { listSocialAccountProviders } from "@logicsrc/plugin-social-accounts"; import { listSocialAccountProviders } from "@logicsrc/plugin-social-accounts";
@ -822,12 +822,14 @@ program
process.exitCode = 1; process.exitCode = 1;
return; return;
} }
console.log(`Updated. Install root: ${installHome()} — config preserved at ~/.logicsrc`); console.log(`Updated. Install root: ${installHome()} — config preserved at ${logicsrcHome()}`);
}); });
program.command("remove").alias("uninstall").option("--purge", "Remove config and auth tokens").description("Remove local LogicSRC CLI.").action((options) => { program.command("remove").alias("uninstall").option("--purge", "Remove config and auth tokens").description("Remove local LogicSRC CLI.").action((options) => {
console.log("Removed LogicSRC CLI."); console.log("Removed LogicSRC CLI.");
console.log(options.purge ? "Removed config and auth tokens from $HOME/.logicsrc." : "Preserved config at $HOME/.logicsrc. Run with --purge to remove config and auth tokens."); console.log(options.purge
? `Removed config and auth tokens from ${logicsrcHome()}.`
: `Preserved config at ${logicsrcHome()}. Run with --purge to remove config and auth tokens.`);
}); });
function validateFile(kindArg: string, file: string) { function validateFile(kindArg: string, file: string) {

View file

@ -12,6 +12,7 @@ import {
defaultApiUrl, defaultApiUrl,
resolveApiUrl, resolveApiUrl,
createCredentialEngine, createCredentialEngine,
identityPath,
unwrapVaultKey, unwrapVaultKey,
wrapVaultKey, wrapVaultKey,
type CredentialEndpoint type CredentialEndpoint
@ -280,7 +281,7 @@ export async function loginAction(options: { apiUrl?: string; token?: string; de
export async function logoutAction(): Promise<void> { export async function logoutAction(): Promise<void> {
await updateIdentity({ apiToken: undefined, email: undefined, userId: undefined }); await updateIdentity({ apiToken: undefined, email: undefined, userId: undefined });
console.error("Logged out (local token cleared; revoke the key at /settings). Identity key retained — delete ~/.logicsrc/identity.json to remove it."); console.error(`Logged out (local token cleared; revoke the key at /settings). Identity key retained — delete ${identityPath()} to remove it.`);
} }
export async function whoamiAction(format: OutputFormat): Promise<void> { export async function whoamiAction(format: OutputFormat): Promise<void> {

View file

@ -26,7 +26,7 @@ export type UpdateStatus = {
latestCommit: string | null; latestCommit: string | null;
}; };
/** Install root the installer uses (not the config dir, which is ~/.logicsrc). */ /** Install root the installer uses (not the config dir, which is ~/.config/logicsrc). */
export function installHome(env: NodeJS.ProcessEnv = process.env): string { export function installHome(env: NodeJS.ProcessEnv = process.env): string {
return env.LOGICSRC_HOME || join(env.HOME || homedir(), ".logicsrc-cli"); return env.LOGICSRC_HOME || join(env.HOME || homedir(), ".logicsrc-cli");
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@logicsrc/plugin-credential-sharing", "name": "@logicsrc/plugin-credential-sharing",
"version": "0.1.0", "version": "0.1.1",
"description": "LogicSRC Credential Sharing OpenSpec plugin: portable, auditable secret sync across .env, Doppler, Railway, and GitHub Secrets.", "description": "LogicSRC Credential Sharing OpenSpec plugin: portable, auditable secret sync across .env, Doppler, Railway, and GitHub Secrets.",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View file

@ -1,4 +1,4 @@
import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync } from "node:fs"; import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync, renameSync } from "node:fs";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path"; import { dirname, join, resolve } from "node:path";
import { generateIdentityKeyPair, publicKeyForSecret, type IdentityKeyPair } from "./crypto.js"; import { generateIdentityKeyPair, publicKeyForSecret, type IdentityKeyPair } from "./crypto.js";
@ -6,7 +6,7 @@ import { generateIdentityKeyPair, publicKeyForSecret, type IdentityKeyPair } fro
/** /**
* Local, machine-bound member identity for team credential sharing. * Local, machine-bound member identity for team credential sharing.
* *
* Stored at `$LOGICSRC_HOME/identity.json` (default `~/.logicsrc/identity.json`), * Stored at `$LOGICSRC_HOME/identity.json` (default `~/.config/logicsrc/identity.json`),
* mode 0600 it holds the member's X25519 SECRET key and the server API token. * mode 0600 it holds the member's X25519 SECRET key and the server API token.
* The secret key never leaves this file; only the public key is uploaded. * The secret key never leaves this file; only the public key is uploaded.
*/ */
@ -25,13 +25,63 @@ export interface LocalIdentity {
updatedAt: string; updatedAt: string;
} }
/**
* The one logicsrc directory for this user, on this machine.
*
* `$LOGICSRC_HOME`, else `$XDG_CONFIG_HOME/logicsrc`, else
* `~/.config/logicsrc`. Never anything derived from the working directory:
* there is a single identity and a single vault per user, and a path that
* moves when you `cd` gives you one of each per directory you happened to be
* standing in which is how a machine ends up with a `.logicsrc/` inside
* unrelated git repos, holding a directory called `credentials/vault`.
*
* A previous install kept this at `~/.logicsrc`. That directory holds the
* X25519 secret key, so it is moved rather than abandoned losing it means
* losing access to every team vault the member was ever given.
*/
export function logicsrcHome(): string { export function logicsrcHome(): string {
if (process.env.LOGICSRC_HOME) { if (process.env.LOGICSRC_HOME) {
return resolve(process.env.LOGICSRC_HOME); return resolve(process.env.LOGICSRC_HOME);
} }
const configHome = process.env.XDG_CONFIG_HOME
? resolve(process.env.XDG_CONFIG_HOME)
: join(homedir(), ".config");
const home = join(configHome, "logicsrc");
migrateLegacyHome(home);
return home;
}
/** Where this lived before the move, kept only to be migrated away from. */
export function legacyLogicsrcHome(): string {
return join(homedir(), ".logicsrc"); return join(homedir(), ".logicsrc");
} }
/**
* Move `~/.logicsrc` to the config dir, once, if the new one is not there yet.
*
* Deliberately a move and not a copy: two directories both claiming to be the
* identity is the state where a login writes to one and a read finds the
* other. If it cannot be moved the failure is named on stderr rather than
* swallowed, because the alternative is a member silently logged out with a
* secret key still sitting somewhere they were not told about.
*/
function migrateLegacyHome(target: string): void {
const legacy = legacyLogicsrcHome();
if (legacy === target || existsSync(target) || !existsSync(legacy)) {
return;
}
try {
mkdirSync(dirname(target), { recursive: true });
renameSync(legacy, target);
} catch (error) {
const why = error instanceof Error ? error.message : String(error);
process.emitWarning(
`logicsrc: could not move ${legacy} to ${target} (${why}). ` +
`Move it by hand — it holds your identity key.`
);
}
}
export function identityPath(): string { export function identityPath(): string {
return process.env.LOGICSRC_IDENTITY_FILE return process.env.LOGICSRC_IDENTITY_FILE
? resolve(process.env.LOGICSRC_IDENTITY_FILE) ? resolve(process.env.LOGICSRC_IDENTITY_FILE)

View file

@ -0,0 +1,122 @@
// Where the identity and the vault live.
//
// These used to be three different answers. The identity was under
// `~/.logicsrc`, the CLI config beside it, and the credential store resolved
// against `process.cwd()` — so the vault was wherever you were standing when
// you ran the command. Running the CLI inside a git checkout wrote a directory
// literally named `credentials/vault` into that repo's working tree: untracked,
// unignored, one `git add -A` from being published.
//
// There is one vault per user, per machine. That is what these pin.
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { logicsrcHome, identityPath, legacyLogicsrcHome } from "./identity.js";
import { defaultCredentialHome } from "./store.js";
const ENV_KEYS = ["LOGICSRC_HOME", "XDG_CONFIG_HOME", "HOME", "LOGICSRC_CREDENTIAL_HOME", "LOGICSRC_IDENTITY_FILE"] as const;
let saved: Record<string, string | undefined>;
let sandbox: string;
beforeEach(() => {
saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
sandbox = mkdtempSync(join(tmpdir(), "logicsrc-paths-"));
for (const k of ENV_KEYS) delete process.env[k];
process.env.HOME = sandbox;
});
afterEach(() => {
for (const [k, v] of Object.entries(saved)) {
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
rmSync(sandbox, { recursive: true, force: true });
});
describe("logicsrc home", () => {
it("defaults to ~/.config/logicsrc", () => {
expect(logicsrcHome()).toBe(join(sandbox, ".config", "logicsrc"));
});
it("honours XDG_CONFIG_HOME", () => {
process.env.XDG_CONFIG_HOME = join(sandbox, "xdg");
expect(logicsrcHome()).toBe(join(sandbox, "xdg", "logicsrc"));
});
it("lets LOGICSRC_HOME override everything", () => {
process.env.LOGICSRC_HOME = join(sandbox, "explicit");
expect(logicsrcHome()).toBe(join(sandbox, "explicit"));
});
});
describe("the credential store", () => {
it("never resolves against the working directory", () => {
// The regression this exists for. Whatever the cwd is, the vault is not
// under it — a `.logicsrc/` appearing inside a project is the bug.
const home = defaultCredentialHome();
expect(home).toBe(join(sandbox, ".config", "logicsrc", "credentials"));
expect(home.startsWith(process.cwd())).toBe(false);
});
it("is the same store no matter where the CLI is run from", () => {
const before = defaultCredentialHome();
const elsewhere = mkdtempSync(join(tmpdir(), "logicsrc-cwd-"));
const original = process.cwd();
try {
process.chdir(elsewhere);
expect(defaultCredentialHome()).toBe(before);
} finally {
process.chdir(original);
rmSync(elsewhere, { recursive: true, force: true });
}
});
it("still takes an explicit LOGICSRC_CREDENTIAL_HOME", () => {
process.env.LOGICSRC_CREDENTIAL_HOME = join(sandbox, "vol", "creds");
expect(defaultCredentialHome()).toBe(join(sandbox, "vol", "creds"));
});
it("sits beside the identity, under one home", () => {
expect(defaultCredentialHome()).toBe(join(logicsrcHome(), "credentials"));
expect(identityPath()).toBe(join(logicsrcHome(), "identity.json"));
});
});
describe("migrating off ~/.logicsrc", () => {
it("moves the old directory, keeping the identity key", () => {
// The secret key is the whole account: losing it loses every team vault
// the member was ever given. So this is a move, not a fresh start.
const legacy = legacyLogicsrcHome();
mkdirSync(legacy, { recursive: true });
writeFileSync(join(legacy, "identity.json"), '{"keys":{"secretKey":"kept"}}');
const home = logicsrcHome();
expect(existsSync(legacy)).toBe(false);
expect(JSON.parse(readFileSync(join(home, "identity.json"), "utf8")).keys.secretKey).toBe("kept");
});
it("leaves the old directory alone once the new one exists", () => {
// Two directories both claiming to be the identity is the state where a
// login writes one and a read finds the other. Whatever is already at the
// new path wins; the legacy one is not merged over it.
const legacy = legacyLogicsrcHome();
mkdirSync(legacy, { recursive: true });
writeFileSync(join(legacy, "identity.json"), '{"keys":{"secretKey":"old"}}');
const home = join(sandbox, ".config", "logicsrc");
mkdirSync(home, { recursive: true });
writeFileSync(join(home, "identity.json"), '{"keys":{"secretKey":"current"}}');
logicsrcHome();
expect(JSON.parse(readFileSync(join(home, "identity.json"), "utf8")).keys.secretKey).toBe("current");
expect(existsSync(legacy)).toBe(true);
});
it("does nothing when there is no legacy directory", () => {
const home = logicsrcHome();
expect(existsSync(legacyLogicsrcHome())).toBe(false);
expect(home).toBe(join(sandbox, ".config", "logicsrc"));
});
});

View file

@ -17,7 +17,7 @@ import type {
* vault). Secret values are encrypted/decrypted on THIS machine with the vault * vault). Secret values are encrypted/decrypted on THIS machine with the vault
* DEK; the server only ever sees ciphertext and the DEK sealed to member keys. * DEK; the server only ever sees ciphertext and the DEK sealed to member keys.
* *
* Auth + identity come from the local `~/.logicsrc/identity.json` (via * Auth + identity come from the local `~/.config/logicsrc/identity.json` (via
* `logicsrc login`), mirroring how `env` reads files and `github-secrets` reads * `logicsrc login`), mirroring how `env` reads files and `github-secrets` reads
* GITHUB_TOKEN the provider is pure I/O over ambient credentials. * GITHUB_TOKEN the provider is pure I/O over ambient credentials.
*/ */
@ -77,7 +77,7 @@ export const teamProvider: CredentialProvider = {
name: "LogicSRC Team Vault", name: "LogicSRC Team Vault",
description: "End-to-end-encrypted team credential vault. Share secrets with teammates by email — the server never sees plaintext.", description: "End-to-end-encrypted team credential vault. Share secrets with teammates by email — the server never sees plaintext.",
status: "available", status: "available",
authRequirements: ["logicsrc login (identity at ~/.logicsrc/identity.json)"], authRequirements: ["logicsrc login (identity at ~/.config/logicsrc/identity.json)"],
capabilities: { readValues: true, readNames: true, write: true, delete: true, rollback: true, audit: true }, capabilities: { readValues: true, readNames: true, write: true, delete: true, rollback: true, audit: true },
async inspect(endpoint: CredentialEndpoint): Promise<CredentialSnapshot> { async inspect(endpoint: CredentialEndpoint): Promise<CredentialSnapshot> {

View file

@ -1,21 +1,23 @@
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs"; import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path"; import { join, resolve } from "node:path";
import { logicsrcHome } from "./identity.js";
import type { CredentialSyncPlan, CredentialSyncRun, CredentialAuditEvent, CredentialValueBag } from "./types.js"; import type { CredentialSyncPlan, CredentialSyncRun, CredentialAuditEvent, CredentialValueBag } from "./types.js";
/** /**
* File-backed store so the CLI can reference plans/runs by id across invocations. * File-backed store so the CLI can reference plans/runs by id across invocations.
* *
* Layout under the base dir (default `$LOGICSRC_CREDENTIAL_HOME` or * Layout under the base dir (default `$LOGICSRC_CREDENTIAL_HOME` or
* `<cwd>/.logicsrc/credentials`): * `~/.config/logicsrc/credentials`):
* plans/<id>.json redacted sync plans (fingerprints only) * plans/<id>.json redacted sync plans (fingerprints only)
* runs/<id>.json run records (fingerprints only) * runs/<id>.json run records (fingerprints only)
* audit/<runId>.json audit events (fingerprints only) * audit/<runId>.json audit events (fingerprints only)
* vault/<runId>.json rollback pre-image RAW prior target values, mode 0600 * vault/<runId>.json rollback pre-image RAW prior target values, mode 0600
* *
* The vault is the only place raw values touch disk, and only to make rollback * The vault is the only place raw values touch disk, and only to make rollback
* possible. It is written 0600 and lives under a `.logicsrc` dir that callers * possible. It is written 0600 and lives in the user's config dir, outside any
* should gitignore. Audit and plan records never contain raw values. * project so there is nothing for a caller to gitignore, and nothing that
* lands in a repo because the CLI was run from inside one. Audit and plan
* records never contain raw values.
*/ */
export interface CredentialStore { export interface CredentialStore {
baseDir: string; baseDir: string;
@ -29,14 +31,26 @@ export interface CredentialStore {
getVault(runId: string): CredentialValueBag | undefined; getVault(runId: string): CredentialValueBag | undefined;
} }
/**
* The one credential store for this user, on this machine.
*
* This used to fall back to `<cwd>/.logicsrc/credentials`, which meant the
* vault was wherever you happened to be standing: run the CLI in a git
* checkout and it wrote a directory named `credentials/vault` into that
* repo's working tree untracked, unignored, one `git add -A` away from
* being published. Worse, the store is meant to be the record of what was
* rotated, and a per-directory store is a record with as many disagreeing
* copies as you have project folders.
*
* There is one vault per user. `$LOGICSRC_CREDENTIAL_HOME` still points it
* somewhere explicit, for tests and for anyone keeping it on a mounted
* volume; nothing derives it from the working directory any more.
*/
export function defaultCredentialHome(): string { export function defaultCredentialHome(): string {
if (process.env.LOGICSRC_CREDENTIAL_HOME) { if (process.env.LOGICSRC_CREDENTIAL_HOME) {
return resolve(process.env.LOGICSRC_CREDENTIAL_HOME); return resolve(process.env.LOGICSRC_CREDENTIAL_HOME);
} }
if (process.env.LOGICSRC_HOME) { return join(logicsrcHome(), "credentials");
return resolve(process.env.LOGICSRC_HOME, "credentials");
}
return resolve(process.cwd(), ".logicsrc", "credentials");
} }
function readJson<T>(file: string): T | undefined { function readJson<T>(file: string): T | undefined {