mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 10:13:11 +02:00
Implement provider vault data model and API
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
b84482de60
commit
ef1f56ef4c
16 files changed
+1026
-15
No files matched your search
@@ -11,6 +11,7 @@ export const API = {
|
||||
goals: `${API_PREFIX}/goals`,
|
||||
approvals: `${API_PREFIX}/approvals`,
|
||||
secrets: `${API_PREFIX}/secrets`,
|
||||
secretProviderConfigs: `${API_PREFIX}/secret-provider-configs`,
|
||||
costs: `${API_PREFIX}/costs`,
|
||||
activity: `${API_PREFIX}/activity`,
|
||||
dashboard: `${API_PREFIX}/dashboard`,
|
||||
|
||||
@@ -395,6 +395,24 @@ export const SECRET_PROVIDERS = [
|
||||
] as const;
|
||||
export type SecretProvider = (typeof SECRET_PROVIDERS)[number];
|
||||
|
||||
export const SECRET_PROVIDER_CONFIG_STATUSES = [
|
||||
"ready",
|
||||
"warning",
|
||||
"coming_soon",
|
||||
"disabled",
|
||||
] as const;
|
||||
export type SecretProviderConfigStatus = (typeof SECRET_PROVIDER_CONFIG_STATUSES)[number];
|
||||
|
||||
export const SECRET_PROVIDER_CONFIG_HEALTH_STATUSES = [
|
||||
"ready",
|
||||
"warning",
|
||||
"error",
|
||||
"coming_soon",
|
||||
"disabled",
|
||||
] as const;
|
||||
export type SecretProviderConfigHealthStatus =
|
||||
(typeof SECRET_PROVIDER_CONFIG_HEALTH_STATUSES)[number];
|
||||
|
||||
export const SECRET_STATUSES = ["active", "disabled", "archived", "deleted"] as const;
|
||||
export type SecretStatus = (typeof SECRET_STATUSES)[number];
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ export {
|
||||
APPROVAL_TYPES,
|
||||
APPROVAL_STATUSES,
|
||||
SECRET_PROVIDERS,
|
||||
SECRET_PROVIDER_CONFIG_STATUSES,
|
||||
SECRET_PROVIDER_CONFIG_HEALTH_STATUSES,
|
||||
STORAGE_PROVIDERS,
|
||||
BILLING_TYPES,
|
||||
FINANCE_EVENT_KINDS,
|
||||
@@ -182,6 +184,8 @@ export {
|
||||
type ApprovalType,
|
||||
type ApprovalStatus,
|
||||
type SecretProvider,
|
||||
type SecretProviderConfigStatus,
|
||||
type SecretProviderConfigHealthStatus,
|
||||
type StorageProvider,
|
||||
type BillingType,
|
||||
type FinanceEventKind,
|
||||
@@ -530,6 +534,10 @@ export type {
|
||||
EnvBinding,
|
||||
AgentEnvConfig,
|
||||
CompanySecret,
|
||||
CompanySecretProviderConfig,
|
||||
SecretProviderConfigPayload,
|
||||
SecretProviderConfigHealthDetails,
|
||||
SecretProviderConfigHealthResponse,
|
||||
CompanySecretBinding,
|
||||
CompanySecretVersion,
|
||||
SecretAccessEvent,
|
||||
@@ -835,6 +843,13 @@ export {
|
||||
envBindingSchema,
|
||||
envConfigSchema,
|
||||
createSecretSchema,
|
||||
createSecretProviderConfigSchema,
|
||||
updateSecretProviderConfigSchema,
|
||||
localEncryptedProviderConfigSchema,
|
||||
awsSecretsManagerProviderConfigSchema,
|
||||
gcpSecretManagerProviderConfigSchema,
|
||||
vaultProviderConfigSchema,
|
||||
secretProviderConfigPayloadSchema,
|
||||
createSecretBindingSchema,
|
||||
rotateSecretSchema,
|
||||
secretBindingTargetSchema,
|
||||
@@ -851,6 +866,8 @@ export {
|
||||
routineRevisionSnapshotV1Schema,
|
||||
routineRevisionSnapshotSchema,
|
||||
type CreateSecret,
|
||||
type CreateSecretProviderConfig,
|
||||
type UpdateSecretProviderConfig,
|
||||
type RotateSecret,
|
||||
type UpdateSecret,
|
||||
type CreateRoutine,
|
||||
|
||||
@@ -244,6 +244,10 @@ export type {
|
||||
EnvBinding,
|
||||
AgentEnvConfig,
|
||||
CompanySecret,
|
||||
CompanySecretProviderConfig,
|
||||
SecretProviderConfigPayload,
|
||||
SecretProviderConfigHealthDetails,
|
||||
SecretProviderConfigHealthResponse,
|
||||
CompanySecretBinding,
|
||||
CompanySecretVersion,
|
||||
SecretAccessEvent,
|
||||
|
||||
@@ -3,11 +3,22 @@ import type {
|
||||
SecretBindingTargetType,
|
||||
SecretManagedMode,
|
||||
SecretProvider,
|
||||
SecretProviderConfigHealthStatus,
|
||||
SecretProviderConfigStatus,
|
||||
SecretStatus,
|
||||
SecretVersionStatus,
|
||||
} from "../constants.js";
|
||||
|
||||
export type { SecretAccessOutcome, SecretBindingTargetType, SecretManagedMode, SecretProvider, SecretStatus, SecretVersionStatus };
|
||||
export type {
|
||||
SecretAccessOutcome,
|
||||
SecretBindingTargetType,
|
||||
SecretManagedMode,
|
||||
SecretProvider,
|
||||
SecretProviderConfigHealthStatus,
|
||||
SecretProviderConfigStatus,
|
||||
SecretStatus,
|
||||
SecretVersionStatus,
|
||||
};
|
||||
|
||||
export type SecretVersionSelector = number | "latest";
|
||||
|
||||
@@ -58,6 +69,74 @@ export interface SecretProviderDescriptor {
|
||||
configured?: boolean;
|
||||
}
|
||||
|
||||
export interface LocalEncryptedProviderConfig {
|
||||
backupReminderAcknowledged?: boolean;
|
||||
}
|
||||
|
||||
export interface AwsSecretsManagerProviderConfig {
|
||||
region: string;
|
||||
namespace?: string | null;
|
||||
secretNamePrefix?: string | null;
|
||||
kmsKeyId?: string | null;
|
||||
ownerTag?: string | null;
|
||||
environmentTag?: string | null;
|
||||
}
|
||||
|
||||
export interface GcpSecretManagerProviderConfig {
|
||||
projectId?: string | null;
|
||||
location?: string | null;
|
||||
namespace?: string | null;
|
||||
secretNamePrefix?: string | null;
|
||||
}
|
||||
|
||||
export interface VaultProviderConfig {
|
||||
address?: string | null;
|
||||
namespace?: string | null;
|
||||
mountPath?: string | null;
|
||||
secretPathPrefix?: string | null;
|
||||
}
|
||||
|
||||
export type SecretProviderConfigPayload =
|
||||
| LocalEncryptedProviderConfig
|
||||
| AwsSecretsManagerProviderConfig
|
||||
| GcpSecretManagerProviderConfig
|
||||
| VaultProviderConfig;
|
||||
|
||||
export interface SecretProviderConfigHealthDetails {
|
||||
code: string;
|
||||
message: string;
|
||||
missingFields?: string[];
|
||||
guidance?: string[];
|
||||
}
|
||||
|
||||
export interface CompanySecretProviderConfig {
|
||||
id: string;
|
||||
companyId: string;
|
||||
provider: SecretProvider;
|
||||
displayName: string;
|
||||
status: SecretProviderConfigStatus;
|
||||
isDefault: boolean;
|
||||
config: SecretProviderConfigPayload;
|
||||
healthStatus: SecretProviderConfigHealthStatus | null;
|
||||
healthCheckedAt: Date | null;
|
||||
healthMessage: string | null;
|
||||
healthDetails: SecretProviderConfigHealthDetails | null;
|
||||
disabledAt: Date | null;
|
||||
createdByAgentId: string | null;
|
||||
createdByUserId: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface SecretProviderConfigHealthResponse {
|
||||
configId: string;
|
||||
provider: SecretProvider;
|
||||
status: SecretProviderConfigHealthStatus;
|
||||
message: string;
|
||||
details: SecretProviderConfigHealthDetails;
|
||||
checkedAt: Date;
|
||||
}
|
||||
|
||||
export interface CompanySecretVersion {
|
||||
id: string;
|
||||
secretId: string;
|
||||
|
||||
@@ -282,12 +282,21 @@ export {
|
||||
envBindingSchema,
|
||||
envConfigSchema,
|
||||
createSecretSchema,
|
||||
createSecretProviderConfigSchema,
|
||||
updateSecretProviderConfigSchema,
|
||||
localEncryptedProviderConfigSchema,
|
||||
awsSecretsManagerProviderConfigSchema,
|
||||
gcpSecretManagerProviderConfigSchema,
|
||||
vaultProviderConfigSchema,
|
||||
secretProviderConfigPayloadSchema,
|
||||
createSecretBindingSchema,
|
||||
rotateSecretSchema,
|
||||
secretBindingTargetSchema,
|
||||
updateSecretSchema,
|
||||
type CreateSecretBinding,
|
||||
type CreateSecret,
|
||||
type CreateSecretProviderConfig,
|
||||
type UpdateSecretProviderConfig,
|
||||
type RotateSecret,
|
||||
type UpdateSecret,
|
||||
} from "./secret.js";
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
import {
|
||||
SECRET_BINDING_TARGET_TYPES,
|
||||
SECRET_MANAGED_MODES,
|
||||
SECRET_PROVIDER_CONFIG_STATUSES,
|
||||
SECRET_PROVIDERS,
|
||||
SECRET_STATUSES,
|
||||
} from "../constants.js";
|
||||
@@ -30,6 +31,7 @@ export const createSecretSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
key: z.string().min(1).regex(/^[a-zA-Z0-9_.-]+$/).optional(),
|
||||
provider: z.enum(SECRET_PROVIDERS).optional(),
|
||||
providerConfigId: z.string().uuid().optional().nullable(),
|
||||
managedMode: z.enum(SECRET_MANAGED_MODES).optional(),
|
||||
value: z.string().min(1).optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
@@ -77,6 +79,7 @@ export const updateSecretSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
key: z.string().min(1).regex(/^[a-zA-Z0-9_.-]+$/).optional(),
|
||||
status: z.enum(SECRET_STATUSES).optional(),
|
||||
providerConfigId: z.string().uuid().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
externalRef: z.string().optional().nullable(),
|
||||
providerMetadata: z.record(z.unknown()).optional().nullable(),
|
||||
@@ -98,3 +101,114 @@ export const createSecretBindingSchema = secretBindingTargetSchema.extend({
|
||||
});
|
||||
|
||||
export type CreateSecretBinding = z.infer<typeof createSecretBindingSchema>;
|
||||
|
||||
const safeShortText = z.string().trim().min(1).max(160);
|
||||
const optionalSafeShortText = safeShortText.optional().nullable();
|
||||
|
||||
const deniedProviderConfigKeyPattern =
|
||||
/^(access[-_]?key([-_]?id)?|secret[-_]?access[-_]?key|secret[-_]?key|token|password|passwd|credential|credentials|private[-_]?key|pem|jwt|session[-_]?token|service[-_]?account([-_]?json)?|client[-_]?secret|secret[-_]?id|unseal[-_]?key|recovery[-_]?key|key[-_]?file([-_]?path)?|token[-_]?file([-_]?path)?)$/i;
|
||||
|
||||
function rejectSensitiveProviderConfigKeys(value: unknown, ctx: z.RefinementCtx) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!deniedProviderConfigKeyPattern.test(key)) continue;
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["config", key],
|
||||
message: `Provider vault config cannot persist sensitive field: ${key}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const localEncryptedProviderConfigSchema = z.object({
|
||||
backupReminderAcknowledged: z.boolean().optional(),
|
||||
}).strict();
|
||||
|
||||
export const awsSecretsManagerProviderConfigSchema = z.object({
|
||||
region: z.string().trim().regex(/^[a-z]{2}(?:-gov)?-[a-z]+-\d+$/, "Invalid AWS region"),
|
||||
namespace: optionalSafeShortText,
|
||||
secretNamePrefix: optionalSafeShortText,
|
||||
kmsKeyId: z.string().trim().min(1).max(512).optional().nullable(),
|
||||
ownerTag: optionalSafeShortText,
|
||||
environmentTag: optionalSafeShortText,
|
||||
}).strict();
|
||||
|
||||
export const gcpSecretManagerProviderConfigSchema = z.object({
|
||||
projectId: z.string().trim().min(1).max(128).regex(/^[a-z][a-z0-9-]{4,127}$/).optional().nullable(),
|
||||
location: optionalSafeShortText,
|
||||
namespace: optionalSafeShortText,
|
||||
secretNamePrefix: optionalSafeShortText,
|
||||
}).strict();
|
||||
|
||||
export const vaultProviderConfigSchema = z.object({
|
||||
address: z.string().trim().url().optional().nullable(),
|
||||
namespace: optionalSafeShortText,
|
||||
mountPath: optionalSafeShortText,
|
||||
secretPathPrefix: optionalSafeShortText,
|
||||
}).strict();
|
||||
|
||||
export const secretProviderConfigPayloadSchema = z.discriminatedUnion("provider", [
|
||||
z.object({ provider: z.literal("local_encrypted"), config: localEncryptedProviderConfigSchema }),
|
||||
z.object({ provider: z.literal("aws_secrets_manager"), config: awsSecretsManagerProviderConfigSchema }),
|
||||
z.object({ provider: z.literal("gcp_secret_manager"), config: gcpSecretManagerProviderConfigSchema }),
|
||||
z.object({ provider: z.literal("vault"), config: vaultProviderConfigSchema }),
|
||||
]);
|
||||
|
||||
export const createSecretProviderConfigSchema = z.object({
|
||||
provider: z.enum(SECRET_PROVIDERS),
|
||||
displayName: z.string().trim().min(1).max(120),
|
||||
status: z.enum(SECRET_PROVIDER_CONFIG_STATUSES).optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
config: z.record(z.unknown()).default({}),
|
||||
}).superRefine((value, ctx) => {
|
||||
rejectSensitiveProviderConfigKeys(value.config, ctx);
|
||||
const parsed = secretProviderConfigPayloadSchema.safeParse({
|
||||
provider: value.provider,
|
||||
config: value.config,
|
||||
});
|
||||
if (!parsed.success) {
|
||||
for (const issue of parsed.error.issues) {
|
||||
ctx.addIssue({
|
||||
...issue,
|
||||
path: issue.path[0] === "config" ? issue.path : ["config", ...issue.path],
|
||||
});
|
||||
}
|
||||
}
|
||||
const status = value.status ?? (["gcp_secret_manager", "vault"].includes(value.provider) ? "coming_soon" : "ready");
|
||||
if ((value.provider === "gcp_secret_manager" || value.provider === "vault") && status !== "coming_soon" && status !== "disabled") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["status"],
|
||||
message: `${value.provider} provider vaults are locked while coming soon`,
|
||||
});
|
||||
}
|
||||
if ((status === "coming_soon" || status === "disabled") && value.isDefault) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["isDefault"],
|
||||
message: "Only ready or warning provider vaults can be default",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type CreateSecretProviderConfig = z.infer<typeof createSecretProviderConfigSchema>;
|
||||
|
||||
export const updateSecretProviderConfigSchema = z.object({
|
||||
displayName: z.string().trim().min(1).max(120).optional(),
|
||||
status: z.enum(SECRET_PROVIDER_CONFIG_STATUSES).optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
config: z.record(z.unknown()).optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
if (value.config !== undefined) {
|
||||
rejectSensitiveProviderConfigKeys(value.config, ctx);
|
||||
}
|
||||
if ((value.status === "coming_soon" || value.status === "disabled") && value.isDefault) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["isDefault"],
|
||||
message: "Only ready or warning provider vaults can be default",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type UpdateSecretProviderConfig = z.infer<typeof updateSecretProviderConfigSchema>;
|
||||
Reference in new issue
Block a user