mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
import {
|
|
getSecretResolver,
|
|
requireSecretResolver,
|
|
} from "./connect/secret-resolver-registry.js";
|
|
|
|
/**
|
|
* Expands "<scheme>://..." references ("op://vault/item/field" for 1Password
|
|
* Connect, and so on) in a resolved host's secret fields into the actual
|
|
* secrets, resolved by whichever plugin registered that scheme through
|
|
* ctx.credentials.registerSecretResolver.
|
|
*
|
|
* Runs once per host resolution, at the single point where every subsystem
|
|
* gets its plaintext credentials - so terminal, SFTP, Docker, metrics and
|
|
* tunnels all see real secrets without knowing references exist.
|
|
*
|
|
* Host resolution is hot (status polls, fleets), so resolved values are
|
|
* cached briefly in memory; a rotated secret shows up within CACHE_TTL_MS.
|
|
*/
|
|
|
|
export const SECRET_FIELDS = [
|
|
"password",
|
|
"key",
|
|
"keyPassword",
|
|
"sudoPassword",
|
|
"socks5Password",
|
|
"rdpPassword",
|
|
"vncPassword",
|
|
"telnetPassword",
|
|
] as const;
|
|
|
|
const REFERENCE_PATTERN = /^([a-z][a-z0-9+.-]*):\/\//i;
|
|
|
|
const CACHE_TTL_MS = 60_000;
|
|
const cache = new Map<string, { value: string; expiresAt: number }>();
|
|
|
|
/** Test seam. */
|
|
export function clearExternalSecretCache(): void {
|
|
cache.clear();
|
|
}
|
|
|
|
export function isSecretReference(value: unknown): value is string {
|
|
return typeof value === "string" && REFERENCE_PATTERN.test(value.trim());
|
|
}
|
|
|
|
function referenceScheme(reference: string): string {
|
|
const match = REFERENCE_PATTERN.exec(reference.trim());
|
|
return (match?.[1] ?? "").toLowerCase();
|
|
}
|
|
|
|
export type SecretResolver = (
|
|
userId: string,
|
|
reference: string,
|
|
) => Promise<string>;
|
|
|
|
async function defaultResolver(
|
|
userId: string,
|
|
reference: string,
|
|
): Promise<string> {
|
|
const scheme = referenceScheme(reference);
|
|
const resolver = requireSecretResolver(scheme);
|
|
return resolver.resolve(userId, reference);
|
|
}
|
|
|
|
export async function resolveSecretReference(
|
|
userId: string,
|
|
reference: string,
|
|
deps: {
|
|
resolver?: SecretResolver;
|
|
now?: () => number;
|
|
} = {},
|
|
): Promise<string> {
|
|
const now = deps.now ?? Date.now;
|
|
const cacheKey = `${userId}:${reference.trim()}`;
|
|
const cached = cache.get(cacheKey);
|
|
if (cached && cached.expiresAt > now()) return cached.value;
|
|
|
|
const value = await (deps.resolver ?? defaultResolver)(userId, reference);
|
|
cache.set(cacheKey, { value, expiresAt: now() + CACHE_TTL_MS });
|
|
return value;
|
|
}
|
|
|
|
/** Replaces every reference in the host's secret fields, in place. */
|
|
export async function resolveExternalSecretRefs(
|
|
host: Record<string, unknown>,
|
|
userId: string,
|
|
deps?: Parameters<typeof resolveSecretReference>[2],
|
|
): Promise<void> {
|
|
for (const field of SECRET_FIELDS) {
|
|
const value = host[field];
|
|
if (!isSecretReference(value)) continue;
|
|
host[field] = await resolveSecretReference(userId, value, deps);
|
|
}
|
|
}
|
|
|
|
/** Whether any plugin currently resolves this reference's scheme. */
|
|
export function hasSecretResolverFor(reference: string): boolean {
|
|
return !!getSecretResolver(referenceScheme(reference));
|
|
}
|