import type { CreateSourcePayload, UpdateSourcePayload, } from "#api"; import type { Credential, Source, SourceAuth, SourceCredentialBinding, SourceId, StoredSourceRecord, StringMap, WorkspaceId, } from "#schema"; import { CredentialIdSchema } from "#schema"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; const decodeStringMap = Schema.decodeUnknown( Schema.NullOr( Schema.Record({ key: Schema.String, value: Schema.String, }), ), ); const trimOrNull = (value: string | null | undefined): string | null => { if (value === null || value === undefined) { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; }; const serializeStringMap = (value: StringMap | null): string | null => value === null ? null : JSON.stringify(value); const parseStringMapJson = ( fieldName: string, value: string | null, ): Effect.Effect => value === null ? Effect.succeed(null) : Effect.try({ try: () => JSON.parse(value), catch: (cause) => cause instanceof Error ? new Error(`Invalid ${fieldName}: ${cause.message}`) : new Error(`Invalid ${fieldName}: ${String(cause)}`), }).pipe( Effect.flatMap((parsed) => decodeStringMap(parsed)), Effect.mapError((cause) => cause instanceof Error ? new Error(`Invalid ${fieldName}: ${cause.message}`) : new Error(`Invalid ${fieldName}: ${String(cause)}`), ), ); const normalizeAuth = ( auth: SourceAuth | undefined, ): Effect.Effect => Effect.gen(function* () { if (auth === undefined || auth.kind === "none") { return { kind: "none" } satisfies SourceAuth; } const headerName = trimOrNull(auth.headerName) ?? "Authorization"; const prefix = auth.prefix ?? "Bearer "; if (auth.kind === "bearer") { const providerId = trimOrNull(auth.token.providerId); const handle = trimOrNull(auth.token.handle); if (providerId === null || handle === null) { return yield* Effect.fail(new Error("Bearer auth requires a token secret ref")); } return { kind: "bearer", headerName, prefix, token: { providerId, handle, }, } satisfies SourceAuth; } const accessProviderId = trimOrNull(auth.accessToken.providerId); const accessHandle = trimOrNull(auth.accessToken.handle); if (accessProviderId === null || accessHandle === null) { return yield* Effect.fail(new Error("OAuth2 auth requires an access token secret ref")); } let refreshToken: { providerId: string; handle: string } | null = null; if (auth.refreshToken !== null) { const refreshProviderId = trimOrNull(auth.refreshToken.providerId); const refreshHandle = trimOrNull(auth.refreshToken.handle); if (refreshProviderId === null || refreshHandle === null) { return yield* Effect.fail( new Error("OAuth2 refresh token ref must include providerId and handle"), ); } refreshToken = { providerId: refreshProviderId, handle: refreshHandle, }; } return { kind: "oauth2", headerName, prefix, accessToken: { providerId: accessProviderId, handle: accessHandle, }, refreshToken, } satisfies SourceAuth; }); const validateSourceByKind = (source: Source): Effect.Effect => Effect.gen(function* () { if (source.kind === "mcp") { if (source.specUrl !== null) { return yield* Effect.fail(new Error("MCP sources cannot define specUrl")); } return source; } if (source.kind === "openapi") { if (trimOrNull(source.specUrl) === null) { return yield* Effect.fail(new Error("OpenAPI sources require specUrl")); } if (source.transport !== null || source.queryParams !== null || source.headers !== null) { return yield* Effect.fail( new Error("OpenAPI sources cannot define MCP transport settings"), ); } return source; } if (source.kind === "graphql") { if (source.transport !== null || source.queryParams !== null || source.headers !== null) { return yield* Effect.fail( new Error("GraphQL sources cannot define MCP transport settings"), ); } if (source.specUrl !== null) { return yield* Effect.fail(new Error("GraphQL sources cannot define specUrl")); } return source; } if (source.transport !== null || source.queryParams !== null || source.headers !== null) { return yield* Effect.fail( new Error(`${source.kind} sources cannot define MCP transport settings`), ); } if (source.specUrl !== null || source.defaultHeaders !== null) { return yield* Effect.fail( new Error(`${source.kind} sources cannot define OpenAPI settings`), ); } return source; }); export const createSourceFromPayload = (input: { workspaceId: WorkspaceId; sourceId: SourceId; payload: CreateSourcePayload; now: number; }): Effect.Effect => Effect.gen(function* () { const auth = yield* normalizeAuth(input.payload.auth); return yield* validateSourceByKind({ id: input.sourceId, workspaceId: input.workspaceId, name: input.payload.name.trim(), kind: input.payload.kind, endpoint: input.payload.endpoint.trim(), status: input.payload.status ?? "draft", enabled: input.payload.enabled ?? true, namespace: trimOrNull(input.payload.namespace), transport: input.payload.transport ?? null, queryParams: input.payload.queryParams ?? null, headers: input.payload.headers ?? null, specUrl: trimOrNull(input.payload.specUrl), defaultHeaders: input.payload.defaultHeaders ?? null, auth, sourceHash: trimOrNull(input.payload.sourceHash), lastError: trimOrNull(input.payload.lastError), createdAt: input.now, updatedAt: input.now, }); }); export const updateSourceFromPayload = (input: { source: Source; payload: UpdateSourcePayload; now: number; }): Effect.Effect => Effect.gen(function* () { const nextAuth = input.payload.auth === undefined ? input.source.auth : yield* normalizeAuth(input.payload.auth); return yield* validateSourceByKind({ ...input.source, name: input.payload.name !== undefined ? input.payload.name.trim() : input.source.name, kind: input.payload.kind ?? input.source.kind, endpoint: input.payload.endpoint !== undefined ? input.payload.endpoint.trim() : input.source.endpoint, status: input.payload.status ?? input.source.status, enabled: input.payload.enabled ?? input.source.enabled, namespace: input.payload.namespace !== undefined ? trimOrNull(input.payload.namespace) : input.source.namespace, transport: input.payload.transport !== undefined ? input.payload.transport : input.source.transport, queryParams: input.payload.queryParams !== undefined ? input.payload.queryParams : input.source.queryParams, headers: input.payload.headers !== undefined ? input.payload.headers : input.source.headers, specUrl: input.payload.specUrl !== undefined ? trimOrNull(input.payload.specUrl) : input.source.specUrl, defaultHeaders: input.payload.defaultHeaders !== undefined ? input.payload.defaultHeaders : input.source.defaultHeaders, auth: nextAuth, sourceHash: input.payload.sourceHash !== undefined ? trimOrNull(input.payload.sourceHash) : input.source.sourceHash, lastError: input.payload.lastError !== undefined ? trimOrNull(input.payload.lastError) : input.source.lastError, updatedAt: input.now, }); }); export const splitSourceForStorage = (input: { source: Source; existingCredentialId?: Credential["id"] | null; existingBindingId?: SourceCredentialBinding["id"] | null; }): { sourceRecord: StoredSourceRecord; credential: Credential | null; credentialBinding: SourceCredentialBinding | null; } => { const sourceRecord: StoredSourceRecord = { id: input.source.id, workspaceId: input.source.workspaceId, name: input.source.name, kind: input.source.kind, endpoint: input.source.endpoint, status: input.source.status, enabled: input.source.enabled, namespace: input.source.namespace, transport: input.source.transport, queryParamsJson: serializeStringMap(input.source.queryParams), headersJson: serializeStringMap(input.source.headers), specUrl: input.source.specUrl, defaultHeadersJson: serializeStringMap(input.source.defaultHeaders), sourceHash: input.source.sourceHash, sourceDocumentText: null, lastError: input.source.lastError, createdAt: input.source.createdAt, updatedAt: input.source.updatedAt, }; if (input.source.auth.kind === "none") { return { sourceRecord, credential: null, credentialBinding: null, }; } const credentialId = input.existingCredentialId ?? CredentialIdSchema.make(`cred_${crypto.randomUUID()}`); const bindingId = input.existingBindingId ?? `src_cred_bind_${crypto.randomUUID()}`; const credential: Credential = { id: credentialId, workspaceId: input.source.workspaceId, authKind: input.source.auth.kind, authHeaderName: input.source.auth.headerName, authPrefix: input.source.auth.prefix, tokenProviderId: input.source.auth.kind === "bearer" ? input.source.auth.token.providerId : input.source.auth.accessToken.providerId, tokenHandle: input.source.auth.kind === "bearer" ? input.source.auth.token.handle : input.source.auth.accessToken.handle, refreshTokenProviderId: input.source.auth.kind === "oauth2" && input.source.auth.refreshToken !== null ? input.source.auth.refreshToken.providerId : null, refreshTokenHandle: input.source.auth.kind === "oauth2" && input.source.auth.refreshToken !== null ? input.source.auth.refreshToken.handle : null, createdAt: input.source.createdAt, updatedAt: input.source.updatedAt, }; return { sourceRecord, credential, credentialBinding: { id: bindingId, workspaceId: input.source.workspaceId, sourceId: input.source.id, credentialId, createdAt: input.source.createdAt, updatedAt: input.source.updatedAt, }, }; }; export const projectSourceFromStorage = (input: { sourceRecord: StoredSourceRecord; credentialBinding: SourceCredentialBinding | null; credential: Credential | null; }): Effect.Effect => Effect.gen(function* () { const queryParams = yield* parseStringMapJson( `queryParamsJson for ${input.sourceRecord.id}`, input.sourceRecord.queryParamsJson, ); const headers = yield* parseStringMapJson( `headersJson for ${input.sourceRecord.id}`, input.sourceRecord.headersJson, ); const defaultHeaders = yield* parseStringMapJson( `defaultHeadersJson for ${input.sourceRecord.id}`, input.sourceRecord.defaultHeadersJson, ); let auth: SourceAuth = { kind: "none" }; if (input.credentialBinding !== null) { const credential = input.credential; if (credential === null) { return yield* Effect.fail( new Error(`Missing credential ${input.credentialBinding.credentialId} for source ${input.sourceRecord.id}`), ); } if (credential.authKind === "bearer") { auth = { kind: "bearer", headerName: credential.authHeaderName, prefix: credential.authPrefix, token: { providerId: credential.tokenProviderId, handle: credential.tokenHandle, }, }; } else { auth = { kind: "oauth2", headerName: credential.authHeaderName, prefix: credential.authPrefix, accessToken: { providerId: credential.tokenProviderId, handle: credential.tokenHandle, }, refreshToken: credential.refreshTokenProviderId !== null && credential.refreshTokenHandle !== null ? { providerId: credential.refreshTokenProviderId, handle: credential.refreshTokenHandle, } : null, }; } } return { id: input.sourceRecord.id, workspaceId: input.sourceRecord.workspaceId, name: input.sourceRecord.name, kind: input.sourceRecord.kind, endpoint: input.sourceRecord.endpoint, status: input.sourceRecord.status, enabled: input.sourceRecord.enabled, namespace: input.sourceRecord.namespace, transport: input.sourceRecord.transport, queryParams, headers, specUrl: input.sourceRecord.specUrl, defaultHeaders, auth, sourceHash: input.sourceRecord.sourceHash, lastError: input.sourceRecord.lastError, createdAt: input.sourceRecord.createdAt, updatedAt: input.sourceRecord.updatedAt, } satisfies Source; }).pipe( Effect.mapError((cause) => cause instanceof Error ? cause : new Error(String(cause)), ), ); export const projectSourcesFromStorage = (input: { sourceRecords: ReadonlyArray; credentialBindings: ReadonlyArray; credentials: ReadonlyArray; }): Effect.Effect, Error, never> => { const bindingsBySourceId = new Map( input.credentialBindings.map((binding) => [binding.sourceId, binding]), ); const credentialsById = new Map( input.credentials.map((credential) => [credential.id, credential]), ); return Effect.forEach(input.sourceRecords, (sourceRecord) => { const credentialBinding = bindingsBySourceId.get(sourceRecord.id) ?? null; return projectSourceFromStorage({ sourceRecord, credentialBinding, credential: credentialBinding === null ? null : credentialsById.get(credentialBinding.credentialId) ?? null, }); }); };