mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-03 21:27:07 +00:00
Every secret now has a category derived from its name (db, social, server,
api, cloud, finance, crypto, ai, email, messaging, storage, dns, analytics,
devtools, auth, config, other). One rule table in @logicsrc/opencreds serves
both vaults; services win over generic words, so STRIPE_WEBHOOK_SECRET is
finance, not auth. Checked against the 1,208 distinct key names in the
profullstack team: 74 fall to "other".
Team vaults (where the shared .env secrets live):
- teams categories [team] the filter words, with per-category counts
- teams secrets <team> [project] [env] --category/-c --search/-s
names + categories, never decrypts; --format csv
- teams export <team> [project] [env] --category -o file.csv [--yes]
decrypts into team,project,env,category,key,
value,updated_at (0600); skips vaults without a
grant and names them
Personal vault (OpenCreds):
- vault list --category, and the category column in list output
- vault export --format csv: one flat row per item, keeps key/account
secrets that a Bitwarden CSV drops; --category on every export format
DX:
- examples in `logicsrc vault help` / -h / --help that start by saying which
of the two vaults you want, plus examples on teams and each subcommand
- password prompts go to stderr, so eval "$(logicsrc vault unlock)" works
- hints name the command you actually ran (logicsrc vault init, not
opencreds init)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
SECRET_CATEGORIES,
|
|
SECRET_CATEGORY_IDS,
|
|
categorizeItem,
|
|
categorizeSecret,
|
|
csvLine,
|
|
parseCategories,
|
|
toSimpleCsv,
|
|
} from "./categories.js";
|
|
import { createItem } from "./items.js";
|
|
|
|
describe("categorizeSecret", () => {
|
|
it("puts every documented example in its own category", () => {
|
|
for (const category of SECRET_CATEGORIES) {
|
|
for (const example of category.examples) {
|
|
expect([example, categorizeSecret(example)]).toEqual([example, category.id]);
|
|
}
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
["DATABASE_URL", "db"],
|
|
["SUPABASE_SERVICE_ROLE_KEY", "db"],
|
|
["SUPABAsE_URL", "db"],
|
|
["TWITTER_API_KEY", "social"],
|
|
["X_CLIENT_SECRET", "social"],
|
|
["SSH_PORT", "server"],
|
|
["SEED1_SUDO_PASSWORD", "server"],
|
|
["TMDB_API_KEY", "api"],
|
|
// a service beats the generic word: rotate Stripe, get its webhook secret too
|
|
["STRIPE_WEBHOOK_SECRET", "finance"],
|
|
["COINPAYPORTAL_API_KEY", "finance"],
|
|
["SMTP_HOST", "email"],
|
|
["DB_HOST", "db"],
|
|
["R2_SECRET_ACCESS_KEY", "storage"],
|
|
["SUPABASE_S3_SECRET_KEY", "storage"],
|
|
["SYSTEM_MNEMONIC_BTC", "crypto"],
|
|
["JWT_SECRET", "auth"],
|
|
["NODE_ENV", "config"],
|
|
["NEXT_PUBLIC_APP_URL", "config"],
|
|
["PLANETSCALE_DATABASE_URL", "db"],
|
|
["SOMETHING_ELSE", "other"],
|
|
])("%s is %s", (name, category) => {
|
|
expect(categorizeSecret(name)).toBe(category);
|
|
});
|
|
|
|
it("reads item names and hosts the same way as env names", () => {
|
|
expect(categorizeSecret("My Postgres")).toBe("db");
|
|
expect(categorizeSecret("api.stripe.com")).toBe("finance");
|
|
});
|
|
});
|
|
|
|
describe("categorizeItem", () => {
|
|
it("uses the item type where it is unambiguous", () => {
|
|
expect(categorizeItem(createItem("card", { name: "Visa" }))).toBe("finance");
|
|
expect(categorizeItem(createItem("key", { name: "laptop", key: { keyType: "ssh" } }))).toBe("server");
|
|
});
|
|
|
|
it("classifies a login by its URL when the name says nothing", () => {
|
|
const item = createItem("login", { name: "work", login: { uris: [{ uri: "https://www.reddit.com/login" }] } });
|
|
expect(categorizeItem(item)).toBe("social");
|
|
});
|
|
|
|
it("classifies an env key by its name", () => {
|
|
expect(categorizeItem(createItem("key", { name: "REDIS_URL", key: { keyType: "env", value: "redis://x" } }))).toBe("db");
|
|
});
|
|
});
|
|
|
|
describe("parseCategories", () => {
|
|
it("accepts commas, repeats and aliases", () => {
|
|
expect(parseCategories(["db,social", "payments"])).toEqual(new Set(["db", "social", "finance"]));
|
|
expect(parseCategories(" DB ")).toEqual(new Set(["db"]));
|
|
});
|
|
|
|
it("means no filter when nothing is given", () => {
|
|
expect(parseCategories(undefined)).toBeUndefined();
|
|
expect(parseCategories([])).toBeUndefined();
|
|
});
|
|
|
|
it("names the valid words when one is wrong", () => {
|
|
expect(() => parseCategories("databse")).toThrow(/Unknown category "databse".*db/);
|
|
});
|
|
|
|
it("lists other last so every secret has a category to filter on", () => {
|
|
expect(SECRET_CATEGORY_IDS.at(-1)).toBe("other");
|
|
});
|
|
});
|
|
|
|
describe("csv", () => {
|
|
it("quotes only what needs quoting, including multi-line keys", () => {
|
|
expect(csvLine(["a", 'b"c', "d,e", "-----BEGIN\nKEY-----", undefined])).toBe('a,"b""c","d,e","-----BEGIN\nKEY-----",');
|
|
});
|
|
|
|
it("keeps key and account secrets, which a Bitwarden CSV drops", () => {
|
|
const items = [
|
|
createItem("key", { name: "DATABASE_URL", key: { keyType: "env", value: "postgres://u:p@h/db" } }),
|
|
createItem("account", { name: "Mastodon", account: { provider: "mastodon", handle: "@me", accessToken: "tok" } }),
|
|
];
|
|
const lines = toSimpleCsv(items, []).trim().split("\n");
|
|
expect(lines[0]).toBe("folder,category,type,name,username,password,url,value,totp,notes");
|
|
expect(lines[1]).toBe(",db,key,DATABASE_URL,,,,postgres://u:p@h/db,,");
|
|
expect(lines[2]).toBe(",social,account,Mastodon,@me,,mastodon,tok,,");
|
|
});
|
|
});
|