Fix secrets create provider selection

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-02 00:36:19 +00:00
1 parent 0ac5ac5467
commit e38f34ef69
2 files changed
+186 -30

No files matched your search

+90 -11
View File
@@ -1,8 +1,9 @@
// @vitest-environment jsdom
import { describe, expect, it } from "vitest";
import type { SecretProviderDescriptor } from "@paperclipai/shared";
import type { CompanySecretProviderConfig, SecretProviderDescriptor } from "@paperclipai/shared";
import {
findCreateProviderReplacement,
getAwsManagedPathPreview,
getCreateProviderBlockReason,
getDefaultProviderConfigId,
@@ -19,6 +20,37 @@ const awsProvider: SecretProviderDescriptor = {
configured: true,
};
const localProvider: SecretProviderDescriptor = {
id: "local_encrypted",
label: "Local encrypted (default)",
requiresExternalRef: false,
supportsManagedValues: true,
supportsExternalReferences: false,
configured: true,
};
function providerConfig(
overrides: Partial<CompanySecretProviderConfig> & Pick<CompanySecretProviderConfig, "id" | "provider">,
): CompanySecretProviderConfig {
return {
companyId: "company-1",
displayName: overrides.id,
status: "ready",
isDefault: false,
config: {},
healthStatus: null,
healthCheckedAt: null,
healthMessage: null,
healthDetails: null,
disabledAt: null,
createdByAgentId: null,
createdByUserId: null,
createdAt: new Date("2026-01-01T00:00:00Z"),
updatedAt: new Date("2026-01-01T00:00:00Z"),
...overrides,
};
}
describe("Secrets page provider helpers", () => {
it("previews the derived AWS managed path from provider health details", () => {
const health: SecretProviderHealthResponse = {
@@ -52,7 +84,9 @@ describe("Secrets page provider helpers", () => {
"managed",
null,
),
).toBe("AWS Secrets Manager is not configured in this deployment.");
).toBe(
"Deployment default AWS Secrets Manager is not configured. Select a ready provider vault or configure the deployment default.",
);
});
it("uses provider health copy when an unconfigured provider reports missing bootstrap inputs", () => {
@@ -74,27 +108,72 @@ describe("Secrets page provider helpers", () => {
health,
),
).toBe(
"AWS Secrets Manager is not configured in this deployment. AWS Secrets Manager provider is not ready: missing PAPERCLIP_SECRETS_AWS_DEPLOYMENT_ID.",
"Deployment default AWS Secrets Manager is not configured. Select a ready provider vault or configure the deployment default. AWS Secrets Manager provider is not ready: missing PAPERCLIP_SECRETS_AWS_DEPLOYMENT_ID.",
);
});
it("allows an unconfigured AWS deployment default when a ready AWS provider vault is selected", () => {
expect(
getCreateProviderBlockReason(
{ ...awsProvider, configured: false },
"external",
null,
providerConfig({
id: "aws-prod",
provider: "aws_secrets_manager",
displayName: "AWS prod",
status: "ready",
}),
),
).toBeNull();
});
it("names the selected provider vault block before deployment-default AWS config", () => {
expect(
getCreateProviderBlockReason(
{ ...awsProvider, configured: false },
"external",
null,
providerConfig({
id: "aws-disabled",
provider: "aws_secrets_manager",
displayName: "AWS disabled",
status: "disabled",
}),
),
).toBe("This provider vault is disabled.");
});
it("blocks provider modes the backend does not support", () => {
expect(
getCreateProviderBlockReason(
{
id: "local_encrypted",
label: "Local encrypted (default)",
requiresExternalRef: false,
supportsManagedValues: true,
supportsExternalReferences: false,
configured: true,
},
localProvider,
"external",
null,
),
).toBe("Local encrypted (default) does not support linked external references.");
});
it("switching to external mode prefers AWS instead of staying on local encrypted when a ready AWS vault exists", () => {
expect(
findCreateProviderReplacement({
providers: [localProvider, { ...awsProvider, configured: false }],
providerConfigs: [
providerConfig({
id: "aws-prod",
provider: "aws_secrets_manager",
displayName: "AWS prod",
status: "ready",
isDefault: true,
}),
],
currentProvider: "local_encrypted",
mode: "external",
health: null,
})?.id,
).toBe("aws_secrets_manager");
});
it("chooses the ready default provider vault for a provider", () => {
expect(
getDefaultProviderConfigId(
+96 -19
View File
@@ -295,6 +295,7 @@ export function getCreateProviderBlockReason(
provider: SecretProviderDescriptor | null | undefined,
mode: CreateMode,
health: SecretProviderHealthResponse | null,
providerConfig?: CompanySecretProviderConfig | null,
) {
if (!provider) return "Select a provider.";
if (mode === "managed" && provider.supportsManagedValues === false) {
@@ -303,11 +304,20 @@ export function getCreateProviderBlockReason(
if (mode === "external" && provider.supportsExternalReferences === false) {
return `${provider.label} does not support linked external references.`;
}
const selectedProviderConfigBlockReason = providerConfig?.provider === provider.id
? getProviderConfigBlockReason(providerConfig)
: null;
const selectedProviderConfigReady =
providerConfig?.provider === provider.id && !selectedProviderConfigBlockReason;
if (provider.configured === false) {
if (selectedProviderConfigReady) return null;
if (selectedProviderConfigBlockReason) return selectedProviderConfigBlockReason;
const healthEntry = healthEntryForProvider(health, provider.id);
const deploymentMessage = `Deployment default ${provider.label} is not configured.`;
const nextStep = " Select a ready provider vault or configure the deployment default.";
return healthEntry?.message
? `${provider.label} is not configured in this deployment. ${healthEntry.message}`
: `${provider.label} is not configured in this deployment.`;
? `${deploymentMessage}${nextStep} ${healthEntry.message}`
: `${deploymentMessage}${nextStep}`;
}
const healthEntry = healthEntryForProvider(health, provider.id);
if (healthEntry?.status === "error") {
@@ -319,8 +329,16 @@ export function getCreateProviderBlockReason(
function providerHealthText(
provider: SecretProviderDescriptor | null | undefined,
health: SecretProviderHealthResponse | null,
providerConfig?: CompanySecretProviderConfig | null,
) {
if (!provider) return null;
if (
provider.configured === false &&
providerConfig?.provider === provider.id &&
!getProviderConfigBlockReason(providerConfig)
) {
return `Using selected provider vault. Deployment default ${provider.label} is not configured.`;
}
const entry = healthEntryForProvider(health, provider.id);
if (!entry) return null;
const warnings = entry.warnings?.join(" ");
@@ -344,20 +362,57 @@ export function getProviderConfigBlockReason(
return null;
}
export function getDefaultProviderConfigId(
export function getSelectableProviderConfig(
configs: CompanySecretProviderConfig[],
provider: SecretProvider,
) {
const providerConfigs = configs.filter((config) => config.provider === provider);
const selectable = providerConfigs.filter((config) => !getProviderConfigBlockReason(config));
return (
selectable.find((config) => config.isDefault)?.id ??
selectable[0]?.id ??
providerConfigs.find((config) => config.isDefault && !getProviderConfigBlockReason(config)) ??
providerConfigs.find((config) => !getProviderConfigBlockReason(config)) ??
null
);
}
export function getDefaultProviderConfigId(
configs: CompanySecretProviderConfig[],
provider: SecretProvider,
) {
const selected = getSelectableProviderConfig(configs, provider);
const providerConfigs = configs.filter((config) => config.provider === provider);
return (
selected?.id ??
providerConfigs.find((config) => config.isDefault)?.id ??
""
);
}
export function findCreateProviderReplacement({
providers,
providerConfigs,
currentProvider,
mode,
health,
}: {
providers: SecretProviderDescriptor[];
providerConfigs: CompanySecretProviderConfig[];
currentProvider: SecretProvider;
mode: CreateMode;
health: SecretProviderHealthResponse | null;
}) {
return (
providers.find((provider) => {
const selectedConfig =
provider.id === currentProvider
? providerConfigs.find(
(config) => config.provider === provider.id && !getProviderConfigBlockReason(config),
) ?? null
: getSelectableProviderConfig(providerConfigs, provider.id);
return !getCreateProviderBlockReason(provider, mode, health, selectedConfig);
}) ?? null
);
}
function providerVaultLabel(configs: CompanySecretProviderConfig[], id: string | null | undefined) {
if (!id) return "Deployment default";
return configs.find((config) => config.id === id)?.displayName ?? "Unknown vault";
@@ -573,11 +628,13 @@ export function Secrets() {
selectedCreateProvider,
createMode,
providerHealthQuery.data ?? null,
selectedCreateProviderConfig,
) ?? getProviderConfigBlockReason(selectedCreateProviderConfig);
const rotateProviderBlockReason = getProviderConfigBlockReason(selectedRotateProviderConfig);
const createProviderHealthText = providerHealthText(
selectedCreateProvider,
providerHealthQuery.data ?? null,
selectedCreateProviderConfig,
);
const awsManagedPathPreview = getAwsManagedPathPreview({
provider: selectedCreateProvider,
@@ -1006,12 +1063,16 @@ export function Secrets() {
providers.find((provider) => provider.id === createForm.provider) ?? null,
createMode,
providerHealthQuery.data ?? null,
providerConfigs.find((config) => config.id === createForm.providerConfigId) ?? null,
);
if (!currentBlockReason) return;
const replacement = providers.find(
(provider) =>
!getCreateProviderBlockReason(provider, createMode, providerHealthQuery.data ?? null),
);
const replacement = findCreateProviderReplacement({
providers,
providerConfigs,
currentProvider: createForm.provider,
mode: createMode,
health: providerHealthQuery.data ?? null,
});
if (replacement && replacement.id !== createForm.provider) {
setCreateForm((current) => ({
...current,
@@ -1635,7 +1696,7 @@ export function Secrets() {
)}
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent className="sm:max-w-lg">
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto p-4 sm:max-w-lg sm:p-6">
<DialogHeader>
<DialogTitle>{editingDefinition ? "Edit user-provided secret" : "Create secret"}</DialogTitle>
<DialogDescription>
@@ -1643,7 +1704,7 @@ export function Secrets() {
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<label className="text-xs font-medium" htmlFor="new-secret-name">Name</label>
<Input
@@ -1721,7 +1782,7 @@ export function Secrets() {
}));
}}
>
<TabsList className="grid w-full grid-cols-2">
<TabsList className="grid h-auto w-full grid-cols-2">
<TabsTrigger value="company">Company</TabsTrigger>
<TabsTrigger value="user">Each user</TabsTrigger>
</TabsList>
@@ -1735,9 +1796,19 @@ export function Secrets() {
{secretValueProvider === "company" ? (
<>
<Tabs value={createMode} onValueChange={(value) => setCreateMode(value as CreateMode)}>
<TabsList className="w-full grid grid-cols-2">
<TabsTrigger value="managed">Managed value</TabsTrigger>
<TabsTrigger value="external">External reference</TabsTrigger>
<TabsList className="grid h-auto w-full grid-cols-2">
<TabsTrigger
value="managed"
className="min-h-9 whitespace-normal px-1.5 text-center text-xs leading-tight sm:text-sm"
>
Managed value
</TabsTrigger>
<TabsTrigger
value="external"
className="min-h-9 whitespace-normal px-1.5 text-center text-xs leading-tight sm:text-sm"
>
External reference
</TabsTrigger>
</TabsList>
</Tabs>
<div>
@@ -1762,12 +1833,18 @@ export function Secrets() {
key={provider.id}
value={provider.id}
disabled={Boolean(
getCreateProviderBlockReason(provider, createMode, providerHealthQuery.data ?? null),
getCreateProviderBlockReason(
provider,
createMode,
providerHealthQuery.data ?? null,
getSelectableProviderConfig(providerConfigs, provider.id),
),
)}
>
{provider.label}
{provider.configured === false
? " (not configured)"
{provider.configured === false &&
!getSelectableProviderConfig(providerConfigs, provider.id)
? " (deployment default missing)"
: provider.requiresExternalRef
? " (external only)"
: ""}