import { Effect, Schema } from "effect"; import { defineSchema, ScopeId, StorageError, type StorageDeps, type StorageFailure,} from "@executor-js/sdk/core"; import { ConfiguredHeaderValue, ConfiguredHeaderBinding, HeaderValue, OAuth2Auth, OAuth2SourceConfig, OpenApiSourceBindingInput, OpenApiSourceBindingRef, OpenApiSourceBindingValue, OperationBinding,} from "./types"; // ---------------------------------------------------------------------------// Schema — three tables:// - openapi_source: one row per onboarded spec (baseUrl, headers, oauth2, ...)// - openapi_operation: one row per operation binding keyed by tool id// - openapi_source_binding: credential bindings for shared sources// --------------------------------------------------------------------------- export const openapiSchema = defineSchema({ openapi_source: { fields: { id: { type: "string", required: true }, scope_id: { type: "string", required: true, index: true }, name: { type: "string", required: true }, spec: { type: "string", required: true }, // Origin URL the spec was fetched from. Set when `addSpec` was // invoked with an http(s) URL; null when the caller passed raw // spec text. Drives `canRefresh` on the core source row and // is the address re-fetched on `refreshSource`. source_url: { type: "string", required: false }, base_url: { type: "string", required: false }, headers: { type: "json", required: false }, query_params: { type: "json", required: false }, oauth2: { type: "json", required: false }, invocation_config: { type: "json", required: true }, }, }, openapi_operation: { fields: { id: { type: "string", required: true }, scope_id: { type: "string", required: true, index: true }, source_id: { type: "string", required: true, index: true }, binding: { type: "json", required: true }, }, }, openapi_source_binding: { fields: { id: { type: "string", required: true }, source_id: { type: "string", required: true, index: true }, source_scope_id: { type: "string", required: true, index: true }, // Intentionally NOT named `scope_id`: this row is visible across // scope stacks and is filtered manually by source/target scope. // The target scope is credential ownership data, not adapter row // ownership. Source owners must be able to delete all descendant // bindings when a shared source is removed. target_scope_id: { type: "string", required: true, index: true }, slot: { type: "string", required: true, index: true }, value: { type: "json", required: true }, created_at: { type: "date", required: true }, updated_at: { type: "date", required: true }, }, },}); export type OpenapiSchema = typeof openapiSchema; // ---------------------------------------------------------------------------// In-memory shapes// --------------------------------------------------------------------------- export interface SourceConfig { readonly spec: string; /** Origin URL when the spec was fetched from http(s). Absent for * raw-text adds. Persisted so `refreshSource` can re-fetch. */ readonly sourceUrl?: string; readonly baseUrl?: string; readonly namespace?: string; readonly headers?: Record<string, ConfiguredHeaderValue>; readonly queryParams?: Record<string, HeaderValue>; readonly specFetchCredentials?: OpenApiSpecFetchCredentials; readonly oauth2?: OAuth2SourceConfig;} export interface OpenApiSpecFetchCredentials { readonly headers?: Record<string, HeaderValue>; readonly queryParams?: Record<string, HeaderValue>;} export interface StoredSource { readonly namespace: string; /** Executor scope id this source row lives in. Writes stamp this on * `scope_id`; reads return whichever scope's row the adapter's * fall-through filter sees first. */ readonly scope: string; readonly name: string; readonly config: SourceConfig; readonly legacy?: { readonly headers?: Record<string, HeaderValue>; readonly oauth2?: OAuth2Auth; };} // ---------------------------------------------------------------------------// Schema-class mirror of StoredSource for the API layer, where we need// an encodable/decodable shape for HTTP responses.// --------------------------------------------------------------------------- export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>( "OpenApiStoredSource",)({ namespace: Schema.String, name: Schema.String, config: Schema.Struct({ spec: Schema.String, sourceUrl: Schema.optional(Schema.String), baseUrl: Schema.optional(Schema.String), namespace: Schema.optional(Schema.String), headers: Schema.optional( Schema.Record(Schema.String, ConfiguredHeaderValue), ), queryParams: Schema.optional( Schema.Record(Schema.String, HeaderValue), ), specFetchCredentials: Schema.optional( Schema.Struct({ headers: Schema.optional( Schema.Record(Schema.String, HeaderValue), ), queryParams: Schema.optional( Schema.Record(Schema.String, HeaderValue), ), }), ), // Canonical source-owned OAuth config. Concrete client credentials // and connection ids live in OpenAPI-owned scoped binding rows. oauth2: Schema.optional(OAuth2SourceConfig), }),}) {} export type StoredSourceSchemaType = typeof StoredSourceSchema.Type; export interface StoredOperation { readonly toolId: string; readonly sourceId: string; readonly binding: OperationBinding;} // ---------------------------------------------------------------------------// Schema encode/decode — OperationBinding has Option fields, so we must use// Schema.encode/decode rather than plain JSON to round-trip correctly.// --------------------------------------------------------------------------- const encodeBinding = Schema.encodeSync(OperationBinding);const decodeBinding = Schema.decodeUnknownSync(OperationBinding); const decodeOAuth2 = Schema.decodeUnknownSync(OAuth2Auth);const encodeOAuth2SourceConfig = Schema.encodeSync(OAuth2SourceConfig);const encodeSourceBindingValue = Schema.encodeSync(OpenApiSourceBindingValue);const decodeSourceBindingValue = Schema.decodeUnknownSync( OpenApiSourceBindingValue,); const asJsonObject = (value: unknown): Record<string, unknown> => { if (value == null) return {}; if (typeof value === "string") return JSON.parse(value) as Record<string, unknown>; return value as Record<string, unknown>;}; const toJsonRecord = (value: unknown): Record<string, unknown> => value as Record<string, unknown>; const toConfiguredHeaderBinding = (value: { readonly slot?: unknown; readonly prefix?: unknown;}): ConfiguredHeaderBinding => new ConfiguredHeaderBinding({ kind: "binding", slot: String(value.slot ?? ""), ...(typeof value.prefix === "string" ? { prefix: value.prefix } : {}), }); const decodeHeaders = (value: unknown): Record<string, HeaderValue> => { if (value == null) return {}; if (typeof value === "string") return JSON.parse(value) as Record<string, HeaderValue>; return value as Record<string, HeaderValue>;}; const slugifySlotPart = (value: string): string => value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "default"; export const headerBindingSlot = (headerName: string): string => `header:${slugifySlotPart(headerName)}`; export const oauth2ClientIdSlot = (securitySchemeName: string): string => `oauth2:${slugifySlotPart(securitySchemeName)}:client-id`; export const oauth2ClientSecretSlot = (securitySchemeName: string): string => `oauth2:${slugifySlotPart(securitySchemeName)}:client-secret`; export const oauth2ConnectionSlot = (securitySchemeName: string): string => `oauth2:${slugifySlotPart(securitySchemeName)}:connection`; const normalizeStoredHeaders = ( value: unknown,): { readonly headers: Record<string, ConfiguredHeaderValue>; readonly legacy: Record<string, HeaderValue>;} => { const raw = decodeHeaders(value); const headers: Record<string, ConfiguredHeaderValue> = {}; const legacy: Record<string, HeaderValue> = {}; for (const [name, header] of Object.entries(raw)) { if (typeof header === "string") { headers[name] = header; legacy[name] = header; continue; } if ( header && typeof header === "object" && "kind" in header && (header as { kind?: unknown }).kind === "binding" ) { headers[name] = toConfiguredHeaderBinding(header); continue; } legacy[name] = header; headers[name] = new ConfiguredHeaderBinding({ kind: "binding", slot: headerBindingSlot(name), prefix: header.prefix, }); } return { headers, legacy };}; const normalizeStoredOAuth2 = ( value: unknown,): { readonly oauth2?: OAuth2SourceConfig; readonly legacy?: OAuth2Auth;} => { if (value == null) return {}; const parsed = typeof value === "string" ? JSON.parse(value) : value; if (parsed && typeof parsed === "object" && "connectionSlot" in parsed) { return { oauth2: Schema.decodeUnknownSync(OAuth2SourceConfig)(parsed), }; } const legacy = decodeOAuth2(parsed); return { legacy, oauth2: new OAuth2SourceConfig({ kind: "oauth2", securitySchemeName: legacy.securitySchemeName, flow: legacy.flow, tokenUrl: legacy.tokenUrl, authorizationUrl: legacy.authorizationUrl, clientIdSlot: oauth2ClientIdSlot(legacy.securitySchemeName), clientSecretSlot: legacy.clientSecretSecretId ? oauth2ClientSecretSlot(legacy.securitySchemeName) : null, connectionSlot: oauth2ConnectionSlot(legacy.securitySchemeName), scopes: [...legacy.scopes], }), };}; // ---------------------------------------------------------------------------// Store interface// --------------------------------------------------------------------------- // Every method routes through the typed adapter (`ctx.storage.adapter`)// so the typed error channel is `StorageFailure`. Schema-decode failures// inside `Effect.gen` land as defects, not typed errors, and are caught// by the HTTP edge's observability middleware.//// Every read/write that targets a single row pins BOTH the natural id// (namespace, toolId, sessionId) AND the owning `scope_id`. The store// runs behind the scoped adapter (which auto-injects `scope_id IN// (stack)`), so a bare `{id}` filter resolves to any matching row in// the stack in adapter-iteration order. For shadowed rows (same id at// multiple scopes — e.g. an org-level openapi source with a per-user// override), that's a scope-isolation bug: updates and deletes can// land on the wrong scope's row. Callers thread the resolved scope in// (typically `path.scopeId` for HTTP, `toolRow.scope_id` /// `input.scope` for invokeTool/lifecycle) so every keyed mutation// targets exactly one row.export interface OpenapiStore { readonly upsertSource: ( input: StoredSource, operations: readonly StoredOperation[], ) => Effect.Effect<void, StorageFailure>; readonly updateSourceMeta: ( namespace: string, scope: string, patch: { readonly name?: string; readonly baseUrl?: string; readonly headers?: Record<string, ConfiguredHeaderValue>; readonly queryParams?: Record<string, HeaderValue>; readonly oauth2?: OAuth2SourceConfig; }, ) => Effect.Effect<void, StorageFailure>; readonly getSource: ( namespace: string, scope: string, ) => Effect.Effect<StoredSource | null, StorageFailure>; readonly listSources: () => Effect.Effect< readonly StoredSource[], StorageFailure >; readonly getOperationByToolId: ( toolId: string, scope: string, ) => Effect.Effect<StoredOperation | null, StorageFailure>; readonly listOperationsBySource: ( sourceId: string, scope: string, ) => Effect.Effect<readonly StoredOperation[], StorageFailure>; readonly removeSource: ( namespace: string, scope: string, ) => Effect.Effect<void, StorageFailure>; readonly listSourceBindings: ( sourceId: string, sourceScope: string, ) => Effect.Effect<readonly OpenApiSourceBindingRef[], StorageFailure>; readonly resolveSourceBinding: ( sourceId: string, sourceScope: string, slot: string, ) => Effect.Effect<OpenApiSourceBindingRef | null, StorageFailure>; readonly setSourceBinding: ( input: OpenApiSourceBindingInput, ) => Effect.Effect<OpenApiSourceBindingRef, StorageFailure>; readonly removeSourceBinding: ( sourceId: string, sourceScope: string, slot: string, scope: string, ) => Effect.Effect<void, StorageFailure>;} // ---------------------------------------------------------------------------// Default store implementation// --------------------------------------------------------------------------- export const makeDefaultOpenapiStore = ({ adapter, scopes,}: StorageDeps<OpenapiSchema>): OpenapiStore => { const scopeIds = scopes.map((scope) => scope.id as string); const scopePrecedence = new Map<string, number>(); scopeIds.forEach((scope, index) => scopePrecedence.set(scope, index)); const scopeRank = (scopeId: string): number => scopePrecedence.get(scopeId) ?? Infinity; const encodeSyntheticRowIdPart = (value: string): string => encodeURIComponent(value); const sourceBindingRowId = ( sourceId: string, sourceScopeId: string, slot: string, scopeId: string, ) => [ "openapi-source-binding", encodeSyntheticRowIdPart(sourceScopeId), encodeSyntheticRowIdPart(sourceId), encodeSyntheticRowIdPart(slot), encodeSyntheticRowIdPart(scopeId), ].join("::"); const rowToSourceBinding = ( row: Record<string, unknown>, ): OpenApiSourceBindingRef => new OpenApiSourceBindingRef({ sourceId: row.source_id as string, sourceScopeId: ScopeId.make(row.source_scope_id as string), scopeId: ScopeId.make(row.target_scope_id as string), slot: row.slot as string, value: decodeSourceBindingValue(asJsonObject(row.value)), createdAt: row.created_at instanceof Date ? row.created_at : new Date(row.created_at as string), updatedAt: row.updated_at instanceof Date ? row.updated_at : new Date(row.updated_at as string), }); const validateBindingTarget = (params: { readonly sourceId: string; readonly sourceScope: string; readonly targetScope: string; }) => Effect.gen(function* () { if (!scopeIds.includes(params.sourceScope)) { return yield* Effect.fail( new StorageError({ message: `OpenAPI source binding references source scope "${params.sourceScope}" ` + `which is not in the executor's scope stack [${scopeIds.join(", ")}].`, cause: undefined, }), ); } if (!scopeIds.includes(params.targetScope)) { return yield* Effect.fail( new StorageError({ message: `OpenAPI source binding targets scope "${params.targetScope}" which is not ` + `in the executor's scope stack [${scopeIds.join(", ")}].`, cause: undefined, }), ); } const source = yield* adapter.findOne({ model: "openapi_source", where: [ { field: "id", value: params.sourceId }, { field: "scope_id", value: params.sourceScope }, ], }); if (!source) { return yield* Effect.fail( new StorageError({ message: `OpenAPI source "${params.sourceId}" does not exist at scope "${params.sourceScope}"`, cause: undefined, }), ); } if (scopeRank(params.targetScope) > scopeRank(params.sourceScope)) { return yield* Effect.fail( new StorageError({ message: `OpenAPI source bindings for "${params.sourceId}" cannot be written at ` + `outer scope "${params.targetScope}" because the base source lives at ` + `"${params.sourceScope}"`, cause: undefined, }), ); } return source; }); const rowToSource = (row: Record<string, unknown>): StoredSource => { const normalizedHeaders = normalizeStoredHeaders(row.headers); const normalizedOAuth2 = normalizeStoredOAuth2(row.oauth2); const invocationConfig = asJsonObject(row.invocation_config); return { namespace: row.id as string, scope: row.scope_id as string, name: row.name as string, config: { spec: row.spec as string, sourceUrl: (row.source_url as string | null | undefined) ?? undefined, baseUrl: (row.base_url as string | null | undefined) ?? undefined, headers: normalizedHeaders.headers, queryParams: decodeHeaders(row.query_params), specFetchCredentials: invocationConfig.specFetchCredentials as | OpenApiSpecFetchCredentials | undefined, oauth2: normalizedOAuth2.oauth2, }, legacy: Object.keys(normalizedHeaders.legacy).length > 0 || normalizedOAuth2.legacy ? { ...(Object.keys(normalizedHeaders.legacy).length > 0Lines 1–500 of 820. Load more
import { Effect, Schema } from "effect"; import { defineSchema, ScopeId, StorageError, type StorageDeps, type StorageFailure, } from "@executor-js/sdk/core"; import { ConfiguredHeaderValue, ConfiguredHeaderBinding, HeaderValue, OAuth2Auth, OAuth2SourceConfig, OpenApiSourceBindingInput, OpenApiSourceBindingRef, OpenApiSourceBindingValue, OperationBinding, } from "./types"; // --------------------------------------------------------------------------- // Schema — three tables: // - openapi_source: one row per onboarded spec (baseUrl, headers, oauth2, ...) // - openapi_operation: one row per operation binding keyed by tool id // - openapi_source_binding: credential bindings for shared sources // --------------------------------------------------------------------------- export const openapiSchema = defineSchema({ openapi_source: { fields: { id: { type: "string", required: true }, scope_id: { type: "string", required: true, index: true }, name: { type: "string", required: true }, spec: { type: "string", required: true }, // Origin URL the spec was fetched from. Set when `addSpec` was // invoked with an http(s) URL; null when the caller passed raw // spec text. Drives `canRefresh` on the core source row and // is the address re-fetched on `refreshSource`. source_url: { type: "string", required: false }, base_url: { type: "string", required: false }, headers: { type: "json", required: false }, query_params: { type: "json", required: false }, oauth2: { type: "json", required: false }, invocation_config: { type: "json", required: true }, }, }, openapi_operation: { fields: { id: { type: "string", required: true }, scope_id: { type: "string", required: true, index: true }, source_id: { type: "string", required: true, index: true }, binding: { type: "json", required: true }, }, }, openapi_source_binding: { fields: { id: { type: "string", required: true }, source_id: { type: "string", required: true, index: true }, source_scope_id: { type: "string", required: true, index: true }, // Intentionally NOT named `scope_id`: this row is visible across // scope stacks and is filtered manually by source/target scope. // The target scope is credential ownership data, not adapter row // ownership. Source owners must be able to delete all descendant // bindings when a shared source is removed. target_scope_id: { type: "string", required: true, index: true }, slot: { type: "string", required: true, index: true }, value: { type: "json", required: true }, created_at: { type: "date", required: true }, updated_at: { type: "date", required: true }, }, }, }); export type OpenapiSchema = typeof openapiSchema; // --------------------------------------------------------------------------- // In-memory shapes // --------------------------------------------------------------------------- export interface SourceConfig { readonly spec: string; /** Origin URL when the spec was fetched from http(s). Absent for * raw-text adds. Persisted so `refreshSource` can re-fetch. */ readonly sourceUrl?: string; readonly baseUrl?: string; readonly namespace?: string; readonly headers?: Record<string, ConfiguredHeaderValue>; readonly queryParams?: Record<string, HeaderValue>; readonly specFetchCredentials?: OpenApiSpecFetchCredentials; readonly oauth2?: OAuth2SourceConfig; } export interface OpenApiSpecFetchCredentials { readonly headers?: Record<string, HeaderValue>; readonly queryParams?: Record<string, HeaderValue>; } export interface StoredSource { readonly namespace: string; /** Executor scope id this source row lives in. Writes stamp this on * `scope_id`; reads return whichever scope's row the adapter's * fall-through filter sees first. */ readonly scope: string; readonly name: string; readonly config: SourceConfig; readonly legacy?: { readonly headers?: Record<string, HeaderValue>; readonly oauth2?: OAuth2Auth; }; } // --------------------------------------------------------------------------- // Schema-class mirror of StoredSource for the API layer, where we need // an encodable/decodable shape for HTTP responses. // --------------------------------------------------------------------------- export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>( "OpenApiStoredSource", )({ namespace: Schema.String, name: Schema.String, config: Schema.Struct({ spec: Schema.String, sourceUrl: Schema.optional(Schema.String), baseUrl: Schema.optional(Schema.String), namespace: Schema.optional(Schema.String), headers: Schema.optional( Schema.Record(Schema.String, ConfiguredHeaderValue), ), queryParams: Schema.optional( Schema.Record(Schema.String, HeaderValue), ), specFetchCredentials: Schema.optional( Schema.Struct({ headers: Schema.optional( Schema.Record(Schema.String, HeaderValue), ), queryParams: Schema.optional( Schema.Record(Schema.String, HeaderValue), ), }), ), // Canonical source-owned OAuth config. Concrete client credentials // and connection ids live in OpenAPI-owned scoped binding rows. oauth2: Schema.optional(OAuth2SourceConfig), }), }) {} export type StoredSourceSchemaType = typeof StoredSourceSchema.Type; export interface StoredOperation { readonly toolId: string; readonly sourceId: string; readonly binding: OperationBinding; } // --------------------------------------------------------------------------- // Schema encode/decode — OperationBinding has Option fields, so we must use // Schema.encode/decode rather than plain JSON to round-trip correctly. // --------------------------------------------------------------------------- const encodeBinding = Schema.encodeSync(OperationBinding); const decodeBinding = Schema.decodeUnknownSync(OperationBinding); const decodeOAuth2 = Schema.decodeUnknownSync(OAuth2Auth); const encodeOAuth2SourceConfig = Schema.encodeSync(OAuth2SourceConfig); const encodeSourceBindingValue = Schema.encodeSync(OpenApiSourceBindingValue); const decodeSourceBindingValue = Schema.decodeUnknownSync( OpenApiSourceBindingValue, ); const asJsonObject = (value: unknown): Record<string, unknown> => { if (value == null) return {}; if (typeof value === "string") return JSON.parse(value) as Record<string, unknown>; return value as Record<string, unknown>; }; const toJsonRecord = (value: unknown): Record<string, unknown> => value as Record<string, unknown>; const toConfiguredHeaderBinding = (value: { readonly slot?: unknown; readonly prefix?: unknown; }): ConfiguredHeaderBinding => new ConfiguredHeaderBinding({ kind: "binding", slot: String(value.slot ?? ""), ...(typeof value.prefix === "string" ? { prefix: value.prefix } : {}), }); const decodeHeaders = (value: unknown): Record<string, HeaderValue> => { if (value == null) return {}; if (typeof value === "string") return JSON.parse(value) as Record<string, HeaderValue>; return value as Record<string, HeaderValue>; }; const slugifySlotPart = (value: string): string => value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "default"; export const headerBindingSlot = (headerName: string): string => `header:${slugifySlotPart(headerName)}`; export const oauth2ClientIdSlot = (securitySchemeName: string): string => `oauth2:${slugifySlotPart(securitySchemeName)}:client-id`; export const oauth2ClientSecretSlot = (securitySchemeName: string): string => `oauth2:${slugifySlotPart(securitySchemeName)}:client-secret`; export const oauth2ConnectionSlot = (securitySchemeName: string): string => `oauth2:${slugifySlotPart(securitySchemeName)}:connection`; const normalizeStoredHeaders = ( value: unknown, ): { readonly headers: Record<string, ConfiguredHeaderValue>; readonly legacy: Record<string, HeaderValue>; } => { const raw = decodeHeaders(value); const headers: Record<string, ConfiguredHeaderValue> = {}; const legacy: Record<string, HeaderValue> = {}; for (const [name, header] of Object.entries(raw)) { if (typeof header === "string") { headers[name] = header; legacy[name] = header; continue; } if ( header && typeof header === "object" && "kind" in header && (header as { kind?: unknown }).kind === "binding" ) { headers[name] = toConfiguredHeaderBinding(header); continue; } legacy[name] = header; headers[name] = new ConfiguredHeaderBinding({ kind: "binding", slot: headerBindingSlot(name), prefix: header.prefix, }); } return { headers, legacy }; }; const normalizeStoredOAuth2 = ( value: unknown, ): { readonly oauth2?: OAuth2SourceConfig; readonly legacy?: OAuth2Auth; } => { if (value == null) return {}; const parsed = typeof value === "string" ? JSON.parse(value) : value; if (parsed && typeof parsed === "object" && "connectionSlot" in parsed) { return { oauth2: Schema.decodeUnknownSync(OAuth2SourceConfig)(parsed), }; } const legacy = decodeOAuth2(parsed); return { legacy, oauth2: new OAuth2SourceConfig({ kind: "oauth2", securitySchemeName: legacy.securitySchemeName, flow: legacy.flow, tokenUrl: legacy.tokenUrl, authorizationUrl: legacy.authorizationUrl, clientIdSlot: oauth2ClientIdSlot(legacy.securitySchemeName), clientSecretSlot: legacy.clientSecretSecretId ? oauth2ClientSecretSlot(legacy.securitySchemeName) : null, connectionSlot: oauth2ConnectionSlot(legacy.securitySchemeName), scopes: [...legacy.scopes], }), }; }; // --------------------------------------------------------------------------- // Store interface // --------------------------------------------------------------------------- // Every method routes through the typed adapter (`ctx.storage.adapter`) // so the typed error channel is `StorageFailure`. Schema-decode failures // inside `Effect.gen` land as defects, not typed errors, and are caught // by the HTTP edge's observability middleware. // // Every read/write that targets a single row pins BOTH the natural id // (namespace, toolId, sessionId) AND the owning `scope_id`. The store // runs behind the scoped adapter (which auto-injects `scope_id IN // (stack)`), so a bare `{id}` filter resolves to any matching row in // the stack in adapter-iteration order. For shadowed rows (same id at // multiple scopes — e.g. an org-level openapi source with a per-user // override), that's a scope-isolation bug: updates and deletes can // land on the wrong scope's row. Callers thread the resolved scope in // (typically `path.scopeId` for HTTP, `toolRow.scope_id` / // `input.scope` for invokeTool/lifecycle) so every keyed mutation // targets exactly one row. export interface OpenapiStore { readonly upsertSource: ( input: StoredSource, operations: readonly StoredOperation[], ) => Effect.Effect<void, StorageFailure>; readonly updateSourceMeta: ( namespace: string, scope: string, patch: { readonly name?: string; readonly baseUrl?: string; readonly headers?: Record<string, ConfiguredHeaderValue>; readonly queryParams?: Record<string, HeaderValue>; readonly oauth2?: OAuth2SourceConfig; }, ) => Effect.Effect<void, StorageFailure>; readonly getSource: ( namespace: string, scope: string, ) => Effect.Effect<StoredSource | null, StorageFailure>; readonly listSources: () => Effect.Effect< readonly StoredSource[], StorageFailure >; readonly getOperationByToolId: ( toolId: string, scope: string, ) => Effect.Effect<StoredOperation | null, StorageFailure>; readonly listOperationsBySource: ( sourceId: string, scope: string, ) => Effect.Effect<readonly StoredOperation[], StorageFailure>; readonly removeSource: ( namespace: string, scope: string, ) => Effect.Effect<void, StorageFailure>; readonly listSourceBindings: ( sourceId: string, sourceScope: string, ) => Effect.Effect<readonly OpenApiSourceBindingRef[], StorageFailure>; readonly resolveSourceBinding: ( sourceId: string, sourceScope: string, slot: string, ) => Effect.Effect<OpenApiSourceBindingRef | null, StorageFailure>; readonly setSourceBinding: ( input: OpenApiSourceBindingInput, ) => Effect.Effect<OpenApiSourceBindingRef, StorageFailure>; readonly removeSourceBinding: ( sourceId: string, sourceScope: string, slot: string, scope: string, ) => Effect.Effect<void, StorageFailure>; } // --------------------------------------------------------------------------- // Default store implementation // --------------------------------------------------------------------------- export const makeDefaultOpenapiStore = ({ adapter, scopes, }: StorageDeps<OpenapiSchema>): OpenapiStore => { const scopeIds = scopes.map((scope) => scope.id as string); const scopePrecedence = new Map<string, number>(); scopeIds.forEach((scope, index) => scopePrecedence.set(scope, index)); const scopeRank = (scopeId: string): number => scopePrecedence.get(scopeId) ?? Infinity; const encodeSyntheticRowIdPart = (value: string): string => encodeURIComponent(value); const sourceBindingRowId = ( sourceId: string, sourceScopeId: string, slot: string, scopeId: string, ) => [ "openapi-source-binding", encodeSyntheticRowIdPart(sourceScopeId), encodeSyntheticRowIdPart(sourceId), encodeSyntheticRowIdPart(slot), encodeSyntheticRowIdPart(scopeId), ].join("::"); const rowToSourceBinding = ( row: Record<string, unknown>, ): OpenApiSourceBindingRef => new OpenApiSourceBindingRef({ sourceId: row.source_id as string, sourceScopeId: ScopeId.make(row.source_scope_id as string), scopeId: ScopeId.make(row.target_scope_id as string), slot: row.slot as string, value: decodeSourceBindingValue(asJsonObject(row.value)), createdAt: row.created_at instanceof Date ? row.created_at : new Date(row.created_at as string), updatedAt: row.updated_at instanceof Date ? row.updated_at : new Date(row.updated_at as string), }); const validateBindingTarget = (params: { readonly sourceId: string; readonly sourceScope: string; readonly targetScope: string; }) => Effect.gen(function* () { if (!scopeIds.includes(params.sourceScope)) { return yield* Effect.fail( new StorageError({ message: `OpenAPI source binding references source scope "${params.sourceScope}" ` + `which is not in the executor's scope stack [${scopeIds.join(", ")}].`, cause: undefined, }), ); } if (!scopeIds.includes(params.targetScope)) { return yield* Effect.fail( new StorageError({ message: `OpenAPI source binding targets scope "${params.targetScope}" which is not ` + `in the executor's scope stack [${scopeIds.join(", ")}].`, cause: undefined, }), ); } const source = yield* adapter.findOne({ model: "openapi_source", where: [ { field: "id", value: params.sourceId }, { field: "scope_id", value: params.sourceScope }, ], }); if (!source) { return yield* Effect.fail( new StorageError({ message: `OpenAPI source "${params.sourceId}" does not exist at scope "${params.sourceScope}"`, cause: undefined, }), ); } if (scopeRank(params.targetScope) > scopeRank(params.sourceScope)) { return yield* Effect.fail( new StorageError({ message: `OpenAPI source bindings for "${params.sourceId}" cannot be written at ` + `outer scope "${params.targetScope}" because the base source lives at ` + `"${params.sourceScope}"`, cause: undefined, }), ); } return source; }); const rowToSource = (row: Record<string, unknown>): StoredSource => { const normalizedHeaders = normalizeStoredHeaders(row.headers); const normalizedOAuth2 = normalizeStoredOAuth2(row.oauth2); const invocationConfig = asJsonObject(row.invocation_config); return { namespace: row.id as string, scope: row.scope_id as string, name: row.name as string, config: { spec: row.spec as string, sourceUrl: (row.source_url as string | null | undefined) ?? undefined, baseUrl: (row.base_url as string | null | undefined) ?? undefined, headers: normalizedHeaders.headers, queryParams: decodeHeaders(row.query_params), specFetchCredentials: invocationConfig.specFetchCredentials as | OpenApiSpecFetchCredentials | undefined, oauth2: normalizedOAuth2.oauth2, }, legacy: Object.keys(normalizedHeaders.legacy).length > 0 || normalizedOAuth2.legacy ? { ...(Object.keys(normalizedHeaders.legacy).length > 0