@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.
- package/dist/actions/update-vault-secret.d.ts.map +1 -1
- package/dist/actions/update-vault-secret.js +34 -6
- package/dist/actions/update-vault-secret.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/extensions.$id.d.ts +3 -0
- package/dist/routes/pages/extensions.$id.d.ts.map +1 -1
- package/dist/routes/pages/extensions.$id.js +5 -2
- package/dist/routes/pages/extensions.$id.js.map +1 -1
- package/dist/routes/pages/extensions._index.d.ts +3 -0
- package/dist/routes/pages/extensions._index.d.ts.map +1 -1
- package/dist/routes/pages/extensions._index.js +5 -2
- package/dist/routes/pages/extensions._index.js.map +1 -1
- package/dist/routes/pages/tools.$id.d.ts +3 -0
- package/dist/routes/pages/tools.$id.d.ts.map +1 -1
- package/dist/routes/pages/tools.$id.js +3 -0
- package/dist/routes/pages/tools.$id.js.map +1 -1
- package/dist/routes/pages/tools._index.d.ts +3 -0
- package/dist/routes/pages/tools._index.d.ts.map +1 -1
- package/dist/routes/pages/tools._index.js +3 -0
- package/dist/routes/pages/tools._index.js.map +1 -1
- package/dist/routes/pages/vault.d.ts.map +1 -1
- package/dist/routes/pages/vault.js +46 -2
- package/dist/routes/pages/vault.js.map +1 -1
- package/dist/server/lib/vault-store.d.ts +7 -1
- package/dist/server/lib/vault-store.d.ts.map +1 -1
- package/dist/server/lib/vault-store.js +73 -4
- package/dist/server/lib/vault-store.js.map +1 -1
- package/package.json +1 -1
- package/src/actions/update-vault-secret.ts +39 -6
- package/src/routes/index.ts +1 -0
- package/src/routes/pages/extensions.$id.tsx +6 -2
- package/src/routes/pages/extensions._index.tsx +6 -2
- package/src/routes/pages/tools.$id.tsx +4 -0
- package/src/routes/pages/tools._index.tsx +4 -0
- package/src/routes/pages/vault.tsx +172 -1
- package/src/server/lib/vault-store.spec.ts +2 -0
- 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 {
|
|
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
|
-
|
|
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({
|
|
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
|
|
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
|
|