@agent-native/dispatch 0.8.2 → 0.8.3

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.
Files changed (39) hide show
  1. package/dist/actions/update-vault-secret.d.ts.map +1 -1
  2. package/dist/actions/update-vault-secret.js +34 -6
  3. package/dist/actions/update-vault-secret.js.map +1 -1
  4. package/dist/routes/index.d.ts.map +1 -1
  5. package/dist/routes/index.js +1 -0
  6. package/dist/routes/index.js.map +1 -1
  7. package/dist/routes/pages/extensions.$id.d.ts +3 -0
  8. package/dist/routes/pages/extensions.$id.d.ts.map +1 -1
  9. package/dist/routes/pages/extensions.$id.js +5 -2
  10. package/dist/routes/pages/extensions.$id.js.map +1 -1
  11. package/dist/routes/pages/extensions._index.d.ts +3 -0
  12. package/dist/routes/pages/extensions._index.d.ts.map +1 -1
  13. package/dist/routes/pages/extensions._index.js +5 -2
  14. package/dist/routes/pages/extensions._index.js.map +1 -1
  15. package/dist/routes/pages/tools.$id.d.ts +3 -0
  16. package/dist/routes/pages/tools.$id.d.ts.map +1 -1
  17. package/dist/routes/pages/tools.$id.js +3 -0
  18. package/dist/routes/pages/tools.$id.js.map +1 -1
  19. package/dist/routes/pages/tools._index.d.ts +3 -0
  20. package/dist/routes/pages/tools._index.d.ts.map +1 -1
  21. package/dist/routes/pages/tools._index.js +3 -0
  22. package/dist/routes/pages/tools._index.js.map +1 -1
  23. package/dist/routes/pages/vault.d.ts.map +1 -1
  24. package/dist/routes/pages/vault.js +46 -2
  25. package/dist/routes/pages/vault.js.map +1 -1
  26. package/dist/server/lib/vault-store.d.ts +7 -1
  27. package/dist/server/lib/vault-store.d.ts.map +1 -1
  28. package/dist/server/lib/vault-store.js +73 -4
  29. package/dist/server/lib/vault-store.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/actions/update-vault-secret.ts +39 -6
  32. package/src/routes/index.ts +1 -0
  33. package/src/routes/pages/extensions.$id.tsx +6 -2
  34. package/src/routes/pages/extensions._index.tsx +6 -2
  35. package/src/routes/pages/tools.$id.tsx +4 -0
  36. package/src/routes/pages/tools._index.tsx +4 -0
  37. package/src/routes/pages/vault.tsx +172 -1
  38. package/src/server/lib/vault-store.spec.ts +2 -0
  39. package/src/server/lib/vault-store.ts +101 -4
@@ -4,6 +4,7 @@ import { toast } from "sonner";
4
4
  import {
5
5
  IconChevronDown,
6
6
  IconChevronRight,
7
+ IconEdit,
7
8
  IconEye,
8
9
  IconEyeOff,
9
10
  IconKey,
@@ -61,6 +62,7 @@ const PROVIDERS = [
61
62
  "anthropic",
62
63
  "other",
63
64
  ];
65
+ const PROVIDER_NONE_VALUE = "__none__";
64
66
 
65
67
  type VaultAccessMode = "all-apps" | "manual";
66
68
 
@@ -177,6 +179,174 @@ function AddSecretDialog() {
177
179
  );
178
180
  }
179
181
 
182
+ function EditSecretDialog({ secret }: { secret: any }) {
183
+ const [open, setOpen] = useState(false);
184
+ const [credentialKey, setCredentialKey] = useState(
185
+ secret.credentialKey || "",
186
+ );
187
+ const [name, setName] = useState(secret.name || "");
188
+ const [value, setValue] = useState(secret.value || "");
189
+ const [provider, setProvider] = useState(secret.provider || "");
190
+ const [description, setDescription] = useState(secret.description || "");
191
+ const [showValue, setShowValue] = useState(false);
192
+
193
+ const update = useActionMutation("update-vault-secret", {
194
+ onSuccess: () => {
195
+ toast.success("Secret updated");
196
+ setOpen(false);
197
+ setShowValue(false);
198
+ },
199
+ onError: (err) => toast.error(String(err)),
200
+ });
201
+
202
+ const resetDraft = () => {
203
+ setCredentialKey(secret.credentialKey || "");
204
+ setName(secret.name || "");
205
+ setValue(secret.value || "");
206
+ setProvider(secret.provider || "");
207
+ setDescription(secret.description || "");
208
+ setShowValue(false);
209
+ };
210
+
211
+ return (
212
+ <Dialog
213
+ open={open}
214
+ onOpenChange={(nextOpen) => {
215
+ if (nextOpen) resetDraft();
216
+ setOpen(nextOpen);
217
+ }}
218
+ >
219
+ <DialogTrigger asChild>
220
+ <Button variant="outline" size="sm">
221
+ <IconEdit size={14} className="mr-1" />
222
+ Edit secret
223
+ </Button>
224
+ </DialogTrigger>
225
+ <DialogContent>
226
+ <DialogHeader>
227
+ <DialogTitle>Edit vault secret</DialogTitle>
228
+ <DialogDescription>
229
+ Update the stored key and metadata. Changes sync to the shared
230
+ credential store.
231
+ </DialogDescription>
232
+ </DialogHeader>
233
+ <form
234
+ className="space-y-4 py-2"
235
+ onSubmit={(event) => {
236
+ event.preventDefault();
237
+ update.mutate({
238
+ id: secret.id,
239
+ credentialKey,
240
+ name,
241
+ value,
242
+ provider: provider || null,
243
+ description: description || null,
244
+ });
245
+ }}
246
+ >
247
+ <div className="space-y-2">
248
+ <Label htmlFor={`vault-secret-name-${secret.id}`}>Name</Label>
249
+ <Input
250
+ id={`vault-secret-name-${secret.id}`}
251
+ value={name}
252
+ onChange={(e) => setName(e.target.value)}
253
+ />
254
+ </div>
255
+ <div className="space-y-2">
256
+ <Label htmlFor={`vault-secret-key-${secret.id}`}>
257
+ Credential key (env var name)
258
+ </Label>
259
+ <Input
260
+ id={`vault-secret-key-${secret.id}`}
261
+ value={credentialKey}
262
+ onChange={(e) => setCredentialKey(e.target.value)}
263
+ className="font-mono text-sm"
264
+ />
265
+ </div>
266
+ <div className="space-y-2">
267
+ <Label htmlFor={`vault-secret-value-${secret.id}`}>Value</Label>
268
+ <div className="flex gap-2">
269
+ <Input
270
+ id={`vault-secret-value-${secret.id}`}
271
+ type={showValue ? "text" : "password"}
272
+ value={value}
273
+ onChange={(e) => setValue(e.target.value)}
274
+ className="font-mono text-sm"
275
+ />
276
+ <Button
277
+ type="button"
278
+ variant="outline"
279
+ size="icon"
280
+ onClick={() => setShowValue((current) => !current)}
281
+ aria-label={
282
+ showValue ? "Hide secret value" : "Show secret value"
283
+ }
284
+ >
285
+ {showValue ? <IconEyeOff size={15} /> : <IconEye size={15} />}
286
+ </Button>
287
+ </div>
288
+ </div>
289
+ <div className="space-y-2">
290
+ <Label>Provider</Label>
291
+ <Select
292
+ value={provider || PROVIDER_NONE_VALUE}
293
+ onValueChange={(nextProvider) =>
294
+ setProvider(
295
+ nextProvider === PROVIDER_NONE_VALUE ? "" : nextProvider,
296
+ )
297
+ }
298
+ >
299
+ <SelectTrigger>
300
+ <SelectValue placeholder="Select a provider..." />
301
+ </SelectTrigger>
302
+ <SelectContent>
303
+ <SelectItem value={PROVIDER_NONE_VALUE}>No provider</SelectItem>
304
+ {PROVIDERS.map((p) => (
305
+ <SelectItem key={p} value={p}>
306
+ {p.charAt(0).toUpperCase() + p.slice(1)}
307
+ </SelectItem>
308
+ ))}
309
+ </SelectContent>
310
+ </Select>
311
+ </div>
312
+ <div className="space-y-2">
313
+ <Label htmlFor={`vault-secret-description-${secret.id}`}>
314
+ Description
315
+ </Label>
316
+ <Textarea
317
+ id={`vault-secret-description-${secret.id}`}
318
+ placeholder="What is this secret used for?"
319
+ value={description}
320
+ onChange={(e) => setDescription(e.target.value)}
321
+ rows={2}
322
+ />
323
+ </div>
324
+ <DialogFooter>
325
+ <Button
326
+ type="button"
327
+ variant="outline"
328
+ onClick={() => setOpen(false)}
329
+ >
330
+ Cancel
331
+ </Button>
332
+ <Button
333
+ type="submit"
334
+ disabled={
335
+ !credentialKey.trim() ||
336
+ !name.trim() ||
337
+ !value ||
338
+ update.isPending
339
+ }
340
+ >
341
+ {update.isPending ? "Saving..." : "Save changes"}
342
+ </Button>
343
+ </DialogFooter>
344
+ </form>
345
+ </DialogContent>
346
+ </Dialog>
347
+ );
348
+ }
349
+
180
350
  function GrantDialog({
181
351
  secretId,
182
352
  secretName,
@@ -429,7 +599,8 @@ function SecretRow({
429
599
  )}
430
600
  </div>
431
601
 
432
- <div className="flex justify-end border-t pt-3">
602
+ <div className="flex justify-end gap-2 border-t pt-3">
603
+ <EditSecretDialog secret={secret} />
433
604
  <AlertDialog>
434
605
  <AlertDialogTrigger asChild>
435
606
  <Button
@@ -1,10 +1,12 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  const mocks = vi.hoisted(() => ({
4
+ deleteAppSecret: vi.fn(),
4
5
  writeAppSecret: vi.fn(),
5
6
  }));
6
7
 
7
8
  vi.mock("@agent-native/core/secrets", () => ({
9
+ deleteAppSecret: mocks.deleteAppSecret,
8
10
  writeAppSecret: mocks.writeAppSecret,
9
11
  }));
10
12
 
@@ -1,7 +1,11 @@
1
1
  import crypto from "node:crypto";
2
2
  import { and, desc, eq, isNull, or } from "drizzle-orm";
3
3
  import { discoverAgents } from "@agent-native/core/server/agent-discovery";
4
- import { writeAppSecret, type SecretScope } from "@agent-native/core/secrets";
4
+ import {
5
+ deleteAppSecret,
6
+ writeAppSecret,
7
+ type SecretScope,
8
+ } from "@agent-native/core/secrets";
5
9
  import {
6
10
  getOrgSetting,
7
11
  getUserSetting,
@@ -300,16 +304,63 @@ export async function createSecret(
300
304
 
301
305
  export async function updateSecret(
302
306
  secretId: string,
303
- value: string,
307
+ input:
308
+ | string
309
+ | {
310
+ credentialKey?: string;
311
+ value?: string;
312
+ name?: string;
313
+ provider?: string | null;
314
+ description?: string | null;
315
+ },
304
316
  ctx: VaultCtx = requireVaultCtx(),
305
317
  ) {
306
318
  const db = getDb();
307
319
  const existing = await getSecret(secretId, ctx);
308
320
  if (!existing) throw new Error("Secret not found");
321
+ const patch = typeof input === "string" ? { value: input } : input;
322
+ const credentialKey =
323
+ patch.credentialKey !== undefined
324
+ ? normalizeCredentialKey(patch.credentialKey)
325
+ : existing.credentialKey;
326
+ if (!credentialKey) throw new Error("Credential key is required");
327
+ const name = patch.name !== undefined ? patch.name.trim() : existing.name;
328
+ if (!name) throw new Error("Secret name is required");
329
+ const value = patch.value !== undefined ? patch.value : existing.value;
330
+ if (!value) throw new Error("Secret value is required");
331
+ const provider =
332
+ patch.provider !== undefined ? patch.provider || null : existing.provider;
333
+ const description =
334
+ patch.description !== undefined
335
+ ? patch.description || null
336
+ : existing.description;
337
+
338
+ if (credentialKey !== existing.credentialKey) {
339
+ const conflict = await db
340
+ .select({ id: schema.vaultSecrets.id })
341
+ .from(schema.vaultSecrets)
342
+ .where(
343
+ and(
344
+ eq(schema.vaultSecrets.credentialKey, credentialKey),
345
+ ctxScope(schema.vaultSecrets, ctx),
346
+ ),
347
+ )
348
+ .limit(1);
349
+ if (conflict[0] && conflict[0].id !== secretId) {
350
+ throw new Error(`Credential key "${credentialKey}" is already in use`);
351
+ }
352
+ }
309
353
 
310
354
  await db
311
355
  .update(schema.vaultSecrets)
312
- .set({ value, updatedAt: now() })
356
+ .set({
357
+ name,
358
+ credentialKey,
359
+ value,
360
+ provider,
361
+ description,
362
+ updatedAt: now(),
363
+ })
313
364
  .where(
314
365
  and(
315
366
  eq(schema.vaultSecrets.id, secretId),
@@ -317,14 +368,60 @@ export async function updateSecret(
317
368
  ),
318
369
  );
319
370
 
371
+ const auditMetadata = {
372
+ name,
373
+ previousName: name !== existing.name ? existing.name : undefined,
374
+ credentialKey,
375
+ previousCredentialKey:
376
+ credentialKey !== existing.credentialKey
377
+ ? existing.credentialKey
378
+ : undefined,
379
+ provider,
380
+ previousProvider:
381
+ provider !== existing.provider ? existing.provider : undefined,
382
+ description,
383
+ previousDescription:
384
+ description !== existing.description ? existing.description : undefined,
385
+ valueChanged: value !== existing.value ? true : undefined,
386
+ };
387
+
320
388
  await recordVaultAudit({
321
389
  action: "secret.updated",
322
390
  secretId,
323
- summary: `Updated value for secret "${existing.name}" (${existing.credentialKey})`,
391
+ summary: `Updated secret "${name}" (${credentialKey})`,
392
+ metadata: auditMetadata,
393
+ });
394
+
395
+ await recordAudit({
396
+ action: "vault.secret.updated",
397
+ targetType: "vault-secret",
398
+ targetId: secretId,
399
+ summary: `Updated vault secret "${name}" (${credentialKey})`,
400
+ metadata: auditMetadata,
324
401
  });
325
402
 
326
403
  const updated = await getSecret(secretId, ctx);
327
404
  if (updated) await syncSecretsToCredentialStore([updated], ctx);
405
+ if (updated && credentialKey !== existing.credentialKey) {
406
+ const stillUsesOldKey = await db
407
+ .select({ id: schema.vaultSecrets.id })
408
+ .from(schema.vaultSecrets)
409
+ .where(
410
+ and(
411
+ eq(schema.vaultSecrets.credentialKey, existing.credentialKey),
412
+ ctxScope(schema.vaultSecrets, ctx),
413
+ ),
414
+ )
415
+ .limit(1);
416
+ if (!stillUsesOldKey[0]) {
417
+ const target = credentialStoreScopeForVaultCtx(ctx);
418
+ await deleteAppSecret({
419
+ key: existing.credentialKey,
420
+ scope: target.scope,
421
+ scopeId: target.scopeId,
422
+ });
423
+ }
424
+ }
328
425
  return updated;
329
426
  }
330
427