import {
useActionMutation,
useActionQuery,
} from "@agent-native/core/client/hooks";
import { useOrgRole } from "@agent-native/core/client/org";
import {
IconChevronDown,
IconChevronRight,
IconEdit,
IconEye,
IconEyeOff,
IconKey,
IconPlus,
IconRefresh,
IconTrash,
IconX,
} from "@tabler/icons-react";
import { useState } from "react";
import { toast } from "sonner";
import { ActionQueryError } from "../../components/action-query-error";
import { DispatchShell } from "../../components/dispatch-shell";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "../../components/ui/alert-dialog";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import { Skeleton } from "../../components/ui/skeleton";
import { Switch } from "../../components/ui/switch";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "../../components/ui/tabs";
import { Textarea } from "../../components/ui/textarea";
const PROVIDERS = [
"google",
"slack",
"sendgrid",
"github",
"stripe",
"hubspot",
"jira",
"bigquery",
"anthropic",
"other",
];
const PROVIDER_NONE_VALUE = "__none__";
type VaultAccessMode = "all-apps" | "manual";
export function meta() {
return [{ title: "Vault — Dispatch" }];
}
function AddSecretDialog() {
const [open, setOpen] = useState(false);
const [credentialKey, setCredentialKey] = useState("");
const [name, setName] = useState("");
const [value, setValue] = useState("");
const [provider, setProvider] = useState("");
const [description, setDescription] = useState("");
const create = useActionMutation("create-vault-secret", {
onSuccess: () => {
toast.success("Secret created");
setOpen(false);
setCredentialKey("");
setName("");
setValue("");
setProvider("");
setDescription("");
},
onError: (err) => toast.error(String(err)),
});
return (
Add secret
Add vault secret
Store a credential that can be granted to workspace apps.
create.mutate({
credentialKey,
name,
value,
provider: provider || undefined,
description: description || undefined,
})
}
disabled={!credentialKey || !name || !value || create.isPending}
>
{create.isPending ? "Creating..." : "Create secret"}
);
}
function EditSecretDialog({ secret }: { secret: any }) {
const [open, setOpen] = useState(false);
const [credentialKey, setCredentialKey] = useState(
secret.credentialKey || "",
);
const [name, setName] = useState(secret.name || "");
const [value, setValue] = useState(secret.value || "");
const [provider, setProvider] = useState(secret.provider || "");
const [description, setDescription] = useState(secret.description || "");
const [showValue, setShowValue] = useState(false);
const update = useActionMutation("update-vault-secret", {
onSuccess: () => {
toast.success("Secret updated");
setOpen(false);
setShowValue(false);
},
onError: (err) => toast.error(String(err)),
});
const resetDraft = () => {
setCredentialKey(secret.credentialKey || "");
setName(secret.name || "");
setValue(secret.value || "");
setProvider(secret.provider || "");
setDescription(secret.description || "");
setShowValue(false);
};
return (
{
if (nextOpen) resetDraft();
setOpen(nextOpen);
}}
>
Edit secret
Edit vault secret
Update the stored key and metadata. Changes sync to the shared
credential store.
);
}
function GrantDialog({
secretId,
secretName,
}: {
secretId: string;
secretName: string;
}) {
const [open, setOpen] = useState(false);
const [appId, setAppId] = useState("");
const { data: catalog } = useActionQuery("list-integrations-catalog", {});
const grant = useActionMutation("create-vault-grant", {
onSuccess: () => {
toast.success(`Granted to ${appId}`);
setOpen(false);
setAppId("");
},
onError: (err) => toast.error(String(err)),
});
const apps = (catalog || []).map((a: any) => ({
id: a.appId,
name: a.appName,
}));
return (
Grant
Grant "{secretName}" to an app
Choose which app should receive this secret.
{apps.map((app: any) => (
{app.name}
))}
grant.mutate({ secretId, appId })}
disabled={!appId || grant.isPending}
>
{grant.isPending ? "Granting..." : "Grant access"}
);
}
function VaultAccessSettingsCard({ mode }: { mode: VaultAccessMode }) {
const update = useActionMutation("set-vault-access-settings", {
onSuccess: (next: any) =>
toast.success(
next?.mode === "manual"
? "Manual vault access enabled"
: "All apps can use vault keys",
),
onError: (err) => toast.error(String(err)),
});
const allApps = mode !== "manual";
return (
All apps can use vault keys
{allApps
? "Every workspace app can receive every saved key."
: "Only apps with explicit grants can receive saved keys."}
update.mutate({ mode: checked ? "all-apps" : "manual" })
}
aria-label="Allow all workspace apps to use vault keys"
/>
);
}
function SecretRow({
secret,
grants,
accessMode,
}: {
secret: any;
grants: any[];
accessMode: VaultAccessMode;
}) {
const [expanded, setExpanded] = useState(false);
const [showValue, setShowValue] = useState(false);
const deleteSecret = useActionMutation("delete-vault-secret", {
onSuccess: () => toast.success("Secret deleted"),
onError: (err) => toast.error(String(err)),
});
const revokeGrant = useActionMutation("revoke-vault-grant", {
onSuccess: () => toast.success("Grant revoked"),
onError: (err) => toast.error(String(err)),
});
const syncToApp = useActionMutation("sync-vault-to-app", {
onSuccess: (data: any) =>
toast.success(`Synced ${data.synced} key(s) to ${data.appId}`),
onError: (err) => toast.error(String(err)),
});
const activeGrants = grants.filter((g) => g.status === "active");
const allApps = accessMode !== "manual";
return (
setExpanded(!expanded)}
>
{expanded ? (
) : (
)}
{secret.name}
{secret.provider && (
{secret.provider}
)}
{secret.credentialKey}
{allApps
? "All apps"
: `${activeGrants.length} grant${activeGrants.length !== 1 ? "s" : ""}`}
{expanded && (
{secret.description && (
{secret.description}
)}
Value:
{showValue ? secret.value : `••••${secret.value.slice(-4)}`}
setShowValue(!showValue)}
className="text-muted-foreground hover:text-foreground cursor-pointer"
>
{showValue ? : }
{allApps ? "Access" : "Grants"}
{!allApps && (
)}
{allApps ? (
Available to every workspace app.
) : activeGrants.length > 0 ? (
{activeGrants.map((grant: any) => (
{grant.appId}
{grant.syncedAt
? `synced ${new Date(grant.syncedAt).toLocaleString()}`
: "not synced"}
syncToApp.mutate({ appId: grant.appId })}
disabled={syncToApp.isPending}
>
revokeGrant.mutate({ grantId: grant.id })
}
disabled={revokeGrant.isPending}
>
))}
) : (
No grants yet.
)}
Delete secret
Delete this secret?
Removing “{secret.name}” revokes all of its grants. Apps
that depended on this credential can lose access on the next
sync. This cannot be undone.
Cancel
deleteSecret.mutate({ id: secret.id })}
>
Delete secret
)}
);
}
function RequestRow({
request,
canManage,
}: {
request: any;
canManage: boolean;
}) {
const [secretValue, setSecretValue] = useState("");
const approve = useActionMutation("approve-vault-request", {
onSuccess: () => {
toast.success("Request approved");
setSecretValue("");
},
onError: (err) => toast.error(String(err)),
});
const deny = useActionMutation("deny-vault-request", {
onSuccess: () => toast.success("Request denied"),
onError: (err) => toast.error(String(err)),
});
return (
{request.credentialKey} for{" "}
{request.appId}
Requested by {request.requestedBy}
{request.reason && ` — "${request.reason}"`}
{request.status === "pending"
? "Pending"
: request.status === "approved"
? `Approved by ${request.reviewedBy}`
: `Denied by ${request.reviewedBy}`}{" "}
· {new Date(request.createdAt).toLocaleString()}
{request.status === "pending" && (
Pending
)}
{request.status === "approved" && (
Approved
)}
{request.status === "denied" && (
Denied
)}
{request.status === "pending" && canManage ? (
) : request.status === "pending" ? (
Waiting for a workspace owner or admin to review this request.
) : null}
);
}
export default function VaultRoute() {
const { org, role, isLoading: orgLoading, error: orgError } = useOrgRole();
const accessReady = !orgLoading && !orgError && !!org;
const canManageVault =
accessReady && (!org.orgId || role === "owner" || role === "admin");
const secretsQuery = useActionQuery(
"list-vault-secrets",
{},
{ enabled: canManageVault },
);
const grantsQuery = useActionQuery(
"list-vault-grants",
{},
{ enabled: canManageVault },
);
const requestsQuery = useActionQuery(
"list-vault-requests",
{},
{ enabled: accessReady },
);
const auditQuery = useActionQuery(
"list-vault-audit",
{ limit: 20 },
{ enabled: canManageVault },
);
const accessQuery = useActionQuery(
"get-vault-access-settings",
{},
{ enabled: accessReady },
);
const { data: secrets, isLoading: secretsLoading } = secretsQuery;
const { data: grants } = grantsQuery;
const { data: requests } = requestsQuery;
const { data: audit } = auditQuery;
const { data: accessSettings } = accessQuery;
const accessMode: VaultAccessMode =
(accessSettings as any)?.mode === "manual" ? "manual" : "all-apps";
const grantsBySecret = (grants || []).reduce(
(acc: Record, g: any) => {
if (!acc[g.secretId]) acc[g.secretId] = [];
acc[g.secretId].push(g);
return acc;
},
{} as Record,
);
const pendingRequests = (requests || []).filter(
(r: any) => r.status === "pending",
);
return (
Secrets {(secrets?.length || 0) > 0 && `(${secrets?.length})`}
Requests{" "}
{pendingRequests.length > 0 && (
{pendingRequests.length}
)}
{canManageVault ? (
Audit
) : null}
{!accessReady ? (
{Array.from({ length: 3 }).map((_, index) => (
))}
) : !canManageVault ? (
Vault management is restricted
Workspace owners and admins manage shared secret values. Use a
request from the app that needs a key, and an admin can review
it from the Requests tab.
) : secretsQuery.isError ||
grantsQuery.isError ||
accessQuery.isError ? (
{
void secretsQuery.refetch();
void grantsQuery.refetch();
void accessQuery.refetch();
}}
/>
) : null}
{accessReady && canManageVault ? (
<>
{secretsLoading ? (
) : (
{`${secrets?.length || 0} secret${(secrets?.length || 0) !== 1 ? "s" : ""}`}
)}
{!secretsQuery.isError &&
secretsLoading &&
(secrets ?? []).length === 0
? Array.from({ length: 3 }).map((_, index) => (
))
: (secrets || []).map((secret: any) => (
))}
{!secretsQuery.isError &&
!secretsLoading &&
(secrets?.length || 0) === 0 && (
No secrets yet
Add your first secret to start sharing credentials across
workspace apps.
)}
>
) : null}
{requestsQuery.isError ? (
void requestsQuery.refetch()}
/>
) : null}
{(requests || []).map((request: any) => (
))}
{!requestsQuery.isError && (requests?.length || 0) === 0 && (
No secret requests yet.
)}
{canManageVault ? (
{auditQuery.isError ? (
void auditQuery.refetch()}
/>
) : null}
{(audit || []).map((event: any) => (
{event.summary}
{event.actor} · {new Date(event.createdAt).toLocaleString()}
))}
{!auditQuery.isError && (audit?.length || 0) === 0 && (
No vault activity yet.
)}
) : null}
);
}