@agent-native/dispatch 0.8.1 → 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 (40) hide show
  1. package/README.md +1 -11
  2. package/dist/actions/update-vault-secret.d.ts.map +1 -1
  3. package/dist/actions/update-vault-secret.js +34 -6
  4. package/dist/actions/update-vault-secret.js.map +1 -1
  5. package/dist/routes/index.d.ts.map +1 -1
  6. package/dist/routes/index.js +1 -0
  7. package/dist/routes/index.js.map +1 -1
  8. package/dist/routes/pages/extensions.$id.d.ts +3 -0
  9. package/dist/routes/pages/extensions.$id.d.ts.map +1 -1
  10. package/dist/routes/pages/extensions.$id.js +5 -2
  11. package/dist/routes/pages/extensions.$id.js.map +1 -1
  12. package/dist/routes/pages/extensions._index.d.ts +3 -0
  13. package/dist/routes/pages/extensions._index.d.ts.map +1 -1
  14. package/dist/routes/pages/extensions._index.js +5 -2
  15. package/dist/routes/pages/extensions._index.js.map +1 -1
  16. package/dist/routes/pages/tools.$id.d.ts +3 -0
  17. package/dist/routes/pages/tools.$id.d.ts.map +1 -1
  18. package/dist/routes/pages/tools.$id.js +3 -0
  19. package/dist/routes/pages/tools.$id.js.map +1 -1
  20. package/dist/routes/pages/tools._index.d.ts +3 -0
  21. package/dist/routes/pages/tools._index.d.ts.map +1 -1
  22. package/dist/routes/pages/tools._index.js +3 -0
  23. package/dist/routes/pages/tools._index.js.map +1 -1
  24. package/dist/routes/pages/vault.d.ts.map +1 -1
  25. package/dist/routes/pages/vault.js +46 -2
  26. package/dist/routes/pages/vault.js.map +1 -1
  27. package/dist/server/lib/vault-store.d.ts +7 -1
  28. package/dist/server/lib/vault-store.d.ts.map +1 -1
  29. package/dist/server/lib/vault-store.js +85 -7
  30. package/dist/server/lib/vault-store.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/actions/update-vault-secret.ts +39 -6
  33. package/src/routes/index.ts +1 -0
  34. package/src/routes/pages/extensions.$id.tsx +6 -2
  35. package/src/routes/pages/extensions._index.tsx +6 -2
  36. package/src/routes/pages/tools.$id.tsx +4 -0
  37. package/src/routes/pages/tools._index.tsx +4 -0
  38. package/src/routes/pages/vault.tsx +172 -1
  39. package/src/server/lib/vault-store.spec.ts +2 -0
  40. package/src/server/lib/vault-store.ts +110 -7
@@ -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,
@@ -257,7 +261,9 @@ export async function createSecret(
257
261
  summary: `Updated vault secret "${input.name}" (${credentialKey})`,
258
262
  });
259
263
 
260
- return getSecret(existing[0].id, ctx);
264
+ const updated = await getSecret(existing[0].id, ctx);
265
+ if (updated) await syncSecretsToCredentialStore([updated], ctx);
266
+ return updated;
261
267
  }
262
268
 
263
269
  const secretId = id();
@@ -291,21 +297,70 @@ export async function createSecret(
291
297
  summary: `Created vault secret "${input.name}" (${credentialKey})`,
292
298
  });
293
299
 
294
- return getSecret(secretId, ctx);
300
+ const created = await getSecret(secretId, ctx);
301
+ if (created) await syncSecretsToCredentialStore([created], ctx);
302
+ return created;
295
303
  }
296
304
 
297
305
  export async function updateSecret(
298
306
  secretId: string,
299
- 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
+ },
300
316
  ctx: VaultCtx = requireVaultCtx(),
301
317
  ) {
302
318
  const db = getDb();
303
319
  const existing = await getSecret(secretId, ctx);
304
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
+ }
305
353
 
306
354
  await db
307
355
  .update(schema.vaultSecrets)
308
- .set({ value, updatedAt: now() })
356
+ .set({
357
+ name,
358
+ credentialKey,
359
+ value,
360
+ provider,
361
+ description,
362
+ updatedAt: now(),
363
+ })
309
364
  .where(
310
365
  and(
311
366
  eq(schema.vaultSecrets.id, secretId),
@@ -313,13 +368,61 @@ export async function updateSecret(
313
368
  ),
314
369
  );
315
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
+
316
388
  await recordVaultAudit({
317
389
  action: "secret.updated",
318
390
  secretId,
319
- summary: `Updated value for secret "${existing.name}" (${existing.credentialKey})`,
391
+ summary: `Updated secret "${name}" (${credentialKey})`,
392
+ metadata: auditMetadata,
320
393
  });
321
394
 
322
- return getSecret(secretId, ctx);
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,
401
+ });
402
+
403
+ const updated = await getSecret(secretId, ctx);
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
+ }
425
+ return updated;
323
426
  }
324
427
 
325
428
  export async function deleteSecret(