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/lib/client.js ADDED
@@ -0,0 +1,627 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-vault",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region \0dsh-css:/Users/zhipeng/Desktop/work/dsh test/dsh-vault/src/client/VaultSection.module.css.mjs
10
+ const css = ".t_j5va_section{flex-direction:column;gap:12px;padding:16px 0;display:flex}.t_j5va_intro{opacity:.75;margin:0;font-size:13px}.t_j5va_toolbar{align-items:center;gap:8px;display:flex}.t_j5va_searchBox{flex:1;display:flex}.t_j5va_searchBox input{border:1px solid var(--color-border,#444);color:inherit;background:0 0;border-radius:6px;flex:1;padding:6px 10px;font-size:13px}.t_j5va_addButton,.t_j5va_saveButton{background:var(--color-accent,#4f8cff);color:#fff;cursor:pointer;border:1px solid #0000;border-radius:6px;padding:6px 12px;font-size:13px}.t_j5va_addButton:disabled,.t_j5va_saveButton:disabled,.t_j5va_rowActions button:disabled{opacity:.5;cursor:not-allowed}.t_j5va_list{flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;display:flex}.t_j5va_row{border:1px solid var(--color-border,#333);border-radius:8px;justify-content:space-between;align-items:center;gap:12px;padding:8px 10px;display:flex}.t_j5va_rowMain{flex-direction:column;gap:2px;min-width:0;display:flex}.t_j5va_title{font-size:14px;font-weight:600}.t_j5va_identity{opacity:.7;text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.t_j5va_totp{font-family:var(--font-mono,monospace);color:var(--color-accent,#4f8cff);font-size:13px}.t_j5va_rowActions{flex-shrink:0;align-items:center;gap:6px;display:flex}.t_j5va_rowActions button{border:1px solid var(--color-border,#444);color:inherit;cursor:pointer;background:0 0;border-radius:6px;padding:4px 8px;font-size:12px}.t_j5va_deleteButton{color:#e5484d}.t_j5va_copied{color:#30a46c;font-size:12px}.t_j5va_empty,.t_j5va_status,.t_j5va_error{opacity:.8;font-size:13px}.t_j5va_error{color:#e5484d}.t_j5va_editor{border:1px solid var(--color-border,#444);border-radius:8px;padding:12px}.t_j5va_editorBody{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px 12px;display:grid}.t_j5va_field{flex-direction:column;gap:4px;font-size:12px;display:flex}.t_j5va_field input,.t_j5va_field select{border:1px solid var(--color-border,#444);color:inherit;background:0 0;border-radius:6px;padding:5px 8px;font-size:13px}.t_j5va_editorActions{justify-content:flex-end;gap:8px;margin-top:12px;display:flex}.t_j5va_editorActions button{border:1px solid var(--color-border,#444);color:inherit;cursor:pointer;background:0 0;border-radius:6px;padding:6px 12px;font-size:13px}";
11
+ const tagId = "dsh-vault/VaultSection.module.css";
12
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
13
+ const tag = document.createElement("style");
14
+ tag.dataset.plugin = "dsh-vault";
15
+ tag.dataset.pluginCss = tagId;
16
+ tag.textContent = css;
17
+ document.head.appendChild(tag);
18
+ }
19
+ var VaultSection_module_css_default = {
20
+ "toolbar": "t_j5va_toolbar",
21
+ "empty": "t_j5va_empty",
22
+ "intro": "t_j5va_intro",
23
+ "editor": "t_j5va_editor",
24
+ "rowMain": "t_j5va_rowMain",
25
+ "editorBody": "t_j5va_editorBody",
26
+ "field": "t_j5va_field",
27
+ "title": "t_j5va_title",
28
+ "row": "t_j5va_row",
29
+ "error": "t_j5va_error",
30
+ "saveButton": "t_j5va_saveButton",
31
+ "editorActions": "t_j5va_editorActions",
32
+ "rowActions": "t_j5va_rowActions",
33
+ "addButton": "t_j5va_addButton",
34
+ "list": "t_j5va_list",
35
+ "status": "t_j5va_status",
36
+ "totp": "t_j5va_totp",
37
+ "section": "t_j5va_section",
38
+ "deleteButton": "t_j5va_deleteButton",
39
+ "identity": "t_j5va_identity",
40
+ "copied": "t_j5va_copied",
41
+ "searchBox": "t_j5va_searchBox"
42
+ };
43
+ //#endregion
44
+ //#region src/client/VaultSection.tsx
45
+ /**
46
+ * Vault settings section: list, search, add, edit, delete, and copy encrypted
47
+ * credentials. All mutations go through the host VaultGateway remote; secrets
48
+ * are shown only inside the edit form or a copy action, never in the list.
49
+ */
50
+ const FORM_FIELDS = [
51
+ {
52
+ key: "title",
53
+ label: "fieldTitle"
54
+ },
55
+ {
56
+ key: "kind",
57
+ label: "fieldKind"
58
+ },
59
+ {
60
+ key: "username",
61
+ label: "fieldUsername"
62
+ },
63
+ {
64
+ key: "email",
65
+ label: "fieldEmail"
66
+ },
67
+ {
68
+ key: "phone",
69
+ label: "fieldPhone"
70
+ },
71
+ {
72
+ key: "password",
73
+ label: "fieldPassword"
74
+ },
75
+ {
76
+ key: "host",
77
+ label: "fieldHost"
78
+ },
79
+ {
80
+ key: "port",
81
+ label: "fieldPort"
82
+ },
83
+ {
84
+ key: "apiKey",
85
+ label: "fieldApiKey"
86
+ },
87
+ {
88
+ key: "secret",
89
+ label: "fieldSecret"
90
+ },
91
+ {
92
+ key: "accessToken",
93
+ label: "fieldAccessToken"
94
+ },
95
+ {
96
+ key: "refreshToken",
97
+ label: "fieldRefreshToken"
98
+ },
99
+ {
100
+ key: "otpSecret",
101
+ label: "fieldOtpSecret"
102
+ },
103
+ {
104
+ key: "url",
105
+ label: "fieldUrl"
106
+ },
107
+ {
108
+ key: "notes",
109
+ label: "fieldNotes"
110
+ }
111
+ ];
112
+ const KIND_KEYS = {
113
+ login: "kindLogin",
114
+ ssh: "kindSsh",
115
+ "api-key": "kindApiKey",
116
+ secret: "kindSecret",
117
+ oauth: "kindOauth",
118
+ custom: "kindCustom"
119
+ };
120
+ function emptyForm() {
121
+ return {
122
+ title: "",
123
+ kind: "login",
124
+ username: "",
125
+ email: "",
126
+ phone: "",
127
+ password: "",
128
+ host: "",
129
+ port: ""
130
+ };
131
+ }
132
+ /** Render the Vault settings section. */
133
+ function VaultSection(props) {
134
+ const { t, list, search, get, add, update, remove, totp } = props;
135
+ const searchId = (0, react.useId)();
136
+ const [query, setQuery] = (0, react.useState)("");
137
+ const [state, setState] = (0, react.useState)({ status: "loading" });
138
+ const [editor, setEditor] = (0, react.useState)({ status: "closed" });
139
+ const [form, setForm] = (0, react.useState)(emptyForm());
140
+ const [busy, setBusy] = (0, react.useState)(false);
141
+ const [message, setMessage] = (0, react.useState)(null);
142
+ const [copiedId, setCopiedId] = (0, react.useState)(null);
143
+ const [codeMap, setCodeMap] = (0, react.useState)({});
144
+ const [tagsDraft, setTagsDraft] = (0, react.useState)("");
145
+ const refresh = (0, react.useMemo)(() => async () => {
146
+ setState({ status: "loading" });
147
+ try {
148
+ setState({
149
+ status: "ready",
150
+ entries: query.trim().length === 0 ? await list() : await search(query.trim())
151
+ });
152
+ } catch {
153
+ setState({ status: "error" });
154
+ }
155
+ }, [
156
+ list,
157
+ search,
158
+ query
159
+ ]);
160
+ (0, react.useEffect)(() => {
161
+ let current = true;
162
+ Promise.resolve().then(refresh).then(() => {}, () => {
163
+ if (current) setState({ status: "error" });
164
+ });
165
+ return () => {
166
+ current = false;
167
+ };
168
+ }, [refresh]);
169
+ /** Open the editor for a new entry. */
170
+ function startCreate() {
171
+ setForm(emptyForm());
172
+ setTagsDraft("");
173
+ setMessage(null);
174
+ setEditor({ status: "creating" });
175
+ }
176
+ /** Open the editor for an existing entry (fetches full secrets). */
177
+ async function startEdit(id) {
178
+ setBusy(true);
179
+ setMessage(null);
180
+ try {
181
+ const result = await get(id);
182
+ if (!result.found || result.entry === void 0) {
183
+ setMessage(t("error"));
184
+ return;
185
+ }
186
+ const entry = result.entry;
187
+ setForm({
188
+ title: entry.title ?? "",
189
+ kind: entry.kind ?? "login",
190
+ username: entry.username ?? "",
191
+ email: entry.email ?? "",
192
+ phone: entry.phone ?? "",
193
+ password: entry.password ?? "",
194
+ host: entry.host ?? "",
195
+ port: entry.port ?? "",
196
+ privateKey: entry.privateKey ?? "",
197
+ apiKey: entry.apiKey ?? "",
198
+ secret: entry.secret ?? "",
199
+ accessToken: entry.accessToken ?? "",
200
+ refreshToken: entry.refreshToken ?? "",
201
+ otpSecret: entry.otpSecret ?? "",
202
+ url: entry.url ?? "",
203
+ notes: entry.notes ?? ""
204
+ });
205
+ setTagsDraft((entry.tags ?? []).join(", "));
206
+ setEditor({
207
+ status: "editing",
208
+ entry
209
+ });
210
+ } catch {
211
+ setMessage(t("error"));
212
+ } finally {
213
+ setBusy(false);
214
+ }
215
+ }
216
+ /** Save the current form (create or update). */
217
+ async function save() {
218
+ if (!form.title.trim()) {
219
+ setMessage(t("error"));
220
+ return;
221
+ }
222
+ setBusy(true);
223
+ setMessage(null);
224
+ try {
225
+ const tags = tagsDraft.split(",").map((x) => x.trim()).filter((x) => x.length > 0);
226
+ const patch = {
227
+ title: form.title.trim(),
228
+ ...form.kind !== void 0 ? { kind: form.kind } : {},
229
+ ...form.username !== void 0 ? { username: form.username } : {},
230
+ ...form.email !== void 0 ? { email: form.email } : {},
231
+ ...form.phone !== void 0 ? { phone: form.phone } : {},
232
+ ...form.password !== void 0 ? { password: form.password } : {},
233
+ ...form.host !== void 0 ? { host: form.host } : {},
234
+ ...form.port !== void 0 ? { port: form.port } : {},
235
+ ...form.privateKey !== void 0 ? { privateKey: form.privateKey } : {},
236
+ ...form.apiKey !== void 0 ? { apiKey: form.apiKey } : {},
237
+ ...form.secret !== void 0 ? { secret: form.secret } : {},
238
+ ...form.accessToken !== void 0 ? { accessToken: form.accessToken } : {},
239
+ ...form.refreshToken !== void 0 ? { refreshToken: form.refreshToken } : {},
240
+ ...form.otpSecret !== void 0 ? { otpSecret: form.otpSecret } : {},
241
+ ...form.url !== void 0 ? { url: form.url } : {},
242
+ ...form.notes !== void 0 ? { notes: form.notes } : {},
243
+ tags
244
+ };
245
+ if (editor.status === "creating") await add(patch);
246
+ else if (editor.status === "editing") await update(editor.entry.id, patch);
247
+ setEditor({ status: "closed" });
248
+ await refresh();
249
+ } catch {
250
+ setMessage(t("error"));
251
+ } finally {
252
+ setBusy(false);
253
+ }
254
+ }
255
+ /** Delete an entry after confirmation. */
256
+ async function removeEntry(id) {
257
+ if (!window.confirm(t("deleteConfirm"))) return;
258
+ setBusy(true);
259
+ try {
260
+ await remove(id);
261
+ await refresh();
262
+ } catch {
263
+ setMessage(t("error"));
264
+ } finally {
265
+ setBusy(false);
266
+ }
267
+ }
268
+ /** Copy a field value to the clipboard and flash the row. */
269
+ async function copyValue(id, value) {
270
+ try {
271
+ await navigator.clipboard.writeText(value);
272
+ } catch {
273
+ const textarea = document.createElement("textarea");
274
+ textarea.value = value;
275
+ document.body.appendChild(textarea);
276
+ textarea.select();
277
+ document.execCommand("copy");
278
+ textarea.remove();
279
+ }
280
+ setCopiedId(id);
281
+ window.setTimeout(() => setCopiedId(null), 1500);
282
+ }
283
+ /** Fetch and display a TOTP code for an entry with an otpSecret. */
284
+ async function showTotp(id) {
285
+ setBusy(true);
286
+ try {
287
+ const result = await totp(id);
288
+ setCodeMap((previous) => ({
289
+ ...previous,
290
+ [id]: `${result.code} (${result.secondsRemaining}s)`
291
+ }));
292
+ } catch {
293
+ setCodeMap((previous) => ({
294
+ ...previous,
295
+ [id]: t("error")
296
+ }));
297
+ } finally {
298
+ setBusy(false);
299
+ }
300
+ }
301
+ /** Field-set summary line for one entry (non-secret). */
302
+ function identityLine(entry) {
303
+ return [
304
+ entry.kind !== void 0 ? t(KIND_KEYS[entry.kind] ?? "kindCustom") : t("kindLogin"),
305
+ entry.username,
306
+ entry.email,
307
+ entry.phone,
308
+ entry.host !== void 0 ? `${entry.host}${entry.port !== void 0 ? `:${entry.port}` : ""}` : void 0,
309
+ entry.url
310
+ ].filter((value) => value !== void 0 && value.length > 0).join(" · ");
311
+ }
312
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
313
+ className: VaultSection_module_css_default.section,
314
+ "aria-labelledby": "vault-heading",
315
+ children: [
316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
317
+ id: "vault-heading",
318
+ children: t("heading")
319
+ }),
320
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
321
+ className: VaultSection_module_css_default.intro,
322
+ children: t("intro")
323
+ }),
324
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
325
+ className: VaultSection_module_css_default.toolbar,
326
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
327
+ className: VaultSection_module_css_default.searchBox,
328
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
329
+ className: "sr-only",
330
+ children: t("searchPlaceholder")
331
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
332
+ id: searchId,
333
+ type: "search",
334
+ placeholder: t("searchPlaceholder"),
335
+ value: query,
336
+ onChange: (event) => setQuery(event.target.value)
337
+ })]
338
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
339
+ type: "button",
340
+ className: VaultSection_module_css_default.addButton,
341
+ onClick: startCreate,
342
+ disabled: busy,
343
+ children: ["+ ", t("add")]
344
+ })]
345
+ }),
346
+ message !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
347
+ role: "alert",
348
+ className: VaultSection_module_css_default.error,
349
+ children: message
350
+ }),
351
+ state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
352
+ className: VaultSection_module_css_default.status,
353
+ children: t("loading")
354
+ }),
355
+ state.status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
356
+ role: "alert",
357
+ className: VaultSection_module_css_default.error,
358
+ children: t("error")
359
+ }),
360
+ state.status === "ready" && state.entries.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
361
+ className: VaultSection_module_css_default.empty,
362
+ children: t("empty")
363
+ }),
364
+ state.status === "ready" && state.entries.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
365
+ className: VaultSection_module_css_default.list,
366
+ children: state.entries.map((entry) => {
367
+ const code = codeMap[entry.id];
368
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
369
+ className: VaultSection_module_css_default.row,
370
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
371
+ className: VaultSection_module_css_default.rowMain,
372
+ children: [
373
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
374
+ className: VaultSection_module_css_default.title,
375
+ children: entry.title
376
+ }),
377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
378
+ className: VaultSection_module_css_default.identity,
379
+ children: identityLine(entry)
380
+ }),
381
+ code !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
382
+ className: VaultSection_module_css_default.totp,
383
+ children: code
384
+ })
385
+ ]
386
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
387
+ className: VaultSection_module_css_default.rowActions,
388
+ children: [
389
+ copiedId === entry.id && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
390
+ className: VaultSection_module_css_default.copied,
391
+ children: t("copied")
392
+ }),
393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
394
+ type: "button",
395
+ onClick: () => void copyValue(entry.id, entry.username ?? entry.title),
396
+ disabled: busy,
397
+ children: t("copy")
398
+ }),
399
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
400
+ type: "button",
401
+ onClick: () => void showTotp(entry.id),
402
+ disabled: busy,
403
+ children: t("totp")
404
+ }),
405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
406
+ type: "button",
407
+ onClick: () => void startEdit(entry.id),
408
+ disabled: busy,
409
+ children: t("edit")
410
+ }),
411
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
412
+ type: "button",
413
+ className: VaultSection_module_css_default.deleteButton,
414
+ onClick: () => void removeEntry(entry.id),
415
+ disabled: busy,
416
+ children: t("delete")
417
+ })
418
+ ]
419
+ })]
420
+ }, entry.id);
421
+ })
422
+ }),
423
+ editor.status !== "closed" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
424
+ className: VaultSection_module_css_default.editor,
425
+ role: "dialog",
426
+ "aria-label": editor.status === "creating" ? t("add") : t("edit"),
427
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
428
+ className: VaultSection_module_css_default.editorBody,
429
+ children: [FORM_FIELDS.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
430
+ className: VaultSection_module_css_default.field,
431
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(field.label) }), field.key === "kind" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
432
+ value: form.kind ?? "login",
433
+ onChange: (event) => setForm((previous) => ({
434
+ ...previous,
435
+ kind: event.target.value
436
+ })),
437
+ children: Object.entries(KIND_KEYS).map(([value, key]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
438
+ value,
439
+ children: t(key)
440
+ }, value))
441
+ }) : field.key === "title" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
442
+ value: form.title ?? "",
443
+ onChange: (event) => setForm((previous) => ({
444
+ ...previous,
445
+ title: event.target.value
446
+ }))
447
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
448
+ type: field.key === "password" || field.key === "otpSecret" ? "password" : "text",
449
+ value: form[field.key] ?? "",
450
+ onChange: (event) => setForm((previous) => ({
451
+ ...previous,
452
+ [field.key]: event.target.value
453
+ }))
454
+ })]
455
+ }, field.key)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
456
+ className: VaultSection_module_css_default.field,
457
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("fieldTags") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
458
+ value: tagsDraft,
459
+ onChange: (event) => setTagsDraft(event.target.value),
460
+ placeholder: "dev, prod"
461
+ })]
462
+ })]
463
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
464
+ className: VaultSection_module_css_default.editorActions,
465
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
466
+ type: "button",
467
+ onClick: () => setEditor({ status: "closed" }),
468
+ disabled: busy,
469
+ children: t("cancel")
470
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
471
+ type: "button",
472
+ className: VaultSection_module_css_default.saveButton,
473
+ onClick: () => void save(),
474
+ disabled: busy,
475
+ children: t("save")
476
+ })]
477
+ })]
478
+ })
479
+ ]
480
+ });
481
+ }
482
+ //#endregion
483
+ //#region src/client/locales.ts
484
+ /** Copy dictionaries for the Vault settings section. */
485
+ /** English strings (the key-set source of truth for this pair). */
486
+ const en = {
487
+ nav: "Vault",
488
+ heading: "Vault",
489
+ intro: "Encrypted credentials stored on this machine.",
490
+ searchPlaceholder: "Search title, username, email, host…",
491
+ loading: "Loading…",
492
+ empty: "No credentials yet. Add one to get started.",
493
+ add: "Add credential",
494
+ cancel: "Cancel",
495
+ save: "Save",
496
+ edit: "Edit",
497
+ delete: "Delete",
498
+ deleteConfirm: "Delete this entry? This cannot be undone.",
499
+ copy: "Copy",
500
+ copied: "Copied",
501
+ totp: "Code",
502
+ fieldTitle: "Title",
503
+ fieldKind: "Kind",
504
+ fieldUsername: "Username",
505
+ fieldEmail: "Email",
506
+ fieldPhone: "Phone",
507
+ fieldPassword: "Password",
508
+ fieldHost: "Host",
509
+ fieldPort: "Port",
510
+ fieldApiKey: "API key",
511
+ fieldSecret: "Secret",
512
+ fieldAccessToken: "Access token",
513
+ fieldRefreshToken: "Refresh token",
514
+ fieldOtpSecret: "OTP secret",
515
+ fieldUrl: "URL",
516
+ fieldNotes: "Notes",
517
+ fieldTags: "Tags",
518
+ error: "Something went wrong.",
519
+ kindLogin: "Login",
520
+ kindSsh: "SSH",
521
+ kindApiKey: "API key",
522
+ kindSecret: "Secret",
523
+ kindOauth: "OAuth",
524
+ kindCustom: "Custom"
525
+ };
526
+ /** Chinese strings (same keys as English). */
527
+ const zh = {
528
+ nav: "凭据库",
529
+ heading: "Vault 凭据库",
530
+ intro: "本机加密存储的凭据。",
531
+ searchPlaceholder: "搜索标题、用户名、邮箱、主机…",
532
+ loading: "加载中…",
533
+ empty: "还没有凭据,添加一条开始使用。",
534
+ add: "新增凭据",
535
+ cancel: "取消",
536
+ save: "保存",
537
+ edit: "编辑",
538
+ delete: "删除",
539
+ deleteConfirm: "确定删除该条目?此操作不可撤销。",
540
+ copy: "复制",
541
+ copied: "已复制",
542
+ totp: "验证码",
543
+ fieldTitle: "标题",
544
+ fieldKind: "类型",
545
+ fieldUsername: "用户名",
546
+ fieldEmail: "邮箱",
547
+ fieldPhone: "手机号",
548
+ fieldPassword: "密码",
549
+ fieldHost: "主机",
550
+ fieldPort: "端口",
551
+ fieldApiKey: "API 密钥",
552
+ fieldSecret: "Secret",
553
+ fieldAccessToken: "访问令牌",
554
+ fieldRefreshToken: "刷新令牌",
555
+ fieldOtpSecret: "TOTP 密钥",
556
+ fieldUrl: "URL",
557
+ fieldNotes: "备注",
558
+ fieldTags: "标签",
559
+ error: "操作失败。",
560
+ kindLogin: "登录",
561
+ kindSsh: "SSH",
562
+ kindApiKey: "API 密钥",
563
+ kindSecret: "Secret",
564
+ kindOauth: "OAuth",
565
+ kindCustom: "自定义"
566
+ };
567
+ //#endregion
568
+ //#region src/client/index.ts
569
+ /** Dictionary namespace owned by this plugin. */
570
+ const NS = "settings.vault";
571
+ /** Services required by the Settings registration and the RPC face. */
572
+ const inject = [
573
+ "slots",
574
+ "locale",
575
+ "connection"
576
+ ];
577
+ /**
578
+ * Register the Vault settings section once the `settings.section` declaration
579
+ * is on the ledger. The section talks to the host through the connection RPC
580
+ * channel (endpoints `vault/*` served by the host VaultGateway remote).
581
+ */
582
+ function apply(ctx) {
583
+ ctx.effect(() => ctx.locale.register(NS, {
584
+ zh,
585
+ en
586
+ }), "dsh-vault: dictionaries");
587
+ const t = ctx.locale.bind(NS);
588
+ const connection = ctx.get("connection");
589
+ /** Invoke one host vault remote and normalize the RPC envelope. */
590
+ async function invoke(method, args = {}) {
591
+ const result = await connection.rpc.call("/api", `vault/${method}`, { args });
592
+ if (!result.ok) throw new Error(`vault.${method} failed: ${result.error.code}: ${result.error.message}`);
593
+ return result.value;
594
+ }
595
+ const injected = () => ({
596
+ t,
597
+ list: () => invoke("list").then((r) => r.entries),
598
+ search: (query, limit) => invoke("search", {
599
+ query,
600
+ limit: limit ?? 50
601
+ }).then((r) => r.entries),
602
+ get: (id) => invoke("get", { id }),
603
+ add: (patch) => invoke("add", { patch }),
604
+ update: (id, patch) => invoke("update", {
605
+ id,
606
+ patch
607
+ }),
608
+ remove: (id) => invoke("delete", { id }),
609
+ totp: (id) => invoke("totp", { id })
610
+ });
611
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
612
+ name: "settings.section",
613
+ id: "vault",
614
+ order: 30,
615
+ label: () => t("nav"),
616
+ inject: injected
617
+ }, VaultSection));
618
+ }
619
+ //#endregion
620
+ exports.NS = NS;
621
+ exports.apply = apply;
622
+ exports.inject = inject;
623
+ return module.exports;
624
+ }
625
+ });
626
+
627
+ //# sourceMappingURL=client.js.map