@mstone6969/vault 0.2.0 → 0.5.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/README.md +198 -6
- package/dist/crypto.d.ts +99 -2
- package/dist/crypto.d.ts.map +1 -1
- package/dist/errors.d.ts +122 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +447 -33
- package/dist/index.js.map +9 -7
- package/dist/providers.d.ts +131 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/stores/file.d.ts +190 -0
- package/dist/stores/file.d.ts.map +1 -0
- package/dist/stores/file.js +1 -0
- package/dist/stores/memory.d.ts +98 -7
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/sqlite.d.ts +140 -9
- package/dist/stores/sqlite.d.ts.map +1 -1
- package/dist/stores/sqlite.js +47 -12
- package/dist/stores/sqlite.js.map +3 -3
- package/dist/types.d.ts +408 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/vault.d.ts +554 -19
- package/dist/vault.d.ts.map +1 -1
- package/docs/README.md +10 -0
- package/docs/index/README.md +48 -0
- package/docs/index/classes/FileStore.md +341 -0
- package/docs/index/classes/MemoryStore.md +240 -0
- package/docs/index/classes/Vault.md +805 -0
- package/docs/index/classes/VaultError.md +371 -0
- package/docs/index/classes/VaultKeyError.md +370 -0
- package/docs/index/functions/envKey.md +43 -0
- package/docs/index/functions/fileKey.md +46 -0
- package/docs/index/functions/generateKey.md +39 -0
- package/docs/index/functions/importKey.md +49 -0
- package/docs/index/functions/isKeyProvider.md +43 -0
- package/docs/index/functions/open.md +67 -0
- package/docs/index/functions/randomValue.md +53 -0
- package/docs/index/functions/seal.md +56 -0
- package/docs/index/functions/staticKey.md +39 -0
- package/docs/index/type-aliases/Generator.md +58 -0
- package/docs/index/type-aliases/HistoryEntry.md +65 -0
- package/docs/index/type-aliases/KeyProvider.md +74 -0
- package/docs/index/type-aliases/PutOptions.md +141 -0
- package/docs/index/type-aliases/RekeyReport.md +37 -0
- package/docs/index/type-aliases/RotationContext.md +49 -0
- package/docs/index/type-aliases/RotationPolicy.md +142 -0
- package/docs/index/type-aliases/SecretRecord.md +214 -0
- package/docs/index/type-aliases/SecretSummary.md +56 -0
- package/docs/index/type-aliases/VaultEvent.md +95 -0
- package/docs/index/type-aliases/VaultOptions.md +142 -0
- package/docs/index/type-aliases/VaultStore.md +156 -0
- package/docs/index/variables/DEFAULT_ALPHABET.md +29 -0
- package/docs/index/variables/DEFAULT_HISTORY_LIMIT.md +28 -0
- package/docs/index/variables/DEFAULT_PREFIX.md +23 -0
- package/docs/stores/sqlite/README.md +11 -0
- package/docs/stores/sqlite/classes/SqliteStore.md +307 -0
- package/package.json +15 -5
package/dist/index.js.map
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/errors.ts", "../src/crypto.ts", "../src/vault.ts", "../src/stores/memory.ts"],
|
|
3
|
+
"sources": ["../src/errors.ts", "../src/crypto.ts", "../src/providers.ts", "../src/vault.ts", "../src/stores/memory.ts", "../src/stores/file.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"
|
|
6
|
-
"import { VaultKeyError } from \"./errors\"\n\n/**\n * AES-256-GCM. Sealed values are `iv:payload`, both base64: the IV is fresh per\n * write, and GCM's tag means a tampered value fails to open rather than\n * decrypting to something wrong.\n */\nconst IV_BYTES = 12\nconst KEY_BYTES = 32\n\n
|
|
7
|
-
"import {
|
|
8
|
-
"import type { SecretRecord, VaultStore } from \"../types\"\n\n/** Keeps sealed values in a Map. Handy for tests and short-lived processes. */\nexport class MemoryStore implements VaultStore {\n private readonly records = new Map<string, SecretRecord>()\n\n private static key(owner: string, name: string): string {\n return `${owner} ${name}`\n }\n\n async get(owner: string, name: string): Promise<SecretRecord | null> {\n return this.records.get(MemoryStore.key(owner, name)) ?? null\n }\n\n async list(owner: string): Promise<SecretRecord[]> {\n return [...this.records.values()].filter((record) => record.owner === owner)\n }\n\n async put(record: {\n owner: string\n name: string\n sealed: string\n metadata: Record<string, string>\n }): Promise<SecretRecord> {\n const key = MemoryStore.key(record.owner, record.name)\n const now = new Date()\n const stored: SecretRecord = {\n ...record,\n // Replacing a value keeps the date it was first stored.\n createdAt: this.records.get(key)?.createdAt ?? now,\n updatedAt: now,\n }\n this.records.set(key, stored)\n return stored\n }\n\n async remove(owner: string, name: string): Promise<boolean> {\n return this.records.delete(MemoryStore.key(owner, name))\n }\n}\n"
|
|
5
|
+
"/**\n * A caller mistake: a bad name, an empty value, a missing secret.\n *\n * @remarks\n * Everything the vault throws on purpose is a `VaultError`, so a caller can\n * tell \"you asked for something that cannot be done\" apart from a bug, and\n * answer accordingly, with one `instanceof`.\n *\n * @example Turning a vault call into an HTTP response\n * ```ts\n * import { Vault, VaultError, MemoryStore, generateKey } from \"@mstone6969/vault\"\n *\n * const vault = new Vault({ key: generateKey(), store: new MemoryStore() })\n *\n * try {\n * return new Response(await vault.open(\"alice\", \"stripe\"))\n * } catch (error) {\n * if (error instanceof VaultError) {\n * return new Response(error.message, { status: error.status })\n * }\n * throw error\n * }\n * ```\n *\n * @see {@link VaultKeyError} for the key and ciphertext failures.\n */\nexport class VaultError extends Error {\n /**\n * @param message What the caller did that the vault would not do. Names\n * and owners appear in it; secret values never do, so it is safe to log.\n * @param status Suggested HTTP status. See {@link VaultError.status}.\n */\n constructor(\n message: string,\n /**\n * Suggested HTTP status, for callers putting this behind an API.\n *\n * @remarks\n * It is a suggestion, not a promise about transport: nothing in the\n * vault speaks HTTP. It exists so a handler can map a failure to a\n * response without knowing which check inside the vault failed.\n *\n * The statuses actually thrown:\n *\n * - `422` — bad input: a name that is not 1–64 characters of letters,\n * numbers, dot, dash or underscore; an empty value; a `randomValue`\n * length below one or an alphabet under two characters; a rotation\n * asked of an entry with no rotation policy.\n * - `404` — no secret under that name for that owner.\n * - `409` — the entry is `final`, so it can be deleted but not\n * replaced.\n * - `403` — the entry is sealed, so `read` will not hand it back.\n * `open` is the only way out.\n * - `410` — the entry's `expiresAt` has passed. The record is still\n * there; it just cannot be used.\n * - `501` — the entry's rotation policy names a generator this vault\n * was not constructed with.\n * - `500` — {@link VaultKeyError}'s default: a key or ciphertext\n * problem, which is the operator's fault rather than the caller's.\n *\n * @defaultValue 422\n */\n readonly status: number = 422\n ) {\n super(message)\n this.name = \"VaultError\"\n }\n}\n\n/**\n * The key is the wrong shape, or cannot open what it was given.\n *\n * @remarks\n * Thrown for a base64 key that is not 32 bytes, a sealed value not in\n * `iv:payload` form, and a value that will not open — which covers both the\n * wrong key and a value someone has altered, since GCM authenticates what it\n * decrypts and cannot tell you which it was. The key providers throw it too,\n * when the environment variable is unset or the key file is missing or empty.\n *\n * Its status is 500 rather than a 4xx because a request that reaches this did\n * nothing wrong: the vault is misconfigured, or its data no longer matches its\n * key.\n *\n * @example Distinguishing a key problem from a caller problem\n * ```ts\n * import { importKey, VaultKeyError } from \"@mstone6969/vault\"\n *\n * try {\n * await importKey(process.env.VAULT_KEY!)\n * } catch (error) {\n * if (error instanceof VaultKeyError) {\n * console.error(\"vault key is unusable:\", error.message)\n * process.exit(1)\n * }\n * throw error\n * }\n * ```\n *\n * @see {@link VaultError} for the mistakes callers can fix themselves.\n */\nexport class VaultKeyError extends VaultError {\n /**\n * @param message What was wrong with the key or the sealed value. It never\n * says which key was tried or what the value held.\n */\n constructor(message: string) {\n super(message, 500)\n this.name = \"VaultKeyError\"\n }\n}\n",
|
|
6
|
+
"import { VaultKeyError } from \"./errors\"\n\n/**\n * AES-256-GCM. Sealed values are `iv:payload`, both base64: the IV is fresh per\n * write, and GCM's tag means a tampered value fails to open rather than\n * decrypting to something wrong.\n *\n * These primitives know nothing about the vault. {@link Vault} builds envelope\n * encryption on top of them: {@link generateKey} mints a data key per value,\n * {@link seal} seals the value under it, and only that data key is sealed under\n * the master key.\n */\n\n/**\n * IV length in bytes.\n *\n * @remarks\n * 12 because that is the width GCM is defined around; anything else makes the\n * cipher derive one, which is slower and buys nothing.\n */\nconst IV_BYTES = 12\n\n/**\n * Key length in bytes — 32, i.e. AES-256. {@link importKey} rejects anything\n * else rather than silently selecting a weaker cipher.\n */\nconst KEY_BYTES = 32\n\n/**\n * A new random key, base64 encoded — store it somewhere safe.\n *\n * @returns 32 random bytes, base64. Nothing keeps a copy, so a key that is lost\n * takes every value sealed under it with it.\n * @remarks\n * Used for master keys you generate once and keep, and — inside the vault — for\n * the throwaway data key minted per written value.\n * @example\n * ```ts\n * import { generateKey, importKey, seal } from \"@mstone6969/vault\"\n *\n * const material = generateKey()\n * const key = await importKey(material)\n * const sealed = await seal(key, \"hunter2\")\n * ```\n * @see {@link importKey} to turn the string back into a usable key.\n */\nexport function generateKey(): string {\n return Buffer.from(crypto.getRandomValues(new Uint8Array(KEY_BYTES))).toString(\"base64\")\n}\n\n/**\n * Imports a base64 key produced by {@link generateKey}.\n *\n * @param base64Key The key material, base64 encoded, decoding to exactly 32\n * bytes.\n * @returns A key usable with {@link seal} and {@link open}.\n * @throws {@link VaultKeyError} When the decoded material is not 32 bytes. Note\n * that base64 decoding is lenient: rubbish that is not base64 at all decodes\n * to too few bytes and surfaces here as a length complaint rather than a\n * parse error.\n * @remarks\n * The imported key is not extractable, so the raw bytes cannot be read back out\n * of it — a value only ever leaves via {@link open}.\n * @example\n * ```ts\n * import { importKey, MemoryStore, Vault } from \"@mstone6969/vault\"\n *\n * const key = await importKey(process.env.VAULT_KEY!)\n * const vault = new Vault({ key, store: new MemoryStore() })\n * ```\n */\nexport async function importKey(base64Key: string): Promise<CryptoKey> {\n const raw = Buffer.from(base64Key, \"base64\")\n if (raw.length !== KEY_BYTES) {\n throw new VaultKeyError(\n `A vault key must be ${KEY_BYTES} bytes, base64 encoded — got ${raw.length}.`\n )\n }\n return crypto.subtle.importKey(\"raw\", raw, \"AES-GCM\", false, [\"encrypt\", \"decrypt\"])\n}\n\n/**\n * Seals a value under a key.\n *\n * @param key The key to seal under.\n * @param plaintext What to seal.\n * @returns `iv:payload`, both base64. A fresh IV every time, so the same value\n * sealed twice gives two different results.\n * @remarks\n * That the output differs every time is the point: an observer with the store\n * in front of them cannot tell that two entries hold the same password, nor\n * that a value was replaced with itself. It also means a sealed string is no\n * good as a cache key or an equality check.\n *\n * Nothing about the key is written into the output, so the caller must\n * remember which key sealed what — the vault does that by keeping each value's\n * data key beside it in {@link SecretRecord.sealedKey}.\n * @example\n * ```ts\n * import { generateKey, importKey, open, seal } from \"@mstone6969/vault\"\n *\n * const key = await importKey(generateKey())\n * const sealed = await seal(key, \"s3cret\")\n * sealed.split(\":\").length // 2\n * await open(key, sealed) // \"s3cret\"\n * ```\n */\nexport async function seal(key: CryptoKey, plaintext: string): Promise<string> {\n const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES))\n const sealed = await crypto.subtle.encrypt(\n { name: \"AES-GCM\", iv },\n key,\n new TextEncoder().encode(plaintext)\n )\n return `${Buffer.from(iv).toString(\"base64\")}:${Buffer.from(sealed).toString(\"base64\")}`\n}\n\n/**\n * Opens a value sealed by {@link seal}.\n *\n * @param key The key it was sealed under.\n * @param sealed The `iv:payload` string to open.\n * @returns The plaintext.\n * @throws {@link VaultKeyError} When the key is wrong, the value has been\n * altered, or it is not in `iv:payload` form. A wrong key fails rather than\n * returning nonsense, because GCM authenticates what it decrypts.\n * @remarks\n * That failure mode is worth relying on. A caller does not need to check\n * whether what came back looks plausible: if this returns at all, the value is\n * byte for byte what was sealed, under the key that sealed it. It is also why\n * {@link Vault.rekey} can try each key in turn and know which one was right,\n * and why a store that silently corrupts a record produces an error rather\n * than a credential that fails somewhere far away.\n *\n * The error deliberately does not say which of the three went wrong: telling\n * an attacker apart from a typo is not worth telling an attacker anything.\n * @example\n * ```ts\n * import { generateKey, importKey, open, seal, VaultKeyError } from \"@mstone6969/vault\"\n *\n * const key = await importKey(generateKey())\n * const other = await importKey(generateKey())\n * const sealed = await seal(key, \"s3cret\")\n *\n * try {\n * await open(other, sealed)\n * } catch (error) {\n * error instanceof VaultKeyError // true — never a wrong plaintext\n * }\n * ```\n */\nexport async function open(key: CryptoKey, sealed: string): Promise<string> {\n const [iv, payload] = sealed.split(\":\")\n if (!iv || !payload) {\n throw new VaultKeyError(\"A sealed value must look like iv:payload.\")\n }\n\n try {\n const opened = await crypto.subtle.decrypt(\n { name: \"AES-GCM\", iv: Buffer.from(iv, \"base64\") },\n key,\n Buffer.from(payload, \"base64\")\n )\n return new TextDecoder().decode(opened)\n } catch {\n throw new VaultKeyError(\n \"That value could not be opened — wrong key, or it has been altered.\"\n )\n }\n}\n",
|
|
7
|
+
"import { VaultKeyError } from \"./errors\"\n\n/**\n * Where the master key comes from.\n *\n * A vault takes a provider rather than a string so the key can live wherever\n * you keep such things — the environment, a file with tight permissions, or a\n * service that hands one over. Write your own for anything else; it is one\n * method.\n *\n * @remarks\n * Whoever holds the provider — {@link Vault} or {@link FileStore} — calls\n * `key` the first time a key is actually needed and keeps the promise, so a\n * provider that reaches over the network is asked once and a vault nobody uses\n * never asks at all. Anything thrown surfaces from that first operation, not\n * from the constructor.\n *\n * @example\n * A provider for something this package does not ship — here a KMS, but the\n * shape is the same for 1Password, age, or a file on a smartcard.\n * ```ts\n * import { Vault, MemoryStore, type KeyProvider } from \"@mstone6969/vault\"\n *\n * function kmsKey(id: string): KeyProvider {\n * return {\n * async key() {\n * const response = await fetch(`https://kms.internal/keys/${id}`)\n * if (!response.ok) throw new Error(`KMS refused key ${id}.`)\n * return (await response.text()).trim() // base64\n * },\n * }\n * }\n *\n * const vault = new Vault({ key: kmsKey(\"vault-master\"), store: new MemoryStore() })\n * await vault.put(\"alice\", \"db\", \"hunter2\") // the KMS is called here, once\n * ```\n *\n * @see {@link staticKey}, {@link envKey} and {@link fileKey} for the ones that\n * ship.\n */\nexport type KeyProvider = {\n /**\n * The key, base64 or already imported. Called once, when first needed.\n *\n * @returns The master key: a base64 string of 32 bytes, or a `CryptoKey`\n * already imported for AES-GCM. May be a promise.\n * @throws Whatever the source of the key throws when it cannot produce\n * one. The ones here throw {@link VaultKeyError}.\n */\n key(): Promise<string | CryptoKey> | string | CryptoKey\n}\n\n/**\n * A key you already have.\n *\n * Mostly for handing a key to something that wants a provider —\n * {@link Vault.rekey}, or a `previousKeys` entry — without wrapping it\n * yourself. Passing raw key material as `key` does this for you.\n *\n * @param key The master key: base64, or already imported.\n * @returns A provider that hands back `key` every time.\n *\n * @example\n * ```ts\n * import { Vault, MemoryStore, generateKey, staticKey } from \"@mstone6969/vault\"\n *\n * const vault = new Vault({ key: staticKey(generateKey()), store: new MemoryStore() })\n * ```\n */\nexport function staticKey(key: string | CryptoKey): KeyProvider {\n return { key: () => key }\n}\n\n/**\n * A key from an environment variable.\n *\n * @param name The variable to read, at the moment the key is first needed —\n * so a process that loads its environment after building the vault still\n * works.\n * @returns A provider that reads `process.env[name]`.\n * @throws {@link VaultKeyError} when the variable is unset or empty, at the first\n * operation that needs a key rather than at construction.\n *\n * @example\n * ```ts\n * import { Vault, MemoryStore, envKey } from \"@mstone6969/vault\"\n *\n * const vault = new Vault({ key: envKey(\"VAULT_KEY\"), store: new MemoryStore() })\n * await vault.put(\"alice\", \"db\", \"hunter2\") // throws here if VAULT_KEY is unset\n * ```\n */\nexport function envKey(name: string): KeyProvider {\n return {\n key() {\n const value = process.env[name]\n if (!value) {\n throw new VaultKeyError(`${name} is not set, so there is no key to open with.`)\n }\n return value\n },\n }\n}\n\n/**\n * A key from a file — the usual way to keep one off the process list and out\n * of shell history. Whitespace around it is ignored, so a trailing newline\n * from `openssl rand -base64 32 > key` does no harm.\n *\n * @param path The key file, read at the moment the key is first needed. It is\n * read once and kept, so replacing the file later does not change the key a\n * running vault uses.\n * @returns A provider that reads and trims the file.\n * @throws {@link VaultKeyError} when the file is missing, or holds nothing but\n * whitespace.\n *\n * @example\n * ```ts\n * import { Vault, FileStore, fileKey } from \"@mstone6969/vault\"\n *\n * // The store's own key opens the file; the vault's key seals the values.\n * const store = new FileStore(\"./secrets.vault\", fileKey(\"/etc/vault.key\"))\n * const vault = new Vault({ key: fileKey(\"/etc/vault.key\"), store })\n * ```\n */\nexport function fileKey(path: string): KeyProvider {\n return {\n async key() {\n const file = Bun.file(path)\n if (!(await file.exists())) {\n throw new VaultKeyError(`No key file at ${path}.`)\n }\n const contents = (await file.text()).trim()\n if (!contents) throw new VaultKeyError(`The key file at ${path} is empty.`)\n return contents\n },\n }\n}\n\n/**\n * True when something is a provider rather than a key.\n *\n * {@link Vault} and {@link FileStore} accept either, and use this to tell them\n * apart: anything with a callable `key` is a provider, everything else is key\n * material to be wrapped in {@link staticKey}. A `CryptoKey` has no `key`\n * method, so it never matches.\n *\n * @param value Anything — key material, a provider, or neither.\n * @returns Whether `value` has a callable `key`, narrowing it to\n * {@link KeyProvider}.\n *\n * @example\n * ```ts\n * import { envKey, isKeyProvider, staticKey } from \"@mstone6969/vault\"\n *\n * isKeyProvider(envKey(\"VAULT_KEY\")) // true\n * isKeyProvider(staticKey(\"...\")) // true\n * isKeyProvider(\"base64-key-material\") // false\n * ```\n */\nexport function isKeyProvider(value: unknown): value is KeyProvider {\n return typeof (value as KeyProvider)?.key === \"function\"\n}\n",
|
|
8
|
+
"import { generateKey, importKey, open, seal } from \"./crypto\"\nimport { VaultError } from \"./errors\"\nimport { isKeyProvider, staticKey, type KeyProvider } from \"./providers\"\nimport type {\n PutOptions,\n RotationPolicy,\n SecretRecord,\n SecretSummary,\n VaultEvent,\n VaultStore,\n} from \"./types\"\n\n/**\n * How a stored value is referenced from configuration.\n *\n * @remarks\n * {@link Vault.resolve} substitutes any value that starts with this. Give a\n * vault its own {@link VaultOptions.prefix} when `@vault:` already means\n * something else in the configuration you are resolving.\n *\n * @defaultValue `\"@vault:\"`\n */\nexport const DEFAULT_PREFIX = \"@vault:\"\n\n/**\n * How many previous values an entry keeps, unless you say otherwise.\n *\n * @remarks\n * Only {@link Vault.rotate} adds to history, so this is how many superseded\n * values {@link Vault.versions} can still hand back.\n *\n * @defaultValue 5\n * @see {@link VaultOptions.historyLimit}\n */\nexport const DEFAULT_HISTORY_LIMIT = 5\n\nconst NAME_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/\n\n/**\n * What a random rotation draws from unless the policy says otherwise.\n *\n * @remarks\n * Letters and digits only, so a generated value survives being pasted into a\n * shell command or a connection string without quoting. Set\n * {@link RotationPolicy.alphabet} for anything narrower or wider.\n *\n * @defaultValue A-Z, a-z and 0-9: 62 characters\n * @see {@link randomValue}\n */\nexport const DEFAULT_ALPHABET =\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\"\n\n/**\n * Everything a generator is told: which entry is being rotated and the\n * non-secret arguments its policy carries. Deliberately not the current value —\n * a generator that needs it can ask the vault for it.\n *\n * @see {@link Generator}, {@link RotationPolicy.arguments}\n */\nexport type RotationContext = {\n /** Whose entry is being rotated. */\n owner: string\n /** Which entry is being rotated. */\n name: string\n /** The non-secret arguments its policy carries. */\n arguments: Record<string, string>\n}\n\n/**\n * Produces the next value for an entry whose policy names it.\n *\n * @remarks\n * Registered under a name in {@link VaultOptions.generators} and asked for by\n * {@link RotationPolicy.generator}. Use one where the vault cannot invent the\n * value itself — a key only the far end can mint, or a password that has to be\n * set on a database before it means anything.\n *\n * @param context Which entry is being rotated and its policy's arguments. Not\n * the value being replaced.\n * @returns The next value, or a promise of it.\n *\n * @example Minting a key at the provider that issues it\n * ```ts\n * const vault = new Vault({\n * key: generateKey(),\n * store: new MemoryStore(),\n * generators: {\n * provider: async ({ arguments: args }) => mintKeyFor(args.account),\n * },\n * })\n *\n * await vault.put(\"alice\", \"api\", \"old-key\", {\n * rotation: {\n * kind: \"generator\",\n * generator: \"provider\",\n * arguments: { account: \"acct_123\" },\n * },\n * })\n * await vault.rotate(\"alice\", \"api\")\n * ```\n */\nexport type Generator = (context: RotationContext) => Promise<string> | string\n\n/**\n * A random string of `length` characters drawn from `alphabet`.\n *\n * Sampling is rejected rather than folded with `%`, so every character is\n * equally likely however odd the alphabet's length.\n *\n * @param length How many characters to produce. Defaults to 32.\n * @param alphabet The characters to draw from. Defaults to\n * {@link DEFAULT_ALPHABET}.\n * @returns A fresh string of exactly `length` characters.\n * @throws {@link VaultError} 422 when `length` is below one, or `alphabet` has\n * fewer than two characters — neither can produce anything unguessable.\n *\n * @example\n * ```ts\n * randomValue() // 32 characters of A-Z, a-z, 0-9\n * randomValue(16, \"0123456789abcdef\") // 16 hex characters\n * ```\n *\n * @see {@link RotationPolicy} to have the vault call this during a rotation.\n */\nexport function randomValue(length = 32, alphabet = DEFAULT_ALPHABET): string {\n if (length < 1) throw new VaultError(\"A generated value needs at least one character.\")\n if (alphabet.length < 2) {\n throw new VaultError(\"A generated value needs an alphabet of at least two characters.\")\n }\n\n const ceiling = Math.floor(256 / alphabet.length) * alphabet.length\n let value = \"\"\n while (value.length < length) {\n const bytes = crypto.getRandomValues(new Uint8Array(length))\n for (const byte of bytes) {\n if (byte >= ceiling) continue\n value += alphabet[byte % alphabet.length]\n if (value.length === length) break\n }\n }\n return value\n}\n\n/**\n * Everything a {@link Vault} is built from.\n *\n * @example\n * ```ts\n * import { Vault, MemoryStore, envKey } from \"@mstone6969/vault\"\n *\n * const vault = new Vault({\n * key: envKey(\"VAULT_KEY\"),\n * store: new MemoryStore(),\n * historyLimit: 2,\n * onAccess: (event) => console.log(event.action, event.owner, event.name),\n * })\n * ```\n */\nexport type VaultOptions = {\n /** The master key: base64, already imported, or a provider that finds one. */\n key: string | CryptoKey | KeyProvider\n /** Where records are kept. */\n store: VaultStore\n /**\n * Reference prefix, `@vault:` unless you say otherwise.\n *\n * @defaultValue {@link DEFAULT_PREFIX}\n */\n prefix?: string\n /**\n * Keys this vault will still open values with, but never seal under.\n *\n * Keep the old key here while a `rekey` is in flight, or after one that did\n * not finish: values left under it stay readable instead of becoming\n * unopenable the moment the primary key changes.\n *\n * @defaultValue none\n * @see {@link Vault.rekey}\n */\n previousKeys?: (string | CryptoKey | KeyProvider)[]\n /**\n * How many previous values `rotate` keeps.\n *\n * @defaultValue {@link DEFAULT_HISTORY_LIMIT}\n */\n historyLimit?: number\n /**\n * Functions that mint new values, by the name a rotation policy uses.\n *\n * The vault stores the *name*, never the function — so what is written down\n * is that an entry can be rotated, not how to impersonate the thing that\n * rotates it.\n *\n * @defaultValue none, so a `generator` policy fails with 501\n */\n generators?: Record<string, Generator>\n /**\n * Called after everything the vault does, for an audit trail. It is never\n * awaited and its failures are ignored — logging must not break a vault.\n *\n * @see {@link VaultEvent} for what it is told, including the refusals,\n * which arrive as `denied` with a `detail` saying which rule was hit.\n */\n onAccess?: (event: VaultEvent) => void\n}\n\n/**\n * What a `rekey` did, and to what it could not do it.\n *\n * @see {@link Vault.rekey}\n */\nexport type RekeyReport = {\n /** How many entries were re-sealed under the new key. */\n rekeyed: number\n /** Entries that would not open, by `owner/name`, left exactly as they were. */\n failed: string[]\n}\n\nfunction toKey(key: string | CryptoKey | KeyProvider): Promise<CryptoKey> {\n const provider = isKeyProvider(key) ? key : staticKey(key)\n return Promise.resolve(provider.key()).then((resolved) =>\n typeof resolved === \"string\" ? importKey(resolved) : resolved\n )\n}\n\nfunction summarise(record: SecretRecord): SecretSummary {\n const { sealed: _sealed, sealedKey: _key, plain, history, ...rest } = record\n const summary: SecretSummary = { ...rest, versions: history.length }\n if (!record.isSealed && plain !== null) summary.value = plain\n return summary\n}\n\nfunction isExpired(record: SecretRecord, now: Date): boolean {\n return record.expiresAt !== null && record.expiresAt.getTime() <= now.getTime()\n}\n\n/**\n * A write-only credential store: values go in, and only `open`, `read` and\n * `resolve` take them out again.\n *\n * Every value is sealed under its own data key, and only that key is sealed\n * under the master key. Changing the master key therefore re-seals a handful of\n * bytes per entry rather than every value, and one exposed data key exposes one\n * value rather than all of them.\n *\n * @example Storing a credential and handing it to the thing that needs it\n * ```ts\n * import { Vault, MemoryStore, generateKey } from \"@mstone6969/vault\"\n *\n * const vault = new Vault({ key: generateKey(), store: new MemoryStore() })\n *\n * await vault.put(\"alice\", \"stripe_key\", \"sk_live_x\", {\n * metadata: { kind: \"api\" },\n * })\n *\n * // Nothing but open, read and resolve gets the value back out.\n * await vault.list(\"alice\") // name, metadata, dates — no value\n * await vault.open(\"alice\", \"stripe_key\")\n * await vault.resolve(\"alice\", { STRIPE_KEY: \"@vault:stripe_key\" })\n * ```\n *\n * @see {@link VaultOptions} for what it is built from, {@link VaultStore} for\n * where the records go, and {@link VaultError} for what it throws.\n */\nexport class Vault {\n private keySource: string | CryptoKey | KeyProvider\n private previousSources: (string | CryptoKey | KeyProvider)[]\n /** Resolved on first use, not at construction: a provider may need to wait,\n * or fail, and a vault nobody uses should do neither. */\n private keyCache: Promise<CryptoKey> | null = null\n private previousCache: Promise<CryptoKey>[] | null = null\n private readonly store: VaultStore\n private readonly historyLimit: number\n private readonly generators: Record<string, Generator>\n private readonly onAccess: ((event: VaultEvent) => void) | undefined\n /** The prefix {@link Vault.resolve} treats as a reference. */\n readonly prefix: string\n\n /**\n * @param options The key, the store and the policies this vault applies.\n * Nothing is contacted here: the key is resolved on first use, so a vault\n * built from a provider that is slow or unreachable costs nothing until\n * something asks it for a value.\n */\n constructor({\n key,\n store,\n prefix = DEFAULT_PREFIX,\n previousKeys = [],\n historyLimit = DEFAULT_HISTORY_LIMIT,\n generators = {},\n onAccess,\n }: VaultOptions) {\n this.keySource = key\n this.previousSources = previousKeys\n this.store = store\n this.prefix = prefix\n this.historyLimit = historyLimit\n this.generators = generators\n this.onAccess = onAccess\n }\n\n private master(): Promise<CryptoKey> {\n this.keyCache ??= toKey(this.keySource)\n return this.keyCache\n }\n\n private retired(): Promise<CryptoKey>[] {\n this.previousCache ??= this.previousSources.map(toKey)\n return this.previousCache\n }\n\n private record(event: Omit<VaultEvent, \"at\">): void {\n if (!this.onAccess) return\n try {\n this.onAccess({ ...event, at: new Date() })\n } catch {\n // An audit trail that throws must not take the vault with it.\n }\n }\n\n /** Opens something with the master key, falling back to retired ones. */\n private async unsealWithMaster(sealed: string): Promise<string> {\n try {\n return await open(await this.master(), sealed)\n } catch (error) {\n for (const previous of this.retired()) {\n try {\n return await open(await previous, sealed)\n } catch {\n // Not this one either; keep going.\n }\n }\n throw error\n }\n }\n\n /** Opens a value: its data key first, then the value under that key. */\n private async unseal(sealed: string, sealedKey: string | null): Promise<string> {\n if (!sealedKey) {\n // Written before envelope encryption: sealed under the master key.\n return this.unsealWithMaster(sealed)\n }\n const dataKey = await importKey(await this.unsealWithMaster(sealedKey))\n return open(dataKey, sealed)\n }\n\n /** Seals a value under a fresh data key, and that key under the master. */\n private async enseal(value: string): Promise<{ sealed: string; sealedKey: string }> {\n const material = generateKey()\n const dataKey = await importKey(material)\n return {\n sealed: await seal(dataKey, value),\n sealedKey: await seal(await this.master(), material),\n }\n }\n\n private checkName(name: string): string {\n const clean = String(name ?? \"\").trim()\n if (!NAME_PATTERN.test(clean)) {\n throw new VaultError(\n \"A name can be up to 64 characters: letters, numbers, dot, dash or underscore.\"\n )\n }\n return clean\n }\n\n /**\n * Everything an owner holds, sorted by name. Never a sealed value.\n *\n * @param owner Whose entries to list.\n * @returns One summary per entry, in name order. A sealed value is absent\n * entirely; an entry stored in the open carries its value in `value`.\n *\n * @remarks\n * Expiry hides nothing here: an entry past its `expiresAt` is still listed,\n * and still refuses to open, until {@link Vault.purgeExpired} clears it.\n *\n * @example\n * ```ts\n * await vault.put(\"alice\", \"stripe_key\", \"sk_live_x\", {\n * metadata: { kind: \"api\" },\n * })\n *\n * const [entry] = await vault.list(\"alice\")\n * entry.name // \"stripe_key\"\n * entry.metadata // { kind: \"api\" }\n * entry.versions // 0\n * ```\n *\n * @see {@link SecretSummary}\n */\n async list(owner: string): Promise<SecretSummary[]> {\n const records = await this.store.list(owner)\n return records.map(summarise).sort((a, b) => a.name.localeCompare(b.name))\n }\n\n /**\n * Stores a value, replacing whatever was under that name.\n *\n * `metadata` is kept in the clear and comes back from `list`, so it must\n * hold nothing secret — a credential's kind, or the username it belongs to,\n * not the password.\n *\n * @param owner Whose entry it is. Owners never see each other's entries.\n * @param name What to call it: 1-64 characters of letters, numbers, dot,\n * dash or underscore. Surrounding whitespace is trimmed.\n * @param value The value to store.\n * @param options Metadata, expiry, rotation policy, and whether the entry\n * is sealed, final, or keeps what it replaces. An option left out is\n * inherited from the existing entry.\n * @returns The stored entry, summarised — never its sealed value.\n * @throws {@link VaultError} 422 when the name is not 1-64 characters of\n * letters, numbers, dot, dash or underscore, or the value is empty.\n * @throws {@link VaultError} 409 when the entry is already there and\n * `final`: it can be deleted, never replaced.\n *\n * @example Replacing a value without restating what the entry is\n * ```ts\n * await vault.put(\"alice\", \"db\", \"first-password\", {\n * metadata: { kind: \"login\", username: \"ada\" },\n * rotation: { kind: \"random\", length: 24 },\n * })\n *\n * // Still a login, still rotatable at 24 characters, still sealed.\n * await vault.put(\"alice\", \"db\", \"second-password\")\n * ```\n *\n * @see {@link Vault.rotate} to replace a value and keep the old one,\n * {@link PutOptions} for the rest of the options.\n */\n async put(\n owner: string,\n name: string,\n value: string,\n options: PutOptions = {}\n ): Promise<SecretSummary> {\n const clean = this.checkName(name)\n if (!value) throw new VaultError(\"A secret needs a value.\")\n\n const existing = await this.store.get(owner, clean)\n if (existing?.isFinal) {\n this.record({ action: \"denied\", owner, name: clean, detail: \"final\" })\n throw new VaultError(\n `\"${clean}\" is final: it cannot be changed, only deleted.`,\n 409\n )\n }\n\n const now = new Date()\n // An option left out means \"as it was\": rotating a credential should\n // not quietly forget what kind it is or when it expires.\n const isSealed = options.open === undefined ? (existing?.isSealed ?? true) : !options.open\n const history =\n options.keepHistory && existing && existing.sealed\n ? [\n {\n sealed: existing.sealed,\n sealedKey: existing.sealedKey,\n createdAt: existing.updatedAt,\n },\n ...existing.history,\n ].slice(0, this.historyLimit)\n : (existing?.history ?? [])\n\n const body = isSealed\n ? { ...(await this.enseal(value)), plain: null }\n : { sealed: \"\", sealedKey: null, plain: value }\n\n const stored = await this.store.put({\n owner,\n name: clean,\n ...body,\n isSealed,\n isFinal: options.final === true,\n rotation:\n options.rotation === undefined\n ? (existing?.rotation ?? null)\n : options.rotation,\n rotatedAt: existing?.rotatedAt ?? null,\n expiresAt:\n options.expiresAt === undefined\n ? (existing?.expiresAt ?? null)\n : options.expiresAt,\n history,\n metadata: options.metadata ?? existing?.metadata ?? {},\n createdAt: existing?.createdAt ?? now,\n updatedAt: now,\n })\n\n this.record({ action: \"put\", owner, name: clean })\n return summarise(stored)\n }\n\n /**\n * Replaces a value, keeping the one it replaces.\n *\n * Called without a value, the entry's rotation policy produces one — which\n * is the point of storing a policy: whatever runs the rotation is told how\n * to make the next password, never what the current one is.\n *\n * Previous values stay openable, so a job that read the credential moments\n * before a rotation can still finish on what it was given.\n *\n * @param owner Whose entry to rotate.\n * @param name The entry to rotate.\n * @param value The new value. Left out, the entry's rotation policy makes\n * one.\n * @param options As {@link Vault.put}, minus `keepHistory`: a rotation\n * always keeps what it replaced, up to {@link VaultOptions.historyLimit}.\n * @returns The rotated entry, summarised, with `rotatedAt` stamped.\n * @throws {@link VaultError} 404 when no value is given and there is no\n * such entry to take a policy from.\n * @throws {@link VaultError} 410 when no value is given and the entry has\n * expired.\n * @throws {@link VaultError} 422 when no value is given and the entry has\n * no rotation policy to make one with.\n * @throws {@link VaultError} 501 when the policy names a generator this\n * vault was not constructed with.\n * @throws {@link VaultError} 409 when the entry is final.\n *\n * @example\n * ```ts\n * await vault.put(\"alice\", \"db\", \"first-password\", {\n * rotation: { kind: \"random\", length: 24 },\n * })\n *\n * const rotated = await vault.rotate(\"alice\", \"db\")\n * rotated.rotatedAt // stamped just now\n * await vault.versions(\"alice\", \"db\") // [\"first-password\"]\n * ```\n *\n * @see {@link Vault.rotationDue} for which entries are asking for this.\n */\n async rotate(\n owner: string,\n name: string,\n value?: string,\n options: Omit<PutOptions, \"keepHistory\"> = {}\n ): Promise<SecretSummary> {\n const clean = this.checkName(name)\n const next = value ?? (await this.generate(owner, clean))\n\n this.record({ action: \"rotate\", owner, name: clean })\n const summary = await this.put(owner, clean, next, { ...options, keepHistory: true })\n\n // Stamped after the fact, so a failed rotation does not look like one.\n const stored = await this.store.get(owner, clean)\n if (stored) await this.store.put({ ...stored, rotatedAt: new Date() })\n return { ...summary, rotatedAt: new Date() }\n }\n\n /** The next value an entry's policy calls for. */\n private async generate(owner: string, name: string): Promise<string> {\n const record = await this.require(owner, name)\n const policy = record.rotation\n\n if (!policy) {\n throw new VaultError(\n `\"${name}\" has no rotation policy, so there is nothing to make the next value with.`\n )\n }\n\n if (policy.kind === \"random\") {\n return randomValue(policy.length, policy.alphabet)\n }\n\n const generator = policy.generator ? this.generators[policy.generator] : undefined\n if (!generator) {\n throw new VaultError(\n `\"${name}\" wants the \"${policy.generator ?? \"unnamed\"}\" generator, which this vault does not have.`,\n 501\n )\n }\n return generator({ owner, name, arguments: policy.arguments ?? {} })\n }\n\n /**\n * Entries whose policy says how often they want rotating, and whose time\n * has come. Nothing rotates them for you — schedule this and act on it.\n *\n * @param now The moment to judge against. Pass a later one to ask what will\n * be due by then.\n * @returns Summaries of every entry, whoever owns it, whose\n * {@link RotationPolicy.every} seconds have passed since it was last\n * rotated — or since it was stored, if it never has been.\n *\n * @remarks\n * One of the three calls that reach across owners, so it belongs to\n * whatever runs the schedule rather than to a request.\n *\n * @example\n * ```ts\n * await vault.put(\"alice\", \"db\", \"x\", {\n * rotation: { kind: \"random\", every: 86_400 },\n * })\n *\n * for (const entry of await vault.rotationDue()) {\n * await vault.rotate(entry.owner, entry.name)\n * }\n * ```\n */\n async rotationDue(now = new Date()): Promise<SecretSummary[]> {\n const records = await this.store.all()\n return records\n .filter((record) => {\n const every = record.rotation?.every\n if (!every) return false\n const last = (record.rotatedAt ?? record.createdAt).getTime()\n return now.getTime() - last >= every * 1000\n })\n .map(summarise)\n }\n\n /**\n * Previous values of an entry, newest first, opened.\n *\n * @param owner Whose entry it is.\n * @param name The entry to look back through.\n * @returns The values it used to hold, newest first, in the clear. Empty\n * for an entry that has never been rotated.\n * @throws {@link VaultError} 404 when there is no such entry.\n * @throws {@link VaultError} 410 when the entry has expired.\n * @throws {@link VaultError} 422 when the name is not a legal one.\n * @throws {@link VaultKeyError} 500 when a kept value will not open under\n * the master key or any of {@link VaultOptions.previousKeys}.\n *\n * @example\n * ```ts\n * await vault.put(\"alice\", \"deploy\", \"v1\")\n * await vault.rotate(\"alice\", \"deploy\", \"v2\")\n * await vault.rotate(\"alice\", \"deploy\", \"v3\")\n *\n * await vault.versions(\"alice\", \"deploy\") // [\"v2\", \"v1\"]\n * ```\n */\n async versions(owner: string, name: string): Promise<string[]> {\n const record = await this.require(owner, name)\n return Promise.all(\n record.history.map((entry) => this.unseal(entry.sealed, entry.sealedKey))\n )\n }\n\n /**\n * True when the owner has a secret under that name, expired or not.\n *\n * @param owner Whose entry to look for.\n * @param name The name to look for.\n * @returns Whether a record exists under it.\n * @throws {@link VaultError} 422 when the name is not a legal one.\n *\n * @remarks\n * Opens nothing and is not stopped by expiry, so it answers \"is this name\n * taken\" rather than \"can this value still be used\".\n */\n async has(owner: string, name: string): Promise<boolean> {\n return (await this.store.get(owner, this.checkName(name))) !== null\n }\n\n /**\n * Removes a secret, returning false if it wasn't there.\n *\n * @param owner Whose entry to delete.\n * @param name The entry to delete.\n * @returns True when something was deleted, false when there was nothing\n * under that name.\n * @throws {@link VaultError} 422 when the name is not a legal one.\n *\n * @remarks\n * Deleting takes the kept previous values with it, and it is the one thing\n * a `final` entry allows.\n */\n async remove(owner: string, name: string): Promise<boolean> {\n const removed = await this.store.remove(owner, this.checkName(name))\n if (removed) this.record({ action: \"remove\", owner, name })\n return removed\n }\n\n private async require(\n owner: string,\n name: string,\n now = new Date()\n ): Promise<SecretRecord> {\n const clean = this.checkName(name)\n const record = await this.store.get(owner, clean)\n if (!record) throw new VaultError(`No secret named \"${clean}\" in the vault.`, 404)\n\n if (isExpired(record, now)) {\n this.record({ action: \"denied\", owner, name: clean, detail: \"expired\" })\n throw new VaultError(`\"${clean}\" expired and can no longer be used.`, 410)\n }\n return record\n }\n\n /**\n * Reads one value back. The only way plaintext leaves a sealed entry — keep\n * it in memory and out of logs and responses.\n *\n * @param owner Whose entry to open.\n * @param name The entry to open.\n * @returns The value, sealed or not.\n * @throws {@link VaultError} 404 when there is no such entry.\n * @throws {@link VaultError} 410 when the entry has expired. The record is\n * still there; it just cannot be used.\n * @throws {@link VaultError} 422 when the name is not a legal one.\n * @throws {@link VaultKeyError} 500 when the value will not open under the\n * master key or any of {@link VaultOptions.previousKeys} — a wrong key or\n * an altered value, which GCM cannot tell apart.\n *\n * @example\n * ```ts\n * await vault.put(\"alice\", \"token\", \"shhh\")\n * await vault.open(\"alice\", \"token\") // \"shhh\"\n * ```\n *\n * @see {@link Vault.read} for entries stored in the open,\n * {@link Vault.resolve} for substituting several at once.\n */\n async open(owner: string, name: string): Promise<string> {\n const record = await this.require(owner, name)\n const value = record.isSealed\n ? await this.unseal(record.sealed, record.sealedKey)\n : (record.plain ?? \"\")\n\n this.record({ action: \"open\", owner, name })\n return value\n }\n\n /**\n * Reads an entry stored in the open. A sealed one refuses.\n *\n * @param owner Whose entry to read.\n * @param name The entry to read.\n * @returns The value, which was stored with `open: true` and was therefore\n * never secret.\n * @throws {@link VaultError} 403 when the entry is sealed.\n * {@link Vault.open} is the only way a sealed value comes out.\n * @throws {@link VaultError} 404 when there is no such entry.\n * @throws {@link VaultError} 410 when the entry has expired.\n * @throws {@link VaultError} 422 when the name is not a legal one.\n *\n * @example\n * ```ts\n * await vault.put(\"alice\", \"region\", \"eu-west-1\", { open: true })\n * await vault.read(\"alice\", \"region\") // \"eu-west-1\"\n *\n * await vault.put(\"alice\", \"token\", \"shhh\")\n * await vault.read(\"alice\", \"token\") // throws: 403, sealed\n * ```\n */\n async read(owner: string, name: string): Promise<string> {\n const record = await this.require(owner, name)\n if (record.isSealed) {\n this.record({ action: \"denied\", owner, name, detail: \"sealed\" })\n throw new VaultError(`\"${name}\" is sealed and cannot be read back.`, 403)\n }\n\n this.record({ action: \"read\", owner, name })\n return record.plain ?? \"\"\n }\n\n /**\n * Substitutes `@vault:<name>` references in a set of values — an\n * environment, a config object — leaving everything else alone.\n *\n * A reference to a secret that isn't there, or that has expired, throws:\n * running with a blank credential is worse than not running.\n *\n * @param owner Whose entries the references name.\n * @param values The set to substitute into. Not modified.\n * @returns A copy with every reference replaced by its value, or the same\n * object back when nothing in it is a reference.\n * @throws {@link VaultError} 404 when a reference names no entry.\n * @throws {@link VaultError} 410 when a referenced entry has expired.\n * @throws {@link VaultError} 422 when what follows the prefix is not a\n * legal name.\n *\n * @remarks\n * Whitespace around the name is ignored, so `\"@vault: token \"` finds\n * `token`. Sealed and open entries both resolve; the prefix comes from\n * {@link Vault.prefix}.\n *\n * @example Filling in an environment before spawning something\n * ```ts\n * await vault.put(\"alice\", \"token\", \"secret\")\n *\n * await vault.resolve(\"alice\", {\n * PLAIN: \"kept\",\n * API_TOKEN: \"@vault:token\",\n * })\n * // { PLAIN: \"kept\", API_TOKEN: \"secret\" }\n * ```\n */\n async resolve(\n owner: string,\n values: Record<string, string>\n ): Promise<Record<string, string>> {\n const references = Object.entries(values).filter(([, value]) =>\n value.startsWith(this.prefix)\n )\n if (references.length === 0) return values\n\n const resolved = { ...values }\n for (const [key, value] of references) {\n resolved[key] = await this.open(owner, value.slice(this.prefix.length).trim())\n }\n return resolved\n }\n\n /**\n * Re-seals values under fresh data keys, without changing the master key.\n *\n * Cheap hygiene: the ciphertext of an unchanged secret stops being\n * comparable between two copies of the database taken at different times.\n *\n * @param owner Limit it to one owner's entries. Left out, it walks the\n * whole store, whoever owns it.\n * @returns How many entries were re-sealed. Entries stored in the open hold\n * nothing to re-seal and are skipped.\n * @throws {@link VaultKeyError} 500 when a value will not open. Unlike\n * {@link Vault.rekey} this stops there, having already re-sealed the\n * entries it got to — those are unharmed, since the key did not change.\n *\n * @example\n * ```ts\n * await vault.reseal(\"alice\") // just this owner\n * await vault.reseal() // the whole store\n * ```\n */\n async reseal(owner?: string): Promise<number> {\n const records = owner ? await this.store.list(owner) : await this.store.all()\n let resealed = 0\n\n for (const record of records) {\n if (!record.isSealed || !record.sealed) continue\n const value = await this.unseal(record.sealed, record.sealedKey)\n await this.store.put({ ...record, ...(await this.enseal(value)) })\n resealed += 1\n }\n return resealed\n }\n\n /**\n * Re-seals every data key under a new master key.\n *\n * The old key is kept as a fallback for the rest of this vault's life, so a\n * run that stops halfway leaves a mix that still opens. Construct the next\n * vault with `key: next, previousKeys: [old]` until you are confident, then\n * drop the old one.\n *\n * An entry that will not open is left untouched and named in the report,\n * because re-sealing what you cannot read would only destroy it.\n *\n * @param next The new master key: base64, already imported, or a provider\n * that finds one.\n * @returns How many entries moved, and which would not, by `owner/name`.\n * @throws {@link VaultKeyError} 500 when `next` is not a usable key. It is\n * resolved before anything is written, so nothing has changed.\n *\n * @remarks\n * Walks every owner, and changes this vault as it goes: from here on it\n * seals under `next` and keeps the key it had as a fallback. An entry\n * stored in the open holds no key and is skipped, and one written before\n * envelope encryption is given a data key on the way past.\n *\n * @example\n * ```ts\n * const next = generateKey()\n * const report = await vault.rekey(next)\n * report.rekeyed // 2\n * report.failed // [\"alice/stranger\"] — left exactly as they were\n *\n * // Until the failures are dealt with, keep the old key readable.\n * const moved = new Vault({ key: next, store, previousKeys: [old] })\n * ```\n *\n * @see {@link RekeyReport}, {@link VaultOptions.previousKeys}\n */\n async rekey(next: string | CryptoKey | KeyProvider): Promise<RekeyReport> {\n const nextKey = await toKey(next)\n const report: RekeyReport = { rekeyed: 0, failed: [] }\n\n for (const record of await this.store.all()) {\n // Entries in the open hold no key to re-seal.\n if (!record.isSealed) continue\n\n try {\n const history = await this.rekeyHistory(record, nextKey)\n\n if (record.sealedKey) {\n // Envelope: only the data key moves.\n const material = await this.unsealWithMaster(record.sealedKey)\n await this.store.put({\n ...record,\n sealedKey: await seal(nextKey, material),\n history,\n })\n } else {\n // Written before envelopes: give it one on the way past.\n const value = await this.unsealWithMaster(record.sealed)\n const material = generateKey()\n const dataKey = await importKey(material)\n await this.store.put({\n ...record,\n sealed: await seal(dataKey, value),\n sealedKey: await seal(nextKey, material),\n history,\n })\n }\n report.rekeyed += 1\n } catch {\n report.failed.push(`${record.owner}/${record.name}`)\n }\n }\n\n // Everything from here seals under the new key; the old one stays\n // readable in case a value was missed.\n this.previousSources = [this.keySource, ...this.previousSources]\n this.keySource = nextKey\n this.keyCache = Promise.resolve(nextKey)\n this.previousCache = null\n\n this.record({\n action: \"rekey\",\n owner: \"\",\n name: null,\n detail: `${report.rekeyed} re-sealed, ${report.failed.length} failed`,\n })\n return report\n }\n\n /** Moves an entry's kept values onto the new master key alongside it. */\n private async rekeyHistory(\n record: SecretRecord,\n nextKey: CryptoKey\n ): Promise<SecretRecord[\"history\"]> {\n return Promise.all(\n record.history.map(async (entry) => {\n if (!entry.sealedKey) return entry\n const material = await this.unsealWithMaster(entry.sealedKey)\n return { ...entry, sealedKey: await seal(nextKey, material) }\n })\n )\n }\n\n /**\n * Deletes entries whose time is up. Returns how many went.\n *\n * @param now The moment to judge against. Pass a later one to see what a\n * run then would take.\n * @returns How many entries were deleted.\n *\n * @remarks\n * Walks every owner. Expiry stops an entry being used, not being stored:\n * until this runs, an expired entry is still there and still listed.\n *\n * @example\n * ```ts\n * await vault.put(\"alice\", \"temporary\", \"x\", {\n * expiresAt: new Date(Date.now() - 1),\n * })\n *\n * await vault.open(\"alice\", \"temporary\") // throws: 410, expired\n * await vault.purgeExpired() // 1\n * await vault.has(\"alice\", \"temporary\") // false\n * ```\n */\n async purgeExpired(now = new Date()): Promise<number> {\n const expired = (await this.store.all()).filter((record) => isExpired(record, now))\n for (const record of expired) {\n await this.store.remove(record.owner, record.name)\n }\n return expired.length\n }\n}\n",
|
|
9
|
+
"import type { SecretRecord, VaultStore } from \"../types\"\n\n/**\n * Keeps sealed values in a Map. Handy for tests and short-lived processes.\n *\n * Nothing is written anywhere: when the process ends every record goes with it.\n * That is the point — a test gets a clean vault per run without a file to\n * create and delete, and a short-lived worker holding a few credentials in\n * memory leaves nothing behind on disk.\n *\n * @remarks\n * Like every {@link VaultStore}, this persists records exactly as given and\n * enforces nothing: finality, expiry and history are rules the {@link Vault}\n * applies before it calls {@link MemoryStore.put}.\n * @example\n * ```ts\n * import { generateKey, MemoryStore, Vault } from \"@mstone6969/vault\"\n *\n * const vault = new Vault({ key: generateKey(), store: new MemoryStore() })\n * await vault.put(\"alice\", \"db-password\", \"hunter2\")\n * ```\n * @see {@link FileStore} for a store that survives the process.\n */\nexport class MemoryStore implements VaultStore {\n /** Records by `owner name`, the key {@link MemoryStore.key} builds. */\n private readonly records: Map<string, SecretRecord>\n\n /**\n * Makes an empty store.\n *\n * @remarks\n * Takes nothing: there is no file to name and no key to hand it, since\n * records never leave the process.\n * @example\n * ```ts\n * import { MemoryStore } from \"@mstone6969/vault\"\n *\n * const store = new MemoryStore()\n * ```\n */\n constructor() {\n this.records = new Map()\n }\n\n /**\n * The Map key for one entry: owners are separate namespaces, so the name\n * alone is not unique.\n *\n * @param owner Whose entry it is.\n * @param name What the entry is called.\n * @returns The two joined by a space.\n */\n private static key(owner: string, name: string): string {\n return `${owner} ${name}`\n }\n\n /**\n * One record, or null when there is none under that name.\n *\n * @param owner Whose entry to look for.\n * @param name What the entry is called.\n * @returns The stored record, or null if this owner has nothing by that\n * name. The record itself is returned, not a copy, so callers must not\n * mutate it.\n * @example\n * ```ts\n * import { MemoryStore } from \"@mstone6969/vault\"\n *\n * const store = new MemoryStore()\n * const record = await store.get(\"alice\", \"db-password\") // null\n * ```\n */\n async get(owner: string, name: string): Promise<SecretRecord | null> {\n return this.records.get(MemoryStore.key(owner, name)) ?? null\n }\n\n /**\n * Every record one owner holds.\n *\n * @param owner Whose entries to return.\n * @returns That owner's records, in no particular order — the vault's\n * `list` sorts them. Empty for an owner the store has never heard of.\n * @see {@link MemoryStore.all} when the caller needs every owner's records.\n */\n async list(owner: string): Promise<SecretRecord[]> {\n return [...this.records.values()].filter((record) => record.owner === owner)\n }\n\n /**\n * Every record, whoever owns it.\n *\n * @returns All records the store holds, in no particular order.\n * @remarks\n * This is the only place anything reaches across owners, and it exists for\n * the vault-wide operations — `rekey`, `reseal` and `purgeExpired` — which\n * have to touch every entry. Ordinary reads go through\n * {@link MemoryStore.list}.\n */\n async all(): Promise<SecretRecord[]> {\n return [...this.records.values()]\n }\n\n /**\n * Writes a record, replacing any under the same owner and name.\n *\n * @param record The record to keep, stored as given.\n * @returns The same record, so a caller can write and use the result in one\n * step.\n * @remarks\n * An `isFinal` record is replaced here without complaint: refusing that is\n * the vault's job, and it checks before it calls this.\n */\n async put(record: SecretRecord): Promise<SecretRecord> {\n this.records.set(MemoryStore.key(record.owner, record.name), record)\n return record\n }\n\n /**\n * Deletes a record, returning false when there was nothing to delete.\n *\n * @param owner Whose entry to delete.\n * @param name What the entry is called.\n * @returns True if a record was removed, false if there was none — so a\n * caller can tell a delete from a no-op.\n */\n async remove(owner: string, name: string): Promise<boolean> {\n return this.records.delete(MemoryStore.key(owner, name))\n }\n}\n",
|
|
10
|
+
"import { open as openFile, readFile, rename, unlink } from \"node:fs/promises\"\nimport { importKey, open, seal } from \"../crypto\"\nimport { VaultKeyError } from \"../errors\"\nimport { isKeyProvider, staticKey, type KeyProvider } from \"../providers\"\nimport type { SecretRecord, VaultStore } from \"../types\"\n\n/** What a vault file starts with, so a wrong file is refused, not parsed. */\nconst MAGIC = \"VAULT1\"\n\n/**\n * Keeps every record in one encrypted file.\n *\n * The other stores seal values and leave the rest in the open: SQLite has an\n * `owner` column and a `name` column, and anyone who can read the file learns\n * what you keep even if they cannot read it. Here the whole index — owners,\n * names, metadata, timestamps, everything — is inside a single AES-256-GCM\n * envelope. What leaks from the file at rest is its size.\n *\n * The trade is that it is loaded and written whole, so it suits hundreds of\n * secrets rather than millions, and one writer rather than several. Writes go\n * to a temporary file and are renamed into place, so a crash mid-write leaves\n * the previous file rather than half of a new one.\n *\n * @remarks\n * The store is encrypted under its own key, separate from the master key the\n * {@link Vault} seals values with, so the file is two layers deep: the index\n * under the store's key, each value under its own data key under the vault's.\n * Handing both layers the same key is allowed and sometimes what you want, but\n * then one key opens both.\n *\n * The key is resolved on the first read or write, not at construction, so a\n * provider that reaches for a file or a network service is asked once and only\n * when a record is actually wanted. A bad key surfaces from that first\n * operation as a {@link VaultKeyError}, not from `new`.\n *\n * Nothing here locks the file. Two stores writing the same path — two\n * processes, or two instances in one — each hold their own copy of the index\n * and write it whole, so the last save wins and the other's writes are gone.\n * Keep one writer per file.\n *\n * @example\n * A vault whose names are as hidden as its values. The store's key opens the\n * file; the vault's key seals what is inside it.\n * ```ts\n * import { Vault, fileKey } from \"@mstone6969/vault\"\n * import { FileStore } from \"@mstone6969/vault/stores/file\"\n *\n * const store = new FileStore(\"./secrets.vault\", fileKey(\"/etc/vault-file.key\"))\n * const vault = new Vault({ key: fileKey(\"/etc/vault-master.key\"), store })\n *\n * await vault.put(\"alice\", \"db\", \"hunter2\") // the file is written here\n * await vault.open(\"alice\", \"db\") // \"hunter2\"\n * ```\n *\n * @see {@link VaultStore} for what a store owes the vault, and `MemoryStore`\n * and `SqliteStore` for the two that keep their index in the open.\n */\nexport class FileStore implements VaultStore {\n private readonly path: string\n private readonly keySource: string | CryptoKey | KeyProvider\n /** Resolved on first read or write, not at construction. */\n private keyCache: Promise<CryptoKey> | null = null\n private records: Map<string, SecretRecord> | null = null\n\n /**\n * @param path Where to keep the file. Created on first write, so a path\n * that does not exist yet is an empty store rather than an error.\n * @param key The key the *file* is encrypted with: base64, already\n * imported, or a {@link KeyProvider} that finds one. Give it one of its\n * own, or hand it the vault's — sharing means one key opens both layers.\n *\n * @example\n * ```ts\n * import { generateKey } from \"@mstone6969/vault\"\n * import { FileStore } from \"@mstone6969/vault/stores/file\"\n *\n * // Nothing is read or written until the first call.\n * const store = new FileStore(\"./secrets.vault\", generateKey())\n * ```\n */\n constructor(path: string, key: string | CryptoKey | KeyProvider) {\n this.path = path\n this.keySource = key\n }\n\n private key(): Promise<CryptoKey> {\n this.keyCache ??= (async () => {\n const provider = isKeyProvider(this.keySource) ? this.keySource : staticKey(this.keySource)\n const resolved = await provider.key()\n return typeof resolved === \"string\" ? importKey(resolved) : resolved\n })()\n return this.keyCache\n }\n\n private static id(owner: string, name: string): string {\n return `${owner} ${name}`\n }\n\n /**\n * Reads and decrypts the file, once, keeping it in memory afterwards.\n *\n * @returns The whole index, keyed by owner and name. The same map every\n * time until {@link FileStore.forget} drops it.\n * @throws {@link VaultKeyError} when the file does not begin with the magic line —\n * something that is not one of ours is refused rather than parsed — or\n * when the key does not open it.\n */\n private async load(): Promise<Map<string, SecretRecord>> {\n if (this.records) return this.records\n\n let contents: string\n try {\n contents = (await readFile(this.path, \"utf8\")).trim()\n } catch (error) {\n // No file yet is an empty store, not a failure. Anything else —\n // a permission problem, a directory in the way — is the caller's\n // to hear about.\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error\n this.records = new Map()\n return this.records\n }\n\n const [magic, payload] = contents.split(\"\\n\")\n if (magic !== MAGIC || !payload) {\n throw new VaultKeyError(`${this.path} is not a vault file.`)\n }\n\n const opened = await open(await this.key(), payload)\n const parsed = JSON.parse(opened) as SecretRecord[]\n\n this.records = new Map(\n parsed.map((record) => [\n FileStore.id(record.owner, record.name),\n {\n ...record,\n expiresAt: record.expiresAt === null ? null : new Date(record.expiresAt),\n history: record.history.map((entry) => ({\n ...entry,\n createdAt: new Date(entry.createdAt),\n })),\n rotatedAt: record.rotatedAt === null ? null : new Date(record.rotatedAt),\n createdAt: new Date(record.createdAt),\n updatedAt: new Date(record.updatedAt),\n },\n ])\n )\n return this.records\n }\n\n /**\n * Seals everything and swaps the file for the new one in a single move.\n *\n * Every record goes into one envelope, so the cost of a write is the size\n * of the whole store, not of the record that changed.\n *\n * @returns Nothing, once the new file is in place.\n * @throws {@link VaultKeyError} when the key cannot be resolved or imported.\n */\n private async save(): Promise<void> {\n const records = await this.load()\n const sealed = await seal(await this.key(), JSON.stringify([...records.values()]))\n\n // A name of its own per write, so two writers collide on the file they\n // are replacing rather than on each other's half-written temporary.\n const temporary = `${this.path}.${process.pid}.${Date.now()}.writing`\n const handle = await openFile(temporary, \"w\")\n try {\n await handle.writeFile(`${MAGIC}\\n${sealed}\\n`)\n // Flush before the rename: without this a power loss can leave the\n // new name pointing at a file the disk never finished writing, and\n // since the whole index is one envelope that loses every record,\n // not one.\n await handle.sync()\n } finally {\n await handle.close()\n }\n\n // Rename is atomic on the same filesystem: readers see one file or the\n // other, never a half-written one.\n await rename(temporary, this.path)\n }\n\n /**\n * One record, or null when there is none under that name.\n *\n * @param owner Whose record to look for.\n * @param name What it is called.\n * @returns The record, or null.\n * @throws {@link VaultKeyError} on the first call if the file is not a vault file,\n * or the key does not open it.\n */\n async get(owner: string, name: string): Promise<SecretRecord | null> {\n return (await this.load()).get(FileStore.id(owner, name)) ?? null\n }\n\n /**\n * Every record one owner holds.\n *\n * The filtering happens in memory, over the whole index — there is no\n * per-owner slice of the file to read on its own.\n *\n * @param owner Whose records to return.\n * @returns That owner's records, in whatever order they were written.\n * @throws {@link VaultKeyError} on the first call if the file cannot be opened.\n */\n async list(owner: string): Promise<SecretRecord[]> {\n return [...(await this.load()).values()].filter((record) => record.owner === owner)\n }\n\n /**\n * Every record, whoever owns it.\n *\n * @returns All of them. Only `rekey` and `purgeExpired` need this.\n * @throws {@link VaultKeyError} on the first call if the file cannot be opened.\n *\n * @example\n * Check a file opens under the key you think it does, before trusting it.\n * ```ts\n * import { VaultKeyError } from \"@mstone6969/vault\"\n * import { FileStore } from \"@mstone6969/vault/stores/file\"\n *\n * try {\n * await new FileStore(\"./secrets.vault\", process.env.VAULT_FILE_KEY!).all()\n * } catch (error) {\n * if (error instanceof VaultKeyError) console.error(error.message)\n * }\n * ```\n */\n async all(): Promise<SecretRecord[]> {\n return [...(await this.load()).values()]\n }\n\n /**\n * Writes a record, replacing any under the same owner and name.\n *\n * The whole file is re-sealed and rewritten, so writes cost the size of the\n * store rather than of the record.\n *\n * @param record The record to keep. Stored as given: finality, expiry and\n * history are the vault's rules, not the store's.\n * @returns The same record, so callers can chain.\n * @throws {@link VaultKeyError} if the file cannot be opened, or the key cannot be\n * resolved.\n */\n async put(record: SecretRecord): Promise<SecretRecord> {\n const records = await this.load()\n const id = FileStore.id(record.owner, record.name)\n const displaced = records.get(id)\n\n records.set(id, record)\n try {\n await this.save()\n } catch (error) {\n // Put the index back: a write that failed must not leave memory\n // claiming something the file does not hold.\n if (displaced) records.set(id, displaced)\n else records.delete(id)\n throw error\n }\n return record\n }\n\n /**\n * Deletes a record, returning false when there was nothing to delete.\n *\n * @param owner Whose record to delete.\n * @param name What it is called.\n * @returns Whether a record was there to delete. The file is only rewritten\n * when one was.\n * @throws {@link VaultKeyError} if the file cannot be opened.\n */\n async remove(owner: string, name: string): Promise<boolean> {\n const records = await this.load()\n const id = FileStore.id(owner, name)\n const removed = records.get(id)\n if (!removed) return false\n\n records.delete(id)\n try {\n await this.save()\n } catch (error) {\n records.set(id, removed)\n throw error\n }\n return true\n }\n\n /**\n * Forgets what it read, so the next call goes back to the file.\n *\n * The point of a store that holds its whole index in memory is that reads\n * are free; the cost is that a file another process rewrote is invisible\n * until you say this. It also drops the decrypted index, which is the only\n * place the names live in the clear.\n *\n * @example\n * ```ts\n * import { FileStore } from \"@mstone6969/vault/stores/file\"\n *\n * const store = new FileStore(\"./secrets.vault\", process.env.VAULT_FILE_KEY!)\n * await store.all() // reads and decrypts the file\n * store.forget()\n * await store.all() // reads it again\n * ```\n */\n forget(): void {\n this.records = null\n }\n\n /**\n * Deletes the file and everything in it.\n *\n * The in-memory index is emptied first, so a store that is used again\n * writes a fresh file rather than resurrecting what was there.\n *\n * @returns Nothing. A file that is already gone is the outcome wanted, not\n * an error.\n */\n async destroy(): Promise<void> {\n this.records = new Map()\n await unlink(this.path).catch(() => {\n // Already gone is the outcome we wanted.\n })\n }\n}\n"
|
|
9
11
|
],
|
|
10
|
-
"mappings": "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",
|
|
11
|
-
"debugId": "
|
|
12
|
+
"mappings": "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",
|
|
13
|
+
"debugId": "D639A370455C926764756E2164756E21",
|
|
12
14
|
"names": []
|
|
13
15
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the master key comes from.
|
|
3
|
+
*
|
|
4
|
+
* A vault takes a provider rather than a string so the key can live wherever
|
|
5
|
+
* you keep such things — the environment, a file with tight permissions, or a
|
|
6
|
+
* service that hands one over. Write your own for anything else; it is one
|
|
7
|
+
* method.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Whoever holds the provider — {@link Vault} or {@link FileStore} — calls
|
|
11
|
+
* `key` the first time a key is actually needed and keeps the promise, so a
|
|
12
|
+
* provider that reaches over the network is asked once and a vault nobody uses
|
|
13
|
+
* never asks at all. Anything thrown surfaces from that first operation, not
|
|
14
|
+
* from the constructor.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* A provider for something this package does not ship — here a KMS, but the
|
|
18
|
+
* shape is the same for 1Password, age, or a file on a smartcard.
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { Vault, MemoryStore, type KeyProvider } from "@mstone6969/vault"
|
|
21
|
+
*
|
|
22
|
+
* function kmsKey(id: string): KeyProvider {
|
|
23
|
+
* return {
|
|
24
|
+
* async key() {
|
|
25
|
+
* const response = await fetch(`https://kms.internal/keys/${id}`)
|
|
26
|
+
* if (!response.ok) throw new Error(`KMS refused key ${id}.`)
|
|
27
|
+
* return (await response.text()).trim() // base64
|
|
28
|
+
* },
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* const vault = new Vault({ key: kmsKey("vault-master"), store: new MemoryStore() })
|
|
33
|
+
* await vault.put("alice", "db", "hunter2") // the KMS is called here, once
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @see {@link staticKey}, {@link envKey} and {@link fileKey} for the ones that
|
|
37
|
+
* ship.
|
|
38
|
+
*/
|
|
39
|
+
export type KeyProvider = {
|
|
40
|
+
/**
|
|
41
|
+
* The key, base64 or already imported. Called once, when first needed.
|
|
42
|
+
*
|
|
43
|
+
* @returns The master key: a base64 string of 32 bytes, or a `CryptoKey`
|
|
44
|
+
* already imported for AES-GCM. May be a promise.
|
|
45
|
+
* @throws Whatever the source of the key throws when it cannot produce
|
|
46
|
+
* one. The ones here throw {@link VaultKeyError}.
|
|
47
|
+
*/
|
|
48
|
+
key(): Promise<string | CryptoKey> | string | CryptoKey;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* A key you already have.
|
|
52
|
+
*
|
|
53
|
+
* Mostly for handing a key to something that wants a provider —
|
|
54
|
+
* {@link Vault.rekey}, or a `previousKeys` entry — without wrapping it
|
|
55
|
+
* yourself. Passing raw key material as `key` does this for you.
|
|
56
|
+
*
|
|
57
|
+
* @param key The master key: base64, or already imported.
|
|
58
|
+
* @returns A provider that hands back `key` every time.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* import { Vault, MemoryStore, generateKey, staticKey } from "@mstone6969/vault"
|
|
63
|
+
*
|
|
64
|
+
* const vault = new Vault({ key: staticKey(generateKey()), store: new MemoryStore() })
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function staticKey(key: string | CryptoKey): KeyProvider;
|
|
68
|
+
/**
|
|
69
|
+
* A key from an environment variable.
|
|
70
|
+
*
|
|
71
|
+
* @param name The variable to read, at the moment the key is first needed —
|
|
72
|
+
* so a process that loads its environment after building the vault still
|
|
73
|
+
* works.
|
|
74
|
+
* @returns A provider that reads `process.env[name]`.
|
|
75
|
+
* @throws {@link VaultKeyError} when the variable is unset or empty, at the first
|
|
76
|
+
* operation that needs a key rather than at construction.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* import { Vault, MemoryStore, envKey } from "@mstone6969/vault"
|
|
81
|
+
*
|
|
82
|
+
* const vault = new Vault({ key: envKey("VAULT_KEY"), store: new MemoryStore() })
|
|
83
|
+
* await vault.put("alice", "db", "hunter2") // throws here if VAULT_KEY is unset
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export declare function envKey(name: string): KeyProvider;
|
|
87
|
+
/**
|
|
88
|
+
* A key from a file — the usual way to keep one off the process list and out
|
|
89
|
+
* of shell history. Whitespace around it is ignored, so a trailing newline
|
|
90
|
+
* from `openssl rand -base64 32 > key` does no harm.
|
|
91
|
+
*
|
|
92
|
+
* @param path The key file, read at the moment the key is first needed. It is
|
|
93
|
+
* read once and kept, so replacing the file later does not change the key a
|
|
94
|
+
* running vault uses.
|
|
95
|
+
* @returns A provider that reads and trims the file.
|
|
96
|
+
* @throws {@link VaultKeyError} when the file is missing, or holds nothing but
|
|
97
|
+
* whitespace.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* import { Vault, FileStore, fileKey } from "@mstone6969/vault"
|
|
102
|
+
*
|
|
103
|
+
* // The store's own key opens the file; the vault's key seals the values.
|
|
104
|
+
* const store = new FileStore("./secrets.vault", fileKey("/etc/vault.key"))
|
|
105
|
+
* const vault = new Vault({ key: fileKey("/etc/vault.key"), store })
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export declare function fileKey(path: string): KeyProvider;
|
|
109
|
+
/**
|
|
110
|
+
* True when something is a provider rather than a key.
|
|
111
|
+
*
|
|
112
|
+
* {@link Vault} and {@link FileStore} accept either, and use this to tell them
|
|
113
|
+
* apart: anything with a callable `key` is a provider, everything else is key
|
|
114
|
+
* material to be wrapped in {@link staticKey}. A `CryptoKey` has no `key`
|
|
115
|
+
* method, so it never matches.
|
|
116
|
+
*
|
|
117
|
+
* @param value Anything — key material, a provider, or neither.
|
|
118
|
+
* @returns Whether `value` has a callable `key`, narrowing it to
|
|
119
|
+
* {@link KeyProvider}.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* import { envKey, isKeyProvider, staticKey } from "@mstone6969/vault"
|
|
124
|
+
*
|
|
125
|
+
* isKeyProvider(envKey("VAULT_KEY")) // true
|
|
126
|
+
* isKeyProvider(staticKey("...")) // true
|
|
127
|
+
* isKeyProvider("base64-key-material") // false
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export declare function isKeyProvider(value: unknown): value is KeyProvider;
|
|
131
|
+
//# sourceMappingURL=providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,MAAM,WAAW,GAAG;IACtB;;;;;;;OAOG;IACH,GAAG,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,SAAS,CAAA;CAC1D,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAE9D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAUhD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAYjD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAElE"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { type KeyProvider } from "../providers";
|
|
2
|
+
import type { SecretRecord, VaultStore } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Keeps every record in one encrypted file.
|
|
5
|
+
*
|
|
6
|
+
* The other stores seal values and leave the rest in the open: SQLite has an
|
|
7
|
+
* `owner` column and a `name` column, and anyone who can read the file learns
|
|
8
|
+
* what you keep even if they cannot read it. Here the whole index — owners,
|
|
9
|
+
* names, metadata, timestamps, everything — is inside a single AES-256-GCM
|
|
10
|
+
* envelope. What leaks from the file at rest is its size.
|
|
11
|
+
*
|
|
12
|
+
* The trade is that it is loaded and written whole, so it suits hundreds of
|
|
13
|
+
* secrets rather than millions, and one writer rather than several. Writes go
|
|
14
|
+
* to a temporary file and are renamed into place, so a crash mid-write leaves
|
|
15
|
+
* the previous file rather than half of a new one.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* The store is encrypted under its own key, separate from the master key the
|
|
19
|
+
* {@link Vault} seals values with, so the file is two layers deep: the index
|
|
20
|
+
* under the store's key, each value under its own data key under the vault's.
|
|
21
|
+
* Handing both layers the same key is allowed and sometimes what you want, but
|
|
22
|
+
* then one key opens both.
|
|
23
|
+
*
|
|
24
|
+
* The key is resolved on the first read or write, not at construction, so a
|
|
25
|
+
* provider that reaches for a file or a network service is asked once and only
|
|
26
|
+
* when a record is actually wanted. A bad key surfaces from that first
|
|
27
|
+
* operation as a {@link VaultKeyError}, not from `new`.
|
|
28
|
+
*
|
|
29
|
+
* Nothing here locks the file. Two stores writing the same path — two
|
|
30
|
+
* processes, or two instances in one — each hold their own copy of the index
|
|
31
|
+
* and write it whole, so the last save wins and the other's writes are gone.
|
|
32
|
+
* Keep one writer per file.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* A vault whose names are as hidden as its values. The store's key opens the
|
|
36
|
+
* file; the vault's key seals what is inside it.
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { Vault, fileKey } from "@mstone6969/vault"
|
|
39
|
+
* import { FileStore } from "@mstone6969/vault/stores/file"
|
|
40
|
+
*
|
|
41
|
+
* const store = new FileStore("./secrets.vault", fileKey("/etc/vault-file.key"))
|
|
42
|
+
* const vault = new Vault({ key: fileKey("/etc/vault-master.key"), store })
|
|
43
|
+
*
|
|
44
|
+
* await vault.put("alice", "db", "hunter2") // the file is written here
|
|
45
|
+
* await vault.open("alice", "db") // "hunter2"
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @see {@link VaultStore} for what a store owes the vault, and `MemoryStore`
|
|
49
|
+
* and `SqliteStore` for the two that keep their index in the open.
|
|
50
|
+
*/
|
|
51
|
+
export declare class FileStore implements VaultStore {
|
|
52
|
+
private readonly path;
|
|
53
|
+
private readonly keySource;
|
|
54
|
+
/** Resolved on first read or write, not at construction. */
|
|
55
|
+
private keyCache;
|
|
56
|
+
private records;
|
|
57
|
+
/**
|
|
58
|
+
* @param path Where to keep the file. Created on first write, so a path
|
|
59
|
+
* that does not exist yet is an empty store rather than an error.
|
|
60
|
+
* @param key The key the *file* is encrypted with: base64, already
|
|
61
|
+
* imported, or a {@link KeyProvider} that finds one. Give it one of its
|
|
62
|
+
* own, or hand it the vault's — sharing means one key opens both layers.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* import { generateKey } from "@mstone6969/vault"
|
|
67
|
+
* import { FileStore } from "@mstone6969/vault/stores/file"
|
|
68
|
+
*
|
|
69
|
+
* // Nothing is read or written until the first call.
|
|
70
|
+
* const store = new FileStore("./secrets.vault", generateKey())
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
constructor(path: string, key: string | CryptoKey | KeyProvider);
|
|
74
|
+
private key;
|
|
75
|
+
private static id;
|
|
76
|
+
/**
|
|
77
|
+
* Reads and decrypts the file, once, keeping it in memory afterwards.
|
|
78
|
+
*
|
|
79
|
+
* @returns The whole index, keyed by owner and name. The same map every
|
|
80
|
+
* time until {@link FileStore.forget} drops it.
|
|
81
|
+
* @throws {@link VaultKeyError} when the file does not begin with the magic line —
|
|
82
|
+
* something that is not one of ours is refused rather than parsed — or
|
|
83
|
+
* when the key does not open it.
|
|
84
|
+
*/
|
|
85
|
+
private load;
|
|
86
|
+
/**
|
|
87
|
+
* Seals everything and swaps the file for the new one in a single move.
|
|
88
|
+
*
|
|
89
|
+
* Every record goes into one envelope, so the cost of a write is the size
|
|
90
|
+
* of the whole store, not of the record that changed.
|
|
91
|
+
*
|
|
92
|
+
* @returns Nothing, once the new file is in place.
|
|
93
|
+
* @throws {@link VaultKeyError} when the key cannot be resolved or imported.
|
|
94
|
+
*/
|
|
95
|
+
private save;
|
|
96
|
+
/**
|
|
97
|
+
* One record, or null when there is none under that name.
|
|
98
|
+
*
|
|
99
|
+
* @param owner Whose record to look for.
|
|
100
|
+
* @param name What it is called.
|
|
101
|
+
* @returns The record, or null.
|
|
102
|
+
* @throws {@link VaultKeyError} on the first call if the file is not a vault file,
|
|
103
|
+
* or the key does not open it.
|
|
104
|
+
*/
|
|
105
|
+
get(owner: string, name: string): Promise<SecretRecord | null>;
|
|
106
|
+
/**
|
|
107
|
+
* Every record one owner holds.
|
|
108
|
+
*
|
|
109
|
+
* The filtering happens in memory, over the whole index — there is no
|
|
110
|
+
* per-owner slice of the file to read on its own.
|
|
111
|
+
*
|
|
112
|
+
* @param owner Whose records to return.
|
|
113
|
+
* @returns That owner's records, in whatever order they were written.
|
|
114
|
+
* @throws {@link VaultKeyError} on the first call if the file cannot be opened.
|
|
115
|
+
*/
|
|
116
|
+
list(owner: string): Promise<SecretRecord[]>;
|
|
117
|
+
/**
|
|
118
|
+
* Every record, whoever owns it.
|
|
119
|
+
*
|
|
120
|
+
* @returns All of them. Only `rekey` and `purgeExpired` need this.
|
|
121
|
+
* @throws {@link VaultKeyError} on the first call if the file cannot be opened.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* Check a file opens under the key you think it does, before trusting it.
|
|
125
|
+
* ```ts
|
|
126
|
+
* import { VaultKeyError } from "@mstone6969/vault"
|
|
127
|
+
* import { FileStore } from "@mstone6969/vault/stores/file"
|
|
128
|
+
*
|
|
129
|
+
* try {
|
|
130
|
+
* await new FileStore("./secrets.vault", process.env.VAULT_FILE_KEY!).all()
|
|
131
|
+
* } catch (error) {
|
|
132
|
+
* if (error instanceof VaultKeyError) console.error(error.message)
|
|
133
|
+
* }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
all(): Promise<SecretRecord[]>;
|
|
137
|
+
/**
|
|
138
|
+
* Writes a record, replacing any under the same owner and name.
|
|
139
|
+
*
|
|
140
|
+
* The whole file is re-sealed and rewritten, so writes cost the size of the
|
|
141
|
+
* store rather than of the record.
|
|
142
|
+
*
|
|
143
|
+
* @param record The record to keep. Stored as given: finality, expiry and
|
|
144
|
+
* history are the vault's rules, not the store's.
|
|
145
|
+
* @returns The same record, so callers can chain.
|
|
146
|
+
* @throws {@link VaultKeyError} if the file cannot be opened, or the key cannot be
|
|
147
|
+
* resolved.
|
|
148
|
+
*/
|
|
149
|
+
put(record: SecretRecord): Promise<SecretRecord>;
|
|
150
|
+
/**
|
|
151
|
+
* Deletes a record, returning false when there was nothing to delete.
|
|
152
|
+
*
|
|
153
|
+
* @param owner Whose record to delete.
|
|
154
|
+
* @param name What it is called.
|
|
155
|
+
* @returns Whether a record was there to delete. The file is only rewritten
|
|
156
|
+
* when one was.
|
|
157
|
+
* @throws {@link VaultKeyError} if the file cannot be opened.
|
|
158
|
+
*/
|
|
159
|
+
remove(owner: string, name: string): Promise<boolean>;
|
|
160
|
+
/**
|
|
161
|
+
* Forgets what it read, so the next call goes back to the file.
|
|
162
|
+
*
|
|
163
|
+
* The point of a store that holds its whole index in memory is that reads
|
|
164
|
+
* are free; the cost is that a file another process rewrote is invisible
|
|
165
|
+
* until you say this. It also drops the decrypted index, which is the only
|
|
166
|
+
* place the names live in the clear.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* import { FileStore } from "@mstone6969/vault/stores/file"
|
|
171
|
+
*
|
|
172
|
+
* const store = new FileStore("./secrets.vault", process.env.VAULT_FILE_KEY!)
|
|
173
|
+
* await store.all() // reads and decrypts the file
|
|
174
|
+
* store.forget()
|
|
175
|
+
* await store.all() // reads it again
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
forget(): void;
|
|
179
|
+
/**
|
|
180
|
+
* Deletes the file and everything in it.
|
|
181
|
+
*
|
|
182
|
+
* The in-memory index is emptied first, so a store that is used again
|
|
183
|
+
* writes a fresh file rather than resurrecting what was there.
|
|
184
|
+
*
|
|
185
|
+
* @returns Nothing. A file that is already gone is the outcome wanted, not
|
|
186
|
+
* an error.
|
|
187
|
+
*/
|
|
188
|
+
destroy(): Promise<void>;
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/stores/file.ts"],"names":[],"mappings":"AAGA,OAAO,EAA4B,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAKxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,qBAAa,SAAU,YAAW,UAAU;IACxC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkC;IAC5D,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,OAAO,CAAyC;IAExD;;;;;;;;;;;;;;;OAeG;gBACS,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW;IAK/D,OAAO,CAAC,GAAG;IASX,OAAO,CAAC,MAAM,CAAC,EAAE;IAIjB;;;;;;;;OAQG;YACW,IAAI;IA0ClB;;;;;;;;OAQG;YACW,IAAI;IAwBlB;;;;;;;;OAQG;IACG,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAIpE;;;;;;;;;OASG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAIlD;;;;;;;;;;;;;;;;;;OAkBG;IACG,GAAG,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAIpC;;;;;;;;;;;OAWG;IACG,GAAG,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAkBtD;;;;;;;;OAQG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAgB3D;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,IAAI,IAAI;IAId;;;;;;;;OAQG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAMjC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FileStore } from "../index.js";
|