credkeep 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wren Automation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # credkeep
2
+
3
+ Keep the credentials an automation signs in with. Sealed at rest, shared through AWS SSM, every use audited, expiries watched.
4
+
5
+ No browser, no UI. The tool that signs in (a browser agent, a worker) reads from it. The tool that mints tokens writes to it.
6
+
7
+ ```sh
8
+ npm install credkeep
9
+ ```
10
+
11
+ ## What is in it
12
+
13
+ | Piece | What it does |
14
+ | --- | --- |
15
+ | `fileCredentials(path, cipher)` | Logins by site name (`github`, `google@ops`). A 0600 JSON file, sealed with AES-256-GCM. |
16
+ | `keychainKey({ service })` | The seal key, kept in the macOS Keychain. Made on first use. |
17
+ | `envCredentials(env, { prefix })` | The same logins read from env, for containers. Read-only. |
18
+ | `pushCredentials` / `pullCredentials` | Move logins between a laptop and the shared store. Passkeys and recovery codes stay on the machine. |
19
+ | `ssmEnvStore(ssm, "/app/config")` | Named values (API keys, tokens), one SSM SecureString each. |
20
+ | `put(name, value, { expiresAt })` + `expiring(list, ms)` | Record when a token stops working. List what lapses soon, without decrypting anything. |
21
+ | `fileAudit(path)` | Where each secret went, one hash-chained line per use. `verifyChain` finds any edit. |
22
+ | `canaryStore(store)` | Reading a tripwire credential records it, tells a person, and throws. |
23
+ | `totp(seed)` | The current code from a TOTP seed. `findTotpSecret(pageText)` finds the seed on an enrollment page. |
24
+ | `newPassword()` | 24 characters, every class, no look-alikes. |
25
+
26
+ ## Example
27
+
28
+ ```ts
29
+ import { SSMClient } from "@aws-sdk/client-ssm";
30
+ import { aesGcmCipher, expiring, fileCredentials, keychainKey, ssmEnvStore, totp } from "credkeep";
31
+
32
+ const logins = fileCredentials("~/.myapp/credentials.json", aesGcmCipher(keychainKey({ service: "myapp" })));
33
+ const github = await logins.get("github");
34
+ const code = github?.totpSecret ? totp(github.totpSecret) : null;
35
+
36
+ const keys = ssmEnvStore(new SSMClient({}), "/myapp/config");
37
+ await keys.put("NPM_TOKEN", token, { expiresAt: "2026-12-21T00:00:00Z" });
38
+ const soon = expiring(await keys.list(), 14 * 86_400_000);
39
+ ```
40
+
41
+ ## Rules it keeps
42
+
43
+ - Values never go into argv, logs or error messages. `list` returns names only.
44
+ - A sealed file opened without its key fails loudly. It never fails as a parse error.
45
+ - Each app has its own Keychain item, SSM path and env prefix, so two apps never share a secret by accident.
46
+
47
+ MIT
@@ -0,0 +1,23 @@
1
+ export interface SecretUse {
2
+ at: string;
3
+ /** The credential's store name (`google`, `google@will`). */
4
+ credential: string;
5
+ field: "password" | "previousPassword" | "secret";
6
+ /** The site the session is on. */
7
+ site: string;
8
+ /** The page, without its query (tokens ride in queries). */
9
+ url: string;
10
+ /** Who used it: `login`, a flow name. */
11
+ by: string;
12
+ allowed: boolean;
13
+ }
14
+ export interface SecretAudit {
15
+ record(use: SecretUse): Promise<void>;
16
+ /** Newest last. */
17
+ recent(n?: number): Promise<SecretUse[]>;
18
+ }
19
+ /** JSON lines, owner-only, appended and hash-chained (chain.ts): `verifyChain` finds any edit. */
20
+ export declare function fileAudit(path: string): SecretAudit;
21
+ export declare function memoryAudit(): SecretAudit & {
22
+ uses: SecretUse[];
23
+ };
package/dist/audit.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Where secrets went. Every use, allowed or refused, is a line: which
3
+ * credential, which field, which site, which page, by whom. Values are
4
+ * never written here.
5
+ */
6
+ import { chainedFile } from "./chain.js";
7
+ /** JSON lines, owner-only, appended and hash-chained (chain.ts): `verifyChain` finds any edit. */
8
+ export function fileAudit(path) {
9
+ const file = chainedFile(path);
10
+ return {
11
+ async record(use) {
12
+ await file.append(use);
13
+ },
14
+ recent: (n = 50) => file.recent(n),
15
+ };
16
+ }
17
+ export function memoryAudit() {
18
+ const uses = [];
19
+ return {
20
+ uses,
21
+ async record(u) {
22
+ uses.push(u);
23
+ },
24
+ async recent(n = 50) {
25
+ return uses.slice(-n);
26
+ },
27
+ };
28
+ }
29
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAsBzC,kGAAkG;AAClG,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,MAAM,IAAI,GAAG,WAAW,CAAY,IAAI,CAAC,CAAC;IAC1C,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,GAAG;YACd,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACzB,CAAC;QACD,MAAM,EAAE,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;KACnC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,MAAM,IAAI,GAAgB,EAAE,CAAC;IAC7B,OAAO;QACL,IAAI;QACJ,KAAK,CAAC,MAAM,CAAC,CAAC;YACZ,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACf,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE;YACjB,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACxB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Canaries: credentials that exist only to be tripped. A real-looking
3
+ * credential under a name a thief or a confused model would reach for
4
+ * (`stripe`), with a random password nobody knows. Nothing legitimate asks
5
+ * for it, so a `get` of it IS the incident: it is written to the audit
6
+ * ledger, a person is told, and the read is refused.
7
+ */
8
+ import type { SecretAudit } from "./audit.js";
9
+ import type { Credential, CredentialInput, CredentialStore } from "./credentials.js";
10
+ export declare class CanaryTripped extends Error {
11
+ readonly credential: string;
12
+ constructor(credential: string);
13
+ }
14
+ export interface CanaryOptions {
15
+ audit?: SecretAudit | undefined;
16
+ /** Tell a person; absent = the ledger line only. */
17
+ notify?: ((title: string, body: string) => Promise<unknown>) | undefined;
18
+ /** Who is reading, for the ledger (`login`, `explore`, a flow). */
19
+ by?: string;
20
+ }
21
+ /** A credential that looks like an account and is not. Pick a username a thief would believe. */
22
+ export declare function canaryCredential(username: string): CredentialInput;
23
+ /**
24
+ * The store with the wire armed: `get` of a canary records, notifies and
25
+ * throws `CanaryTripped`; `list` and `put` pass through, so the operator
26
+ * can still see and manage it by name.
27
+ */
28
+ export declare function canaryStore(inner: CredentialStore, o?: CanaryOptions): CredentialStore;
29
+ /** True when this credential must never be typed anywhere. */
30
+ export declare const isCanary: (cred: Pick<Credential, "canary"> | null | undefined) => boolean;
package/dist/canary.js ADDED
@@ -0,0 +1,44 @@
1
+ import { newPassword } from "./passwords.js";
2
+ export class CanaryTripped extends Error {
3
+ credential;
4
+ constructor(credential) {
5
+ super(`credential ${credential} is a canary: nothing should read it`);
6
+ this.credential = credential;
7
+ this.name = "CanaryTripped";
8
+ }
9
+ }
10
+ /** A credential that looks like an account and is not. Pick a username a thief would believe. */
11
+ export function canaryCredential(username) {
12
+ return { username, password: newPassword(20), canary: true };
13
+ }
14
+ /**
15
+ * The store with the wire armed: `get` of a canary records, notifies and
16
+ * throws `CanaryTripped`; `list` and `put` pass through, so the operator
17
+ * can still see and manage it by name.
18
+ */
19
+ export function canaryStore(inner, o = {}) {
20
+ return {
21
+ ...inner,
22
+ async get(site) {
23
+ const cred = await inner.get(site);
24
+ if (!cred?.canary)
25
+ return cred;
26
+ await o.audit?.record({
27
+ at: new Date().toISOString(),
28
+ credential: site,
29
+ field: "password",
30
+ site,
31
+ url: "",
32
+ by: `${o.by ?? "get"} (canary)`,
33
+ allowed: false,
34
+ });
35
+ await o
36
+ .notify?.(`canary tripped: ${site}`, `something read the ${site} credential. Nothing legitimate does; check the audit ledger.`)
37
+ .catch(() => undefined);
38
+ throw new CanaryTripped(site);
39
+ },
40
+ };
41
+ }
42
+ /** True when this credential must never be typed anywhere. */
43
+ export const isCanary = (cred) => cred?.canary === true;
44
+ //# sourceMappingURL=canary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canary.js","sourceRoot":"","sources":["../src/canary.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,OAAO,aAAc,SAAQ,KAAK;IACjB;IAArB,YAAqB,UAAkB;QACrC,KAAK,CAAC,cAAc,UAAU,sCAAsC,CAAC,CAAC;QADnD,eAAU,GAAV,UAAU,CAAQ;QAErC,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAUD,iGAAiG;AACjG,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC/D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAsB,EAAE,IAAmB,EAAE;IACvE,OAAO;QACL,GAAG,KAAK;QACR,KAAK,CAAC,GAAG,CAAC,IAAI;YACZ,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,CAAC,IAAI,EAAE,MAAM;gBAAE,OAAO,IAAI,CAAC;YAC/B,MAAM,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC;gBACpB,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBAC5B,UAAU,EAAE,IAAI;gBAChB,KAAK,EAAE,UAAU;gBACjB,IAAI;gBACJ,GAAG,EAAE,EAAE;gBACP,EAAE,EAAE,GAAG,CAAC,CAAC,EAAE,IAAI,KAAK,WAAW;gBAC/B,OAAO,EAAE,KAAK;aACf,CAAC,CAAC;YACH,MAAM,CAAC;iBACJ,MAAM,EAAE,CACP,mBAAmB,IAAI,EAAE,EACzB,sBAAsB,IAAI,+DAA+D,CAC1F;iBACA,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YAC1B,MAAM,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC;KACF,CAAC;AACJ,CAAC;AAED,8DAA8D;AAC9D,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,IAAmD,EAAW,EAAE,CACvF,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC"}
@@ -0,0 +1,26 @@
1
+ export interface Chained {
2
+ prev: string;
3
+ hash: string;
4
+ }
5
+ /** Hash of one row: `prev`, then the row's fields in key order, so the value is stable however it was built. */
6
+ export declare function rowHash(prev: string, row: object): string;
7
+ export interface ChainedFile<T extends object> {
8
+ /** Appends the row with its chain fields; one writer per process (appends are serialized here). */
9
+ append(row: T): Promise<T & Chained>;
10
+ /** Newest last; the chain fields come along. */
11
+ recent(n?: number): Promise<(T & Chained)[]>;
12
+ }
13
+ /**
14
+ * The whole file checked: `brokenAt` is the first row (0-based) whose hash
15
+ * or prev does not fit. Rows written before chaining (no `hash`) may only
16
+ * lead the file; they are counted in `unchained`, never verified.
17
+ */
18
+ export interface Verification {
19
+ rows: number;
20
+ unchained: number;
21
+ ok: boolean;
22
+ brokenAt: number | null;
23
+ }
24
+ export declare function chainedFile<T extends object>(path: string): ChainedFile<T>;
25
+ /** Walk every row from the start. */
26
+ export declare function verifyChain(path: string): Promise<Verification>;
package/dist/chain.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * An append-only JSONL file whose rows are hash-chained: each row carries
3
+ * `prev` (the hash of the row before it, "" for the first) and `hash`
4
+ * (SHA-256 over prev + the row's own fields). Editing, dropping or
5
+ * reordering any row breaks every hash after it, so `verify` finds
6
+ * tampering; nothing here stops it. The audit ledger is built on it, and
7
+ * any other ledger an app keeps can be.
8
+ */
9
+ import { createHash } from "node:crypto";
10
+ import { createReadStream } from "node:fs";
11
+ import { appendFile, chmod, mkdir } from "node:fs/promises";
12
+ import { dirname } from "node:path";
13
+ import { createInterface } from "node:readline";
14
+ import { tailJson } from "./tail.js";
15
+ /** Hash of one row: `prev`, then the row's fields in key order, so the value is stable however it was built. */
16
+ export function rowHash(prev, row) {
17
+ const fields = row;
18
+ const keys = Object.keys(fields)
19
+ .filter((k) => k !== "prev" && k !== "hash")
20
+ .sort();
21
+ const h = createHash("sha256").update(prev);
22
+ for (const k of keys)
23
+ h.update("\0").update(k).update("=").update(JSON.stringify(fields[k]));
24
+ return h.digest("hex");
25
+ }
26
+ export function chainedFile(path) {
27
+ let ready = null;
28
+ /** The last hash; read once from the file's tail, then carried in memory. */
29
+ let tip = "";
30
+ let queue = Promise.resolve();
31
+ const ensure = () => (ready ??= (async () => {
32
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
33
+ await appendFile(path, "", { mode: 0o600 });
34
+ await chmod(path, 0o600);
35
+ const [last] = await tailJson(path, 1);
36
+ // A file from before chaining: the first chained row binds every old line, so nothing
37
+ // can be slipped in front of the chain later. Read whole once, here only.
38
+ tip = typeof last?.hash === "string" ? last.hash : last ? await prefixHash(path) : "";
39
+ return tip;
40
+ })());
41
+ return {
42
+ append(row) {
43
+ const next = queue.then(async () => {
44
+ await ensure();
45
+ const chained = { ...row, prev: tip, hash: rowHash(tip, row) };
46
+ await appendFile(path, `${JSON.stringify(chained)}\n`, { mode: 0o600 });
47
+ tip = chained.hash;
48
+ return chained;
49
+ });
50
+ queue = next.catch(() => undefined);
51
+ return next;
52
+ },
53
+ async recent(n = 50) {
54
+ return tailJson(path, n);
55
+ },
56
+ };
57
+ }
58
+ async function openLines(path) {
59
+ try {
60
+ const stream = createReadStream(path, { encoding: "utf8" });
61
+ await new Promise((ok, no) => stream.once("open", () => ok()).once("error", no));
62
+ return createInterface({ input: stream, crlfDelay: Infinity });
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ /** SHA-256 over the file's unchained lines, in order: what the first chained row's `prev` carries. */
69
+ async function prefixHash(path) {
70
+ const lines = await openLines(path);
71
+ const h = createHash("sha256");
72
+ if (lines)
73
+ for await (const line of lines)
74
+ if (line)
75
+ h.update(line).update("\n");
76
+ return h.digest("hex");
77
+ }
78
+ /** Walk every row from the start. */
79
+ export async function verifyChain(path) {
80
+ const lines = await openLines(path);
81
+ if (!lines)
82
+ return { rows: 0, unchained: 0, ok: true, brokenAt: null };
83
+ let prev = "";
84
+ let rows = 0;
85
+ let unchained = 0;
86
+ let brokenAt = null;
87
+ const prefix = createHash("sha256");
88
+ // Streamed line by line: a ledger is read from the end everywhere else, whole only here.
89
+ for await (const line of lines) {
90
+ if (!line)
91
+ continue;
92
+ const i = rows++;
93
+ if (brokenAt !== null)
94
+ continue;
95
+ let row;
96
+ try {
97
+ row = JSON.parse(line);
98
+ }
99
+ catch {
100
+ brokenAt = i;
101
+ continue;
102
+ }
103
+ if (!("hash" in row) && unchained === i) {
104
+ unchained++;
105
+ prefix.update(line).update("\n");
106
+ continue;
107
+ }
108
+ if (i === unchained && unchained > 0)
109
+ prev = prefix.digest("hex");
110
+ if (row.prev !== prev || row.hash !== rowHash(prev, row))
111
+ brokenAt = i;
112
+ else
113
+ prev = row.hash;
114
+ }
115
+ return { rows, unchained, ok: brokenAt === null, brokenAt };
116
+ }
117
+ //# sourceMappingURL=chain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain.js","sourceRoot":"","sources":["../src/chain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ export interface Cipher {
2
+ seal(plain: string): string;
3
+ open(sealed: string): string;
4
+ }
5
+ /**
6
+ * No sealing. `open` still refuses a sealed file: read as plain text it
7
+ * would fail as a parse error three layers away, when the cause is always
8
+ * that this machine seals and the caller passed no key.
9
+ */
10
+ export declare const plainCipher: Cipher;
11
+ /** Sealed text is one JSON line: {magic, iv, tag, data}, all base64. */
12
+ export declare function aesGcmCipher(key: Buffer): Cipher;
13
+ export declare function isSealed(text: string): boolean;
14
+ /** The Keychain item holding the key: one per app, so two tools never share a key by accident. */
15
+ export interface KeychainItem {
16
+ service: string;
17
+ account?: string;
18
+ }
19
+ /**
20
+ * The 32-byte key from the login Keychain; created on first use with the
21
+ * `security` tool trusted, so no dialog on later reads. Throws off macOS
22
+ * or when the Keychain says no.
23
+ */
24
+ export declare function keychainKey(item: KeychainItem): Buffer;
25
+ /** Re-store the existing key with the tool trusted (an item made before `TRUST` prompts on every read). */
26
+ export declare function trustKeychainKey(item: KeychainItem): "retrusted" | "none";
package/dist/cipher.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * A file at rest. AES-256-GCM with a key that lives in the macOS Keychain
3
+ * (made on first use, read through `security -i` so it is never on a
4
+ * command line). Elsewhere (Linux, a container) secrets come from env and
5
+ * the cipher is plain.
6
+ */
7
+ import { spawnSync } from "node:child_process";
8
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
9
+ /**
10
+ * No sealing. `open` still refuses a sealed file: read as plain text it
11
+ * would fail as a parse error three layers away, when the cause is always
12
+ * that this machine seals and the caller passed no key.
13
+ */
14
+ export const plainCipher = {
15
+ seal: (p) => p,
16
+ open: (s) => {
17
+ if (isSealed(s))
18
+ throw new Error("the file is sealed, but it was opened with no cipher key");
19
+ return s;
20
+ },
21
+ };
22
+ const MAGIC = "credkeep-sealed-v1";
23
+ /** Files sealed before the vault left autobrowse: same format, older name. Read, and resealed on the next write. */
24
+ const MAGICS = [MAGIC, "autobrowse-sealed-v1"];
25
+ /** Sealed text is one JSON line: {magic, iv, tag, data}, all base64. */
26
+ export function aesGcmCipher(key) {
27
+ if (key.length !== 32)
28
+ throw new Error("cipher key must be 32 bytes");
29
+ return {
30
+ seal(plain) {
31
+ const iv = randomBytes(12);
32
+ const c = createCipheriv("aes-256-gcm", key, iv);
33
+ const data = Buffer.concat([c.update(plain, "utf8"), c.final()]);
34
+ return JSON.stringify({
35
+ magic: MAGIC,
36
+ iv: iv.toString("base64"),
37
+ tag: c.getAuthTag().toString("base64"),
38
+ data: data.toString("base64"),
39
+ });
40
+ },
41
+ open(sealed) {
42
+ const parsed = JSON.parse(sealed);
43
+ if (!MAGICS.includes(parsed.magic ?? "") || !parsed.iv || !parsed.tag || !parsed.data)
44
+ throw new Error("the file is not sealed by credkeep");
45
+ const d = createDecipheriv("aes-256-gcm", key, Buffer.from(parsed.iv, "base64"));
46
+ d.setAuthTag(Buffer.from(parsed.tag, "base64"));
47
+ return Buffer.concat([d.update(Buffer.from(parsed.data, "base64")), d.final()]).toString("utf8");
48
+ },
49
+ };
50
+ }
51
+ export function isSealed(text) {
52
+ const head = text.trimStart();
53
+ return MAGICS.some((m) => head.startsWith(`{"magic":"${m}"`));
54
+ }
55
+ /** Runs `security` in interactive mode so nothing secret is an argv. */
56
+ function security(commands) {
57
+ const r = spawnSync("security", ["-i"], { input: `${commands}\n`, encoding: "utf8" });
58
+ return { status: r.status ?? 1, out: `${r.stdout}${r.stderr}` };
59
+ }
60
+ /** The item trusts the `security` tool itself, so reading it never raises the Keychain dialog. */
61
+ const TRUST = "-T /usr/bin/security";
62
+ const ACCOUNT = "credentials-key";
63
+ const ids = (item) => `-s ${item.service} -a ${item.account ?? ACCOUNT}`;
64
+ function store(item, hex) {
65
+ const added = security(`add-generic-password ${ids(item)} -w ${hex} -U ${TRUST}`);
66
+ if (added.status !== 0)
67
+ throw new Error(`keychain: could not store the key (${added.out.trim().slice(0, 120)})`);
68
+ }
69
+ /**
70
+ * The 32-byte key from the login Keychain; created on first use with the
71
+ * `security` tool trusted, so no dialog on later reads. Throws off macOS
72
+ * or when the Keychain says no.
73
+ */
74
+ export function keychainKey(item) {
75
+ if (process.platform !== "darwin")
76
+ throw new Error("keychain cipher needs macOS");
77
+ // One `security` subprocess per item per process: the key does not change while we run.
78
+ const cached = keyCache.get(ids(item));
79
+ if (cached)
80
+ return cached;
81
+ const found = security(`find-generic-password ${ids(item)} -w`);
82
+ let hex = found.status === 0 ? found.out.match(/\b[0-9a-f]{64}\b/)?.[0] : undefined;
83
+ if (!hex) {
84
+ hex = randomBytes(32).toString("hex");
85
+ store(item, hex);
86
+ }
87
+ const key = Buffer.from(hex, "hex");
88
+ keyCache.set(ids(item), key);
89
+ return key;
90
+ }
91
+ const keyCache = new Map();
92
+ /** Re-store the existing key with the tool trusted (an item made before `TRUST` prompts on every read). */
93
+ export function trustKeychainKey(item) {
94
+ const found = security(`find-generic-password ${ids(item)} -w`);
95
+ const hex = found.out.match(/\b[0-9a-f]{64}\b/)?.[0];
96
+ if (found.status !== 0 || !hex)
97
+ return "none";
98
+ security(`delete-generic-password ${ids(item)}`);
99
+ store(item, hex);
100
+ keyCache.delete(ids(item));
101
+ return "retrusted";
102
+ }
103
+ //# sourceMappingURL=cipher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cipher.js","sourceRoot":"","sources":["../src/cipher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import { z } from "zod";
2
+ import { type Cipher } from "./cipher.js";
3
+ export declare const credentialSchema: z.ZodObject<{
4
+ username: z.ZodString;
5
+ password: z.ZodOptional<z.ZodString>;
6
+ previousPassword: z.ZodOptional<z.ZodString>;
7
+ totpSecret: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodString>>;
8
+ recoveryCodes: z.ZodDefault<z.ZodArray<z.ZodString>>;
9
+ codesInbox: z.ZodOptional<z.ZodString>;
10
+ passkeys: z.ZodDefault<z.ZodArray<z.ZodObject<{
11
+ rpId: z.ZodString;
12
+ credentialId: z.ZodString;
13
+ privateKey: z.ZodString;
14
+ userHandle: z.ZodOptional<z.ZodString>;
15
+ signCount: z.ZodNumber;
16
+ isResidentCredential: z.ZodBoolean;
17
+ }, z.core.$strip>>>;
18
+ via: z.ZodOptional<z.ZodString>;
19
+ canary: z.ZodOptional<z.ZodBoolean>;
20
+ url: z.ZodOptional<z.ZodString>;
21
+ madeAt: z.ZodOptional<z.ZodString>;
22
+ }, z.core.$strip>;
23
+ export type Credential = z.infer<typeof credentialSchema>;
24
+ export type CredentialInput = z.input<typeof credentialSchema>;
25
+ export interface CredentialStore {
26
+ get(site: string): Promise<Credential | null>;
27
+ put(site: string, cred: CredentialInput): Promise<void>;
28
+ /** Site names only; never values. */
29
+ list(): Promise<string[]>;
30
+ }
31
+ /**
32
+ * `{ "sites": { "<site>": Credential } }` at `path`, mode 0600, written
33
+ * atomically, sealed with `cipher` (a plain file written earlier is still
34
+ * read, and sealed on the next write).
35
+ */
36
+ export declare function fileCredentials(path: string, cipher?: Cipher): CredentialStore;
37
+ export declare function memoryCredentials(init?: Record<string, CredentialInput>): CredentialStore;
38
+ /** How credentials travel as env: `<prefix><SITE>_<FIELD>`. Each app picks its own prefix. */
39
+ export interface EnvNaming {
40
+ /** Default `CRED_`. */
41
+ prefix?: string;
42
+ }
43
+ /** `CRED_<SITE>_<FIELD>`: the env name a credential field travels under. */
44
+ export declare function credentialEnvName(site: string, field: string, o?: EnvNaming): string;
45
+ /**
46
+ * A credential as env entries, for the store the box reads: username,
47
+ * password, TOTP seed, via provider, codes inbox. Recovery codes and
48
+ * passkeys stay on the machine that holds the file.
49
+ */
50
+ export declare function credentialEnv(site: string, cred: Credential, o?: EnvNaming): {
51
+ name: string;
52
+ value: string;
53
+ }[];
54
+ /**
55
+ * `CRED_<SITE>_USERNAME` / `_PASSWORD` / `_TOTP_SECRET` / `_VIA` /
56
+ * `_CODES_INBOX`: the container form, where a Secret becomes env. Read-only.
57
+ */
58
+ export declare function envCredentials(env?: NodeJS.ProcessEnv, o?: EnvNaming): CredentialStore;
59
+ /** The shared store's side of a sync (SSM, see env-store.ts). */
60
+ export interface CredentialEnvStore {
61
+ all(): Promise<{
62
+ name: string;
63
+ value: string;
64
+ }[]>;
65
+ put(name: string, value: string): Promise<void>;
66
+ }
67
+ /**
68
+ * This machine's credentials into the env store, every site or the named
69
+ * ones: username, password, TOTP seed, via, codes inbox. Canaries never
70
+ * travel (a tripwire belongs to one machine); passkeys and recovery codes
71
+ * cannot. Answers what was pushed, never a value.
72
+ */
73
+ export declare function pushCredentials(local: CredentialStore, store: CredentialEnvStore, sites?: string[], o?: EnvNaming): Promise<{
74
+ site: string;
75
+ names: string[];
76
+ }[]>;
77
+ /**
78
+ * The env store's credentials into this machine's file, the other way: a
79
+ * second laptop, or a box's file for a passkey site. A site already here is
80
+ * kept unless `overwrite`, and even then its passkeys and recovery codes
81
+ * stay (env never carries them). Answers what was written and what was kept.
82
+ */
83
+ export declare function pullCredentials(store: CredentialEnvStore, local: CredentialStore, sites?: string[], o?: {
84
+ overwrite?: boolean;
85
+ } & EnvNaming): Promise<{
86
+ written: string[];
87
+ kept: string[];
88
+ }>;
89
+ /** First store that has the site wins; writes go to `write`, which defaults to the last store. */
90
+ export declare function layeredCredentials(stores: CredentialStore[], write?: CredentialStore | undefined): CredentialStore;