fix: close catalog review gaps
Publish documentation / publish (push) Successful in 38s

This commit is contained in:
Codex
2026-08-31 16:28:42 +02:00
parent 64fbe642ef
commit f586152636
7 changed files with 301 additions and 48 deletions
+14 -9
View File
@@ -3,6 +3,18 @@ export interface ConfiguredTransportUrlOptions {
originOnly?: boolean;
}
export function parseCredentialFreeHttpUrl(value: string): URL | undefined {
let url: URL;
try {
url = new URL(value);
} catch {
return undefined;
}
if (!["http:", "https:"].includes(url.protocol)
|| url.username || url.password || url.search || url.hash) return undefined;
return url;
}
function canonicalLoopbackAuthority(value: string): boolean {
const match = /^http:\/\/([^/?#]+)(?:[/?#]|$)/.exec(value);
if (!match) return false;
@@ -27,15 +39,8 @@ export function parseConfiguredTransportUrl(
value: string,
options: ConfiguredTransportUrlOptions,
): URL | undefined {
let url: URL;
try {
url = new URL(value);
} catch {
return undefined;
}
if (url.username || url.password || url.search || url.hash || (options.originOnly && url.pathname !== "/")) {
return undefined;
}
const url = parseCredentialFreeHttpUrl(value);
if (!url || (options.originOnly && url.pathname !== "/")) return undefined;
if (url.protocol === "https:") return url;
if (options.allowLoopbackHttp && url.protocol === "http:" && canonicalLoopbackAuthority(value)) return url;
return undefined;
+5 -3
View File
@@ -159,7 +159,7 @@ export class CatalogService {
for (const id of Object.values(CATALOG_SECRET_IDS)) this.secretStore.forget(workspaceId, id);
}
async test(database: WorkspaceDatabase): Promise<DatabaseTestResult> {
async test(database: WorkspaceDatabase): Promise<WorkspaceDatabase | undefined> {
return await this.operations.run(database.id, async () => {
const testedAt = new Date().toISOString();
const controller = new AbortController();
@@ -168,6 +168,7 @@ export class CatalogService {
? database.binding.restAuth === "none" ? [] : [CATALOG_SECRET_IDS.apiKey]
: [];
const materialized = this.secretStore.materialize(database.workspaceId, required);
let result: DatabaseTestResult;
try {
if (database.binding.transport !== "rest_api") {
const client = await this.postgres.connect(database, controller.signal);
@@ -205,13 +206,13 @@ export class CatalogService {
},
});
}
return {
result = {
connectionStatus: "reachable",
testedVersion: database.version,
lastTestedAt: testedAt,
};
} catch {
return {
result = {
connectionStatus: "failed",
testedVersion: database.version,
lastTestedAt: testedAt,
@@ -223,6 +224,7 @@ export class CatalogService {
controller.abort();
materialized.release();
}
return await this.repository.recordTest(database.id, database.version, result);
});
}
}
+5 -2
View File
@@ -1,6 +1,7 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { isPrincipalContext, requirePermission } from "../auth/authorization.js";
import { parseCredentialFreeHttpUrl } from "../auth/url-policy.js";
import { CatalogService, type CatalogSecretName } from "../catalog/service.js";
import { WorkspaceRegistryError } from "../workspaces/git-repository.js";
import {
@@ -26,7 +27,9 @@ const bindingSchema = z.object({
host: optionalText,
port: port.optional(),
username: optionalText,
baseUrl: z.url().max(2048).optional(),
baseUrl: z.string().max(2048)
.refine((value) => parseCredentialFreeHttpUrl(value) !== undefined)
.optional(),
restPath: z.string().regex(/^\/(?!\/)[^?#\\\u0000-\u001f]*$/).max(512).optional(),
restAuth: z.enum(["none", "bearer", "x-api-key"]).optional(),
tlsServername: optionalText,
@@ -209,7 +212,7 @@ export function catalogDatabaseRoutes(
const database = await deps.repository.get(id);
if (!database) return reply.code(404).send({ code: "database_not_found", message: "Database configuration was not found." });
if (database.version !== version) return reply.code(409).send({ code: "database_stale", message: "Database configuration changed. Reload and try again." });
const tested = await deps.repository.recordTest(id, version, await deps.service.test(database));
const tested = await deps.service.test(database);
if (!tested) return reply.code(409).send({ code: "database_stale", message: "Database configuration changed. Reload and try again." });
return { ...tested, configured: true, secrets: deps.service.configuredSecrets(tested.workspaceId) };
} catch (error) { return safeError(reply, error); }