stack secrets from external providers (1Password, HashiCorp Vault, Infisical and Doppler)

This commit is contained in:
jarek committed 2026-08-14 09:35:36 +02:00
1 parent 78d67d2dc1
commit 32a86129d0
11 files changed
+560 -137

No files matched your search

@@ -0,0 +1,83 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getSecretProviderById } from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import { getProvider } from '$lib/server/secretproviders';
import { UnsupportedOperationError, parseProviderError } from '$lib/server/secretproviders/shared';
import { probeBulkKeysCached } from '$lib/server/secretproviders/probe-cache';
/**
* Live probe of a stored provider: given a bulk selector and/or inline op://
* references, return which KEY NAMES currently exist in the provider. Only names
* are returned - secret values never leave the server. Powers the editor's live
* "IN VAULT" marker.
*/
export const POST: RequestHandler = async ({ params, cookies, request }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !(await auth.can('secrets', 'view'))) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const id = Number.parseInt(params.id);
if (Number.isNaN(id)) {
return json({ error: 'Invalid secret provider ID' }, { status: 400 });
}
const row = await getSecretProviderById(id);
if (!row) {
return json({ error: 'Secret provider not found' }, { status: 404 });
}
const provider = getProvider(row.type);
if (!provider) {
return json({ error: `Unknown secret provider type: ${row.type}` }, { status: 400 });
}
let selector: string | undefined;
let refs: string[] = [];
try {
const body = await request.json();
if (typeof body?.selector === 'string' && body.selector.trim()) {
selector = body.selector.trim();
}
if (Array.isArray(body?.refs)) {
refs = body.refs.filter((r: unknown): r is string => typeof r === 'string' && !!r.trim());
}
} catch {
// empty/invalid body: nothing to probe
}
let bulkKeys: string[] = [];
let resolvedRefs: string[] = [];
// A provider missing a mode throws UnsupportedOperationError; that is not a
// failure - it just means that mode contributes no keys. Any other throw
// (network, bad token, bad path) is a real probe failure.
if (selector && provider.supportsBulk) {
try {
bulkKeys = await probeBulkKeysCached(id, provider, row.config, selector);
} catch (e) {
if (!(e instanceof UnsupportedOperationError)) {
return json({ ok: false, error: shortError(e) }, { status: 200 });
}
}
}
if (refs.length && provider.supportsReferences) {
try {
const resolved = await provider.resolveSecretReferences(row.config, refs);
resolvedRefs = [...resolved.keys()];
} catch (e) {
if (!(e instanceof UnsupportedOperationError)) {
return json({ ok: false, error: shortError(e) }, { status: 200 });
}
}
}
return json({ ok: true, bulkKeys, resolvedRefs });
};
/** A short, non-reflecting error message for the probe-failed UI line. */
function shortError(e: unknown): string {
const raw = e instanceof Error ? e.message : String(e);
return parseProviderError(raw) ?? raw.slice(0, 200);
}
+11 -2
View File
@@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit';
import { getStackEnvVars, setStackEnvVars, getStackSource } from '$lib/server/db';
import { getStackEnvVars, setStackEnvVars, getStackSource, getStackInjectedSecretKeys, getSecretProviderById } from '$lib/server/db';
import { findStackDir } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { existsSync, readFileSync } from 'node:fs';
@@ -107,7 +107,16 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
}
}
return json({ variables });
// Provider-injected key NAMES from the last deploy (no values) + the bound
// provider's type/name, so the UI can show "these were loaded from <provider>".
const injectedSecretKeys = [...await getStackInjectedSecretKeys(stackName, envIdNum ?? undefined)];
let secretProvider: { type: string; name: string } | null = null;
if (source?.secretProviderId) {
const p = await getSecretProviderById(source.secretProviderId);
if (p) secretProvider = { type: p.type, name: p.name };
}
return json({ variables, injectedSecretKeys, secretProvider });
} catch (error) {
console.error('Error getting stack env vars:', error);
return json({ error: 'Failed to get environment variables' }, { status: 500 });