507 lines
23 KiB
TypeScript
507 lines
23 KiB
TypeScript
import Fastify, { type FastifyInstance, type FastifyRequest } from "fastify";
|
|
import cors from "@fastify/cors";
|
|
import cookie from "@fastify/cookie";
|
|
import rateLimit from "@fastify/rate-limit";
|
|
import { dirname, isAbsolute, join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import type { AppConfig } from "./config.js";
|
|
import { ThtRunner } from "./tht/tht-runner.js";
|
|
import { PiProcessManager } from "./pi/pi-process-manager.js";
|
|
import { SseHub } from "./sse/sse-hub.js";
|
|
import { authenticateSession, captureAuthConfigSnapshot, configuredOrigin } from "./auth/auth.js";
|
|
import type { PrincipalContext } from "./auth/principal.js";
|
|
import type { LoadedAuthConfig } from "./auth/types.js";
|
|
import { createCurrentLocalUserRegistryResolver, type LocalUserRegistry } from "./auth/local-registry.js";
|
|
import { AuthSessionOperationalError, createFileAuthSessionStore, type AuthSessionStore, type AuthSessionValidity } from "./auth/session-store.js";
|
|
import type { WindowsAuthStorageBridge } from "./auth/windows-auth-storage.js";
|
|
import { registerAuthRoutes } from "./auth/routes.js";
|
|
import { createOidcProtocol, type OidcProtocol, type OidcProtocolOptions } from "./auth/oidc-client.js";
|
|
import { createConfiguredAuthDiagnoser } from "./auth/diagnostic-command.js";
|
|
import type { AuthDiagnoser } from "./auth/diagnostics.js";
|
|
import { isUsableAuthenticationSecret } from "./auth/secret-policy.js";
|
|
import { secretValue } from "./config/secret-bundle.js";
|
|
import { sessionRoutes } from "./routes/sessions.js";
|
|
import { sqlRoutes } from "./routes/sql.js";
|
|
import { metaRoutes } from "./routes/meta.js";
|
|
import type { ListModelsFn } from "./pi/list-models.js";
|
|
import { settingsRoutes, effectiveSettings } from "./routes/settings.js";
|
|
import { createPiModelLister } from "./pi/list-models.js";
|
|
import { createPiManagement, type PiManagementService } from "./pi/management.js";
|
|
import { loadSettings, type Settings } from "./settings/settings-store.js";
|
|
import { ReadinessManager } from "./runtime/readiness-manager.js";
|
|
import { MaintenanceBarrier } from "./runtime/maintenance-gate.js";
|
|
import { WorkspaceRegistry } from "./workspaces/registry.js";
|
|
import { createProductionWorkspaceDiagnoser } from "./workspaces/diagnostics.js";
|
|
import { workspaceRoutes, type WorkspaceDiagnoser } from "./routes/workspaces.js";
|
|
import { piManagementRoutes } from "./routes/pi-management.js";
|
|
import { supportsSessionRuntime } from "./workspaces/bindings.js";
|
|
import { resolveRuntimeBindingsWithWorkspaceSecrets } from "./workspaces/secret-requirements.js";
|
|
import type { WorkspaceDescriptor } from "./workspaces/schema.js";
|
|
import { WorkspaceSecretStore } from "./workspaces/secret-store.js";
|
|
import { createCatalogRepository } from "./catalog/repository.js";
|
|
import type { CatalogRepository } from "./catalog/types.js";
|
|
import { CatalogService } from "./catalog/service.js";
|
|
import { catalogDatabaseRoutes } from "./routes/catalog-databases.js";
|
|
import { CatalogOperationCoordinator } from "./catalog/operation-coordinator.js";
|
|
import { ConcreteCatalogPostgresAccess, type CatalogPostgresAccess } from "./catalog/postgres-access.js";
|
|
import { CatalogTableService } from "./catalog/table-service.js";
|
|
import { catalogTableRoutes } from "./routes/catalog-tables.js";
|
|
import { ConcreteCatalogSchemaIntrospector, type CatalogSchemaIntrospector } from "./catalog/schema-introspector.js";
|
|
import { CatalogSyncWorker } from "./catalog/sync-worker.js";
|
|
import { catalogSchemaRoutes } from "./routes/catalog-schema.js";
|
|
import {
|
|
loadMetadataGenerationModels,
|
|
type MetadataGenerationModels,
|
|
} from "./catalog/metadata-generation-models.js";
|
|
import { metadataGenerationModelRoutes } from "./routes/metadata-generation-models.js";
|
|
import { catalogDescriptionConsolidationRoutes } from "./routes/catalog-description-consolidation.js";
|
|
import { PythonModelCompleter, type ModelCompleter } from "./catalog/model-completer.js";
|
|
import { DescriptionGenerationWorker } from "./catalog/description-generation-worker.js";
|
|
import { SensitiveDataSuggester } from "./catalog/sensitive-data-suggester.js";
|
|
import { SensitiveDataSuggestionRunner } from "./catalog/sensitive-data-suggestion-runner.js";
|
|
import {
|
|
ConcreteDescriptionSourceSampler,
|
|
type DescriptionSourceSampler,
|
|
} from "./catalog/description-source-sampler.js";
|
|
import { catalogDescriptionGenerationRoutes } from "./routes/catalog-description-generation.js";
|
|
import { CatalogLogicalRelationshipService } from "./catalog/logical-relationship-service.js";
|
|
import { catalogLogicalRelationshipRoutes } from "./routes/catalog-logical-relationships.js";
|
|
import { EffectiveRelationshipSnapshotProvider } from "./catalog/effective-relationship-snapshot.js";
|
|
import { loadRuntimeModelCatalog, type RuntimeModelCatalog } from "./models/runtime-model-catalog.js";
|
|
|
|
export interface BuildAppDeps {
|
|
thtRunner?: ThtRunner;
|
|
mgr?: PiProcessManager;
|
|
spawnFn?: () => any;
|
|
listModels?: ListModelsFn;
|
|
getSettings?: (principal?: PrincipalContext) => Settings | Promise<Settings>;
|
|
readiness?: ReadinessManager;
|
|
hub?: SseHub;
|
|
workspaceRegistry?: WorkspaceRegistry;
|
|
workspaceDiagnoser?: WorkspaceDiagnoser;
|
|
workspaceSecretStore?: WorkspaceSecretStore;
|
|
catalogRepository?: CatalogRepository;
|
|
catalogService?: CatalogService;
|
|
catalogPostgresAccess?: CatalogPostgresAccess;
|
|
catalogTableService?: CatalogTableService;
|
|
catalogLogicalRelationshipService?: CatalogLogicalRelationshipService;
|
|
effectiveRelationshipSnapshotProvider?: EffectiveRelationshipSnapshotProvider;
|
|
catalogSchemaIntrospector?: CatalogSchemaIntrospector;
|
|
catalogSyncWorker?: CatalogSyncWorker;
|
|
catalogOperationCoordinator?: CatalogOperationCoordinator;
|
|
metadataGenerationModels?: MetadataGenerationModels;
|
|
runtimeModelCatalog?: RuntimeModelCatalog;
|
|
modelCompleter?: ModelCompleter;
|
|
descriptionSourceSampler?: DescriptionSourceSampler;
|
|
workspaceRuntimeSupport?: (workspace: WorkspaceDescriptor) => boolean;
|
|
maintenanceBarrier?: MaintenanceBarrier;
|
|
piManagement?: PiManagementService;
|
|
localUserRegistry?: LocalUserRegistry;
|
|
authSessionStore?: AuthSessionStore;
|
|
/** Explicit test-only transport seam; production always invokes the hidden tht bridge. */
|
|
authStorageBridgeForTest?: WindowsAuthStorageBridge;
|
|
oidcProtocol?: OidcProtocol;
|
|
authDiagnoser?: AuthDiagnoser;
|
|
/** Explicit test seam; production uses the provider-neutral OIDC constructor. */
|
|
oidcProtocolFactory?: (options: OidcProtocolOptions) => OidcProtocol;
|
|
}
|
|
|
|
export interface AppWithAuthSessionStore extends FastifyInstance {
|
|
thothiiAuthSessionStore?: AuthSessionStore;
|
|
}
|
|
|
|
export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstance {
|
|
const app = Fastify({ logger: { level: "warn" }, disableRequestLogging: true });
|
|
app.decorateRequest("authConfigSnapshot", undefined);
|
|
app.decorateRequest("authConfigSnapshotCaptured", false);
|
|
app.decorateRequest("authConfigSnapshotUnavailable", false);
|
|
const isolatedTestRoot = process.env.VITEST === "true"
|
|
? join(tmpdir(), `thothii-workspace-secrets-vitest-${process.pid}`)
|
|
: undefined;
|
|
const workspaceSecretStore = deps?.workspaceSecretStore ?? new WorkspaceSecretStore({
|
|
root: isolatedTestRoot ?? config.workspaceSecretStoreRoot,
|
|
runtimeRoot: isolatedTestRoot === undefined
|
|
? config.workspaceSecretRuntimeRoot
|
|
: join(isolatedTestRoot, "runtime"),
|
|
installationId: config.workspaceRegistry.installationId,
|
|
});
|
|
|
|
const cookieAuth = config.authMode === "local" || config.authMode === "oidc";
|
|
app.register(cors, {
|
|
// The delegator runs at CORS's onRequest hook. It owns the one request-scoped config load
|
|
// which subsequent auth hooks and routes consume, including preflights that end here.
|
|
delegator: (request, callback) => {
|
|
const snapshot = captureAuthConfigSnapshot(request, config.authentication);
|
|
const origin = configuredOrigin(snapshot);
|
|
const snapshotUsesCookies = snapshot?.value.mode === "local" || snapshot?.value.mode === "oidc";
|
|
callback(null, {
|
|
origin: snapshotUsesCookies && origin ? corsOrigin(request, origin) : cookieAuth ? false : true,
|
|
credentials: snapshotUsesCookies,
|
|
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
});
|
|
},
|
|
});
|
|
// Cookie parsing and the rate-limit plugin must precede every auth/application route.
|
|
app.register(cookie);
|
|
app.register(rateLimit, { global: false });
|
|
|
|
const tht = deps?.thtRunner ?? new ThtRunner({
|
|
thtBin: config.thtBin,
|
|
harnessDir: config.harnessDir,
|
|
configPath: process.env.THT_CONFIG ?? "config/tht.yaml",
|
|
dataRoot: config.dataRoot,
|
|
runtimeSnapshotRoot: join(config.workspaceRegistry.root, "snapshots", "runtime"),
|
|
secretRoots: config.workspaceRegistry.secretRoots,
|
|
secretsFile: config.secretsFile,
|
|
secretFiles: config.secretFiles,
|
|
workspaceSecretStore,
|
|
semanticRuntime: {
|
|
internalQdrantUrl: config.internalQdrantUrl,
|
|
internalEmbeddingUrl: config.internalEmbeddingUrl,
|
|
internalEmbeddingId: config.internalEmbeddingId,
|
|
internalEmbeddingModel: config.internalEmbeddingModel,
|
|
internalEmbeddingDimensions: config.internalEmbeddingDimensions,
|
|
},
|
|
});
|
|
const hub = deps?.hub ?? new SseHub();
|
|
const workspaceRegistry = deps?.workspaceRegistry ?? new WorkspaceRegistry(config.workspaceRegistry);
|
|
const catalogRepository = deps?.catalogRepository ?? createCatalogRepository(config.catalogDatabase);
|
|
const catalogOperationCoordinator = deps?.catalogOperationCoordinator ?? new CatalogOperationCoordinator();
|
|
const runtimeModelCatalog = deps?.runtimeModelCatalog ?? loadRuntimeModelCatalog(config.modelCatalogFile);
|
|
const mgr = deps?.mgr ?? new PiProcessManager(config, {
|
|
...(deps?.spawnFn ? { spawnFn: deps.spawnFn } : {}),
|
|
modelCatalog: runtimeModelCatalog,
|
|
});
|
|
const metadataGenerationModels = deps?.metadataGenerationModels ?? loadMetadataGenerationModels({
|
|
catalogFile: config.modelCatalogFile,
|
|
secretsFile: config.secretsFile,
|
|
});
|
|
const modelCompleter = deps?.modelCompleter ?? new PythonModelCompleter({
|
|
pythonExecutable: isAbsolute(config.thtBin) ? join(dirname(config.thtBin), "python") : "python3",
|
|
cwd: config.harnessDir,
|
|
});
|
|
const catalogPostgresAccess = deps?.catalogPostgresAccess ?? new ConcreteCatalogPostgresAccess(
|
|
workspaceSecretStore,
|
|
{ connectTimeoutMs: config.workspaceDiagnosticTimeoutMs },
|
|
);
|
|
const descriptionSourceSampler = deps?.descriptionSourceSampler
|
|
?? new ConcreteDescriptionSourceSampler(catalogPostgresAccess, workspaceSecretStore);
|
|
const descriptionGenerationWorker = new DescriptionGenerationWorker(
|
|
catalogRepository,
|
|
workspaceRegistry,
|
|
metadataGenerationModels,
|
|
modelCompleter,
|
|
catalogOperationCoordinator,
|
|
descriptionSourceSampler,
|
|
);
|
|
const sensitiveDataSuggester = new SensitiveDataSuggester(
|
|
catalogRepository,
|
|
metadataGenerationModels,
|
|
modelCompleter,
|
|
);
|
|
const sensitiveDataSuggestionRunner = new SensitiveDataSuggestionRunner(
|
|
catalogRepository,
|
|
sensitiveDataSuggester,
|
|
);
|
|
const catalogService = deps?.catalogService ?? new CatalogService(
|
|
catalogRepository,
|
|
workspaceRegistry,
|
|
workspaceSecretStore,
|
|
config.workspaceRegistry.secretRoots,
|
|
config.workspaceDiagnosticTimeoutMs,
|
|
catalogPostgresAccess,
|
|
catalogOperationCoordinator,
|
|
);
|
|
const catalogTableService = deps?.catalogTableService ?? new CatalogTableService(catalogRepository);
|
|
const catalogLogicalRelationshipService = deps?.catalogLogicalRelationshipService
|
|
?? new CatalogLogicalRelationshipService(catalogRepository);
|
|
const effectiveRelationships = deps?.effectiveRelationshipSnapshotProvider
|
|
?? (config.catalogDatabase === undefined
|
|
? undefined
|
|
: new EffectiveRelationshipSnapshotProvider(
|
|
catalogRepository,
|
|
catalogLogicalRelationshipService,
|
|
catalogOperationCoordinator,
|
|
));
|
|
const catalogSchemaIntrospector = deps?.catalogSchemaIntrospector ?? new ConcreteCatalogSchemaIntrospector(
|
|
catalogPostgresAccess,
|
|
workspaceSecretStore,
|
|
);
|
|
const catalogSyncWorker = deps?.catalogSyncWorker ?? new CatalogSyncWorker(
|
|
catalogRepository,
|
|
catalogSchemaIntrospector,
|
|
catalogOperationCoordinator,
|
|
config.catalogSyncTimeoutMs,
|
|
);
|
|
app.addHook("onReady", async () => { await catalogSyncWorker.initialize(); });
|
|
app.addHook("onReady", async () => { await descriptionGenerationWorker.initialize(); });
|
|
app.addHook("onReady", async () => { await sensitiveDataSuggestionRunner.initialize(); });
|
|
if (!deps?.catalogRepository && catalogRepository.close) {
|
|
app.addHook("onClose", async () => { await catalogRepository.close?.(); });
|
|
}
|
|
app.addHook("onClose", async () => { await catalogSyncWorker.stop(); });
|
|
app.addHook("onClose", async () => { await descriptionGenerationWorker.stop(); });
|
|
const workspaceDiagnoser = deps?.workspaceDiagnoser
|
|
?? createProductionWorkspaceDiagnoser(config.workspaceDiagnosticTimeoutMs, undefined, {
|
|
internalQdrantUrl: config.internalQdrantUrl,
|
|
internalEmbeddingUrl: config.internalEmbeddingUrl,
|
|
internalEmbeddingId: config.internalEmbeddingId,
|
|
internalEmbeddingModel: config.internalEmbeddingModel,
|
|
internalEmbeddingDimensions: config.internalEmbeddingDimensions,
|
|
});
|
|
const workspaceRuntimeSupport = deps?.workspaceRuntimeSupport ?? ((workspace: WorkspaceDescriptor) => {
|
|
const lease = resolveRuntimeBindingsWithWorkspaceSecrets(
|
|
workspace,
|
|
process.env,
|
|
config.workspaceRegistry.secretRoots,
|
|
workspaceSecretStore,
|
|
);
|
|
try {
|
|
return supportsSessionRuntime(lease.bindings);
|
|
} finally {
|
|
lease.release();
|
|
}
|
|
});
|
|
const readiness = deps?.readiness ?? new ReadinessManager(
|
|
tht as ThtRunner,
|
|
Math.round(config.ollamaEnsureTimeoutMs / 1000),
|
|
);
|
|
|
|
const listModels = deps?.listModels ?? createPiModelLister(config, {
|
|
warn: (detail) => app.log.warn(
|
|
{ component: "pi-model-list", detail },
|
|
"Pi enabled-model configuration warning",
|
|
),
|
|
});
|
|
const runnerFor = (principal: PrincipalContext): any => {
|
|
const candidate = tht as any;
|
|
return typeof candidate.withPrincipal === "function" ? candidate.withPrincipal(principal) : candidate;
|
|
};
|
|
const getSettings = async (principal: PrincipalContext): Promise<Settings> => {
|
|
if (deps?.getSettings) return await deps.getSettings(principal);
|
|
const stored = loadSettings(config);
|
|
const effective = effectiveSettings(config, stored, runtimeModelCatalog);
|
|
// In the registry system the legacy `harness/workspaces/*.yaml` default is obsolete: when no
|
|
// installation workspace is pinned, default to the first active registry workspace.
|
|
if (!stored.workspace) {
|
|
try {
|
|
const revisions = await workspaceRegistry.list();
|
|
if (revisions.length > 0) effective.workspace = revisions[0].id;
|
|
} catch {
|
|
// Registry not bootstrapped yet; keep the legacy fallback.
|
|
}
|
|
}
|
|
return effective;
|
|
};
|
|
const piManagement = deps?.piManagement ?? createPiManagement(config, {
|
|
modelCatalog: runtimeModelCatalog,
|
|
});
|
|
|
|
const maintenanceBarrier = deps?.maintenanceBarrier ?? new MaintenanceBarrier(config.maintenanceFile);
|
|
const localRegistryResolver = deps?.localUserRegistry === undefined
|
|
? createCurrentLocalUserRegistryResolver()
|
|
: undefined;
|
|
const resolveLocalUserRegistry = (loaded: LoadedAuthConfig) => {
|
|
return deps?.localUserRegistry ?? localRegistryResolver?.resolve(loaded);
|
|
};
|
|
const localUserForSnapshot = async (loaded: LoadedAuthConfig, subject: string) => {
|
|
try {
|
|
if (loaded.value.mode !== "local") return { revision: loaded.revision, user: undefined };
|
|
const registry = resolveLocalUserRegistry(loaded);
|
|
if (!registry) throw new AuthSessionOperationalError();
|
|
const user = await registry.findBySubject(subject);
|
|
return {
|
|
revision: loaded.revision,
|
|
user: user === undefined ? undefined : {
|
|
enabled: user.enabled,
|
|
authRevision: user.authRevision,
|
|
roles: user.roles,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
if (error instanceof AuthSessionOperationalError) throw error;
|
|
throw new AuthSessionOperationalError();
|
|
}
|
|
};
|
|
const sessionValidityForSnapshot = (loaded: LoadedAuthConfig): AuthSessionValidity => ({
|
|
currentAuthConfigRevision: () => loaded.revision,
|
|
currentLocalUser: (subject) => localUserForSnapshot(loaded, subject),
|
|
});
|
|
const resolveOidcProtocol = (loaded: LoadedAuthConfig): OidcProtocol | undefined => {
|
|
if (deps?.oidcProtocol) return deps.oidcProtocol;
|
|
if (loaded.value.mode !== "oidc") return undefined;
|
|
try {
|
|
const clientSecret = secretValue(config, loaded.value.oidc.clientSecretRef);
|
|
if (!isUsableAuthenticationSecret("THT_OIDC_CLIENT_SECRET", clientSecret)) return undefined;
|
|
return (deps?.oidcProtocolFactory ?? createOidcProtocol)({
|
|
issuer: loaded.value.oidc.issuer,
|
|
clientId: loaded.value.oidc.clientId,
|
|
clientSecret,
|
|
callbackUrl: new URL("/api/auth/oidc/callback", loaded.value.publicUrl).href,
|
|
scopes: loaded.value.oidc.scopes,
|
|
groupsClaim: loaded.value.oidc.groupsClaim,
|
|
});
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
};
|
|
const authDiagnoser = deps?.authDiagnoser ?? createConfiguredAuthDiagnoser(config, {
|
|
localUserRegistry: resolveLocalUserRegistry,
|
|
oidcProtocol: resolveOidcProtocol,
|
|
});
|
|
const authSessionStore = deps?.authSessionStore ?? (config.authMode === "local" || config.authMode === "oidc"
|
|
? createFileAuthSessionStore(config.authStateRoot, {
|
|
currentAuthConfigRevision: () => {
|
|
try {
|
|
return config.authentication?.current().revision ?? "";
|
|
} catch {
|
|
throw new AuthSessionOperationalError();
|
|
}
|
|
},
|
|
currentLocalUser: async (subject) => {
|
|
try {
|
|
const loaded = config.authentication?.current();
|
|
if (!loaded) return { revision: "", user: undefined };
|
|
return await localUserForSnapshot(loaded, subject);
|
|
} catch (error) {
|
|
if (error instanceof AuthSessionOperationalError) throw error;
|
|
throw new AuthSessionOperationalError();
|
|
}
|
|
},
|
|
}, deps?.authStorageBridgeForTest === undefined
|
|
? undefined
|
|
: process.platform === "win32"
|
|
? { windowsStorageBridge: deps.authStorageBridgeForTest }
|
|
: { posixStorageBridge: deps.authStorageBridgeForTest })
|
|
: undefined);
|
|
(app as AppWithAuthSessionStore).thothiiAuthSessionStore = authSessionStore;
|
|
const authenticate = authenticateSession({
|
|
mode: config.authMode,
|
|
publicExposure: config.publicExposure,
|
|
authentication: config.authentication,
|
|
sessionStore: authSessionStore,
|
|
sessionValidityForSnapshot,
|
|
});
|
|
app.addHook("preHandler", (req, reply, done) => {
|
|
if (isMaintenanceControl(req.url)) {
|
|
if (!isLoopback(req.ip)) {
|
|
reply.code(403).send({ error: "loopback maintenance control required" });
|
|
}
|
|
}
|
|
done();
|
|
});
|
|
app.addHook("preHandler", authenticate);
|
|
app.get("/health", async () => ({ status: "ok" }));
|
|
app.get("/health/dwh", async () => {
|
|
// In the registry system there is no single legacy DWH config: ping the first active
|
|
// workspace's rendered runtime config. If the registry is not bootstrapped yet, do not
|
|
// block the app — per-workspace diagnostics and the session precheck own reachability.
|
|
try {
|
|
const revisions = await workspaceRegistry.list();
|
|
if (revisions.length > 0) {
|
|
return await tht.dbPing(revisions[0].snapshotPath);
|
|
}
|
|
} catch {
|
|
// fall through
|
|
}
|
|
return { ok: true, detail: "workspace diagnostics own DWH reachability" };
|
|
});
|
|
registerAuthRoutes(app, {
|
|
authMode: config.authMode,
|
|
authentication: config.authentication,
|
|
sessionStore: authSessionStore,
|
|
localUserRegistry: deps?.localUserRegistry,
|
|
resolveLocalUserRegistry,
|
|
resolveOidcProtocol,
|
|
});
|
|
sessionRoutes(app, {
|
|
mgr, tht: tht as ThtRunner, hub, getSettings, readiness, listModels, workspaceRegistry,
|
|
dwhPrecheck: config.dwhPrecheck,
|
|
legacyWorkspaceMode: config.legacyWorkspaceMode,
|
|
workspaceRuntimeSupport,
|
|
modelCatalog: runtimeModelCatalog,
|
|
maintenanceBarrier,
|
|
effectiveRelationships,
|
|
});
|
|
app.post("/internal/maintenance/activate", async (req, reply) => {
|
|
try {
|
|
await maintenanceBarrier.activate();
|
|
return maintenanceBarrier.status();
|
|
} catch {
|
|
return reply.code(500).send({
|
|
...maintenanceBarrier.status(),
|
|
code: "maintenance_durability_failed",
|
|
error: "maintenance activation durability was not acknowledged",
|
|
});
|
|
}
|
|
});
|
|
app.post("/internal/maintenance/deactivate", async (req, reply) => {
|
|
try {
|
|
maintenanceBarrier.deactivate();
|
|
return maintenanceBarrier.status();
|
|
} catch {
|
|
return reply.code(500).send({
|
|
...maintenanceBarrier.status(),
|
|
code: "maintenance_durability_failed",
|
|
error: "maintenance deactivation durability was not acknowledged",
|
|
});
|
|
}
|
|
});
|
|
app.get("/internal/maintenance/status", async (req, reply) => {
|
|
return maintenanceBarrier.status();
|
|
});
|
|
sqlRoutes(app, { tht: tht as ThtRunner, getSettings, workspaceRegistry });
|
|
metaRoutes(app, { harnessDir: config.harnessDir, modelCatalog: runtimeModelCatalog });
|
|
workspaceRoutes(app, {
|
|
registry: workspaceRegistry,
|
|
config: config.workspaceRegistry,
|
|
diagnose: workspaceDiagnoser,
|
|
authDiagnoser,
|
|
secretStore: workspaceSecretStore,
|
|
});
|
|
catalogDatabaseRoutes(app, { repository: catalogRepository, service: catalogService, operations: catalogOperationCoordinator });
|
|
catalogTableRoutes(app, {
|
|
repository: catalogRepository,
|
|
service: catalogTableService,
|
|
operations: catalogOperationCoordinator,
|
|
});
|
|
catalogSchemaRoutes(app, {
|
|
repository: catalogRepository,
|
|
worker: catalogSyncWorker,
|
|
operations: catalogOperationCoordinator,
|
|
});
|
|
catalogLogicalRelationshipRoutes(app, {
|
|
service: catalogLogicalRelationshipService,
|
|
operations: catalogOperationCoordinator,
|
|
});
|
|
catalogDescriptionConsolidationRoutes(app, {
|
|
repository: catalogRepository,
|
|
operations: catalogOperationCoordinator,
|
|
});
|
|
metadataGenerationModelRoutes(app, metadataGenerationModels);
|
|
catalogDescriptionGenerationRoutes(app, {
|
|
repository: catalogRepository,
|
|
worker: descriptionGenerationWorker,
|
|
sensitiveDataSuggestionRunner,
|
|
});
|
|
settingsRoutes(app, { cfg: config, getSettings });
|
|
piManagementRoutes(app, { service: piManagement });
|
|
|
|
return app;
|
|
}
|
|
|
|
function corsOrigin(request: FastifyRequest, expectedOrigin: string): string | false {
|
|
const supplied = request.headers.origin;
|
|
if (typeof supplied !== "string") return false;
|
|
try {
|
|
return new URL(supplied).origin === expectedOrigin ? expectedOrigin : false;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isLoopback(ip: string): boolean { return ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1"; }
|
|
function isMaintenanceControl(url: string): boolean {
|
|
return /^\/internal\/maintenance\/(?:activate|deactivate|status)(?:\?|$)/.test(url);
|
|
}
|