@moxxy/plugin-vault 0.0.33

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 Moxxy (moxxy.ai)
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.
@@ -0,0 +1,30 @@
1
+ export interface EncryptedBlob {
2
+ readonly iv: string;
3
+ readonly tag: string;
4
+ readonly data: string;
5
+ }
6
+ export declare function deriveKey(passphrase: string, salt: Buffer): Buffer;
7
+ /**
8
+ * Off-thread KDF. scrypt (N=16384) is a CPU-bound stall of ~tens of ms;
9
+ * `scryptSync` blocks the single-threaded runner event loop for that whole
10
+ * window, while this runs on libuv's threadpool and yields. Functionally
11
+ * identical to {@link deriveKey} (same params, same output). Used on the
12
+ * runtime key-resolution hot paths; the sync variant stays for callers that
13
+ * derive a key eagerly to construct a static key source.
14
+ */
15
+ export declare function deriveKeyAsync(passphrase: string, salt: Buffer): Promise<Buffer>;
16
+ export declare function generateSalt(): Buffer;
17
+ export declare function encrypt(plaintext: string, key: Buffer): EncryptedBlob;
18
+ export declare function decrypt(blob: EncryptedBlob, key: Buffer): string;
19
+ /**
20
+ * A uniformly-random decimal string of exactly `digits` digits (zero-padded).
21
+ *
22
+ * Draws enough entropy for the requested width (not a fixed 4 bytes, which
23
+ * capped any code wider than ~9 digits — 10**digits overflowed a uint32, so
24
+ * the leading digits were always 0) and rejection-samples to strip the
25
+ * modulo bias that `% 10**digits` introduces when the byte range isn't an
26
+ * exact multiple of the modulus. Used for short pairing codes today; correct
27
+ * for any width.
28
+ */
29
+ export declare function randomCode(digits?: number): string;
30
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAkBD,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAElE;AASD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEhF;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,CAUrE;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,MAAM,SAAI,GAAG,MAAM,CAqB7C"}
package/dist/crypto.js ADDED
@@ -0,0 +1,87 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, scrypt, scryptSync } from 'node:crypto';
2
+ import { promisify } from 'node:util';
3
+ const KEY_BYTES = 32;
4
+ const IV_BYTES = 12;
5
+ const SCRYPT_N = 16384;
6
+ const SCRYPT_R = 8;
7
+ const SCRYPT_P = 1;
8
+ const SCRYPT_OPTS = {
9
+ N: SCRYPT_N,
10
+ r: SCRYPT_R,
11
+ p: SCRYPT_P,
12
+ // scrypt's internal buffer is ~128 * N * r bytes (~16 MiB here). Node's
13
+ // default maxmem (32 MiB) is enough today, but pin it explicitly so a future
14
+ // parameter bump can't silently start throwing "memory limit exceeded".
15
+ maxmem: 64 * 1024 * 1024,
16
+ };
17
+ export function deriveKey(passphrase, salt) {
18
+ return scryptSync(passphrase, salt, KEY_BYTES, SCRYPT_OPTS);
19
+ }
20
+ const scryptAsync = promisify(scrypt);
21
+ /**
22
+ * Off-thread KDF. scrypt (N=16384) is a CPU-bound stall of ~tens of ms;
23
+ * `scryptSync` blocks the single-threaded runner event loop for that whole
24
+ * window, while this runs on libuv's threadpool and yields. Functionally
25
+ * identical to {@link deriveKey} (same params, same output). Used on the
26
+ * runtime key-resolution hot paths; the sync variant stays for callers that
27
+ * derive a key eagerly to construct a static key source.
28
+ */
29
+ export function deriveKeyAsync(passphrase, salt) {
30
+ return scryptAsync(passphrase, salt, KEY_BYTES, SCRYPT_OPTS);
31
+ }
32
+ export function generateSalt() {
33
+ return randomBytes(16);
34
+ }
35
+ export function encrypt(plaintext, key) {
36
+ const iv = randomBytes(IV_BYTES);
37
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
38
+ const data = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
39
+ const tag = cipher.getAuthTag();
40
+ return {
41
+ iv: iv.toString('base64'),
42
+ tag: tag.toString('base64'),
43
+ data: data.toString('base64'),
44
+ };
45
+ }
46
+ export function decrypt(blob, key) {
47
+ const iv = Buffer.from(blob.iv, 'base64');
48
+ const tag = Buffer.from(blob.tag, 'base64');
49
+ const data = Buffer.from(blob.data, 'base64');
50
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
51
+ decipher.setAuthTag(tag);
52
+ const plaintext = Buffer.concat([decipher.update(data), decipher.final()]);
53
+ return plaintext.toString('utf8');
54
+ }
55
+ /**
56
+ * A uniformly-random decimal string of exactly `digits` digits (zero-padded).
57
+ *
58
+ * Draws enough entropy for the requested width (not a fixed 4 bytes, which
59
+ * capped any code wider than ~9 digits — 10**digits overflowed a uint32, so
60
+ * the leading digits were always 0) and rejection-samples to strip the
61
+ * modulo bias that `% 10**digits` introduces when the byte range isn't an
62
+ * exact multiple of the modulus. Used for short pairing codes today; correct
63
+ * for any width.
64
+ */
65
+ export function randomCode(digits = 6) {
66
+ if (!Number.isInteger(digits) || digits < 1) {
67
+ throw new Error(`randomCode: digits must be a positive integer, got ${digits}`);
68
+ }
69
+ const modulus = 10n ** BigInt(digits);
70
+ // Enough whole bytes to cover the modulus, with one spare byte so the
71
+ // accepted region is a large fraction of the draw space (few rejections).
72
+ const byteLen = Math.ceil((digits * Math.log2(10)) / 8) + 1;
73
+ const space = 1n << BigInt(byteLen * 8);
74
+ // Largest multiple of `modulus` that fits in `space`; draws at or above it
75
+ // would bias the low digits, so reject and redraw.
76
+ const limit = space - (space % modulus);
77
+ for (;;) {
78
+ let value = 0n;
79
+ for (const byte of randomBytes(byteLen)) {
80
+ value = (value << 8n) | BigInt(byte);
81
+ }
82
+ if (value < limit) {
83
+ return (value % modulus).toString().padStart(digits, '0');
84
+ }
85
+ }
86
+ }
87
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { type Plugin } from '@moxxy/sdk';
2
+ import { type MasterKeySource } from './keysource.js';
3
+ import { VaultStore } from './store.js';
4
+ export { VaultStore, VaultPassphraseError } from './store.js';
5
+ export type { VaultEntry, VaultEntryInfo, VaultStoreOptions } from './store.js';
6
+ export { createCombinedKeySource, createStaticKeySource, type MasterKeySource } from './keysource.js';
7
+ export { resolveString, resolveValue, containsPlaceholder } from './placeholder.js';
8
+ export { deriveKey, deriveKeyAsync, encrypt, decrypt, generateSalt, randomCode } from './crypto.js';
9
+ export interface BuildVaultPluginOptions {
10
+ readonly filePath?: string;
11
+ readonly keySource?: MasterKeySource;
12
+ readonly passphrasePrompt?: () => Promise<string>;
13
+ readonly envVar?: string;
14
+ readonly disableKeytar?: boolean;
15
+ }
16
+ export declare function defaultVaultPath(): string;
17
+ export declare function buildVaultPlugin(opts?: BuildVaultPluginOptions): {
18
+ plugin: Plugin;
19
+ vault: VaultStore;
20
+ };
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,MAAM,EAIZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EAA2B,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAE/E,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtG,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACpF,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEpG,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,CAAC;IACrC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,uBAA4B,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,CA+L1G"}
package/dist/index.js ADDED
@@ -0,0 +1,212 @@
1
+ import { z, defineTool, definePlugin, MoxxyError, } from '@moxxy/sdk';
2
+ import { moxxyPath } from '@moxxy/sdk/server';
3
+ import { createCombinedKeySource } from './keysource.js';
4
+ import { resolveString } from './placeholder.js';
5
+ import { VaultStore } from './store.js';
6
+ export { VaultStore, VaultPassphraseError } from './store.js';
7
+ export { createCombinedKeySource, createStaticKeySource } from './keysource.js';
8
+ export { resolveString, resolveValue, containsPlaceholder } from './placeholder.js';
9
+ export { deriveKey, deriveKeyAsync, encrypt, decrypt, generateSalt, randomCode } from './crypto.js';
10
+ export function defaultVaultPath() {
11
+ return moxxyPath('vault.json');
12
+ }
13
+ export function buildVaultPlugin(opts = {}) {
14
+ const filePath = opts.filePath ?? defaultVaultPath();
15
+ const keySource = opts.keySource ??
16
+ createCombinedKeySource({
17
+ passphrasePrompt: opts.passphrasePrompt ?? defaultPrompt,
18
+ envVar: opts.envVar,
19
+ disableKeytar: opts.disableKeytar,
20
+ });
21
+ const vault = new VaultStore({ filePath, keySource });
22
+ // `/vault` slash command. Lets the USER store a secret out-of-band: the
23
+ // value travels in the slash-command args, which channels intercept and
24
+ // never send to the model. After storing, we inject a note into the event
25
+ // log telling the model only the REFERENCE (`${vault:NAME}`) — never the
26
+ // plaintext — so the model can wire it up (config/tools) without seeing it.
27
+ const vaultCmd = {
28
+ name: 'vault',
29
+ description: 'Store a secret or list stored names',
30
+ argumentHint: 'set <name> <value>',
31
+ pendingNotice: 'updating vault',
32
+ handler: async (ctx) => {
33
+ const trimmed = ctx.args.trim();
34
+ const [sub, ...rest] = trimmed.split(/\s+/);
35
+ if (!sub || sub === 'help') {
36
+ return { kind: 'text', text: vaultCommandUsage() };
37
+ }
38
+ if (sub === 'list') {
39
+ const entries = await vault.list();
40
+ if (entries.length === 0) {
41
+ return { kind: 'text', text: 'Vault is empty. Store a secret with `/vault set <name> <value>`.' };
42
+ }
43
+ const lines = entries.map((e) => ` ${e.name}${e.tags && e.tags.length ? ` [${e.tags.join(', ')}]` : ''} → \${vault:${e.name}}`);
44
+ return { kind: 'text', text: `Stored secrets (values hidden):\n${lines.join('\n')}` };
45
+ }
46
+ if (sub === 'set') {
47
+ const name = rest[0];
48
+ const value = rest.slice(1).join(' ');
49
+ if (!name || !/^[A-Za-z0-9_.-]+$/.test(name)) {
50
+ return {
51
+ kind: 'error',
52
+ message: 'usage: /vault set <name> <value> (name may contain letters, digits, _ . -)',
53
+ };
54
+ }
55
+ if (!value) {
56
+ return { kind: 'error', message: `usage: /vault set ${name} <value> — a value is required` };
57
+ }
58
+ await vault.set(name, value);
59
+ // Inform the model with a reference only — never the plaintext. Keep it
60
+ // terse: the behavioral guidance ("never ask for plaintext") lives in
61
+ // the vault-setup skill. The note projects as a message on the next turn
62
+ // and renders as a dim system note in the TUI (see EventLine).
63
+ const note = `[vault] Secret "${name}" stored. Reference it as \${vault:${name}} — its value is hidden from you.`;
64
+ const s = ctx.session;
65
+ if (typeof s.log?.append === 'function' && typeof s.startTurn === 'function') {
66
+ try {
67
+ await s.log.append({
68
+ type: 'user_prompt',
69
+ sessionId: ctx.sessionId,
70
+ turnId: s.startTurn().turnId,
71
+ source: 'system',
72
+ text: note,
73
+ });
74
+ }
75
+ catch (err) {
76
+ // The session exposes the log API but the append itself failed
77
+ // (e.g. disk full / log-rotation race). The secret IS stored, so
78
+ // still confirm to the user below, but surface a warning so a
79
+ // broken log path is diagnosable rather than a silent partial
80
+ // failure where the model never learns the reference exists.
81
+ console.warn(`[vault] stored "${name}" but failed to record its reference note: ` +
82
+ (err instanceof Error ? err.message : String(err)));
83
+ }
84
+ }
85
+ // If the host session doesn't expose log/startTurn (unexpected),
86
+ // we skip the note entirely and just confirm storage below.
87
+ return {
88
+ kind: 'text',
89
+ text: `✓ Stored "${name}" securely in the vault. ` +
90
+ `The assistant can reference it as \${vault:${name}} without ever seeing the value.`,
91
+ };
92
+ }
93
+ return { kind: 'error', message: `unknown subcommand "${sub}".\n${vaultCommandUsage()}` };
94
+ },
95
+ };
96
+ const plugin = definePlugin({
97
+ name: '@moxxy/plugin-vault',
98
+ version: '0.0.0',
99
+ // Publish the secret store on the inter-plugin service registry so sibling
100
+ // plugins (oauth, telegram, mcp, …) can resolve it in their own onInit
101
+ // instead of being hand-built with `{ vault }` — the seam that lets them be
102
+ // discovery-loaded. Consumers declare a requirement on @moxxy/plugin-vault
103
+ // so this onInit runs first.
104
+ hooks: {
105
+ onInit: (ctx) => {
106
+ ctx.services.register('vault', vault);
107
+ // Also publish a `${vault:NAME}`-placeholder resolver so consumers (mcp's
108
+ // server env/headers) can resolve secrets without depending on the vault
109
+ // package's `resolveString` helper directly.
110
+ ctx.services.register('resolveSecrets', (value) => resolveString(value, vault));
111
+ },
112
+ },
113
+ commands: [vaultCmd],
114
+ tools: [
115
+ defineTool({
116
+ name: 'vault_set',
117
+ description: 'Store a secret in the encrypted vault. Overwrites if name exists. ' +
118
+ 'IMPORTANT: do NOT use this for a secret the USER supplies — that would route the ' +
119
+ 'plaintext through the conversation. Instead tell the user to run `/vault set <name> <value>` ' +
120
+ 'themselves; you only receive a ${vault:<name>} reference. Use this tool only for a value you ' +
121
+ 'legitimately already hold and may see.',
122
+ inputSchema: z.object({
123
+ name: z.string().min(1).regex(/^[A-Za-z0-9_.-]+$/),
124
+ value: z.string().min(1),
125
+ tags: z.array(z.string()).optional(),
126
+ }),
127
+ permission: { action: 'prompt' },
128
+ handler: async ({ name, value, tags }) => {
129
+ await vault.set(name, value, tags);
130
+ return `stored ${name} (${value.length} chars) in vault`;
131
+ },
132
+ }),
133
+ defineTool({
134
+ name: 'vault_get',
135
+ description: 'Fetch a secret from the encrypted vault by name. Returns the plaintext value.',
136
+ inputSchema: z.object({ name: z.string().min(1) }),
137
+ permission: { action: 'prompt' },
138
+ handler: async ({ name }) => {
139
+ const value = await vault.get(name);
140
+ if (value === null) {
141
+ throw new MoxxyError({
142
+ code: 'TOOL_ERROR',
143
+ message: `vault: '${name}' not found`,
144
+ hint: 'Use `vault_list` to see stored entry names.',
145
+ context: { name },
146
+ });
147
+ }
148
+ return value;
149
+ },
150
+ }),
151
+ defineTool({
152
+ name: 'vault_list',
153
+ description: 'List entries in the vault. Returns names + metadata only, never plaintext.',
154
+ inputSchema: z.object({}),
155
+ permission: { action: 'prompt' },
156
+ handler: async () => {
157
+ const entries = await vault.list();
158
+ return entries.map((e) => ({ name: e.name, createdAt: e.createdAt, tags: e.tags ?? [] }));
159
+ },
160
+ }),
161
+ defineTool({
162
+ name: 'vault_delete',
163
+ description: 'Delete a vault entry by name.',
164
+ inputSchema: z.object({ name: z.string().min(1) }),
165
+ permission: { action: 'prompt' },
166
+ handler: async ({ name }) => {
167
+ const removed = await vault.delete(name);
168
+ return removed ? `deleted ${name}` : `not found: ${name}`;
169
+ },
170
+ }),
171
+ defineTool({
172
+ name: 'vault_status',
173
+ description: 'Report which key source unlocked the vault (keychain, env, passphrase).',
174
+ inputSchema: z.object({}),
175
+ handler: async () => {
176
+ await vault.open();
177
+ const entries = await vault.list();
178
+ return { source: vault.sourceName, entries: entries.length };
179
+ },
180
+ }),
181
+ ],
182
+ });
183
+ return { plugin, vault };
184
+ }
185
+ function vaultCommandUsage() {
186
+ return ('usage:\n' +
187
+ ' /vault set <name> <value> store a secret (the value is hidden from the assistant)\n' +
188
+ ' /vault list list stored secret names');
189
+ }
190
+ async function defaultPrompt() {
191
+ // Headless default: refuse if no TTY. Interactive shells override this via opts.
192
+ if (!process.stdin.isTTY) {
193
+ throw new Error('vault: passphrase required but no interactive terminal. Set MOXXY_VAULT_PASSPHRASE or pass a custom passphrasePrompt.');
194
+ }
195
+ const readline = await import('node:readline/promises');
196
+ // Write to stdout (not stderr) and include a leading newline + bold-ish
197
+ // banner so users can't miss the prompt — the bare `vault passphrase: `
198
+ // sent to stderr was easy to overlook (looked like a hang).
199
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
200
+ try {
201
+ process.stdout.write('\nmoxxy vault needs a passphrase.\n' +
202
+ 'Pick one now (it\'s stored in your OS keychain if available).\n' +
203
+ 'Set MOXXY_VAULT_PASSPHRASE to skip this prompt.\n\n');
204
+ const answer = (await rl.question('passphrase: ')).trim();
205
+ process.stdout.write('\n');
206
+ return answer;
207
+ }
208
+ finally {
209
+ rl.close();
210
+ }
211
+ }
212
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,CAAC,EACD,UAAU,EACV,YAAY,EACZ,UAAU,GAKX,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAwB,MAAM,gBAAgB,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAE9D,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAwB,MAAM,gBAAgB,CAAC;AACtG,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACpF,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAUpG,MAAM,UAAU,gBAAgB;IAC9B,OAAO,SAAS,CAAC,YAAY,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAgC,EAAE;IACjE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,gBAAgB,EAAE,CAAC;IACrD,MAAM,SAAS,GACb,IAAI,CAAC,SAAS;QACd,uBAAuB,CAAC;YACtB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,IAAI,aAAa;YACxD,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,aAAa,EAAE,IAAI,CAAC,aAAa;SAClC,CAAC,CAAC;IACL,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;IAEtD,wEAAwE;IACxE,wEAAwE;IACxE,0EAA0E;IAC1E,yEAAyE;IACzE,4EAA4E;IAC5E,MAAM,QAAQ,GAAe;QAC3B,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,qCAAqC;QAClD,YAAY,EAAE,oBAAoB;QAClC,aAAa,EAAE,gBAAgB;QAC/B,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACrB,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YAChC,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAE5C,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;gBAC3B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,EAAE,CAAC;YACrD,CAAC;YAED,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;gBACnB,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;gBACnC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACzB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,kEAAkE,EAAE,CAAC;gBACpG,CAAC;gBACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CACvB,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,IAAI,GAAG,CACzG,CAAC;gBACF,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,oCAAoC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;YACxF,CAAC;YAED,IAAI,GAAG,KAAK,KAAK,EAAE,CAAC;gBAClB,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;gBACrB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACtC,IAAI,CAAC,IAAI,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC7C,OAAO;wBACL,IAAI,EAAE,OAAO;wBACb,OAAO,EAAE,6EAA6E;qBACvF,CAAC;gBACJ,CAAC;gBACD,IAAI,CAAC,KAAK,EAAE,CAAC;oBACX,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,IAAI,iCAAiC,EAAE,CAAC;gBAChG,CAAC;gBAED,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAE7B,wEAAwE;gBACxE,sEAAsE;gBACtE,yEAAyE;gBACzE,+DAA+D;gBAC/D,MAAM,IAAI,GACR,mBAAmB,IAAI,sCAAsC,IAAI,mCAAmC,CAAC;gBACvG,MAAM,CAAC,GAAG,GAAG,CAAC,OAAuC,CAAC;gBACtD,IAAI,OAAO,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,UAAU,IAAI,OAAO,CAAC,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;oBAC7E,IAAI,CAAC;wBACH,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;4BACjB,IAAI,EAAE,aAAa;4BACnB,SAAS,EAAE,GAAG,CAAC,SAAS;4BACxB,MAAM,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,MAAM;4BAC5B,MAAM,EAAE,QAAQ;4BAChB,IAAI,EAAE,IAAI;yBACX,CAAC,CAAC;oBACL,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACb,+DAA+D;wBAC/D,iEAAiE;wBACjE,8DAA8D;wBAC9D,8DAA8D;wBAC9D,6DAA6D;wBAC7D,OAAO,CAAC,IAAI,CACV,mBAAmB,IAAI,6CAA6C;4BAClE,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CACrD,CAAC;oBACJ,CAAC;gBACH,CAAC;gBACD,iEAAiE;gBACjE,4DAA4D;gBAE5D,OAAO;oBACL,IAAI,EAAE,MAAM;oBACZ,IAAI,EACF,aAAa,IAAI,2BAA2B;wBAC5C,8CAA8C,IAAI,kCAAkC;iBACvF,CAAC;YACJ,CAAC;YAED,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,uBAAuB,GAAG,OAAO,iBAAiB,EAAE,EAAE,EAAE,CAAC;QAC5F,CAAC;KACF,CAAC;IAEF,MAAM,MAAM,GAAG,YAAY,CAAC;QAC1B,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,OAAO;QAChB,2EAA2E;QAC3E,uEAAuE;QACvE,4EAA4E;QAC5E,2EAA2E;QAC3E,6BAA6B;QAC7B,KAAK,EAAE;YACL,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE;gBACd,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;gBACtC,0EAA0E;gBAC1E,yEAAyE;gBACzE,6CAA6C;gBAC7C,GAAG,CAAC,QAAQ,CAAC,QAAQ,CACnB,gBAAgB,EAChB,CAAC,KAAa,EAAmB,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,CAChE,CAAC;YACJ,CAAC;SACF;QACD,QAAQ,EAAE,CAAC,QAAQ,CAAC;QACpB,KAAK,EAAE;YACL,UAAU,CAAC;gBACT,IAAI,EAAE,WAAW;gBACjB,WAAW,EACT,oEAAoE;oBACpE,mFAAmF;oBACnF,+FAA+F;oBAC/F,+FAA+F;oBAC/F,wCAAwC;gBAC1C,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;oBACpB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC;oBAClD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;oBACxB,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;iBACrC,CAAC;gBACF,UAAU,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;gBAChC,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE;oBACvC,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;oBACnC,OAAO,UAAU,IAAI,KAAK,KAAK,CAAC,MAAM,kBAAkB,CAAC;gBAC3D,CAAC;aACF,CAAC;YACF,UAAU,CAAC;gBACT,IAAI,EAAE,WAAW;gBACjB,WAAW,EAAE,+EAA+E;gBAC5F,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClD,UAAU,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;gBAChC,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;oBAC1B,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBACpC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;wBACnB,MAAM,IAAI,UAAU,CAAC;4BACnB,IAAI,EAAE,YAAY;4BAClB,OAAO,EAAE,WAAW,IAAI,aAAa;4BACrC,IAAI,EAAE,6CAA6C;4BACnD,OAAO,EAAE,EAAE,IAAI,EAAE;yBAClB,CAAC,CAAC;oBACL,CAAC;oBACD,OAAO,KAAK,CAAC;gBACf,CAAC;aACF,CAAC;YACF,UAAU,CAAC;gBACT,IAAI,EAAE,YAAY;gBAClB,WAAW,EAAE,4EAA4E;gBACzF,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;gBACzB,UAAU,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;gBAChC,OAAO,EAAE,KAAK,IAAI,EAAE;oBAClB,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;oBACnC,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;gBAC5F,CAAC;aACF,CAAC;YACF,UAAU,CAAC;gBACT,IAAI,EAAE,cAAc;gBACpB,WAAW,EAAE,+BAA+B;gBAC5C,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClD,UAAU,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;gBAChC,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;oBAC1B,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;oBACzC,OAAO,OAAO,CAAC,CAAC,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,CAAC,cAAc,IAAI,EAAE,CAAC;gBAC5D,CAAC;aACF,CAAC;YACF,UAAU,CAAC;gBACT,IAAI,EAAE,cAAc;gBACpB,WAAW,EAAE,yEAAyE;gBACtF,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;gBACzB,OAAO,EAAE,KAAK,IAAI,EAAE;oBAClB,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;oBACnB,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;oBACnC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;gBAC/D,CAAC;aACF,CAAC;SACH;KACF,CAAC,CAAC;IAEH,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAC3B,CAAC;AAYD,SAAS,iBAAiB;IACxB,OAAO,CACL,UAAU;QACV,yFAAyF;QACzF,wDAAwD,CACzD,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,aAAa;IAC1B,iFAAiF;IACjF,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,uHAAuH,CACxH,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAC;IACxD,wEAAwE;IACxE,wEAAwE;IACxE,4DAA4D;IAC5D,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACtF,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,wDAAwD;YACtD,iEAAiE;YACjE,8DAA8D,CACjE,CAAC;QACF,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3B,OAAO,MAAM,CAAC;IAChB,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
@@ -0,0 +1,50 @@
1
+ export interface MasterKeySource {
2
+ /** Returns the raw 32-byte AES key. May open a keychain or prompt the user. */
3
+ obtain(salt: Buffer): Promise<Buffer>;
4
+ /**
5
+ * Force-persist a master key obtained out-of-band for future sessions.
6
+ *
7
+ * NB: `VaultStore` does NOT call this — `obtain()` already persists the key
8
+ * as a side effect on the passphrase-prompt path, so the running system
9
+ * never needs it. It exists as a forced-rewrite hook for callers that hold a
10
+ * key themselves (e.g. a hypothetical `moxxy doctor --reseed`). Optional, so
11
+ * sources that can't persist (env/static) simply omit it.
12
+ */
13
+ persist?(key: Buffer, salt: Buffer): Promise<void>;
14
+ readonly name: string;
15
+ }
16
+ export interface CombinedKeySourceOptions {
17
+ readonly passphrasePrompt: () => Promise<string>;
18
+ readonly envVar?: string;
19
+ /**
20
+ * Skip the OS keychain (`@napi-rs/keyring`) entirely, using only the disk
21
+ * cache + passphrase. Named `disableKeytar` for backwards compatibility —
22
+ * the underlying keychain library is now `@napi-rs/keyring`, not keytar.
23
+ */
24
+ readonly disableKeytar?: boolean;
25
+ /**
26
+ * Disk fallback for the master key, used when the OS keychain isn't
27
+ * available (no native binary, or it refuses to bind — common on headless
28
+ * Linux). Stored as base64 at this path with mode 0o600 — less secure than
29
+ * the OS keychain, but means the user types their passphrase ONCE instead
30
+ * of every run. Set to false to disable.
31
+ *
32
+ * Default: `~/.moxxy/vault.key`.
33
+ */
34
+ readonly diskKeyPath?: string | false;
35
+ }
36
+ /**
37
+ * Resolves the vault master key in priority order:
38
+ * 1. `MOXXY_VAULT_PASSPHRASE` env var (derive on each call — no persistence).
39
+ * 2. OS keychain via `@napi-rs/keyring`.
40
+ * 3. On-disk cached key at `~/.moxxy/vault.key` (mode 0600).
41
+ * 4. Interactive passphrase prompt.
42
+ *
43
+ * The first successful prompt persists the derived key to BOTH the OS keychain
44
+ * (if available) and the disk cache so subsequent runs are silent. The chosen
45
+ * source's name is exposed via `.name` so `moxxy doctor` can surface it
46
+ * ("vault unlocked via keychain" / "via ~/.moxxy/vault.key").
47
+ */
48
+ export declare function createCombinedKeySource(opts: CombinedKeySourceOptions): MasterKeySource;
49
+ export declare function createStaticKeySource(key: Buffer): MasterKeySource;
50
+ //# sourceMappingURL=keysource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keysource.d.ts","sourceRoot":"","sources":["../src/keysource.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC;;;;;;;;OAQG;IACH,OAAO,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACvC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,wBAAwB,GAAG,eAAe,CAuEvF;AA+DD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAOlE"}
@@ -0,0 +1,145 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { writeFileAtomic, moxxyPath } from '@moxxy/sdk/server';
3
+ import { deriveKeyAsync } from './crypto.js';
4
+ const KEYCHAIN_SERVICE = 'moxxy';
5
+ const KEYCHAIN_ACCOUNT = 'vault-master-key';
6
+ /**
7
+ * Resolves the vault master key in priority order:
8
+ * 1. `MOXXY_VAULT_PASSPHRASE` env var (derive on each call — no persistence).
9
+ * 2. OS keychain via `@napi-rs/keyring`.
10
+ * 3. On-disk cached key at `~/.moxxy/vault.key` (mode 0600).
11
+ * 4. Interactive passphrase prompt.
12
+ *
13
+ * The first successful prompt persists the derived key to BOTH the OS keychain
14
+ * (if available) and the disk cache so subsequent runs are silent. The chosen
15
+ * source's name is exposed via `.name` so `moxxy doctor` can surface it
16
+ * ("vault unlocked via keychain" / "via ~/.moxxy/vault.key").
17
+ */
18
+ export function createCombinedKeySource(opts) {
19
+ let resolvedName = 'unknown';
20
+ const diskPath = resolveDiskPath(opts.diskKeyPath);
21
+ // Memoize the env-path scrypt derivation. obtain() is called on every
22
+ // VaultStore.open(), and scryptSync (N=16384) is a CPU-bound stall of the
23
+ // single-threaded event loop; without this cache an env-configured process
24
+ // re-pays the full KDF cost every time a fresh VaultStore opens. Keyed by
25
+ // (passphrase, salt) so a salt change still re-derives. Single entry — the
26
+ // env passphrase and the on-disk salt are stable for a process lifetime.
27
+ let envCache = null;
28
+ const deriveEnvKey = async (passphrase, salt) => {
29
+ const saltB64 = salt.toString('base64');
30
+ if (envCache && envCache.passphrase === passphrase && envCache.saltB64 === saltB64) {
31
+ return envCache.key;
32
+ }
33
+ const key = await deriveKeyAsync(passphrase, salt);
34
+ envCache = { passphrase, saltB64, key };
35
+ return key;
36
+ };
37
+ const persistKey = async (keyB64) => {
38
+ if (!opts.disableKeytar)
39
+ await tryKeychainSet(keyB64);
40
+ if (diskPath)
41
+ await tryDiskSet(diskPath, keyB64);
42
+ };
43
+ return {
44
+ get name() {
45
+ return resolvedName;
46
+ },
47
+ async obtain(salt) {
48
+ const envName = opts.envVar ?? 'MOXXY_VAULT_PASSPHRASE';
49
+ const envValue = process.env[envName];
50
+ if (envValue) {
51
+ resolvedName = `env:${envName}`;
52
+ return await deriveEnvKey(envValue, salt);
53
+ }
54
+ if (!opts.disableKeytar) {
55
+ const fromKeychain = await tryKeychainGet();
56
+ if (fromKeychain) {
57
+ resolvedName = 'keychain';
58
+ // Backfill the disk cache so a future keychain outage doesn't
59
+ // suddenly force a passphrase prompt. Awaited (both helpers swallow
60
+ // their own errors) so the "subsequent runs are silent" guarantee
61
+ // actually holds before obtain() resolves, and a process that exits
62
+ // immediately after first open() still persisted the backfill.
63
+ if (diskPath)
64
+ await tryDiskSet(diskPath, fromKeychain);
65
+ return Buffer.from(fromKeychain, 'base64');
66
+ }
67
+ }
68
+ if (diskPath) {
69
+ const fromDisk = await tryDiskGet(diskPath);
70
+ if (fromDisk) {
71
+ resolvedName = `file:${diskPath}`;
72
+ // Backfill the keychain if it became available since the file was written.
73
+ if (!opts.disableKeytar)
74
+ await tryKeychainSet(fromDisk);
75
+ return Buffer.from(fromDisk, 'base64');
76
+ }
77
+ }
78
+ const passphrase = await opts.passphrasePrompt();
79
+ resolvedName = 'passphrase';
80
+ const key = await deriveKeyAsync(passphrase, salt);
81
+ await persistKey(key.toString('base64'));
82
+ return key;
83
+ },
84
+ async persist(key) {
85
+ await persistKey(key.toString('base64'));
86
+ },
87
+ };
88
+ }
89
+ function resolveDiskPath(supplied) {
90
+ if (supplied === false)
91
+ return null;
92
+ if (typeof supplied === 'string')
93
+ return supplied;
94
+ return moxxyPath('vault.key');
95
+ }
96
+ async function tryDiskGet(filePath) {
97
+ try {
98
+ const raw = await fs.readFile(filePath, 'utf8');
99
+ return raw.trim() || null;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ async function tryDiskSet(filePath, value) {
106
+ try {
107
+ // Crash-atomic, owner-only (0o600) — this is the cached master key.
108
+ await writeFileAtomic(filePath, value + '\n', { mode: 0o600 });
109
+ }
110
+ catch {
111
+ // Best-effort; if we can't write, the next run will just re-prompt.
112
+ }
113
+ }
114
+ async function tryKeychainGet() {
115
+ try {
116
+ const mod = (await import('@napi-rs/keyring'));
117
+ if (!mod.Entry)
118
+ return null;
119
+ return new mod.Entry(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT).getPassword() || null;
120
+ }
121
+ catch {
122
+ // Not installed, no stored entry, or keychain locked — fall back.
123
+ return null;
124
+ }
125
+ }
126
+ async function tryKeychainSet(value) {
127
+ try {
128
+ const mod = (await import('@napi-rs/keyring'));
129
+ if (!mod.Entry)
130
+ return;
131
+ new mod.Entry(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT).setPassword(value);
132
+ }
133
+ catch {
134
+ // Best-effort; keychain failures must not break the vault.
135
+ }
136
+ }
137
+ export function createStaticKeySource(key) {
138
+ return {
139
+ name: 'static',
140
+ async obtain() {
141
+ return key;
142
+ },
143
+ };
144
+ }
145
+ //# sourceMappingURL=keysource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keysource.js","sourceRoot":"","sources":["../src/keysource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,gBAAgB,GAAG,OAAO,CAAC;AACjC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAuC5C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAA8B;IACpE,IAAI,YAAY,GAAG,SAAS,CAAC;IAC7B,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACnD,sEAAsE;IACtE,0EAA0E;IAC1E,2EAA2E;IAC3E,0EAA0E;IAC1E,2EAA2E;IAC3E,yEAAyE;IACzE,IAAI,QAAQ,GAAgE,IAAI,CAAC;IACjF,MAAM,YAAY,GAAG,KAAK,EAAE,UAAkB,EAAE,IAAY,EAAmB,EAAE;QAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,QAAQ,IAAI,QAAQ,CAAC,UAAU,KAAK,UAAU,IAAI,QAAQ,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YACnF,OAAO,QAAQ,CAAC,GAAG,CAAC;QACtB,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,cAAc,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACnD,QAAQ,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;QACxC,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;IAEF,MAAM,UAAU,GAAG,KAAK,EAAE,MAAc,EAAiB,EAAE;QACzD,IAAI,CAAC,IAAI,CAAC,aAAa;YAAE,MAAM,cAAc,CAAC,MAAM,CAAC,CAAC;QACtD,IAAI,QAAQ;YAAE,MAAM,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACnD,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,IAAI;YACN,OAAO,YAAY,CAAC;QACtB,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,IAAI;YACf,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,wBAAwB,CAAC;YACxD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACtC,IAAI,QAAQ,EAAE,CAAC;gBACb,YAAY,GAAG,OAAO,OAAO,EAAE,CAAC;gBAChC,OAAO,MAAM,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAC5C,CAAC;YAED,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;gBACxB,MAAM,YAAY,GAAG,MAAM,cAAc,EAAE,CAAC;gBAC5C,IAAI,YAAY,EAAE,CAAC;oBACjB,YAAY,GAAG,UAAU,CAAC;oBAC1B,8DAA8D;oBAC9D,oEAAoE;oBACpE,kEAAkE;oBAClE,oEAAoE;oBACpE,+DAA+D;oBAC/D,IAAI,QAAQ;wBAAE,MAAM,UAAU,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;oBACvD,OAAO,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;gBAC7C,CAAC;YACH,CAAC;YAED,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;gBAC5C,IAAI,QAAQ,EAAE,CAAC;oBACb,YAAY,GAAG,QAAQ,QAAQ,EAAE,CAAC;oBAClC,2EAA2E;oBAC3E,IAAI,CAAC,IAAI,CAAC,aAAa;wBAAE,MAAM,cAAc,CAAC,QAAQ,CAAC,CAAC;oBACxD,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;gBACzC,CAAC;YACH,CAAC;YAED,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACjD,YAAY,GAAG,YAAY,CAAC;YAC5B,MAAM,GAAG,GAAG,MAAM,cAAc,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YACnD,MAAM,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;YACzC,OAAO,GAAG,CAAC;QACb,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,GAAG;YACf,MAAM,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC3C,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,QAAoC;IAC3D,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAClD,OAAO,SAAS,CAAC,WAAW,CAAC,CAAC;AAChC,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,QAAgB;IACxC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAChD,OAAO,GAAG,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,QAAgB,EAAE,KAAa;IACvD,IAAI,CAAC;QACH,oEAAoE;QACpE,MAAM,eAAe,CAAC,QAAQ,EAAE,KAAK,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;IACtE,CAAC;AACH,CAAC;AAkBD,KAAK,UAAU,cAAc;IAC3B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAkB,CAAC;QAChE,IAAI,CAAC,GAAG,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAC5B,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC;IACjF,CAAC;IAAC,MAAM,CAAC;QACP,kEAAkE;QAClE,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,KAAa;IACzC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAkB,CAAC;QAChE,IAAI,CAAC,GAAG,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,GAAG,CAAC,KAAK,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,2DAA2D;IAC7D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAC/C,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,KAAK,CAAC,MAAM;YACV,OAAO,GAAG,CAAC;QACb,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { VaultStore } from './store.js';
2
+ /**
3
+ * Resolve every `${vault:NAME}` placeholder in a string against the vault. If
4
+ * any referenced key is missing, throws — secret refs are not optional.
5
+ */
6
+ export declare function resolveString(input: string, vault: VaultStore): Promise<string>;
7
+ /** Walk an arbitrary value, resolving all vault placeholders in nested strings. */
8
+ export declare function resolveValue(value: unknown, vault: VaultStore): Promise<unknown>;
9
+ export declare function containsPlaceholder(value: unknown): boolean;
10
+ //# sourceMappingURL=placeholder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placeholder.d.ts","sourceRoot":"","sources":["../src/placeholder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAkB7C;;;GAGG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAsBrF;AAED,mFAAmF;AACnF,wBAAsB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAEtF;AAgCD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D"}