820 lines · 27.1 KB · TypeScript
RawDownload
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

Lines 1–500 of 820.