@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.
- package/README.md +1 -11
- 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 +85 -7
- 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 +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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
|
391
|
+
summary: `Updated secret "${name}" (${credentialKey})`,
|
|
392
|
+
metadata: auditMetadata,
|
|
320
393
|
});
|
|
321
394
|
|
|
322
|
-
|
|
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(
|