dsh-vault 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +627 -0
- package/lib/client.js.map +1 -0
- package/lib/crypto.js +87 -0
- package/lib/index.js +614 -0
- package/lib/password.js +80 -0
- package/lib/store.js +318 -0
- package/lib/totp.js +161 -0
- package/package.json +81 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vault: an encrypted credential vault for DeepSeek Harness.
|
|
3
|
+
*
|
|
4
|
+
* Stores usernames, emails, phone numbers, passwords, and TOTP secrets as
|
|
5
|
+
* individual entries encrypted with AES-256-GCM under a scrypt-derived key,
|
|
6
|
+
* and exposes them to the model through a small tool set:
|
|
7
|
+
*
|
|
8
|
+
* - `vault_add` / `vault_get` / `vault_update` / `vault_delete` — CRUD
|
|
9
|
+
* - `vault_search` — non-secret summary search across all text fields
|
|
10
|
+
* - `vault_totp` — current 6-digit code for a stored TOTP secret
|
|
11
|
+
* - `vault_generate_password` — cryptographically strong password generator
|
|
12
|
+
*
|
|
13
|
+
* The master password is deployment configuration, never a model argument:
|
|
14
|
+
* it is read from the `masterPassword` config field or, when
|
|
15
|
+
* `masterPasswordEnv` is set, from that environment variable at unlock time.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-vault
|
|
18
|
+
*/
|
|
19
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
20
|
+
var useValue = arguments.length > 2;
|
|
21
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
22
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
23
|
+
}
|
|
24
|
+
return useValue ? value : void 0;
|
|
25
|
+
};
|
|
26
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
27
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
28
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
29
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
30
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
31
|
+
var _, done = false;
|
|
32
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
33
|
+
var context = {};
|
|
34
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
35
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
36
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
37
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
38
|
+
if (kind === "accessor") {
|
|
39
|
+
if (result === void 0) continue;
|
|
40
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
41
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
42
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
43
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
44
|
+
}
|
|
45
|
+
else if (_ = accept(result)) {
|
|
46
|
+
if (kind === "field") initializers.unshift(_);
|
|
47
|
+
else descriptor[key] = _;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
51
|
+
done = true;
|
|
52
|
+
};
|
|
53
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
54
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
55
|
+
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
56
|
+
import { openVault, defaultVaultPath } from "./store.js";
|
|
57
|
+
import { totp } from "./totp.js";
|
|
58
|
+
import { generatePassword } from "./password.js";
|
|
59
|
+
export const name = 'dsh-vault';
|
|
60
|
+
export const inject = ['tools'];
|
|
61
|
+
export const Config = Schema.object({
|
|
62
|
+
masterPassword: Schema.string(),
|
|
63
|
+
masterPasswordEnv: Schema.string(),
|
|
64
|
+
path: Schema.string(),
|
|
65
|
+
name: Schema.string(),
|
|
66
|
+
});
|
|
67
|
+
export function apply(ctx, config) {
|
|
68
|
+
const masterPassword = resolveMasterPassword(config);
|
|
69
|
+
/** Ensure the shared store is open (lazily on first use, so a missing
|
|
70
|
+
* master password fails at the first tool call with a clear message). */
|
|
71
|
+
async function ensureStore() {
|
|
72
|
+
return sharedVaultStore(masterPassword, config);
|
|
73
|
+
}
|
|
74
|
+
/** Read a full entry (with secrets) by id. */
|
|
75
|
+
async function readEntry(id) {
|
|
76
|
+
const s = await ensureStore();
|
|
77
|
+
return s.get(id);
|
|
78
|
+
}
|
|
79
|
+
ctx.tools.register(defineTool({
|
|
80
|
+
name: 'vault_add',
|
|
81
|
+
description: 'Add a new credential entry to the encrypted vault. '
|
|
82
|
+
+ 'Stores login credentials (username/email/phone/password), SSH connections (host/port/privateKey), '
|
|
83
|
+
+ 'API keys (apiKey/secret), OAuth tokens (accessToken/refreshToken/expiresAt), TOTP secrets, or any combination, '
|
|
84
|
+
+ 'under a human title. The entry is encrypted at rest with AES-256-GCM; only its summary (no secrets) is returned. '
|
|
85
|
+
+ 'Use the returned entry id in later vault_get/vault_update/vault_delete calls.',
|
|
86
|
+
parameters: {
|
|
87
|
+
title: { type: 'string', required: true, description: 'Human title, e.g. "GitHub personal" or "prod-db ssh".' },
|
|
88
|
+
kind: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Entry category: login (default), ssh, api-key, secret, oauth, or custom.',
|
|
91
|
+
enum: ['login', 'ssh', 'api-key', 'secret', 'oauth', 'custom'],
|
|
92
|
+
},
|
|
93
|
+
username: { type: 'string', description: 'Account username/login.' },
|
|
94
|
+
email: { type: 'string', description: 'Account email.' },
|
|
95
|
+
phone: { type: 'string', description: 'Account phone number.' },
|
|
96
|
+
password: { type: 'string', description: 'The password to store.' },
|
|
97
|
+
host: { type: 'string', description: 'SSH host or service hostname.' },
|
|
98
|
+
port: { type: 'string', description: 'SSH/service port, e.g. "22" or "3306".' },
|
|
99
|
+
privateKey: { type: 'string', description: 'SSH private key (PEM).' },
|
|
100
|
+
apiKey: { type: 'string', description: 'API key.' },
|
|
101
|
+
secret: { type: 'string', description: 'Generic secret (client secret, shared secret, …).' },
|
|
102
|
+
accessToken: { type: 'string', description: 'OAuth access token.' },
|
|
103
|
+
refreshToken: { type: 'string', description: 'OAuth refresh token.' },
|
|
104
|
+
expiresAt: { type: 'integer', description: 'Token/credential expiry epoch millis.' },
|
|
105
|
+
otpSecret: { type: 'string', description: 'TOTP secret: bare Base32 or an otpauth:// URI.' },
|
|
106
|
+
url: { type: 'string', description: 'Associated URL (login page or service home).' },
|
|
107
|
+
notes: { type: 'string', description: 'Free-form notes.' },
|
|
108
|
+
tags: { type: 'array', description: 'Searchable tags.', items: { type: 'string' } },
|
|
109
|
+
fields: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
additionalProperties: true,
|
|
112
|
+
properties: {},
|
|
113
|
+
description: 'Arbitrary additional key/value fields, e.g. {"region": "us-east-1"}.',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
output: {
|
|
117
|
+
schema: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
properties: {
|
|
121
|
+
id: { type: 'string', required: true },
|
|
122
|
+
title: { type: 'string', required: true },
|
|
123
|
+
message: { type: 'string', required: true },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
render: (_args, value) => [{ type: 'text', text: `${value.message} (id: ${value.id})` }],
|
|
127
|
+
},
|
|
128
|
+
async execute(args) {
|
|
129
|
+
if (!args.title.trim())
|
|
130
|
+
throw new Error('vault_add: title must not be empty');
|
|
131
|
+
const s = await ensureStore();
|
|
132
|
+
const entry = await s.add({
|
|
133
|
+
title: args.title.trim(),
|
|
134
|
+
...(args.kind !== undefined ? { kind: args.kind } : {}),
|
|
135
|
+
...(args.username !== undefined ? { username: args.username } : {}),
|
|
136
|
+
...(args.email !== undefined ? { email: args.email } : {}),
|
|
137
|
+
...(args.phone !== undefined ? { phone: args.phone } : {}),
|
|
138
|
+
...(args.password !== undefined ? { password: args.password } : {}),
|
|
139
|
+
...(args.host !== undefined ? { host: args.host } : {}),
|
|
140
|
+
...(args.port !== undefined ? { port: args.port } : {}),
|
|
141
|
+
...(args.privateKey !== undefined ? { privateKey: args.privateKey } : {}),
|
|
142
|
+
...(args.apiKey !== undefined ? { apiKey: args.apiKey } : {}),
|
|
143
|
+
...(args.secret !== undefined ? { secret: args.secret } : {}),
|
|
144
|
+
...(args.accessToken !== undefined ? { accessToken: args.accessToken } : {}),
|
|
145
|
+
...(args.refreshToken !== undefined ? { refreshToken: args.refreshToken } : {}),
|
|
146
|
+
...(args.expiresAt !== undefined ? { expiresAt: args.expiresAt } : {}),
|
|
147
|
+
...(args.otpSecret !== undefined ? { otpSecret: args.otpSecret } : {}),
|
|
148
|
+
...(args.url !== undefined ? { url: args.url } : {}),
|
|
149
|
+
...(args.notes !== undefined ? { notes: args.notes } : {}),
|
|
150
|
+
...(args.tags !== undefined ? { tags: args.tags } : {}),
|
|
151
|
+
...(args.fields !== undefined ? { fields: args.fields } : {}),
|
|
152
|
+
});
|
|
153
|
+
return { id: entry.id, title: entry.title, message: 'added credential entry' };
|
|
154
|
+
},
|
|
155
|
+
}));
|
|
156
|
+
ctx.tools.register(defineTool({
|
|
157
|
+
name: 'vault_get',
|
|
158
|
+
description: 'Read one credential entry from the vault by its id, including the stored password and TOTP secret. '
|
|
159
|
+
+ 'Secrets are returned only to this tool call; prefer vault_search for non-secret summaries.',
|
|
160
|
+
parameters: {
|
|
161
|
+
id: { type: 'string', required: true, description: 'The entry id returned by vault_add or vault_search.' },
|
|
162
|
+
},
|
|
163
|
+
output: {
|
|
164
|
+
schema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
additionalProperties: false,
|
|
167
|
+
properties: {
|
|
168
|
+
found: { type: 'boolean', required: true },
|
|
169
|
+
entry: { type: 'json' },
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
render: (_args, value) => [{ type: 'text', text: value.found ? JSON.stringify(value.entry) : 'entry not found' }],
|
|
173
|
+
},
|
|
174
|
+
async execute(args) {
|
|
175
|
+
const entry = await readEntry(args.id);
|
|
176
|
+
if (!entry)
|
|
177
|
+
return { found: false };
|
|
178
|
+
return { found: true, entry: stripTimestamps(entry) };
|
|
179
|
+
},
|
|
180
|
+
}));
|
|
181
|
+
ctx.tools.register(defineTool({
|
|
182
|
+
name: 'vault_search',
|
|
183
|
+
description: 'Search the encrypted vault across titles, categories, usernames, emails, phone numbers, hosts, ports, '
|
|
184
|
+
+ 'URLs, notes, tags, and custom field values. '
|
|
185
|
+
+ 'Returns non-secret summaries (id, title, kind, username, email, phone, host, port, url, tags) — never passwords, '
|
|
186
|
+
+ 'keys, tokens, or TOTP secrets. Use vault_get with a result id to read the full entry.',
|
|
187
|
+
parameters: {
|
|
188
|
+
query: { type: 'string', required: true, description: 'Search text; matches case-insensitively.' },
|
|
189
|
+
limit: { type: 'number', description: 'Maximum results (default 20).' },
|
|
190
|
+
},
|
|
191
|
+
output: {
|
|
192
|
+
schema: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
additionalProperties: false,
|
|
195
|
+
properties: {
|
|
196
|
+
results: {
|
|
197
|
+
type: 'array',
|
|
198
|
+
required: true,
|
|
199
|
+
items: { type: 'json' },
|
|
200
|
+
},
|
|
201
|
+
total: { type: 'integer', required: true },
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
render: (_args, value) => [{
|
|
205
|
+
type: 'text',
|
|
206
|
+
text: value.total === 0
|
|
207
|
+
? 'no matching entries'
|
|
208
|
+
: JSON.stringify(value.results),
|
|
209
|
+
}],
|
|
210
|
+
},
|
|
211
|
+
async execute(args) {
|
|
212
|
+
const s = await ensureStore();
|
|
213
|
+
const limit = validateLimit(args.limit, 'vault_search');
|
|
214
|
+
const results = s.search(args.query, limit);
|
|
215
|
+
return { results, total: results.length };
|
|
216
|
+
},
|
|
217
|
+
}));
|
|
218
|
+
ctx.tools.register(defineTool({
|
|
219
|
+
name: 'vault_update',
|
|
220
|
+
description: 'Update fields of an existing vault entry by id. Only the provided fields change; secrets and other '
|
|
221
|
+
+ 'fields are preserved. Pass an empty-string value to clear a field. Returns the updated entry summary.',
|
|
222
|
+
parameters: {
|
|
223
|
+
id: { type: 'string', required: true, description: 'The entry id to update.' },
|
|
224
|
+
title: { type: 'string', description: 'New title.' },
|
|
225
|
+
kind: {
|
|
226
|
+
type: 'string',
|
|
227
|
+
description: 'New category.',
|
|
228
|
+
enum: ['login', 'ssh', 'api-key', 'secret', 'oauth', 'custom'],
|
|
229
|
+
},
|
|
230
|
+
username: { type: 'string', description: 'New username.' },
|
|
231
|
+
email: { type: 'string', description: 'New email.' },
|
|
232
|
+
phone: { type: 'string', description: 'New phone number.' },
|
|
233
|
+
password: { type: 'string', description: 'New password.' },
|
|
234
|
+
host: { type: 'string', description: 'New SSH host or hostname.' },
|
|
235
|
+
port: { type: 'string', description: 'New port.' },
|
|
236
|
+
privateKey: { type: 'string', description: 'New SSH private key.' },
|
|
237
|
+
apiKey: { type: 'string', description: 'New API key.' },
|
|
238
|
+
secret: { type: 'string', description: 'New secret.' },
|
|
239
|
+
accessToken: { type: 'string', description: 'New OAuth access token.' },
|
|
240
|
+
refreshToken: { type: 'string', description: 'New OAuth refresh token.' },
|
|
241
|
+
expiresAt: { type: 'integer', description: 'New expiry epoch millis.' },
|
|
242
|
+
otpSecret: { type: 'string', description: 'New TOTP secret.' },
|
|
243
|
+
url: { type: 'string', description: 'New URL.' },
|
|
244
|
+
notes: { type: 'string', description: 'New notes.' },
|
|
245
|
+
tags: { type: 'array', description: 'New tags.', items: { type: 'string' } },
|
|
246
|
+
fields: {
|
|
247
|
+
type: 'object',
|
|
248
|
+
additionalProperties: true,
|
|
249
|
+
properties: {},
|
|
250
|
+
description: 'Replace the arbitrary key/value fields.',
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
output: {
|
|
254
|
+
schema: {
|
|
255
|
+
type: 'object',
|
|
256
|
+
additionalProperties: false,
|
|
257
|
+
properties: {
|
|
258
|
+
found: { type: 'boolean', required: true },
|
|
259
|
+
entry: { type: 'json' },
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
render: (_args, value) => [{ type: 'text', text: value.found ? 'entry updated' : 'entry not found' }],
|
|
263
|
+
},
|
|
264
|
+
async execute(args) {
|
|
265
|
+
const s = await ensureStore();
|
|
266
|
+
const patch = {};
|
|
267
|
+
for (const key of [
|
|
268
|
+
'title', 'kind', 'username', 'email', 'phone', 'password', 'host', 'port', 'privateKey',
|
|
269
|
+
'apiKey', 'secret', 'accessToken', 'refreshToken', 'expiresAt', 'otpSecret', 'url', 'notes', 'tags', 'fields',
|
|
270
|
+
]) {
|
|
271
|
+
const value = args[key];
|
|
272
|
+
if (value !== undefined) {
|
|
273
|
+
;
|
|
274
|
+
patch[key] = value;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const updated = await s.update(args.id, patch);
|
|
278
|
+
if (!updated)
|
|
279
|
+
return { found: false };
|
|
280
|
+
return { found: true, entry: toSummaryJson(updated) };
|
|
281
|
+
},
|
|
282
|
+
}));
|
|
283
|
+
ctx.tools.register(defineTool({
|
|
284
|
+
name: 'vault_delete',
|
|
285
|
+
description: 'Delete a vault entry by id. Returns whether the entry existed. This cannot be undone.',
|
|
286
|
+
parameters: {
|
|
287
|
+
id: { type: 'string', required: true, description: 'The entry id to delete.' },
|
|
288
|
+
},
|
|
289
|
+
output: {
|
|
290
|
+
schema: {
|
|
291
|
+
type: 'object',
|
|
292
|
+
additionalProperties: false,
|
|
293
|
+
properties: {
|
|
294
|
+
deleted: { type: 'boolean', required: true },
|
|
295
|
+
message: { type: 'string', required: true },
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
render: (_args, value) => [{ type: 'text', text: value.message }],
|
|
299
|
+
},
|
|
300
|
+
async execute(args) {
|
|
301
|
+
const s = await ensureStore();
|
|
302
|
+
const deleted = await s.delete(args.id);
|
|
303
|
+
return { deleted, message: deleted ? 'entry deleted' : 'entry not found' };
|
|
304
|
+
},
|
|
305
|
+
}));
|
|
306
|
+
ctx.tools.register(defineTool({
|
|
307
|
+
name: 'vault_totp',
|
|
308
|
+
description: 'Generate the current time-based one-time password (TOTP) for a secret stored in the vault or for a '
|
|
309
|
+
+ 'bare Base32 secret / otpauth:// URI passed directly. Useful for two-factor authentication codes. '
|
|
310
|
+
+ 'The code is valid only for the current 30-second window.',
|
|
311
|
+
parameters: {
|
|
312
|
+
id: { type: 'string', description: 'Vault entry id whose otpSecret to use. Provide exactly one of id or secret.' },
|
|
313
|
+
secret: { type: 'string', description: 'Bare Base32 secret or otpauth:// URI. Provide exactly one of id or secret.' },
|
|
314
|
+
},
|
|
315
|
+
output: {
|
|
316
|
+
schema: {
|
|
317
|
+
type: 'object',
|
|
318
|
+
additionalProperties: false,
|
|
319
|
+
properties: {
|
|
320
|
+
code: { type: 'string', required: true },
|
|
321
|
+
label: { type: 'string', description: 'Entry title or issuer when known.' },
|
|
322
|
+
secondsRemaining: { type: 'integer', required: true },
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
render: (_args, value) => [{
|
|
326
|
+
type: 'text',
|
|
327
|
+
text: `TOTP code${value.label ? ` for ${value.label}` : ''}: ${value.code} (${value.secondsRemaining}s left)`,
|
|
328
|
+
}],
|
|
329
|
+
},
|
|
330
|
+
async execute(args) {
|
|
331
|
+
if ((args.id === undefined) === (args.secret === undefined)) {
|
|
332
|
+
throw new Error('vault_totp: provide exactly one of id or secret');
|
|
333
|
+
}
|
|
334
|
+
let secret;
|
|
335
|
+
let label;
|
|
336
|
+
if (args.id !== undefined) {
|
|
337
|
+
const entry = await readEntry(args.id);
|
|
338
|
+
if (!entry?.otpSecret) {
|
|
339
|
+
throw new Error(`vault_totp: entry ${args.id} has no otpSecret`);
|
|
340
|
+
}
|
|
341
|
+
secret = entry.otpSecret;
|
|
342
|
+
label = entry.title;
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
secret = args.secret;
|
|
346
|
+
}
|
|
347
|
+
const nowMs = Date.now();
|
|
348
|
+
const code = totp(secret, nowMs);
|
|
349
|
+
const secondsRemaining = 30 - Math.floor(nowMs / 1000) % 30;
|
|
350
|
+
return { code, ...(label !== undefined ? { label } : {}), secondsRemaining };
|
|
351
|
+
},
|
|
352
|
+
}));
|
|
353
|
+
ctx.tools.register(defineTool({
|
|
354
|
+
name: 'vault_generate_password',
|
|
355
|
+
description: 'Generate a cryptographically strong random password with configurable length and character classes. '
|
|
356
|
+
+ 'Use this when a user needs a new password; the generated value is returned and is not stored automatically — '
|
|
357
|
+
+ 'call vault_add or vault_update to persist it.',
|
|
358
|
+
parameters: {
|
|
359
|
+
length: { type: 'integer', description: 'Total length (default 20, min = number of selected classes).' },
|
|
360
|
+
lowercase: { type: 'boolean', description: 'Include lowercase (default true).' },
|
|
361
|
+
uppercase: { type: 'boolean', description: 'Include uppercase (default true).' },
|
|
362
|
+
digits: { type: 'boolean', description: 'Include digits (default true).' },
|
|
363
|
+
symbols: { type: 'boolean', description: 'Include symbols (default true).' },
|
|
364
|
+
excludeAmbiguous: { type: 'boolean', description: 'Exclude 0/O/1/l/I (default false).' },
|
|
365
|
+
group: { type: 'integer', description: 'Insert "-" every N characters (e.g. 3 → vK7-mQ2-zt9).' },
|
|
366
|
+
},
|
|
367
|
+
output: {
|
|
368
|
+
schema: {
|
|
369
|
+
type: 'object',
|
|
370
|
+
additionalProperties: false,
|
|
371
|
+
properties: {
|
|
372
|
+
password: { type: 'string', required: true },
|
|
373
|
+
length: { type: 'integer', required: true },
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
render: (_args, value) => [{ type: 'text', text: value.password }],
|
|
377
|
+
},
|
|
378
|
+
async execute(args) {
|
|
379
|
+
const password = generatePassword({
|
|
380
|
+
...(args.length !== undefined ? { length: args.length } : {}),
|
|
381
|
+
...(args.lowercase !== undefined ? { lowercase: args.lowercase } : {}),
|
|
382
|
+
...(args.uppercase !== undefined ? { uppercase: args.uppercase } : {}),
|
|
383
|
+
...(args.digits !== undefined ? { digits: args.digits } : {}),
|
|
384
|
+
...(args.symbols !== undefined ? { symbols: args.symbols } : {}),
|
|
385
|
+
...(args.excludeAmbiguous !== undefined ? { excludeAmbiguous: args.excludeAmbiguous } : {}),
|
|
386
|
+
...(args.group !== undefined ? { group: args.group } : {}),
|
|
387
|
+
});
|
|
388
|
+
return { password, length: password.length };
|
|
389
|
+
},
|
|
390
|
+
}));
|
|
391
|
+
// UI-facing Remote gateway: the browser Settings Vault page talks to these
|
|
392
|
+
// methods through the /api RPC channel (loopback-trusted), bypassing the
|
|
393
|
+
// model-tool layer entirely. Secrets are returned because the UI is the
|
|
394
|
+
// user's own browser on their own machine; the RPC authority is
|
|
395
|
+
// trusted-host/loopback (see packages/client/connection).
|
|
396
|
+
ctx.plugin(VaultGateway, {
|
|
397
|
+
masterPassword,
|
|
398
|
+
...(config.path !== undefined ? { path: config.path } : {}),
|
|
399
|
+
...(config.name !== undefined ? { name: config.name } : {}),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Remote gateway exposing vault CRUD/search to the browser Settings UI.
|
|
404
|
+
* Registered as an ordinary Cordis plugin; the Typert gateway discovers its
|
|
405
|
+
* `typertRemote` binding and `@Remote` methods at runtime (source-mode), so
|
|
406
|
+
* no code generation is required for an independently distributed plugin.
|
|
407
|
+
*/
|
|
408
|
+
let VaultGateway = (() => {
|
|
409
|
+
let _classSuper = TypertRemoteService;
|
|
410
|
+
let _instanceExtraInitializers = [];
|
|
411
|
+
let _list_decorators;
|
|
412
|
+
let _get_decorators;
|
|
413
|
+
let _search_decorators;
|
|
414
|
+
let _add_decorators;
|
|
415
|
+
let _update_decorators;
|
|
416
|
+
let _delete_decorators;
|
|
417
|
+
let _totp_decorators;
|
|
418
|
+
return class VaultGateway extends _classSuper {
|
|
419
|
+
static {
|
|
420
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
421
|
+
_list_decorators = [Remote('list')];
|
|
422
|
+
_get_decorators = [Remote('get')];
|
|
423
|
+
_search_decorators = [Remote('search')];
|
|
424
|
+
_add_decorators = [Remote('add')];
|
|
425
|
+
_update_decorators = [Remote('update')];
|
|
426
|
+
_delete_decorators = [Remote('delete')];
|
|
427
|
+
_totp_decorators = [Remote('totp')];
|
|
428
|
+
__esDecorate(this, null, _list_decorators, { kind: "method", name: "list", static: false, private: false, access: { has: obj => "list" in obj, get: obj => obj.list }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
429
|
+
__esDecorate(this, null, _get_decorators, { kind: "method", name: "get", static: false, private: false, access: { has: obj => "get" in obj, get: obj => obj.get }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
430
|
+
__esDecorate(this, null, _search_decorators, { kind: "method", name: "search", static: false, private: false, access: { has: obj => "search" in obj, get: obj => obj.search }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
431
|
+
__esDecorate(this, null, _add_decorators, { kind: "method", name: "add", static: false, private: false, access: { has: obj => "add" in obj, get: obj => obj.add }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
432
|
+
__esDecorate(this, null, _update_decorators, { kind: "method", name: "update", static: false, private: false, access: { has: obj => "update" in obj, get: obj => obj.update }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
433
|
+
__esDecorate(this, null, _delete_decorators, { kind: "method", name: "delete", static: false, private: false, access: { has: obj => "delete" in obj, get: obj => obj.delete }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
434
|
+
__esDecorate(this, null, _totp_decorators, { kind: "method", name: "totp", static: false, private: false, access: { has: obj => "totp" in obj, get: obj => obj.totp }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
435
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
436
|
+
}
|
|
437
|
+
static inject = ['tools'];
|
|
438
|
+
masterPassword = __runInitializers(this, _instanceExtraInitializers);
|
|
439
|
+
vaultPath;
|
|
440
|
+
vaultName;
|
|
441
|
+
constructor(ctx, config) {
|
|
442
|
+
super(ctx, 'vault');
|
|
443
|
+
this.masterPassword = config.masterPassword ?? resolveMasterPassword(config);
|
|
444
|
+
this.vaultPath = config.path;
|
|
445
|
+
this.vaultName = config.name;
|
|
446
|
+
}
|
|
447
|
+
async ensureStore() {
|
|
448
|
+
return sharedVaultStore(this.masterPassword, {
|
|
449
|
+
...(this.vaultPath !== undefined ? { path: this.vaultPath } : {}),
|
|
450
|
+
...(this.vaultName !== undefined ? { name: this.vaultName } : {}),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
/** List every entry as a non-secret summary. */
|
|
454
|
+
async list() {
|
|
455
|
+
const store = await this.ensureStore();
|
|
456
|
+
return { entries: store.list().map(toSummary) };
|
|
457
|
+
}
|
|
458
|
+
/** Read one full entry (including secrets) by id. */
|
|
459
|
+
async get(id) {
|
|
460
|
+
const store = await this.ensureStore();
|
|
461
|
+
const entry = store.get(id);
|
|
462
|
+
if (entry === undefined)
|
|
463
|
+
return { found: false };
|
|
464
|
+
return { found: true, entry: toWire(entry) };
|
|
465
|
+
}
|
|
466
|
+
/** Search entries across text fields; returns non-secret summaries. */
|
|
467
|
+
async search(query, limit) {
|
|
468
|
+
const store = await this.ensureStore();
|
|
469
|
+
return { entries: store.search(query, limit) };
|
|
470
|
+
}
|
|
471
|
+
/** Add a new entry; returns its id and summary. */
|
|
472
|
+
async add(patch) {
|
|
473
|
+
if (!patch.title.trim())
|
|
474
|
+
throw new Error('vault: title must not be empty');
|
|
475
|
+
const store = await this.ensureStore();
|
|
476
|
+
const entry = await store.add(patch);
|
|
477
|
+
return toSummary(entry);
|
|
478
|
+
}
|
|
479
|
+
/** Update an existing entry's fields; returns the updated summary or not-found. */
|
|
480
|
+
async update(id, patch) {
|
|
481
|
+
const store = await this.ensureStore();
|
|
482
|
+
const updated = await store.update(id, patch);
|
|
483
|
+
if (updated === undefined)
|
|
484
|
+
return { found: false };
|
|
485
|
+
return { found: true, entry: toSummary(updated) };
|
|
486
|
+
}
|
|
487
|
+
/** Delete an entry by id. */
|
|
488
|
+
async delete(id) {
|
|
489
|
+
const store = await this.ensureStore();
|
|
490
|
+
return { deleted: await store.delete(id) };
|
|
491
|
+
}
|
|
492
|
+
/** Generate the current TOTP code for a stored otpSecret (or bare secret). */
|
|
493
|
+
async totp(id, secret) {
|
|
494
|
+
if ((id === undefined) === (secret === undefined)) {
|
|
495
|
+
throw new Error('vault.totp: provide exactly one of id or secret');
|
|
496
|
+
}
|
|
497
|
+
let input;
|
|
498
|
+
let label;
|
|
499
|
+
if (id !== undefined) {
|
|
500
|
+
const store = await this.ensureStore();
|
|
501
|
+
const entry = store.get(id);
|
|
502
|
+
if (entry?.otpSecret === undefined)
|
|
503
|
+
throw new Error(`vault.totp: entry ${id} has no otpSecret`);
|
|
504
|
+
input = entry.otpSecret;
|
|
505
|
+
label = entry.title;
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
input = secret;
|
|
509
|
+
}
|
|
510
|
+
const nowMs = Date.now();
|
|
511
|
+
return {
|
|
512
|
+
code: totp(input, nowMs),
|
|
513
|
+
...(label !== undefined ? { label } : {}),
|
|
514
|
+
secondsRemaining: 30 - (Math.floor(nowMs / 1000) % 30),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
})();
|
|
519
|
+
export { VaultGateway };
|
|
520
|
+
/** Project a stored entry onto its wire summary. */
|
|
521
|
+
function toSummary(entry) {
|
|
522
|
+
return {
|
|
523
|
+
id: entry.id,
|
|
524
|
+
title: entry.title,
|
|
525
|
+
...(entry.kind !== undefined ? { kind: entry.kind } : {}),
|
|
526
|
+
...(entry.username !== undefined ? { username: entry.username } : {}),
|
|
527
|
+
...(entry.email !== undefined ? { email: entry.email } : {}),
|
|
528
|
+
...(entry.phone !== undefined ? { phone: entry.phone } : {}),
|
|
529
|
+
...(entry.host !== undefined ? { host: entry.host } : {}),
|
|
530
|
+
...(entry.port !== undefined ? { port: entry.port } : {}),
|
|
531
|
+
...(entry.url !== undefined ? { url: entry.url } : {}),
|
|
532
|
+
...(entry.tags !== undefined ? { tags: entry.tags } : {}),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
/** Project a stored entry onto its full wire shape (timestamps stripped). */
|
|
536
|
+
function toWire(entry) {
|
|
537
|
+
const { createdAt, updatedAt, ...rest } = entry;
|
|
538
|
+
return rest;
|
|
539
|
+
}
|
|
540
|
+
/** Resolve the master password from config or the named environment variable. */
|
|
541
|
+
function resolveMasterPassword(config) {
|
|
542
|
+
if (config.masterPasswordEnv !== undefined) {
|
|
543
|
+
const fromEnv = process.env[config.masterPasswordEnv];
|
|
544
|
+
if (fromEnv === undefined || fromEnv.length === 0) {
|
|
545
|
+
throw new Error(`dsh-vault: environment variable ${config.masterPasswordEnv} is not set`);
|
|
546
|
+
}
|
|
547
|
+
return fromEnv;
|
|
548
|
+
}
|
|
549
|
+
if (config.masterPassword !== undefined && config.masterPassword.length > 0) {
|
|
550
|
+
return config.masterPassword;
|
|
551
|
+
}
|
|
552
|
+
throw new Error('dsh-vault: configure masterPassword or masterPasswordEnv to unlock the vault');
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Shared vault-store instances keyed by resolved path + master password. The
|
|
556
|
+
* model tools and the UI-facing VaultGateway must observe ONE store so writes
|
|
557
|
+
* through either surface are visible to the other immediately — two
|
|
558
|
+
* independent `openVault()` calls would each cache their own snapshot and
|
|
559
|
+
* drift apart. Keying on the password too keeps two deployments pointing at
|
|
560
|
+
* the same file but configured with different master passwords from sharing
|
|
561
|
+
* (and silently "unlocking") each other's store.
|
|
562
|
+
*/
|
|
563
|
+
const sharedVaultStores = new Map();
|
|
564
|
+
/** Resolve the canonical vault file path for a config (path override or name). */
|
|
565
|
+
function resolveVaultPath(config) {
|
|
566
|
+
if (config.path !== undefined)
|
|
567
|
+
return config.path;
|
|
568
|
+
return defaultVaultPath(config.name);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Open (or reuse) the vault store for one deployment configuration. All
|
|
572
|
+
* callers within the process share the same instance for the same path and
|
|
573
|
+
* master password, so a write via a model tool is immediately visible to the
|
|
574
|
+
* Settings UI and vice versa.
|
|
575
|
+
*/
|
|
576
|
+
async function sharedVaultStore(masterPassword, config) {
|
|
577
|
+
const path = resolveVaultPath(config);
|
|
578
|
+
// The master password is part of the identity: a different password must
|
|
579
|
+
// open its own store (and fail authentication) rather than reuse a store
|
|
580
|
+
// unlocked with another password.
|
|
581
|
+
const cacheKey = `${path}\0${masterPassword}`;
|
|
582
|
+
const existing = sharedVaultStores.get(cacheKey);
|
|
583
|
+
if (existing !== undefined)
|
|
584
|
+
return existing;
|
|
585
|
+
const opening = openVault({
|
|
586
|
+
masterPassword,
|
|
587
|
+
path,
|
|
588
|
+
}).catch((error) => {
|
|
589
|
+
// A failed open must not poison the cache for later retries.
|
|
590
|
+
sharedVaultStores.delete(cacheKey);
|
|
591
|
+
throw error;
|
|
592
|
+
});
|
|
593
|
+
sharedVaultStores.set(cacheKey, opening);
|
|
594
|
+
return opening;
|
|
595
|
+
}
|
|
596
|
+
/** Validate a model-supplied result limit: a positive integer capped at 100. */
|
|
597
|
+
function validateLimit(value, tool) {
|
|
598
|
+
if (value === undefined)
|
|
599
|
+
return 20;
|
|
600
|
+
if (!Number.isInteger(value) || value < 1 || value > 100) {
|
|
601
|
+
throw new Error(`${tool}: limit must be an integer between 1 and 100`);
|
|
602
|
+
}
|
|
603
|
+
return value;
|
|
604
|
+
}
|
|
605
|
+
/** A summary view of an entry without timestamps or secrets (used by update output). */
|
|
606
|
+
function toSummaryJson(entry) {
|
|
607
|
+
return toSummary(entry);
|
|
608
|
+
}
|
|
609
|
+
/** Strip timestamps from an entry for model-visible output (keeps secrets
|
|
610
|
+
* when the caller asked for the full entry via vault_get). */
|
|
611
|
+
function stripTimestamps(entry) {
|
|
612
|
+
const { createdAt, updatedAt, ...rest } = entry;
|
|
613
|
+
return rest;
|
|
614
|
+
}
|