@executor-js/cli 0.0.1-beta.3 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/index.js +539 -46
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/commands/generate.ts","../../storage-core/src/factory.ts","../../sdk/src/ids.ts","../../sdk/src/scope.ts","../../sdk/src/errors.ts","../../sdk/src/types.ts","../../sdk/src/core-schema.ts","../../sdk/src/secrets.ts","../../sdk/src/elicitation.ts","../../sdk/src/blob.ts","../../sdk/src/executor.ts","../../storage-core/src/testing/memory.ts","../src/utils/get-config.ts","../src/generators/drizzle.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from \"commander\";\nimport { generate } from \"./commands/generate.js\";\n\nprocess.on(\"SIGINT\", () => process.exit(0));\nprocess.on(\"SIGTERM\", () => process.exit(0));\n\nconst program = new Command(\"executor\")\n .version(\"0.0.1\")\n .description(\"Executor CLI\")\n .addCommand(generate)\n .action(() => program.help());\n\nprogram.parse();\n","import { existsSync } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\nimport { collectSchemas } from \"@executor/sdk/core\";\nimport { getConfig } from \"../utils/get-config.js\";\nimport { generateDrizzleSchema } from \"../generators/drizzle.js\";\n\nasync function generateAction(opts: {\n cwd: string;\n config?: string;\n output?: string;\n}) {\n const cwd = path.resolve(opts.cwd);\n if (!existsSync(cwd)) {\n console.error(`The directory \"${cwd}\" does not exist.`);\n process.exit(1);\n }\n\n const config = await getConfig({ cwd, configPath: opts.config });\n if (!config) {\n console.error(\n \"No configuration file found. Add an `executor.config.ts` file to \" +\n \"your project or pass the path using the `--config` flag.\",\n );\n process.exit(1);\n }\n\n const schema = collectSchemas(config.plugins);\n\n const result = await generateDrizzleSchema({\n schema,\n dialect: config.dialect,\n file: opts.output,\n });\n\n if (!result.code) {\n console.log(\"Schema is already up to date.\");\n process.exit(0);\n }\n\n const outPath = path.resolve(cwd, result.fileName);\n const outDir = path.dirname(outPath);\n if (!existsSync(outDir)) {\n await fs.mkdir(outDir, { recursive: true });\n }\n\n await fs.writeFile(outPath, result.code);\n console.log(`Schema generated: ${path.relative(cwd, outPath)}`);\n}\n\nexport const generate = new Command(\"generate\")\n .description(\"Generate a drizzle schema file from the executor config\")\n .option(\n \"-c, --cwd <cwd>\",\n \"the working directory\",\n process.cwd(),\n )\n .option(\n \"--config <config>\",\n \"path to the executor config file\",\n )\n .option(\n \"--output <output>\",\n \"output file path for the generated schema\",\n )\n .action(generateAction);\n","// ---------------------------------------------------------------------------\n// createAdapter — factory that wraps a CustomAdapter into a DBAdapter.\n//\n// Vendored from better-auth (packages/core/src/db/adapter/factory.ts) under\n// MIT. Adapted for executor:\n// - Promise/async → Effect.Effect<T, Error>\n// - Stripped auth-specific concerns (numeric serial ids, joins, telemetry\n// spans, logger, plural model name resolution, BetterAuthOptions)\n// - Contract matches our CustomAdapter + DBAdapterFactoryConfig in\n// ./adapter.ts (simpler than better-auth's equivalents)\n//\n// Responsibilities:\n// - id generation (auto + customIdGenerator + forceAllowId)\n// - transformInput: apply defaultValue / onUpdate / transform.input,\n// map logical field names → physical column names, serialize JSON /\n// dates / booleans / arrays based on supports* flags\n// - transformOutput: map physical column names → logical field names,\n// deserialize JSON / dates / booleans / arrays, apply transform.output,\n// filter by `returned: false`\n// - transformWhereClause: fill in CleanedWhere defaults, rename field,\n// re-encode RHS to match the write path\n// - createMany fallback: loop create when the CustomAdapter doesn't\n// implement it natively\n// - transaction: delegate to config.transaction when provided; fall back\n// to running the callback against the current adapter\n// ---------------------------------------------------------------------------\n\nimport { Effect } from \"effect\";\n\nimport type {\n CleanedWhere,\n CustomAdapter,\n DBAdapter,\n DBAdapterFactoryConfig,\n DBTransactionAdapter,\n JoinConfig,\n JoinOption,\n Where,\n} from \"./adapter\";\nimport type { DBFieldAttribute, DBSchema, DBPrimitive } from \"./schema\";\n\n// ---------------------------------------------------------------------------\n// Id generation\n// ---------------------------------------------------------------------------\n\nconst defaultGenerateId = (): string =>\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `id_${Math.random().toString(36).slice(2)}_${Date.now().toString(36)}`;\n\n// ---------------------------------------------------------------------------\n// Default value helpers — mirrors better-auth's `withApplyDefault`.\n// ---------------------------------------------------------------------------\n\nconst withApplyDefault = (\n value: unknown,\n field: DBFieldAttribute,\n action: \"create\" | \"update\",\n): unknown => {\n if (action === \"update\") {\n // Only apply onUpdate when the caller DID NOT supply a value. An explicit\n // `updatedAt: someDate` in the update payload should win over the\n // plugin's onUpdate hook — matches upstream.\n if (value === undefined && field.onUpdate !== undefined) {\n return field.onUpdate();\n }\n return value;\n }\n // Create: apply defaultValue only when the caller omitted the field, OR\n // when they passed null for a required field (upstream convention —\n // explicit null on an optional/nullable field is preserved). Without the\n // `required` gate we'd silently overwrite legitimate null writes.\n const triggerDefault =\n value === undefined || (field.required === true && value === null);\n if (triggerDefault && field.defaultValue !== undefined) {\n return typeof field.defaultValue === \"function\"\n ? (field.defaultValue as () => DBPrimitive)()\n : field.defaultValue;\n }\n return value;\n};\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\nexport interface CreateAdapterOptions {\n readonly schema: DBSchema;\n readonly config: DBAdapterFactoryConfig;\n readonly adapter: CustomAdapter;\n}\n\n/**\n * Wrap a CustomAdapter into a full DBAdapter that applies schema-driven\n * transforms. This is the single codepath every backend shares.\n */\nexport const createAdapter = (\n options: CreateAdapterOptions,\n): DBAdapter => {\n const { schema, adapter: inner } = options;\n const config: Required<\n Pick<\n DBAdapterFactoryConfig,\n | \"adapterId\"\n | \"supportsJSON\"\n | \"supportsDates\"\n | \"supportsBooleans\"\n | \"supportsArrays\"\n | \"disableIdGeneration\"\n >\n > & DBAdapterFactoryConfig = {\n ...options.config,\n supportsJSON: options.config.supportsJSON ?? false,\n supportsDates: options.config.supportsDates ?? true,\n supportsBooleans: options.config.supportsBooleans ?? true,\n supportsArrays: options.config.supportsArrays ?? false,\n disableIdGeneration: options.config.disableIdGeneration ?? false,\n };\n\n const idGen = (model: string): string => {\n if (config.customIdGenerator) {\n return config.customIdGenerator({ model });\n }\n return defaultGenerateId();\n };\n\n const getModelDef = (model: string): Effect.Effect<DBSchema[string], Error> =>\n Effect.gen(function* () {\n const def = schema[model];\n if (!def) {\n return yield* Effect.fail(\n new Error(`[storage-core] unknown model \"${model}\"`),\n );\n }\n return def;\n });\n\n // Sync accessor for call sites that can't sit inside Effect.gen (cleanWhere,\n // getModelName, getPhysicalField). These are all fed model names that have\n // already been validated upstream by the typed API, so unknown-model throws\n // here are a caller bug, not a runtime failure channel.\n const getModelDefSync = (model: string): DBSchema[string] => {\n const def = schema[model];\n if (!def) throw new Error(`[storage-core] unknown model \"${model}\"`);\n return def;\n };\n\n // Map physical table name → logical model key, for renaming incoming model\n // arg in mapKeysTransformInput/Output when callers pass physical names.\n // We deliberately *don't* support plural or physical-name inputs — our\n // plugins always pass the logical key — so getModelName is identity.\n const getModelName = (model: string): string =>\n getModelDefSync(model).modelName ?? model;\n\n // Field name (logical → physical). Honors mapKeysTransformInput override.\n const getPhysicalField = (model: string, logical: string): string => {\n if (logical === \"id\") return config.mapKeysTransformInput?.[\"id\"] ?? \"id\";\n const override = config.mapKeysTransformInput?.[logical];\n if (override) return override;\n const attr = getModelDefSync(model).fields[logical];\n return attr?.fieldName ?? logical;\n };\n\n // Inverse of mapKeysTransformOutput: on the output path we may need to\n // rename a logical field to a different output key for the caller (symmetric\n // to mapKeysTransformInput on the write path). Upstream better-auth wires\n // this in the same place.\n const getOutputKey = (logical: string): string =>\n config.mapKeysTransformOutput?.[logical] ?? logical;\n\n // ---------------------------------------------------------------------------\n // Value encode / decode based on supports* flags.\n // ---------------------------------------------------------------------------\n\n const encodeValue = (\n attr: DBFieldAttribute | undefined,\n value: unknown,\n ): unknown => {\n if (value === undefined) return undefined;\n if (value === null) return null;\n if (!attr) return value;\n const type = attr.type;\n if (type === \"json\") {\n if (!config.supportsJSON) return JSON.stringify(value);\n return value;\n }\n if (type === \"date\") {\n if (value instanceof Date) {\n return config.supportsDates ? value : value.toISOString();\n }\n if (typeof value === \"string\" && !config.supportsDates) {\n // Keep ISO strings as-is\n return value;\n }\n return value;\n }\n if (type === \"boolean\") {\n if (config.supportsBooleans) return value;\n return value ? 1 : 0;\n }\n if (\n (type === \"string[]\" || type === \"number[]\") &&\n Array.isArray(value) &&\n !config.supportsArrays\n ) {\n return JSON.stringify(value);\n }\n return value;\n };\n\n const decodeValue = (\n attr: DBFieldAttribute | undefined,\n value: unknown,\n ): unknown => {\n if (value === undefined || value === null) return value;\n if (!attr) return value;\n const type = attr.type;\n if (type === \"json\" && typeof value === \"string\") {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n if (type === \"date\") {\n if (value instanceof Date) return value;\n if (typeof value === \"string\" || typeof value === \"number\") {\n return new Date(value);\n }\n return value;\n }\n if (type === \"boolean\" && typeof value === \"number\") {\n return value === 1;\n }\n if (\n (type === \"string[]\" || type === \"number[]\") &&\n typeof value === \"string\"\n ) {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n if (type === \"number\" && typeof value === \"string\") {\n const n = Number(value);\n return Number.isNaN(n) ? value : n;\n }\n return value;\n };\n\n // ---------------------------------------------------------------------------\n // transformInput — logical row → physical row, with defaults + id gen\n // ---------------------------------------------------------------------------\n\n const transformInput = (\n model: string,\n data: Record<string, unknown>,\n action: \"create\" | \"update\",\n forceAllowId: boolean,\n ): Effect.Effect<Record<string, unknown>, Error> =>\n Effect.gen(function* () {\n const def = yield* getModelDef(model);\n const out: Record<string, unknown> = {};\n\n // id handling on create\n if (action === \"create\") {\n if (forceAllowId && \"id\" in data && data.id !== undefined && data.id !== null) {\n out[getPhysicalField(model, \"id\")] = data.id;\n } else if (!config.disableIdGeneration) {\n out[getPhysicalField(model, \"id\")] = idGen(model);\n }\n }\n\n for (const [logical, attr] of Object.entries(def.fields)) {\n if (logical === \"id\") continue;\n if (attr.input === false) continue;\n let value: unknown = data[logical];\n\n // Date coercion from string\n if (\n attr.type === \"date\" &&\n value !== undefined &&\n value !== null &&\n !(value instanceof Date) &&\n typeof value === \"string\"\n ) {\n try {\n value = new Date(value);\n } catch {\n // leave as-is\n }\n }\n\n // defaultValue / onUpdate\n value = withApplyDefault(value, attr, action);\n\n // transform.input\n if (attr.transform?.input) {\n const res = attr.transform.input(value as DBPrimitive);\n // Sync only in executor path; if a plugin returns a Promise we\n // await it via tryPromise to keep the Effect pure.\n if (res && typeof (res as { then?: unknown }).then === \"function\") {\n value = yield* Effect.tryPromise({\n try: () => res as Promise<DBPrimitive>,\n catch: (e) =>\n new Error(\n `[storage-core] transform.input for \"${model}.${logical}\" failed: ${String(e)}`,\n ),\n });\n } else {\n value = res;\n }\n }\n\n if (value === undefined) continue;\n\n const physical = getPhysicalField(model, logical);\n let encoded = encodeValue(attr, value);\n\n // customTransformInput — user-land per-field hook, runs after the\n // built-in encode step. Effect-ified from upstream's sync `any`\n // return — plugins that don't need async work can wrap with\n // `Effect.succeed`.\n if (config.customTransformInput) {\n encoded = yield* config.customTransformInput({\n data: encoded,\n fieldAttributes: attr,\n field: physical,\n action,\n model: getModelName(model),\n schema,\n });\n }\n\n out[physical] = encoded;\n }\n\n return out;\n });\n\n // ---------------------------------------------------------------------------\n // transformOutput — physical row → logical row, filter `returned: false`\n // ---------------------------------------------------------------------------\n\n const transformOutput = (\n model: string,\n row: Record<string, unknown> | null,\n select?: string[],\n ): Effect.Effect<Record<string, unknown> | null, Error> =>\n Effect.gen(function* () {\n if (row === null || row === undefined) return null;\n const def = yield* getModelDef(model);\n const out: Record<string, unknown> = {};\n\n // id always returned\n const idPhysical = getPhysicalField(model, \"id\");\n const idOutputKey = getOutputKey(\"id\");\n if (idPhysical in row && row[idPhysical] !== undefined) {\n out[idOutputKey] = row[idPhysical];\n } else if (\"id\" in row) {\n out[idOutputKey] = row[\"id\"];\n }\n\n for (const [logical, attr] of Object.entries(def.fields)) {\n if (logical === \"id\") continue;\n if (attr.returned === false) continue;\n if (select && select.length > 0 && !select.includes(logical)) continue;\n\n const physical = getPhysicalField(model, logical);\n if (!(physical in row)) continue;\n\n let value: unknown = decodeValue(attr, row[physical]);\n\n if (attr.transform?.output) {\n const res = attr.transform.output(value as DBPrimitive);\n if (res && typeof (res as { then?: unknown }).then === \"function\") {\n value = yield* Effect.tryPromise({\n try: () => res as Promise<DBPrimitive>,\n catch: (e) =>\n new Error(\n `[storage-core] transform.output for \"${model}.${logical}\" failed: ${String(e)}`,\n ),\n });\n } else {\n value = res;\n }\n }\n\n // customTransformOutput — user-land per-field hook, runs after the\n // built-in decode step. Mirrors upstream threading.\n if (config.customTransformOutput) {\n value = yield* config.customTransformOutput({\n data: value,\n fieldAttributes: attr,\n field: logical,\n select: select ?? [],\n model: getModelName(model),\n schema,\n });\n }\n\n out[getOutputKey(logical)] = value;\n }\n return out;\n });\n\n // ---------------------------------------------------------------------------\n // transformWhereClause — Where[] → CleanedWhere[]\n //\n // Fills in defaults, renames logical → physical, re-encodes RHS so that\n // filter comparisons line up with the wire representation produced by\n // transformInput.\n // ---------------------------------------------------------------------------\n\n // ---------------------------------------------------------------------------\n // Join resolver — JoinOption (caller) → JoinConfig (custom adapter)\n //\n // For each requested join target `T` (keyed by logical model name), we\n // look at both sides of the schema:\n //\n // - If the *base* model has a field whose `references.model === T`,\n // this is a \"child → parent\" lookup: relation = one-to-one,\n // `on.from` = the base field, `on.to` = the referenced field on T.\n // - Otherwise, if model T has a field whose `references.model === base`,\n // this is a \"parent → children\" lookup: relation = one-to-many,\n // `on.from` = the referenced field on base (usually \"id\"),\n // `on.to` = the field on T carrying the FK.\n //\n // If neither side declares a reference we throw — a caller asking for a\n // join that the schema can't resolve is a bug, not a runtime state.\n // ---------------------------------------------------------------------------\n\n const resolveJoin = (base: string, join: JoinOption): JoinConfig => {\n const baseDef = getModelDefSync(base);\n const out: JoinConfig = {};\n for (const [target, raw] of Object.entries(join)) {\n if (raw === false) continue;\n const targetDef = getModelDefSync(target);\n const limit =\n typeof raw === \"object\" && raw.limit !== undefined ? raw.limit : undefined;\n\n // child → parent\n let found: JoinConfig[string] | undefined;\n for (const [fieldName, attr] of Object.entries(baseDef.fields)) {\n if (attr.references?.model === target) {\n found = {\n on: {\n from: getPhysicalField(base, fieldName),\n to:\n getPhysicalField(target, attr.references.field) ||\n attr.references.field,\n },\n relation: \"one-to-one\",\n ...(limit !== undefined ? { limit } : {}),\n };\n break;\n }\n }\n // parent → children\n if (!found) {\n for (const [fieldName, attr] of Object.entries(targetDef.fields)) {\n if (attr.references?.model === base) {\n found = {\n on: {\n from:\n getPhysicalField(base, attr.references.field) ||\n attr.references.field,\n to: getPhysicalField(target, fieldName),\n },\n relation: \"one-to-many\",\n ...(limit !== undefined ? { limit } : {}),\n };\n break;\n }\n }\n }\n if (!found) {\n throw new Error(\n `[storage-core] cannot resolve join \"${base}\" → \"${target}\": neither model declares a \\`references\\` for the other`,\n );\n }\n out[target] = found;\n }\n return out;\n };\n\n const cleanWhere = (\n model: string,\n where: readonly Where[] | undefined,\n ): CleanedWhere[] | undefined => {\n if (!where) return undefined;\n const def = getModelDefSync(model);\n return where.map((w) => {\n const operator = w.operator ?? \"eq\";\n const connector = w.connector ?? \"AND\";\n const mode = w.mode ?? \"sensitive\";\n const logical = w.field;\n const attr =\n logical === \"id\" ? undefined : def.fields[logical];\n const physical = getPhysicalField(model, logical);\n\n let value: Where[\"value\"] = w.value;\n if (attr) {\n if (Array.isArray(value)) {\n value = (value as unknown[]).map((v) =>\n encodeValue(attr, v),\n ) as typeof value;\n } else {\n value = encodeValue(attr, value) as typeof value;\n }\n }\n\n return {\n operator,\n connector,\n mode,\n field: physical,\n value,\n } satisfies CleanedWhere;\n });\n };\n\n // ---------------------------------------------------------------------------\n // Transform skip helpers — disableTransformInput/Output let backend authors\n // bypass the factory's built-in transform when they want the raw shape\n // passed through. Matches upstream's `if (!config.disableTransform*)`.\n // ---------------------------------------------------------------------------\n\n const maybeTransformInput = (\n model: string,\n data: Record<string, unknown>,\n action: \"create\" | \"update\",\n forceAllowId: boolean,\n ): Effect.Effect<Record<string, unknown>, Error> =>\n config.disableTransformInput\n ? Effect.succeed(data)\n : transformInput(model, data, action, forceAllowId);\n\n // ---------------------------------------------------------------------------\n // attachJoinedRows — re-decode nested join payloads.\n //\n // `transformOutput` only knows about the base model's fields and drops\n // anything else. If the caller asked for `join: { tag: true }` we need\n // to run the nested rows through `transformOutput` keyed on the target\n // model and then stick the decoded payload onto the base row under the\n // logical join key. Mirrors the upstream factory's nested-result path.\n // ---------------------------------------------------------------------------\n\n const attachJoinedRows = (\n base: Record<string, unknown> | null,\n raw: Record<string, unknown> | null,\n join: JoinOption | undefined,\n ): Effect.Effect<Record<string, unknown> | null, Error> =>\n Effect.gen(function* () {\n if (!base || !raw || !join) return base;\n const merged: Record<string, unknown> = { ...base };\n for (const [target, flag] of Object.entries(join)) {\n if (flag === false) continue;\n const nested = raw[target];\n if (nested === undefined) continue;\n if (nested === null) {\n merged[target] = null;\n continue;\n }\n if (Array.isArray(nested)) {\n const decoded: unknown[] = [];\n for (const n of nested) {\n if (n && typeof n === \"object\") {\n const t = yield* transformOutput(\n target,\n n as Record<string, unknown>,\n );\n decoded.push(t);\n } else {\n decoded.push(n);\n }\n }\n merged[target] = decoded;\n } else if (typeof nested === \"object\") {\n merged[target] = yield* transformOutput(\n target,\n nested as Record<string, unknown>,\n );\n } else {\n merged[target] = nested;\n }\n }\n return merged;\n });\n\n const maybeTransformOutput = (\n model: string,\n row: Record<string, unknown> | null,\n select?: string[],\n ): Effect.Effect<Record<string, unknown> | null, Error> =>\n config.disableTransformOutput\n ? Effect.succeed(row)\n : transformOutput(model, row, select);\n\n // ---------------------------------------------------------------------------\n // DBAdapter surface\n // ---------------------------------------------------------------------------\n\n const self: DBAdapter = {\n id: config.adapterId,\n\n create: <T extends Record<string, unknown>, R = T>(data: {\n model: string;\n data: Omit<T, \"id\">;\n select?: string[] | undefined;\n forceAllowId?: boolean | undefined;\n }) =>\n Effect.gen(function* () {\n const input = yield* maybeTransformInput(\n data.model,\n data.data as Record<string, unknown>,\n \"create\",\n data.forceAllowId === true,\n );\n const res = yield* inner.create({\n model: getModelName(data.model),\n data: input,\n select: data.select,\n });\n const out = yield* maybeTransformOutput(\n data.model,\n res as Record<string, unknown>,\n data.select,\n );\n return out as unknown as R;\n }),\n\n createMany: <T extends Record<string, unknown>, R = T>(data: {\n model: string;\n data: ReadonlyArray<Omit<T, \"id\">>;\n forceAllowId?: boolean | undefined;\n }) =>\n Effect.gen(function* () {\n // Delegates straight to the backend's native bulk insert — no\n // per-row fallback, because over a real network connection\n // (Hyperdrive, etc.) N round-trips would blow the request\n // budget for specs with thousands of operations. Transforms\n // still run per-row so JSON / dates / booleans serialize the\n // same as single `create`.\n const inputs: Record<string, unknown>[] = [];\n for (const row of data.data) {\n inputs.push(\n yield* maybeTransformInput(\n data.model,\n row as Record<string, unknown>,\n \"create\",\n data.forceAllowId === true,\n ),\n );\n }\n const res = yield* inner.createMany({\n model: getModelName(data.model),\n data: inputs,\n });\n const out: R[] = [];\n for (const row of res) {\n out.push(\n (yield* maybeTransformOutput(\n data.model,\n row as Record<string, unknown>,\n undefined,\n )) as unknown as R,\n );\n }\n return out as unknown as readonly R[];\n }),\n\n findOne: <T>(data: {\n model: string;\n where: Where[];\n select?: string[] | undefined;\n join?: JoinOption | undefined;\n }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where) ?? [];\n const join = data.join ? resolveJoin(data.model, data.join) : undefined;\n const res = yield* inner.findOne<Record<string, unknown>>({\n model: getModelName(data.model),\n where,\n select: data.select,\n join,\n });\n const out = yield* maybeTransformOutput(data.model, res, data.select);\n const merged = yield* attachJoinedRows(out, res, data.join);\n return merged as unknown as T | null;\n }),\n\n findMany: <T>(data: {\n model: string;\n where?: Where[] | undefined;\n limit?: number | undefined;\n select?: string[] | undefined;\n sortBy?: { field: string; direction: \"asc\" | \"desc\" } | undefined;\n offset?: number | undefined;\n join?: JoinOption | undefined;\n }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where);\n const sortBy = data.sortBy\n ? {\n field: getPhysicalField(data.model, data.sortBy.field),\n direction: data.sortBy.direction,\n }\n : undefined;\n const join = data.join ? resolveJoin(data.model, data.join) : undefined;\n const res = yield* inner.findMany<Record<string, unknown>>({\n model: getModelName(data.model),\n where,\n limit: data.limit,\n select: data.select,\n sortBy,\n offset: data.offset,\n join,\n });\n const out: unknown[] = [];\n for (const r of res) {\n const t = yield* maybeTransformOutput(data.model, r, data.select);\n const merged = yield* attachJoinedRows(t, r, data.join);\n out.push(merged);\n }\n return out as readonly T[];\n }),\n\n count: (data: { model: string; where?: Where[] | undefined }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where);\n return yield* inner.count({\n model: getModelName(data.model),\n where,\n });\n }),\n\n update: <T>(data: {\n model: string;\n where: Where[];\n update: Record<string, unknown>;\n }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where) ?? [];\n const update = yield* maybeTransformInput(\n data.model,\n data.update,\n \"update\",\n false,\n );\n const res = yield* inner.update<Record<string, unknown>>({\n model: getModelName(data.model),\n where,\n update,\n });\n const out = yield* maybeTransformOutput(data.model, res);\n return out as unknown as T | null;\n }),\n\n updateMany: (data: {\n model: string;\n where: Where[];\n update: Record<string, unknown>;\n }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where) ?? [];\n const update = yield* maybeTransformInput(\n data.model,\n data.update,\n \"update\",\n false,\n );\n return yield* inner.updateMany({\n model: getModelName(data.model),\n where,\n update,\n });\n }),\n\n delete: (data: { model: string; where: Where[] }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where) ?? [];\n yield* inner.delete({\n model: getModelName(data.model),\n where,\n });\n }),\n\n deleteMany: (data: { model: string; where: Where[] }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where) ?? [];\n return yield* inner.deleteMany({\n model: getModelName(data.model),\n where,\n });\n }),\n\n transaction: <R, E>(\n callback: (trx: DBTransactionAdapter) => Effect.Effect<R, E>,\n ) => {\n const txFn = config.transaction;\n if (!txFn) {\n // No real transaction support — just run the callback against self.\n return callback(self);\n }\n return txFn(callback);\n },\n\n // Forward the backend's createSchema verbatim. Upstream better-auth\n // mutates the `tables` set here to drop session when secondaryStorage\n // is set; we intentionally don't replicate that auth-specific concern.\n createSchema: inner.createSchema\n ? (props) => inner.createSchema!(props)\n : undefined,\n\n // Expose the full factory config + the inner adapter's own options to\n // plugin authors at runtime. Mirrors upstream's `options` field on\n // DBAdapter.\n options: {\n adapterConfig: options.config,\n ...(inner.options ?? {}),\n },\n };\n\n return self;\n};\n","import { Schema } from \"effect\";\n\nexport const ScopeId = Schema.String.pipe(Schema.brand(\"ScopeId\"));\nexport type ScopeId = typeof ScopeId.Type;\n\nexport const ToolId = Schema.String.pipe(Schema.brand(\"ToolId\"));\nexport type ToolId = typeof ToolId.Type;\n\nexport const SecretId = Schema.String.pipe(Schema.brand(\"SecretId\"));\nexport type SecretId = typeof SecretId.Type;\n\nexport const PolicyId = Schema.String.pipe(Schema.brand(\"PolicyId\"));\nexport type PolicyId = typeof PolicyId.Type;\n","import { Schema } from \"effect\";\n\nimport { ScopeId } from \"./ids\";\n\nexport class Scope extends Schema.Class<Scope>(\"Scope\")({\n id: ScopeId,\n name: Schema.String,\n createdAt: Schema.DateFromNumber,\n}) {}\n","import { Data, Schema } from \"effect\";\n\nimport { ToolId, SecretId } from \"./ids\";\n\n// ---------------------------------------------------------------------------\n// Tool lifecycle\n// ---------------------------------------------------------------------------\n\nexport class ToolNotFoundError extends Schema.TaggedError<ToolNotFoundError>()(\n \"ToolNotFoundError\",\n { toolId: ToolId },\n) {}\n\nexport class ToolInvocationError extends Data.TaggedError(\"ToolInvocationError\")<{\n readonly toolId: ToolId;\n readonly message: string;\n readonly cause?: unknown;\n}> {}\n\n/** Tool row exists in the DB but its owning plugin isn't loaded. Means\n * the tool was registered by a plugin that's no longer present in the\n * current executor config — usually a stale row from an older session. */\nexport class PluginNotLoadedError extends Schema.TaggedError<PluginNotLoadedError>()(\n \"PluginNotLoadedError\",\n {\n pluginId: Schema.String,\n toolId: ToolId,\n },\n) {}\n\n/** Tool was found but its owning plugin has no `invokeTool` handler —\n * the plugin only declares static tools and this one's id matched\n * dynamically somehow. Shouldn't happen in practice; guards against\n * programmer error. */\nexport class NoHandlerError extends Schema.TaggedError<NoHandlerError>()(\n \"NoHandlerError\",\n {\n toolId: ToolId,\n pluginId: Schema.String,\n },\n) {}\n\n// ---------------------------------------------------------------------------\n// Source lifecycle\n// ---------------------------------------------------------------------------\n\nexport class SourceNotFoundError extends Schema.TaggedError<SourceNotFoundError>()(\n \"SourceNotFoundError\",\n { sourceId: Schema.String },\n) {}\n\n/** `executor.sources.remove(id)` was called on a source with\n * `canRemove: false` — typically a static source declared by a plugin\n * at startup. Removing static sources is a bug in the caller. */\nexport class SourceRemovalNotAllowedError extends Schema.TaggedError<SourceRemovalNotAllowedError>()(\n \"SourceRemovalNotAllowedError\",\n { sourceId: Schema.String },\n) {}\n\n// ---------------------------------------------------------------------------\n// Secrets\n// ---------------------------------------------------------------------------\n\nexport class SecretNotFoundError extends Schema.TaggedError<SecretNotFoundError>()(\n \"SecretNotFoundError\",\n { secretId: SecretId },\n) {}\n\nexport class SecretResolutionError extends Schema.TaggedError<SecretResolutionError>()(\n \"SecretResolutionError\",\n {\n secretId: SecretId,\n message: Schema.String,\n },\n) {}\n\n// ---------------------------------------------------------------------------\n// Union type for convenience in signatures.\n// ---------------------------------------------------------------------------\n\nexport type ExecutorError =\n | ToolNotFoundError\n | ToolInvocationError\n | PluginNotLoadedError\n | NoHandlerError\n | SourceNotFoundError\n | SourceRemovalNotAllowedError\n | SecretNotFoundError\n | SecretResolutionError;\n","// ---------------------------------------------------------------------------\n// Public projections — what consumers see when they call\n// `executor.sources.list()` / `executor.tools.list()`. Deliberately leaner\n// than the row shapes in core-schema.ts: no audit columns, no raw JSON.\n// ---------------------------------------------------------------------------\n\nimport { Schema } from \"effect\";\n\nimport type { ToolAnnotations } from \"./core-schema\";\nimport { ToolId } from \"./ids\";\n\nexport interface Source {\n readonly id: string;\n readonly kind: string;\n readonly name: string;\n readonly url?: string;\n /** Which plugin owns this source. */\n readonly pluginId: string;\n /** Whether the user can remove this source via\n * `executor.sources.remove(id)`. `false` for static / built-in\n * sources declared by plugins at startup. */\n readonly canRemove: boolean;\n /** Whether the plugin supports `executor.sources.refresh(id)`. */\n readonly canRefresh: boolean;\n /** Whether the source has editable config (headers, base url, etc.).\n * Editing is done via plugin-specific extension methods\n * (`executor.openapi.updateSource(id, patch)` etc.) — this flag is\n * just a UI signal. */\n readonly canEdit: boolean;\n /** True if the source was declared statically by a plugin at startup\n * (in-memory only, no DB row). False if it was added at runtime via\n * `ctx.core.sources.register(...)`. UI differentiates built-in vs\n * user-added with this. */\n readonly runtime: boolean;\n}\n\nexport interface Tool {\n readonly id: string;\n readonly sourceId: string;\n /** Which plugin owns this tool. Matches the owning source's `pluginId`. */\n readonly pluginId: string;\n readonly name: string;\n readonly description: string;\n readonly inputSchema?: unknown;\n readonly outputSchema?: unknown;\n readonly annotations?: ToolAnnotations;\n}\n\n// ---------------------------------------------------------------------------\n// ToolSchema — the full schema-side view of a tool, returned by\n// `executor.tools.schema(toolId)`. Includes JSON schemas with `$defs`\n// attached at read time AND TypeScript preview strings rendered from\n// them via `schemaToTypeScriptPreview`. The UI uses the TS previews to\n// show \"calling this tool looks like this\" code samples.\n// ---------------------------------------------------------------------------\n\nexport class ToolSchema extends Schema.Class<ToolSchema>(\"ToolSchema\")({\n id: ToolId,\n name: Schema.optional(Schema.String),\n description: Schema.optional(Schema.String),\n inputSchema: Schema.optional(Schema.Unknown),\n outputSchema: Schema.optional(Schema.Unknown),\n inputTypeScript: Schema.optional(Schema.String),\n outputTypeScript: Schema.optional(Schema.String),\n typeScriptDefinitions: Schema.optional(\n Schema.Record({ key: Schema.String, value: Schema.String }),\n ),\n}) {}\n\n// ---------------------------------------------------------------------------\n// Source detection — optional capability on `PluginSpec.detect`. When a\n// user pastes a URL in the onboarding UI, `executor.sources.detect(url)`\n// asks every plugin \"is this yours?\" and returns the best-confidence\n// match so the UI can auto-fill the onboarding form for the right\n// plugin.\n// ---------------------------------------------------------------------------\n\nexport class SourceDetectionResult extends Schema.Class<SourceDetectionResult>(\n \"SourceDetectionResult\",\n)({\n /** Plugin id that recognized the URL (e.g. \"openapi\", \"graphql\"). */\n kind: Schema.String,\n /** Confidence tier — UI uses this to pick a winner when multiple\n * plugins claim a URL. */\n confidence: Schema.Literal(\"high\", \"medium\", \"low\"),\n /** The (possibly normalized) endpoint the plugin will use. */\n endpoint: Schema.String,\n /** Human-readable name suggestion, typically derived from spec title\n * or URL hostname. */\n name: Schema.String,\n /** Namespace suggestion — the plugin's recommendation for the source\n * id. UI may override. */\n namespace: Schema.String,\n}) {}\n\n// ---------------------------------------------------------------------------\n// Filter passed to `executor.tools.list(...)`. Empty filter = all tools.\n// ---------------------------------------------------------------------------\n\nexport interface ToolListFilter {\n /** Only tools under this source id. */\n readonly sourceId?: string;\n /** Case-insensitive substring match against `name` OR `description`. */\n readonly query?: string;\n}\n","// ---------------------------------------------------------------------------\n// Core data model — the SDK owns these tables. Plugins write into them via\n// `ctx.core.sources.register(...)` and `ctx.core.definitions.register(...)`;\n// the executor reads from them directly on every list / invoke / schema\n// call. There is no in-memory registry layered on top.\n//\n// Static (code-declared) sources and tools are NOT in these tables — they\n// live in an in-memory map built at executor startup from each plugin's\n// `staticSources` declaration. See executor.ts. The DB only holds\n// dynamic (runtime-registered) rows.\n// ---------------------------------------------------------------------------\n\nimport type {\n DBSchema,\n InferDBFieldsOutput,\n} from \"@executor/storage-core\";\n\nexport const coreSchema = {\n source: {\n fields: {\n id: { type: \"string\", required: true },\n scope_id: { type: \"string\", required: true, index: true },\n plugin_id: { type: \"string\", required: true, index: true },\n kind: { type: \"string\", required: true },\n name: { type: \"string\", required: true },\n url: { type: \"string\", required: false },\n can_remove: {\n type: \"boolean\",\n required: true,\n defaultValue: true,\n },\n can_refresh: {\n type: \"boolean\",\n required: true,\n defaultValue: false,\n },\n can_edit: {\n type: \"boolean\",\n required: true,\n defaultValue: false,\n },\n created_at: { type: \"date\", required: true },\n updated_at: { type: \"date\", required: true },\n },\n },\n tool: {\n fields: {\n id: { type: \"string\", required: true },\n scope_id: { type: \"string\", required: true, index: true },\n source_id: { type: \"string\", required: true, index: true },\n plugin_id: { type: \"string\", required: true, index: true },\n name: { type: \"string\", required: true },\n description: { type: \"string\", required: true },\n input_schema: { type: \"json\", required: false },\n output_schema: { type: \"json\", required: false },\n // NOTE: tool annotations (requiresApproval, approvalDescription,\n // mayElicit) are NOT stored on this row. They're derived at read\n // time from plugin-owned data via `plugin.resolveAnnotations`,\n // because the source of truth already lives in each plugin's own\n // storage (openapi's OperationBinding, etc.) and duplicating it\n // here would just mean bulk-rewriting rows every time the\n // derivation logic changes.\n created_at: { type: \"date\", required: true },\n updated_at: { type: \"date\", required: true },\n },\n },\n // Shared JSON-schema `$defs` stored once per source. Tool input/output\n // schemas carry `$ref: \"#/$defs/X\"` pointers; the read path attaches\n // matching defs under `$defs` before returning. Keyed by synthetic id\n // `${source_id}.${name}` so cleanup on source removal is a single\n // deleteMany by source_id.\n definition: {\n fields: {\n id: { type: \"string\", required: true },\n scope_id: { type: \"string\", required: true, index: true },\n source_id: { type: \"string\", required: true, index: true },\n plugin_id: { type: \"string\", required: true, index: true },\n name: { type: \"string\", required: true },\n schema: { type: \"json\", required: true },\n created_at: { type: \"date\", required: true },\n },\n },\n // Secrets live in the core surface as metadata (id, display name,\n // provider key). Actual values never touch this table — they live in\n // the secret provider (keychain, 1password, file, etc.) and are\n // resolved on demand via `ctx.secrets.get(id)`.\n secret: {\n fields: {\n id: { type: \"string\", required: true },\n scope_id: { type: \"string\", required: true, index: true },\n name: { type: \"string\", required: true },\n provider: { type: \"string\", required: true, index: true },\n created_at: { type: \"date\", required: true },\n },\n },\n} as const satisfies DBSchema;\n\nexport type CoreSchema = typeof coreSchema;\n\n// ---------------------------------------------------------------------------\n// Row types — derived from the schema. Adding a field to coreSchema.fields\n// adds it to the row type automatically.\n// ---------------------------------------------------------------------------\n\nexport type SourceRow = InferDBFieldsOutput<CoreSchema[\"source\"][\"fields\"]> &\n Record<string, unknown>;\n\nexport type ToolRow = InferDBFieldsOutput<CoreSchema[\"tool\"][\"fields\"]> &\n Record<string, unknown>;\n\nexport type DefinitionRow = InferDBFieldsOutput<\n CoreSchema[\"definition\"][\"fields\"]\n> &\n Record<string, unknown>;\n\nexport type SecretRow = InferDBFieldsOutput<CoreSchema[\"secret\"][\"fields\"]> &\n Record<string, unknown>;\n\n// ---------------------------------------------------------------------------\n// Tool annotations — default-policy metadata the executor consults\n// before invocation. Returned by `plugin.resolveAnnotations` (dynamic\n// tools) or declared inline on `StaticToolDecl` (static tools). Never\n// stored on `tool` rows — every field here is derived at read time\n// from plugin-owned data.\n//\n// OpenAPI derives from HTTP method:\n// - GET / HEAD / OPTIONS → {} (auto-approved)\n// - POST / PUT / PATCH / DELETE → { requiresApproval: true,\n// approvalDescription: \"DELETE /users/:id\" }\n//\n// MCP derives from the server's tool declaration (mcp has its own\n// may-elicit and approval signals).\n// ---------------------------------------------------------------------------\n\nexport interface ToolAnnotations {\n /** If true, the executor will call the invoke-time elicitation handler\n * before running the tool and abort if the user declines. */\n readonly requiresApproval?: boolean;\n /** Free-text message shown in the approval prompt. Falls back to the\n * tool's id / description if unset. */\n readonly approvalDescription?: string;\n /** Hint for UI — tool may suspend to ask the user for input mid-invocation.\n * Not enforced by the executor; purely a UI signal. */\n readonly mayElicit?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// SourceInput — what a plugin passes to `ctx.core.sources.register(...)`.\n// Writes both the source row and all its tool rows in one transaction.\n// Annotations are NOT part of this input — they're computed from\n// plugin-owned data via `plugin.resolveAnnotations` when the executor\n// needs them.\n// ---------------------------------------------------------------------------\n\nexport interface SourceInputTool {\n readonly name: string;\n readonly description: string;\n readonly inputSchema?: unknown;\n readonly outputSchema?: unknown;\n}\n\nexport interface SourceInput {\n readonly id: string;\n readonly kind: string;\n readonly name: string;\n readonly url?: string;\n readonly canRemove?: boolean;\n readonly canRefresh?: boolean;\n readonly canEdit?: boolean;\n readonly tools: readonly SourceInputTool[];\n}\n\n// ---------------------------------------------------------------------------\n// DefinitionsInput — paired with SourceInput when a plugin registers\n// shared JSON-schema `$defs` alongside a source. Usually called inside\n// the same `ctx.transaction` as `sources.register` so a failure rolls\n// back both the source rows and the def rows.\n// ---------------------------------------------------------------------------\n\nexport interface DefinitionsInput {\n readonly sourceId: string;\n readonly definitions: Record<string, unknown>;\n}\n","import { Effect, Schema } from \"effect\";\n\nimport { SecretId, ScopeId } from \"./ids\";\n\n// ---------------------------------------------------------------------------\n// SecretProvider — what a concrete backend (keychain, 1password, file,\n// memory, workos-vault, …) implements. Providers are contributed by\n// plugins via `plugin.secretProviders` and registered in the executor\n// at startup; there's no runtime registration.\n//\n// The `key` field is the provider's identifier in the secret table's\n// `provider` column and in `executor.secrets.set(id, value, provider?)`.\n// Unique per executor.\n// ---------------------------------------------------------------------------\n\nexport interface SecretProvider {\n /** Unique key (e.g. \"keychain\", \"env\", \"1password\", \"memory\"). */\n readonly key: string;\n /** If false, `set` and `delete` are never called. The executor\n * honours this before routing writes — trying to write to a\n * read-only provider is an error, not a silent drop. */\n readonly writable: boolean;\n /** Get a secret value by id. Returns null if not found. Errors are\n * real failures (provider unreachable, decryption failed, etc.). */\n readonly get: (id: string) => Effect.Effect<string | null, Error>;\n /** Set a secret value. Only called on writable providers. */\n readonly set?: (id: string, value: string) => Effect.Effect<void, Error>;\n /** Delete a secret. Only called on writable providers. Returns true\n * if something was deleted. */\n readonly delete?: (id: string) => Effect.Effect<boolean, Error>;\n /** Enumerate known secret entries. Optional — not all backends can\n * enumerate (env-backed providers, for example). */\n readonly list?: () => Effect.Effect<\n readonly { readonly id: string; readonly name: string }[],\n Error\n >;\n}\n\n// ---------------------------------------------------------------------------\n// SecretRef — metadata about a stored secret. Returned from\n// `executor.secrets.list()`. The actual value lives in the provider\n// and is only reachable via `executor.secrets.get(id)`.\n// ---------------------------------------------------------------------------\n\nexport class SecretRef extends Schema.Class<SecretRef>(\"SecretRef\")({\n id: SecretId,\n scopeId: ScopeId,\n /** Human-readable label (e.g. \"Cloudflare API Token\") */\n name: Schema.String,\n /** Which provider holds the value */\n provider: Schema.String,\n createdAt: Schema.DateFromNumber,\n}) {}\n\n// ---------------------------------------------------------------------------\n// SetSecretInput — all the metadata to write a secret in one call.\n// `executor.secrets.set(input)` takes this and writes both the\n// value (to the provider) and the ref (to the `secret` table).\n// ---------------------------------------------------------------------------\n\nexport class SetSecretInput extends Schema.Class<SetSecretInput>(\n \"SetSecretInput\",\n)({\n id: SecretId,\n /** Display name shown in secret-list UI. */\n name: Schema.String,\n /** The secret value itself — never persisted outside the provider. */\n value: Schema.String,\n /** Optional provider routing. If unset the executor picks the first\n * writable provider in registration order. */\n provider: Schema.optional(Schema.String),\n}) {}\n","import { Effect, Schema } from \"effect\";\n\nimport { ToolId } from \"./ids\";\n\n// ---------------------------------------------------------------------------\n// Elicitation request — what a tool sends when it needs user input\n// ---------------------------------------------------------------------------\n\n/** Tool needs structured input from the user (render a form) */\nexport class FormElicitation extends Schema.TaggedClass<FormElicitation>()(\"FormElicitation\", {\n message: Schema.String,\n /** JSON Schema describing the fields to collect */\n requestedSchema: Schema.Record({ key: Schema.String, value: Schema.Unknown }),\n}) {}\n\n/** Tool needs the user to visit a URL (OAuth, approval page, etc.) */\nexport class UrlElicitation extends Schema.TaggedClass<UrlElicitation>()(\"UrlElicitation\", {\n message: Schema.String,\n url: Schema.String,\n /** Unique ID so the host can correlate the callback */\n elicitationId: Schema.String,\n}) {}\n\nexport type ElicitationRequest = FormElicitation | UrlElicitation;\n\n// ---------------------------------------------------------------------------\n// Elicitation response — what the host sends back\n// ---------------------------------------------------------------------------\n\nexport const ElicitationAction = Schema.Literal(\"accept\", \"decline\", \"cancel\");\nexport type ElicitationAction = typeof ElicitationAction.Type;\n\nexport class ElicitationResponse extends Schema.Class<ElicitationResponse>(\"ElicitationResponse\")({\n action: ElicitationAction,\n /** Present when action is \"accept\" — the data the user provided */\n content: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })),\n}) {}\n\n// ---------------------------------------------------------------------------\n// Elicitation handler — the host provides this to handle requests\n// ---------------------------------------------------------------------------\n\nexport interface ElicitationContext {\n readonly toolId: ToolId;\n readonly args: unknown;\n readonly request: ElicitationRequest;\n}\n\n/**\n * A function the host provides to handle elicitation.\n * The SDK calls this when a tool suspends to ask for user input.\n * The host renders UI / prompts the user / does OAuth / etc.\n */\nexport type ElicitationHandler = (ctx: ElicitationContext) => Effect.Effect<ElicitationResponse>;\n\n// ---------------------------------------------------------------------------\n// Elicitation error — tool was declined or cancelled\n// ---------------------------------------------------------------------------\n\nexport class ElicitationDeclinedError extends Schema.TaggedError<ElicitationDeclinedError>()(\n \"ElicitationDeclinedError\",\n {\n toolId: ToolId,\n action: Schema.Literal(\"decline\", \"cancel\"),\n },\n) {}\n","// ---------------------------------------------------------------------------\n// BlobStore — the seam for large, opaque, write-once data. Separate from\n// the relational adapter on purpose: blobs want different lifecycle,\n// durability, and placement (think S3/R2 in cloud, flat files locally)\n// than the metadata that indexes them.\n//\n// Plugins see a `ScopedBlobStore` that's already namespaced to the plugin\n// id, so key collisions across plugins are structurally impossible.\n// ---------------------------------------------------------------------------\n\nimport { Effect } from \"effect\";\n\nexport interface BlobStore {\n readonly get: (\n namespace: string,\n key: string,\n ) => Effect.Effect<string | null, Error>;\n readonly put: (\n namespace: string,\n key: string,\n value: string,\n ) => Effect.Effect<void, Error>;\n readonly delete: (\n namespace: string,\n key: string,\n ) => Effect.Effect<void, Error>;\n readonly has: (\n namespace: string,\n key: string,\n ) => Effect.Effect<boolean, Error>;\n}\n\nexport interface ScopedBlobStore {\n readonly get: (key: string) => Effect.Effect<string | null, Error>;\n readonly put: (key: string, value: string) => Effect.Effect<void, Error>;\n readonly delete: (key: string) => Effect.Effect<void, Error>;\n readonly has: (key: string) => Effect.Effect<boolean, Error>;\n}\n\nexport const scopeBlobStore = (\n store: BlobStore,\n namespace: string,\n): ScopedBlobStore => ({\n get: (key) => store.get(namespace, key),\n put: (key, value) => store.put(namespace, key, value),\n delete: (key) => store.delete(namespace, key),\n has: (key) => store.has(namespace, key),\n});\n\n/**\n * Minimal in-memory BlobStore — good for tests and trivial hosts. Real\n * backends (filesystem, S3/R2, SQLite-table-backed) implement the same\n * interface.\n */\nexport const makeInMemoryBlobStore = (): BlobStore => {\n const store = new Map<string, string>();\n const k = (ns: string, key: string) => `${ns}::${key}`;\n return {\n get: (ns, key) => Effect.sync(() => store.get(k(ns, key)) ?? null),\n put: (ns, key, value) =>\n Effect.sync(() => {\n store.set(k(ns, key), value);\n }),\n delete: (ns, key) =>\n Effect.sync(() => {\n store.delete(k(ns, key));\n }),\n has: (ns, key) => Effect.sync(() => store.has(k(ns, key))),\n };\n};\n","import { Effect, FiberRef } from \"effect\";\nimport {\n typedAdapter,\n type DBAdapter,\n type DBSchema,\n type DBTransactionAdapter,\n type TypedAdapter,\n} from \"@executor/storage-core\";\n\nimport {\n scopeBlobStore,\n type BlobStore,\n} from \"./blob\";\nimport {\n coreSchema,\n type CoreSchema,\n type DefinitionsInput,\n type SourceInput,\n type SourceRow,\n type ToolAnnotations,\n type ToolRow,\n} from \"./core-schema\";\nimport {\n ElicitationDeclinedError,\n ElicitationResponse,\n FormElicitation,\n type ElicitationHandler,\n type ElicitationRequest,\n} from \"./elicitation\";\nimport {\n NoHandlerError,\n PluginNotLoadedError,\n SourceRemovalNotAllowedError,\n ToolInvocationError,\n ToolNotFoundError,\n} from \"./errors\";\nimport { SecretId, ToolId } from \"./ids\";\nimport type {\n AnyPlugin,\n Elicit,\n PluginCtx,\n PluginExtensions,\n StaticSourceDecl,\n StaticToolDecl,\n StorageDeps,\n} from \"./plugin\";\nimport type { Scope } from \"./scope\";\nimport {\n SecretRef,\n SetSecretInput,\n type SecretProvider,\n} from \"./secrets\";\nimport {\n ToolSchema,\n type Source,\n type SourceDetectionResult,\n type Tool,\n type ToolListFilter,\n} from \"./types\";\nimport { buildToolTypeScriptPreview } from \"./schema-types\";\nimport { scopeAdapter } from \"./scoped-adapter\";\n\n// ---------------------------------------------------------------------------\n// InvokeOptions — passed to `executor.tools.invoke(id, args, options)`.\n// The `onElicitation` handler is threaded into the `elicit` function\n// exposed on plugin ctx / InvokeToolInput. Tools that never elicit\n// simply don't call it.\n//\n// The \"accept-all\" sentinel is convenient for tests and CLI automation —\n// every elicitation request gets auto-accepted with an empty content\n// payload. For real interactive hosts, pass a real handler.\n// ---------------------------------------------------------------------------\n\nexport interface InvokeOptions {\n readonly onElicitation?: ElicitationHandler | \"accept-all\";\n}\n\nconst acceptAllHandler: ElicitationHandler = () =>\n Effect.succeed(new ElicitationResponse({ action: \"accept\" }));\n\nconst resolveElicitationHandler = (\n options: InvokeOptions | undefined,\n): ElicitationHandler => {\n const handler = options?.onElicitation;\n if (!handler || handler === \"accept-all\") return acceptAllHandler;\n return handler;\n};\n\n// ---------------------------------------------------------------------------\n// Executor — public surface. Every list/invoke/schema call is a direct\n// core-table query (for dynamic rows) unioned with the in-memory static\n// pool. No ToolRegistry, no SourceRegistry, no SecretStore services.\n// ---------------------------------------------------------------------------\n\nexport type Executor<TPlugins extends readonly AnyPlugin[] = []> = {\n readonly scope: Scope;\n\n readonly tools: {\n readonly list: (\n filter?: ToolListFilter,\n ) => Effect.Effect<readonly Tool[], Error>;\n /** Fetch a tool's full schema view: JSON schemas with `$defs`\n * attached from the core `definition` table, plus TypeScript\n * preview strings rendered from them. Returns `null` for unknown\n * tool ids. */\n readonly schema: (\n toolId: string,\n ) => Effect.Effect<ToolSchema | null, Error>;\n /** Every `$defs` entry across every source, grouped by source id.\n * Used for bulk schema export and downstream TypeScript rendering. */\n readonly definitions: () => Effect.Effect<\n Record<string, Record<string, unknown>>,\n Error\n >;\n readonly invoke: (\n toolId: string,\n args: unknown,\n options?: InvokeOptions,\n ) => Effect.Effect<\n unknown,\n | ToolNotFoundError\n | PluginNotLoadedError\n | NoHandlerError\n | ToolInvocationError\n | ElicitationDeclinedError\n | Error\n >;\n };\n\n readonly sources: {\n readonly list: () => Effect.Effect<readonly Source[], Error>;\n readonly remove: (\n sourceId: string,\n ) => Effect.Effect<void, SourceRemovalNotAllowedError | Error>;\n readonly refresh: (sourceId: string) => Effect.Effect<void, Error>;\n /** URL autodetection — fans out to every plugin's `detect` hook\n * (if declared), returns every high/medium/low-confidence match.\n * UI picks a winner from the list. */\n readonly detect: (\n url: string,\n ) => Effect.Effect<readonly SourceDetectionResult[], Error>;\n /** All `$defs` registered for a single source, keyed by def name. */\n readonly definitions: (\n sourceId: string,\n ) => Effect.Effect<Record<string, unknown>, Error>;\n };\n\n readonly secrets: {\n readonly get: (id: string) => Effect.Effect<string | null, Error>;\n /** Fast-path existence check — hits the core `secret` routing table\n * only, never calls the provider. Use this for UI state (\"secret\n * missing, prompt to add\") to avoid keychain permission prompts\n * or 1password IPC roundtrips on a pre-flight check. */\n readonly status: (\n id: string,\n ) => Effect.Effect<\"resolved\" | \"missing\", Error>;\n readonly set: (input: SetSecretInput) => Effect.Effect<SecretRef, Error>;\n readonly remove: (id: string) => Effect.Effect<void, Error>;\n readonly list: () => Effect.Effect<readonly SecretRef[], Error>;\n readonly providers: () => Effect.Effect<readonly string[]>;\n };\n\n readonly close: () => Effect.Effect<void, Error>;\n} & PluginExtensions<TPlugins>;\n\nexport interface ExecutorConfig<\n TPlugins extends readonly AnyPlugin[] = [],\n> {\n readonly scope: Scope;\n readonly adapter: DBAdapter;\n readonly blobs: BlobStore;\n readonly plugins?: TPlugins;\n}\n\n// ---------------------------------------------------------------------------\n// collectSchemas — merge coreSchema with every plugin's declared schema.\n// Hosts call this and pass the result to the migration runner (or to\n// the adapter factory for backends that auto-migrate from a schema\n// manifest) before constructing the executor.\n// ---------------------------------------------------------------------------\n\nexport const collectSchemas = (\n plugins: readonly AnyPlugin[],\n): DBSchema => {\n const merged: Record<string, DBSchema[string]> = { ...coreSchema };\n for (const plugin of plugins) {\n if (!plugin.schema) continue;\n for (const [modelKey, model] of Object.entries(plugin.schema)) {\n if (merged[modelKey]) {\n throw new Error(\n `Duplicate model \"${modelKey}\" contributed by plugin \"${plugin.id}\"` +\n ` (reserved by core or another plugin)`,\n );\n }\n merged[modelKey] = model as DBSchema[string];\n }\n }\n return merged;\n};\n\n// ---------------------------------------------------------------------------\n// Row → public projection conversions\n// ---------------------------------------------------------------------------\n\nconst rowToSource = (row: SourceRow): Source => ({\n id: row.id,\n kind: row.kind,\n name: row.name,\n url: row.url ?? undefined,\n pluginId: row.plugin_id,\n canRemove: Boolean(row.can_remove),\n canRefresh: Boolean(row.can_refresh),\n canEdit: Boolean(row.can_edit),\n runtime: false,\n});\n\nconst staticDeclToSource = (\n decl: StaticSourceDecl,\n pluginId: string,\n): Source => ({\n id: decl.id,\n kind: decl.kind,\n name: decl.name,\n url: decl.url,\n pluginId,\n canRemove: decl.canRemove ?? false,\n canRefresh: decl.canRefresh ?? false,\n canEdit: decl.canEdit ?? false,\n runtime: true,\n});\n\nconst decodeJsonColumn = (value: unknown): unknown => {\n if (value === null || value === undefined) return undefined;\n if (typeof value !== \"string\") return value;\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n};\n\nconst rowToTool = (\n row: ToolRow,\n annotations?: ToolAnnotations,\n): Tool => ({\n id: row.id,\n sourceId: row.source_id,\n pluginId: row.plugin_id,\n name: row.name,\n description: row.description,\n inputSchema: decodeJsonColumn(row.input_schema),\n outputSchema: decodeJsonColumn(row.output_schema),\n annotations,\n});\n\nconst staticDeclToTool = (\n source: StaticSourceDecl,\n tool: StaticToolDecl,\n pluginId: string,\n): Tool => ({\n id: `${source.id}.${tool.name}`,\n sourceId: source.id,\n pluginId,\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n outputSchema: tool.outputSchema,\n annotations: tool.annotations,\n});\n\n// ---------------------------------------------------------------------------\n// Dynamic-row writers — used by ctx.core.sources.register. Static sources\n// never touch these functions.\n// ---------------------------------------------------------------------------\n\n// Upsert shape: delete any existing source + tools + definitions for\n// `input.id` before creating fresh rows. Keeps replayable — boot-time\n// sync from executor.jsonc can call register() on rows that already\n// exist without tripping a UNIQUE constraint.\nconst writeSourceInput = (\n core: TypedAdapter<CoreSchema>,\n pluginId: string,\n input: SourceInput,\n): Effect.Effect<void, Error> =>\n Effect.gen(function* () {\n yield* deleteSourceById(core, input.id);\n\n const now = new Date();\n yield* core.create({\n model: \"source\",\n data: {\n id: input.id,\n plugin_id: pluginId,\n kind: input.kind,\n name: input.name,\n url: input.url ?? undefined,\n can_remove: input.canRemove ?? true,\n can_refresh: input.canRefresh ?? false,\n can_edit: input.canEdit ?? false,\n created_at: now,\n updated_at: now,\n },\n forceAllowId: true,\n });\n\n if (input.tools.length > 0) {\n yield* core.createMany({\n model: \"tool\",\n data: input.tools.map((tool) => ({\n id: `${input.id}.${tool.name}`,\n source_id: input.id,\n plugin_id: pluginId,\n name: tool.name,\n description: tool.description,\n input_schema: tool.inputSchema ?? undefined,\n output_schema: tool.outputSchema ?? undefined,\n created_at: now,\n updated_at: now,\n })),\n forceAllowId: true,\n });\n }\n });\n\nconst deleteSourceById = (\n core: TypedAdapter<CoreSchema>,\n sourceId: string,\n): Effect.Effect<void, Error> =>\n Effect.gen(function* () {\n yield* core.deleteMany({\n model: \"tool\",\n where: [{ field: \"source_id\", value: sourceId }],\n });\n yield* core.deleteMany({\n model: \"definition\",\n where: [{ field: \"source_id\", value: sourceId }],\n });\n yield* core.delete({\n model: \"source\",\n where: [{ field: \"id\", value: sourceId }],\n });\n });\n\nconst writeDefinitions = (\n core: TypedAdapter<CoreSchema>,\n pluginId: string,\n input: DefinitionsInput,\n): Effect.Effect<void, Error> =>\n Effect.gen(function* () {\n yield* core.deleteMany({\n model: \"definition\",\n where: [{ field: \"source_id\", value: input.sourceId }],\n });\n const entries = Object.entries(input.definitions);\n if (entries.length === 0) return;\n const now = new Date();\n yield* core.createMany({\n model: \"definition\",\n data: entries.map(([name, schema]) => ({\n id: `${input.sourceId}.${name}`,\n source_id: input.sourceId,\n plugin_id: pluginId,\n name,\n schema: schema as Record<string, unknown>,\n created_at: now,\n })),\n forceAllowId: true,\n });\n });\n\n// ---------------------------------------------------------------------------\n// Filtering — shared between dynamic (DB) and static (in-memory) pools\n// so `tools.list({ query, sourceId })` matches across both.\n// ---------------------------------------------------------------------------\n\nconst toolMatchesFilter = (tool: Tool, filter: ToolListFilter): boolean => {\n if (filter.sourceId && tool.sourceId !== filter.sourceId) return false;\n if (filter.query) {\n const q = filter.query.toLowerCase();\n const hay = `${tool.name} ${tool.description}`.toLowerCase();\n if (!hay.includes(q)) return false;\n }\n return true;\n};\n\n// ---------------------------------------------------------------------------\n// Active-adapter FiberRef. Nested plugin writes read this ref so that\n// `ctx.transaction` can swap in a tx-bound adapter handle without changing\n// the ctx shape — the root adapter returned by `buildAdapterRouter` below\n// resolves its target per call, so any Effect running inside\n// `Effect.locally(_, activeAdapterRef, trx)` automatically routes every\n// query through the same sql.begin connection. This is what makes nested\n// writes atomic on postgres + Hyperdrive without deadlocking a pool of 1.\n// ---------------------------------------------------------------------------\nconst activeAdapterRef = FiberRef.unsafeMake<DBTransactionAdapter | null>(\n null,\n);\n\n// A `DBAdapter` whose methods dispatch to the active adapter (tx handle or\n// root) on every call. Stable identity for consumers (plugin storage,\n// `typedAdapter`, etc.) — they see one adapter object, but the routing is\n// decided at call time via the FiberRef above.\nconst buildAdapterRouter = (root: DBAdapter): DBAdapter => {\n const pick = <A, E>(\n use: (active: DBTransactionAdapter) => Effect.Effect<A, E>,\n ): Effect.Effect<A, E> =>\n Effect.flatMap(FiberRef.get(activeAdapterRef), (active) =>\n use((active ?? (root as DBTransactionAdapter))),\n );\n\n return {\n id: root.id,\n create: (data) => pick((a) => a.create(data)),\n createMany: (data) => pick((a) => a.createMany(data)),\n findOne: (data) => pick((a) => a.findOne(data)),\n findMany: (data) => pick((a) => a.findMany(data)),\n count: (data) => pick((a) => a.count(data)),\n update: (data) => pick((a) => a.update(data)),\n updateMany: (data) => pick((a) => a.updateMany(data)),\n delete: (data) => pick((a) => a.delete(data)),\n deleteMany: (data) => pick((a) => a.deleteMany(data)),\n // transaction() always opens a real boundary on the ROOT adapter so the\n // tx uses one real connection from the pool. If we're already inside a\n // parent tx (FiberRef set), skip opening a nested sql.begin — that's\n // the postgres.js + Hyperdrive deadlock path — and just run the\n // callback with the existing tx handle. In both cases the callback\n // sees a FiberRef-substituted adapter so further nested writes thread\n // through.\n transaction: (callback) =>\n Effect.flatMap(FiberRef.get(activeAdapterRef), (active) => {\n if (active) return callback(active);\n return root.transaction((trx) =>\n Effect.locally(callback(trx), activeAdapterRef, trx),\n );\n }),\n } as DBAdapter;\n};\n\n// ---------------------------------------------------------------------------\n// createExecutor\n// ---------------------------------------------------------------------------\n\ninterface StaticTools {\n readonly source: StaticSourceDecl;\n readonly tool: StaticToolDecl;\n readonly pluginId: string;\n readonly ctx: PluginCtx<unknown>;\n}\n\ninterface StaticSources {\n readonly source: StaticSourceDecl;\n readonly pluginId: string;\n}\n\ninterface PluginRuntime {\n readonly plugin: AnyPlugin;\n readonly storage: unknown;\n readonly ctx: PluginCtx<unknown>;\n}\n\nexport const createExecutor = <\n const TPlugins extends readonly AnyPlugin[] = [],\n>(\n config: ExecutorConfig<TPlugins>,\n): Effect.Effect<Executor<TPlugins>, Error> =>\n Effect.gen(function* () {\n const {\n scope,\n adapter: rootAdapter,\n blobs,\n plugins = [] as unknown as TPlugins,\n } = config;\n\n // Scope-wrap the root adapter so every read on a tenant-scoped table\n // filters by the current scope stack and every write stamps the\n // write target. Today the stack has one element; the adapter's\n // `ScopeContext` shape already accepts an ordered list so layering\n // (org → workspace → user) can land later without changing plugin\n // code. Only tables whose schema declares `scope_id` are scoped.\n const schema = collectSchemas(plugins);\n const scopedRoot = scopeAdapter(\n rootAdapter,\n { read: [scope.id], write: scope.id },\n schema,\n );\n const adapter = buildAdapterRouter(scopedRoot);\n const core = typedAdapter<CoreSchema>(adapter);\n\n // Populated once, never mutated after startup.\n const staticTools = new Map<string, StaticTools>();\n const staticSources = new Map<string, StaticSources>();\n\n // Per-plugin runtime state.\n const runtimes = new Map<string, PluginRuntime>();\n // Secret providers keyed by `provider.key`.\n const secretProviders = new Map<string, SecretProvider>();\n const extensions: Record<string, object> = {};\n\n // ------------------------------------------------------------------\n // Secrets facade — fast path is the core `secret` routing table\n // (explicit set()s, keychain entries, etc). Fallback is a walk\n // across providers that implement `list()`, because those are the\n // providers that own their own inventories (1password, file-secrets,\n // workos-vault, env) and enumerate-without-register. Providers\n // without a list() implementation (keychain) never hit the fallback\n // walk because their secrets must be registered through set() to\n // be known at all.\n // ------------------------------------------------------------------\n const secretsGet = (id: string): Effect.Effect<string | null, Error> =>\n Effect.gen(function* () {\n // Fast path: routing table\n const row = yield* core.findOne({\n model: \"secret\",\n where: [{ field: \"id\", value: id }],\n });\n if (row) {\n const provider = secretProviders.get(row.provider);\n if (!provider) return null;\n return yield* provider.get(id);\n }\n\n // Fallback: ask every enumerating provider in parallel. First\n // non-null in registration order wins. Providers that throw\n // are treated as \"don't have it\" so one flaky provider can't\n // block resolution via others.\n const candidates = [...secretProviders.values()].filter(\n (p) => p.list,\n );\n const values = yield* Effect.all(\n candidates.map((p) =>\n p.get(id).pipe(Effect.catchAll(() => Effect.succeed(null))),\n ),\n { concurrency: \"unbounded\" },\n );\n for (const value of values) if (value !== null) return value;\n return null;\n });\n\n const secretsSet = (\n input: SetSecretInput,\n ): Effect.Effect<SecretRef, Error> =>\n Effect.gen(function* () {\n // Pick provider: explicit or first-writable.\n let target: SecretProvider | undefined;\n if (input.provider) {\n target = secretProviders.get(input.provider);\n if (!target) {\n return yield* Effect.fail(\n new Error(`Unknown secret provider: ${input.provider}`),\n );\n }\n } else {\n for (const provider of secretProviders.values()) {\n if (provider.writable && provider.set) {\n target = provider;\n break;\n }\n }\n if (!target) {\n return yield* Effect.fail(\n new Error(\"No writable secret providers registered\"),\n );\n }\n }\n if (!target.writable || !target.set) {\n return yield* Effect.fail(\n new Error(`Secret provider \"${target.key}\" is read-only`),\n );\n }\n\n yield* target.set(input.id, input.value);\n\n // Upsert metadata row in the core `secret` table.\n const now = new Date();\n yield* core.delete({\n model: \"secret\",\n where: [{ field: \"id\", value: input.id }],\n });\n yield* core.create({\n model: \"secret\",\n data: {\n id: input.id,\n name: input.name,\n provider: target.key,\n created_at: now,\n },\n forceAllowId: true,\n });\n\n return new SecretRef({\n id: input.id,\n scopeId: scope.id,\n name: input.name,\n provider: target.key,\n createdAt: now,\n });\n });\n\n const secretsRemove = (id: string): Effect.Effect<void, Error> =>\n Effect.gen(function* () {\n // Providers don't coordinate on which of them own the id — they\n // each get asked. Most calls are no-ops; fan them out so one\n // slow provider doesn't serialize the rest.\n const deleters = [...secretProviders.values()].filter(\n (p): p is typeof p & { delete: NonNullable<typeof p.delete> } =>\n !!(p.writable && p.delete),\n );\n yield* Effect.all(\n deleters.map((p) => p.delete(id)),\n { concurrency: \"unbounded\" },\n );\n yield* core.delete({\n model: \"secret\",\n where: [{ field: \"id\", value: id }],\n });\n });\n\n // List is a union of two sources of truth:\n //\n // 1. Core `secret` rows — secrets explicitly registered via\n // executor.secrets.set(...). These carry their pinned provider\n // and are authoritative for routing (get() uses them).\n // 2. Each provider's own `list()` — for read-only or\n // already-populated providers (1password, file-secrets,\n // workos-vault, env), the provider enumerates what's actually\n // in its backend. These show up in the list even if the user\n // never called set() through the executor.\n //\n // Dedupe by secret id; core rows win over provider-enumerated ones\n // so that routing information in the core table is authoritative.\n // Providers without a list() method (e.g. keychain) contribute\n // only via the core table path.\n const secretsList = (): Effect.Effect<readonly SecretRef[], Error> =>\n Effect.gen(function* () {\n const byId = new Map<string, SecretRef>();\n\n // Core routing rows first\n const rows = yield* core.findMany({ model: \"secret\" });\n for (const row of rows) {\n byId.set(\n row.id,\n new SecretRef({\n id: SecretId.make(row.id),\n scopeId: scope.id,\n name: row.name,\n provider: row.provider,\n createdAt:\n row.created_at instanceof Date\n ? row.created_at\n : new Date(row.created_at as string),\n }),\n );\n }\n\n // Then every provider that can enumerate itself, in parallel.\n // If a provider fails to list (unlocked vault, network error),\n // swallow the failure so one flaky provider can't block the\n // whole list. Merge in registration order afterwards so the\n // \"first provider wins\" precedence stays deterministic.\n const listers = [...secretProviders.entries()].filter(\n ([, p]) => p.list,\n );\n const lists = yield* Effect.all(\n listers.map(([key, p]) =>\n p\n .list!()\n .pipe(\n Effect.catchAll(() => Effect.succeed([] as const)),\n Effect.map((entries) => ({ key, entries })),\n ),\n ),\n { concurrency: \"unbounded\" },\n );\n for (const { key, entries } of lists) {\n for (const entry of entries) {\n if (byId.has(entry.id)) continue; // core row wins\n byId.set(\n entry.id,\n new SecretRef({\n id: SecretId.make(entry.id),\n scopeId: scope.id,\n name: entry.name,\n provider: key,\n createdAt: new Date(),\n }),\n );\n }\n }\n\n return Array.from(byId.values());\n });\n\n // Same union shape as secretsList but projected to the leaner\n // SecretListEntry shape that plugins get via ctx.secrets.list().\n const secretsListForCtx = () =>\n Effect.gen(function* () {\n const list = yield* secretsList();\n return list.map((ref) => ({\n id: ref.id as unknown as string,\n name: ref.name,\n provider: ref.provider,\n }));\n });\n\n // ------------------------------------------------------------------\n // Plugin wiring — build ctx, run extension, populate static pools,\n // register secret providers. No adapter reads here.\n // ------------------------------------------------------------------\n for (const plugin of plugins) {\n if (runtimes.has(plugin.id)) {\n return yield* Effect.fail(\n new Error(`Duplicate plugin id: ${plugin.id}`),\n );\n }\n\n // `typedAdapter(adapter)` is a zero-cost cast at the type level —\n // the runtime value IS the adapter, but the type is whatever the\n // plugin's schema declares. Plugins never import `typedAdapter` or\n // `DBAdapter` themselves; they just destructure `{ adapter }` in\n // their storage factory and get a store typed to their own schema.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const storageDeps: StorageDeps<any> = {\n scope,\n adapter: typedAdapter(adapter) as never,\n // Blob keys are namespaced by `<scope>/<plugin>` so two tenants\n // sharing a backing BlobStore can't collide or leak on the same\n // `(plugin, key)` pair. Mirrors the adapter's scope-stamping.\n blobs: scopeBlobStore(blobs, `${scope.id}/${plugin.id}`),\n };\n const storage = plugin.storage(storageDeps);\n\n const ctx: PluginCtx<unknown> = {\n scope,\n storage,\n core: {\n sources: {\n register: (input: SourceInput) =>\n Effect.gen(function* () {\n // Guard: reject a dynamic source whose id collides with\n // a static source id, or any of whose would-be tool ids\n // collide with a static tool id. Tool ids are\n // `${source_id}.${tool.name}` — static and dynamic\n // share the same string space.\n if (staticSources.has(input.id)) {\n return yield* Effect.fail(\n new Error(\n `Source id \"${input.id}\" collides with a static source`,\n ),\n );\n }\n for (const tool of input.tools) {\n const fqid = `${input.id}.${tool.name}`;\n if (staticTools.has(fqid)) {\n return yield* Effect.fail(\n new Error(\n `Tool id \"${fqid}\" collides with a static tool`,\n ),\n );\n }\n }\n // Wrap in adapter.transaction so a standalone register()\n // call is atomic (source create + tools createMany group\n // together). When already inside a parent ctx.transaction,\n // the router short-circuits to the active tx handle\n // instead of opening a nested sql.begin — that nested\n // sql.begin is the postgres.js + pool=1 deadlock path.\n yield* adapter.transaction(() =>\n writeSourceInput(core, plugin.id, input),\n );\n }),\n unregister: (sourceId: string) =>\n adapter.transaction(() => deleteSourceById(core, sourceId)),\n },\n definitions: {\n register: (input: DefinitionsInput) =>\n adapter.transaction(() =>\n writeDefinitions(core, plugin.id, input),\n ),\n },\n },\n secrets: {\n get: secretsGet,\n list: secretsListForCtx,\n set: secretsSet,\n remove: secretsRemove,\n },\n // Open one real tx boundary and route every nested write inside\n // `effect` through that same handle via the activeAdapterRef —\n // see buildAdapterRouter above for the dispatch logic.\n transaction: <A, E>(effect: Effect.Effect<A, E>) =>\n adapter.transaction(() => effect) as Effect.Effect<A, E | Error>,\n };\n\n // Build extension FIRST so it's available as `self` when resolving\n // staticSources. Field ordering in the plugin spec matters — TS\n // infers TExtension from `extension`'s return type, then NoInfer\n // locks `self` to that inferred type on `staticSources`.\n const extension: object = plugin.extension\n ? plugin.extension(ctx)\n : {};\n if (plugin.extension) {\n extensions[plugin.id] = extension;\n }\n\n // Resolve static declarations to the in-memory pools. NO DB WRITES.\n const decls = plugin.staticSources\n ? plugin.staticSources(extension)\n : [];\n for (const source of decls) {\n if (staticSources.has(source.id)) {\n return yield* Effect.fail(\n new Error(\n `Duplicate static source id: ${source.id} (plugin ${plugin.id})`,\n ),\n );\n }\n staticSources.set(source.id, { source, pluginId: plugin.id });\n\n for (const tool of source.tools) {\n const fqid = `${source.id}.${tool.name}`;\n if (staticTools.has(fqid)) {\n return yield* Effect.fail(\n new Error(\n `Duplicate static tool id: ${fqid} (plugin ${plugin.id})`,\n ),\n );\n }\n staticTools.set(fqid, {\n source,\n tool,\n pluginId: plugin.id,\n ctx,\n });\n }\n }\n\n runtimes.set(plugin.id, { plugin, storage, ctx });\n\n if (plugin.secretProviders) {\n const providers =\n typeof plugin.secretProviders === \"function\"\n ? plugin.secretProviders(ctx)\n : plugin.secretProviders;\n for (const provider of providers) {\n if (secretProviders.has(provider.key)) {\n return yield* Effect.fail(\n new Error(\n `Duplicate secret provider key: ${provider.key} (from plugin ${plugin.id})`,\n ),\n );\n }\n secretProviders.set(provider.key, provider);\n }\n }\n }\n\n // ------------------------------------------------------------------\n // Executor surface\n // ------------------------------------------------------------------\n const listSources = () =>\n Effect.gen(function* () {\n const dynamic = yield* core.findMany({ model: \"source\" });\n const staticList: Source[] = [];\n for (const { source, pluginId } of staticSources.values()) {\n staticList.push(staticDeclToSource(source, pluginId));\n }\n return [...staticList, ...dynamic.map(rowToSource)];\n });\n\n // Bulk-resolve annotations across a set of dynamic tool rows by\n // grouping them under their owning plugin's resolveAnnotations\n // callback. One plugin call per (plugin_id, source_id) pair, not\n // per row. Plugins without a resolver simply contribute no\n // annotations for their rows.\n const resolveAnnotationsFor = (rows: readonly ToolRow[]) =>\n Effect.gen(function* () {\n const result = new Map<string, ToolAnnotations>();\n if (rows.length === 0) return result;\n\n // Group by (plugin_id, source_id)\n const groups = new Map<string, ToolRow[]>();\n for (const row of rows) {\n const key = `${row.plugin_id}\\u0000${row.source_id}`;\n const bucket = groups.get(key);\n if (bucket) bucket.push(row);\n else groups.set(key, [row]);\n }\n\n for (const [key, groupRows] of groups) {\n const [pluginId, sourceId] = key.split(\"\\u0000\") as [\n string,\n string,\n ];\n const runtime = runtimes.get(pluginId);\n if (!runtime?.plugin.resolveAnnotations) continue;\n const map = yield* runtime.plugin.resolveAnnotations({\n ctx: runtime.ctx,\n sourceId,\n toolRows: groupRows,\n });\n for (const [toolId, annotations] of Object.entries(map)) {\n result.set(toolId, annotations);\n }\n }\n return result;\n });\n\n const listTools = (filter?: ToolListFilter) =>\n Effect.gen(function* () {\n const dynamic = yield* core.findMany({ model: \"tool\" });\n const annotations = yield* resolveAnnotationsFor(dynamic);\n\n const out: Tool[] = [];\n // Static tools — annotations from the declaration, not a resolver.\n for (const entry of staticTools.values()) {\n out.push(staticDeclToTool(entry.source, entry.tool, entry.pluginId));\n }\n for (const row of dynamic) {\n out.push(rowToTool(row, annotations.get(row.id)));\n }\n if (!filter) return out;\n return out.filter((t) => toolMatchesFilter(t, filter));\n });\n\n // Load all definitions for a single source as a plain map.\n const loadDefinitionsForSource = (sourceId: string) =>\n Effect.gen(function* () {\n const defRows = yield* core.findMany({\n model: \"definition\",\n where: [{ field: \"source_id\", value: sourceId }],\n });\n const out: Record<string, unknown> = {};\n for (const row of defRows) out[row.name] = row.schema;\n return out;\n });\n\n // Render the ToolSchema view for a tool — wraps the raw JSON schemas\n // with attached `$defs` and runs them through the TypeScript preview\n // helpers so the UI gets ready-to-display code samples.\n const buildToolSchemaView = (opts: {\n toolId: string;\n name?: string;\n description?: string;\n sourceId: string | undefined;\n rawInput: unknown;\n rawOutput: unknown;\n }) =>\n Effect.gen(function* () {\n const defs: Record<string, unknown> = opts.sourceId\n ? yield* loadDefinitionsForSource(opts.sourceId)\n : {};\n\n const attachDefs = (schema: unknown): unknown => {\n if (schema == null || typeof schema !== \"object\") return schema;\n if (Object.keys(defs).length === 0) return schema;\n return { ...(schema as Record<string, unknown>), $defs: defs };\n };\n\n const inputSchema = attachDefs(opts.rawInput);\n const outputSchema = attachDefs(opts.rawOutput);\n\n const defsMap = new Map<string, unknown>(Object.entries(defs));\n const preview = buildToolTypeScriptPreview({\n inputSchema,\n outputSchema,\n defs: defsMap,\n });\n\n return new ToolSchema({\n id: ToolId.make(opts.toolId),\n name: opts.name,\n description: opts.description,\n inputSchema,\n outputSchema,\n inputTypeScript: preview.inputTypeScript ?? undefined,\n outputTypeScript: preview.outputTypeScript ?? undefined,\n typeScriptDefinitions: preview.typeScriptDefinitions ?? undefined,\n });\n });\n\n const toolSchema = (toolId: string) =>\n Effect.gen(function* () {\n // Static pool first — static tools have no source in the DB so\n // no `$defs` attach; just wrap the declared schemas.\n const staticEntry = staticTools.get(toolId);\n if (staticEntry) {\n return yield* buildToolSchemaView({\n toolId,\n name: staticEntry.tool.name,\n description: staticEntry.tool.description,\n sourceId: undefined,\n rawInput: staticEntry.tool.inputSchema,\n rawOutput: staticEntry.tool.outputSchema,\n });\n }\n const row = yield* core.findOne({\n model: \"tool\",\n where: [{ field: \"id\", value: toolId }],\n });\n if (!row) return null;\n return yield* buildToolSchemaView({\n toolId,\n name: row.name,\n description: row.description,\n sourceId: row.source_id,\n rawInput: decodeJsonColumn(row.input_schema),\n rawOutput: decodeJsonColumn(row.output_schema),\n });\n });\n\n // Bulk definitions accessor — every source's $defs, grouped by\n // source id. One query against the definition table, plus an\n // in-memory group-by.\n const toolsDefinitions = () =>\n Effect.gen(function* () {\n const rows = yield* core.findMany({ model: \"definition\" });\n const out: Record<string, Record<string, unknown>> = {};\n for (const row of rows) {\n let bucket = out[row.source_id];\n if (!bucket) {\n bucket = {};\n out[row.source_id] = bucket;\n }\n bucket[row.name] = row.schema;\n }\n return out;\n });\n\n const buildElicit = (toolId: string, args: unknown, options: InvokeOptions | undefined): Elicit => {\n const handler = resolveElicitationHandler(options);\n return (request: ElicitationRequest) =>\n Effect.gen(function* () {\n const tid = ToolId.make(toolId);\n const response: ElicitationResponse = yield* handler({\n toolId: tid,\n args,\n request,\n });\n if (response.action !== \"accept\") {\n return yield* new ElicitationDeclinedError({\n toolId: tid,\n action: response.action,\n });\n }\n return response;\n });\n };\n\n const enforceApproval = (\n annotations: ToolAnnotations | undefined,\n toolId: string,\n args: unknown,\n options: InvokeOptions | undefined,\n ) =>\n Effect.gen(function* () {\n if (!annotations?.requiresApproval) return;\n const handler = resolveElicitationHandler(options);\n const tid = ToolId.make(toolId);\n const request = new FormElicitation({\n message: annotations.approvalDescription ?? `Approve ${toolId}?`,\n requestedSchema: {},\n });\n const response = yield* handler({ toolId: tid, args, request });\n if (response.action !== \"accept\") {\n return yield* new ElicitationDeclinedError({\n toolId: tid,\n action: response.action,\n });\n }\n });\n\n const invokeTool = (\n toolId: string,\n args: unknown,\n options?: InvokeOptions,\n ) =>\n Effect.gen(function* () {\n const wrapInvocationError = <A, E>(\n effect: Effect.Effect<A, E>,\n ): Effect.Effect<A, ToolInvocationError> =>\n effect.pipe(\n Effect.mapError(\n (cause) =>\n new ToolInvocationError({\n toolId: ToolId.make(toolId),\n message:\n cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n ),\n );\n\n // Static path — O(1) map lookup, no DB hit.\n const staticEntry = staticTools.get(toolId);\n if (staticEntry) {\n yield* enforceApproval(\n staticEntry.tool.annotations,\n toolId,\n args,\n options,\n );\n return yield* wrapInvocationError(\n staticEntry.tool.handler({\n ctx: staticEntry.ctx,\n args,\n elicit: buildElicit(toolId, args, options),\n }),\n );\n }\n\n // Dynamic path — DB lookup + delegate to owning plugin.\n const row = yield* core.findOne({\n model: \"tool\",\n where: [{ field: \"id\", value: toolId }],\n });\n if (!row) {\n return yield* new ToolNotFoundError({\n toolId: ToolId.make(toolId),\n });\n }\n const runtime = runtimes.get(row.plugin_id);\n if (!runtime) {\n return yield* new PluginNotLoadedError({\n pluginId: row.plugin_id,\n toolId: ToolId.make(toolId),\n });\n }\n if (!runtime.plugin.invokeTool) {\n return yield* new NoHandlerError({\n toolId: ToolId.make(toolId),\n pluginId: row.plugin_id,\n });\n }\n\n // Ask the plugin to derive annotations for this one row, if it\n // has a resolver. Cheap because the plugin typically already\n // needs to load its enrichment data to invoke the tool —\n // implementations should structure their resolver + invokeTool\n // around a single storage read.\n let annotations: ToolAnnotations | undefined;\n if (runtime.plugin.resolveAnnotations) {\n const map = yield* runtime.plugin.resolveAnnotations({\n ctx: runtime.ctx,\n sourceId: row.source_id,\n toolRows: [row],\n });\n annotations = map[toolId];\n }\n yield* enforceApproval(annotations, toolId, args, options);\n\n return yield* wrapInvocationError(\n runtime.plugin.invokeTool({\n ctx: runtime.ctx,\n toolRow: row,\n args,\n elicit: buildElicit(toolId, args, options),\n }),\n );\n });\n\n const removeSource = (sourceId: string) =>\n Effect.gen(function* () {\n // Block removal of static sources structurally.\n if (staticSources.has(sourceId)) {\n return yield* new SourceRemovalNotAllowedError({ sourceId });\n }\n const sourceRow = yield* core.findOne({\n model: \"source\",\n where: [{ field: \"id\", value: sourceId }],\n });\n if (!sourceRow) return;\n if (!sourceRow.can_remove) {\n return yield* new SourceRemovalNotAllowedError({ sourceId });\n }\n const runtime = runtimes.get(sourceRow.plugin_id);\n // Group the plugin's own cleanup + the core row delete into one\n // tx so a removeSource never leaves orphan rows on failure. The\n // router short-circuits on nested calls when the caller is\n // already inside a parent ctx.transaction.\n yield* adapter.transaction(() =>\n Effect.gen(function* () {\n if (runtime?.plugin.removeSource) {\n yield* runtime.plugin.removeSource({\n ctx: runtime.ctx,\n sourceId,\n });\n }\n yield* deleteSourceById(core, sourceId);\n }),\n );\n });\n\n const refreshSource = (sourceId: string) =>\n Effect.gen(function* () {\n if (staticSources.has(sourceId)) return;\n const sourceRow = yield* core.findOne({\n model: \"source\",\n where: [{ field: \"id\", value: sourceId }],\n });\n if (!sourceRow) return;\n const runtime = runtimes.get(sourceRow.plugin_id);\n if (runtime?.plugin.refreshSource) {\n yield* runtime.plugin.refreshSource({\n ctx: runtime.ctx,\n sourceId,\n });\n }\n });\n\n // URL autodetection — fan out across every plugin that declared a\n // `detect` hook. Collect all non-null results. Plugin-level detect\n // implementations should swallow fetch errors and return null, so\n // one flaky plugin doesn't block the whole dispatch.\n const detectSource = (url: string) =>\n Effect.gen(function* () {\n const results: SourceDetectionResult[] = [];\n for (const runtime of runtimes.values()) {\n if (!runtime.plugin.detect) continue;\n const result = yield* runtime.plugin\n .detect({ ctx: runtime.ctx, url })\n .pipe(Effect.catchAll(() => Effect.succeed(null)));\n if (result) results.push(result);\n }\n return results;\n });\n\n // Per-source definitions accessor — one query, one mapping pass.\n const sourceDefinitions = (sourceId: string) =>\n loadDefinitionsForSource(sourceId);\n\n // Fast-path existence check. Hits the core `secret` routing row\n // first (no provider call). If no routing row, walks enumerating\n // providers and checks their lists for a matching id — same\n // fallback strategy as secretsGet. Still avoids provider.get()\n // so no keychain permission prompts / 1password value IPC fires\n // just to ask \"does this exist?\"\n const secretsStatus = (\n id: string,\n ): Effect.Effect<\"resolved\" | \"missing\", Error> =>\n Effect.gen(function* () {\n const row = yield* core.findOne({\n model: \"secret\",\n where: [{ field: \"id\", value: id }],\n });\n if (row) return \"resolved\";\n\n for (const provider of secretProviders.values()) {\n if (!provider.list) continue;\n const entries = yield* provider\n .list()\n .pipe(Effect.catchAll(() => Effect.succeed([] as const)));\n if (entries.some((e) => e.id === id)) return \"resolved\";\n }\n return \"missing\";\n });\n\n const close = () =>\n Effect.gen(function* () {\n for (const runtime of runtimes.values()) {\n if (runtime.plugin.close) {\n yield* runtime.plugin.close();\n }\n }\n });\n\n const base = {\n scope,\n tools: {\n list: listTools,\n schema: toolSchema,\n definitions: toolsDefinitions,\n invoke: invokeTool,\n },\n sources: {\n list: listSources,\n remove: removeSource,\n refresh: refreshSource,\n detect: detectSource,\n definitions: sourceDefinitions,\n },\n secrets: {\n get: secretsGet,\n status: secretsStatus,\n set: secretsSet,\n remove: secretsRemove,\n list: secretsList,\n providers: () =>\n Effect.sync(\n () => Array.from(secretProviders.keys()) as readonly string[],\n ),\n },\n close,\n };\n\n return Object.assign(base, extensions) as Executor<TPlugins>;\n });\n","// ---------------------------------------------------------------------------\n// In-memory CustomAdapter, piped through createAdapter to produce a full\n// DBAdapter. Used by the conformance suite and for unit tests that don't\n// need real persistence.\n//\n// Vendored from better-auth (packages/memory-adapter/src/memory-adapter.ts)\n// under MIT. Adapted for executor:\n// - Promise/async → Effect.Effect<T, Error>\n// - Stripped the BetterAuthOptions layering (our config is simpler)\n// - Reuses the filter logic from better-auth's memoryAdapter, minus\n// join support (join resolution is not needed for the storage-core\n// testing path — plugins in-process do their own joining)\n// - Transaction uses structuredClone snapshot/rollback, same as\n// better-auth's memory-adapter\n// ---------------------------------------------------------------------------\n\nimport { Effect } from \"effect\";\n\nimport type {\n CleanedWhere,\n CustomAdapter,\n DBAdapter,\n DBAdapterFactoryConfig,\n JoinConfig,\n} from \"../adapter\";\nimport type { DBSchema } from \"../schema\";\nimport { createAdapter } from \"../factory\";\n\ntype Row = Record<string, unknown>;\ntype Store = Record<string, Row[]>;\n\nconst evalClause = (record: Row, clause: CleanedWhere): boolean => {\n const { field, value, operator, mode } = clause;\n const isInsensitive =\n mode === \"insensitive\" &&\n (typeof value === \"string\" ||\n (Array.isArray(value) &&\n (value as unknown[]).every((v) => typeof v === \"string\")));\n\n const lhs = record[field];\n const lowerStr = (v: unknown) =>\n typeof v === \"string\" ? v.toLowerCase() : v;\n\n const cmp = (a: unknown, b: unknown): boolean =>\n isInsensitive ? lowerStr(a) === lowerStr(b) : a === b;\n\n switch (operator) {\n case \"in\":\n if (!Array.isArray(value)) throw new Error(\"Value must be an array\");\n return (value as unknown[]).some((v) => cmp(lhs, v));\n case \"not_in\":\n if (!Array.isArray(value)) throw new Error(\"Value must be an array\");\n return !(value as unknown[]).some((v) => cmp(lhs, v));\n case \"contains\": {\n if (typeof lhs !== \"string\" || typeof value !== \"string\") return false;\n return isInsensitive\n ? lhs.toLowerCase().includes(value.toLowerCase())\n : lhs.includes(value);\n }\n case \"starts_with\": {\n if (typeof lhs !== \"string\" || typeof value !== \"string\") return false;\n return isInsensitive\n ? lhs.toLowerCase().startsWith(value.toLowerCase())\n : lhs.startsWith(value);\n }\n case \"ends_with\": {\n if (typeof lhs !== \"string\" || typeof value !== \"string\") return false;\n return isInsensitive\n ? lhs.toLowerCase().endsWith(value.toLowerCase())\n : lhs.endsWith(value);\n }\n case \"ne\":\n return !cmp(lhs, value);\n case \"gt\":\n return value != null && (lhs as never) > (value as never);\n case \"gte\":\n return value != null && (lhs as never) >= (value as never);\n case \"lt\":\n return value != null && (lhs as never) < (value as never);\n case \"lte\":\n return value != null && (lhs as never) <= (value as never);\n case \"eq\":\n default:\n return cmp(lhs, value);\n }\n};\n\n// Split-group AND/OR grouping: clauses with `connector: \"AND\"` (or no\n// connector) are conjoined, clauses with `connector: \"OR\"` are disjoined,\n// and the two groups are ANDed together. Mirrors the upstream better-auth\n// drizzle adapter's `convertWhereClause` so every backend (memory, sqlite,\n// postgres) observes the same mixed-connector semantics under the shared\n// conformance suite. This diverges from upstream's *memory* adapter, which\n// still uses a left-to-right fold; we prefer drizzle parity so that a\n// plugin that works against memory always works against SQL.\nconst matchAll = (record: Row, where: readonly CleanedWhere[]): boolean => {\n if (where.length === 0) return true;\n if (where.length === 1) return evalClause(record, where[0]!);\n const andGroup = where.filter(\n (w) => w.connector === \"AND\" || !w.connector,\n );\n const orGroup = where.filter((w) => w.connector === \"OR\");\n const andResult =\n andGroup.length === 0 ? true : andGroup.every((w) => evalClause(record, w));\n const orResult =\n orGroup.length === 0 ? true : orGroup.some((w) => evalClause(record, w));\n return andResult && orResult;\n};\n\nconst filterWhere = (rows: Row[], where: readonly CleanedWhere[]): Row[] =>\n rows.filter((r) => matchAll(r, where));\n\nconst cloneStore = (s: Store): Store => {\n const out: Store = {};\n for (const [k, v] of Object.entries(s)) {\n out[k] = v.map((r) => ({ ...r }));\n }\n return out;\n};\n\n// ---------------------------------------------------------------------------\n// makeMemoryAdapter — builds a DBAdapter wired up through createAdapter.\n// ---------------------------------------------------------------------------\n\nexport interface MakeMemoryAdapterOptions {\n readonly schema: DBSchema;\n readonly adapterId?: string;\n readonly generateId?: () => string;\n}\n\nexport const makeMemoryAdapter = (\n options: MakeMemoryAdapterOptions,\n): DBAdapter => {\n let store: Store = {};\n\n const tableFor = (model: string): Row[] => {\n if (!store[model]) store[model] = [];\n return store[model]!;\n };\n\n // Join resolver — mirrors the upstream memory adapter's path. Given a\n // base row and a resolved JoinConfig, look up matching rows in the\n // target model's table and attach them under the target's logical name.\n // For one-to-one we attach a single row (or null), otherwise we attach\n // an array capped at `limit`.\n const attachJoins = (base: Row, join: JoinConfig): Row => {\n const out: Row = { ...base };\n for (const [target, cfg] of Object.entries(join)) {\n const targetRows = tableFor(target);\n const matches = targetRows.filter(\n (r) => r[cfg.on.to] === base[cfg.on.from],\n );\n if (cfg.relation === \"one-to-one\") {\n out[target] = matches[0] ?? null;\n } else {\n const limit = cfg.limit ?? 100;\n out[target] = matches.slice(0, limit);\n }\n }\n return out;\n };\n\n const custom: CustomAdapter = {\n create: ({ model, data }) =>\n Effect.sync(() => {\n const table = tableFor(model);\n table.push(data as Row);\n return data;\n }),\n\n createMany: ({ model, data }) =>\n Effect.sync(() => {\n const table = tableFor(model);\n for (const row of data) table.push(row as Row);\n return data.slice() as never;\n }),\n\n findOne: ({ model, where, join }) =>\n Effect.sync(() => {\n const rows = filterWhere(tableFor(model), where);\n const first = rows[0];\n if (!first) return null as never;\n return (join ? attachJoins(first, join) : first) as never;\n }),\n\n findMany: ({ model, where, limit, sortBy, offset, join }) =>\n Effect.sync(() => {\n let rows = filterWhere(tableFor(model), where ?? []);\n if (sortBy) {\n const { field, direction } = sortBy;\n const sign = direction === \"asc\" ? 1 : -1;\n rows = rows.slice().sort((a, b) => {\n const av = a[field];\n const bv = b[field];\n if (av === bv) return 0;\n return (av as never) < (bv as never) ? -sign : sign;\n });\n }\n if (offset !== undefined) rows = rows.slice(offset);\n if (limit !== undefined && limit > 0) rows = rows.slice(0, limit);\n if (join) {\n return rows.map((r) => attachJoins(r, join)) as never[];\n }\n return rows as never[];\n }),\n\n count: ({ model, where }) =>\n Effect.sync(() => filterWhere(tableFor(model), where ?? []).length),\n\n update: ({ model, where, update }) =>\n Effect.sync(() => {\n const rows = filterWhere(tableFor(model), where);\n const first = rows[0];\n if (!first) return null;\n Object.assign(first, update as Row);\n return first as never;\n }),\n\n updateMany: ({ model, where, update }) =>\n Effect.sync(() => {\n const rows = filterWhere(tableFor(model), where);\n for (const r of rows) Object.assign(r, update);\n return rows.length;\n }),\n\n delete: ({ model, where }) =>\n Effect.sync(() => {\n const table = tableFor(model);\n const matches = filterWhere(table, where);\n const first = matches[0];\n if (!first) return;\n const idx = table.indexOf(first);\n if (idx >= 0) table.splice(idx, 1);\n }),\n\n deleteMany: ({ model, where }) =>\n Effect.sync(() => {\n const table = tableFor(model);\n const matches = new Set(filterWhere(table, where));\n let count = 0;\n store[model] = table.filter((r) => {\n if (matches.has(r)) {\n count++;\n return false;\n }\n return true;\n });\n return count;\n }),\n };\n\n // Snapshot-based transaction: clone on entry, restore on failure.\n const txFn: DBAdapterFactoryConfig[\"transaction\"] = <R, E>(\n cb: (trx: Parameters<DBAdapter[\"transaction\"]>[0] extends (\n t: infer T,\n ) => unknown\n ? T\n : never) => Effect.Effect<R, E>,\n ) =>\n Effect.gen(function* () {\n const snapshot = cloneStore(store);\n const result = yield* cb(adapter).pipe(\n Effect.catchAll((e) => {\n store = snapshot;\n return Effect.fail(e);\n }),\n );\n return result;\n }) as Effect.Effect<R, E | Error>;\n\n const adapter: DBAdapter = createAdapter({\n schema: options.schema,\n config: {\n adapterId: options.adapterId ?? \"memory\",\n supportsJSON: true,\n supportsDates: true,\n supportsBooleans: true,\n supportsArrays: true,\n customIdGenerator: options.generateId\n ? () => options.generateId!()\n : undefined,\n transaction: txFn,\n },\n adapter: custom,\n });\n\n return adapter;\n};\n","import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { createJiti } from \"jiti\";\nimport type { ExecutorCliConfig } from \"@executor/sdk/core\";\n\nconst defaultPaths = [\n \"executor.config.ts\",\n \"executor.config.js\",\n \"src/executor.config.ts\",\n \"src/executor.config.js\",\n];\n\nexport const getConfig = async (opts: {\n cwd: string;\n configPath?: string;\n}): Promise<ExecutorCliConfig | null> => {\n const { cwd, configPath } = opts;\n\n let resolvedPath: string | undefined;\n\n if (configPath) {\n resolvedPath = path.resolve(cwd, configPath);\n if (!existsSync(resolvedPath)) {\n console.error(`Config file not found: ${resolvedPath}`);\n return null;\n }\n } else {\n for (const p of defaultPaths) {\n const candidate = path.resolve(cwd, p);\n if (existsSync(candidate)) {\n resolvedPath = candidate;\n break;\n }\n }\n }\n\n if (!resolvedPath) return null;\n\n const jiti = createJiti(cwd, {\n interopDefault: true,\n moduleCache: false,\n });\n\n const mod = await jiti.import(resolvedPath);\n const config = (mod as { default?: ExecutorCliConfig }).default ?? mod;\n return config as ExecutorCliConfig;\n};\n","// ---------------------------------------------------------------------------\n// Drizzle schema generator — DBSchema → drizzle-orm TS source.\n//\n// Ported from better-auth (packages/cli/src/generators/drizzle.ts) under\n// MIT. Adapted for executor:\n// - Reads our DBSchema shape (modelName optional, key = default)\n// - No auth-specific logic (uuid/serial id modes, usePlural, camelCase)\n// - Always emits text primary keys\n// - Dialect from ExecutorCliConfig, not from adapter.options.provider\n// ---------------------------------------------------------------------------\n\nimport { existsSync } from \"node:fs\";\nimport type { DBSchema, DBFieldAttribute } from \"@executor/storage-core\";\nimport type { SchemaGenerator } from \"./types.js\";\n\ntype Dialect = \"pg\" | \"sqlite\" | \"mysql\";\n\nconst getModelName = (key: string, def: DBSchema[string]): string =>\n def.modelName ?? key;\n\nconst getType = (\n name: string,\n field: DBFieldAttribute,\n dialect: Dialect,\n): string => {\n if (field.references?.field === \"id\") {\n return `text('${name}')`;\n }\n\n const type = field.type;\n\n if (typeof type !== \"string\") {\n // Enum array — e.g. [\"active\", \"inactive\"]\n if (Array.isArray(type) && type.every((x) => typeof x === \"string\")) {\n return {\n sqlite: `text({ enum: [${type.map((x) => `'${x}'`).join(\", \")}] })`,\n pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(\", \")}] })`,\n mysql: `mysqlEnum([${type.map((x) => `'${x}'`).join(\", \")}])`,\n }[dialect];\n }\n throw new TypeError(\n `Invalid field type for field ${name}`,\n );\n }\n\n const typeMap: Record<string, Record<Dialect, string>> = {\n string: {\n sqlite: `text('${name}')`,\n pg: `text('${name}')`,\n mysql: field.unique\n ? `varchar('${name}', { length: 255 })`\n : field.references\n ? `varchar('${name}', { length: 36 })`\n : field.sortable\n ? `varchar('${name}', { length: 255 })`\n : field.index\n ? `varchar('${name}', { length: 255 })`\n : `text('${name}')`,\n },\n boolean: {\n sqlite: `integer('${name}', { mode: 'boolean' })`,\n pg: `boolean('${name}')`,\n mysql: `boolean('${name}')`,\n },\n number: {\n sqlite: `integer('${name}')`,\n pg: field.bigint\n ? `bigint('${name}', { mode: 'number' })`\n : `integer('${name}')`,\n mysql: field.bigint\n ? `bigint('${name}', { mode: 'number' })`\n : `int('${name}')`,\n },\n date: {\n sqlite: `integer('${name}', { mode: 'timestamp_ms' })`,\n pg: `timestamp('${name}')`,\n mysql: `timestamp('${name}', { fsp: 3 })`,\n },\n \"number[]\": {\n sqlite: `text('${name}', { mode: \"json\" })`,\n pg: field.bigint\n ? `bigint('${name}', { mode: 'number' }).array()`\n : `integer('${name}').array()`,\n mysql: `text('${name}', { mode: 'json' })`,\n },\n \"string[]\": {\n sqlite: `text('${name}', { mode: \"json\" })`,\n pg: `text('${name}').array()`,\n mysql: `text('${name}', { mode: \"json\" })`,\n },\n json: {\n sqlite: `text('${name}', { mode: \"json\" })`,\n pg: `jsonb('${name}')`,\n mysql: `json('${name}', { mode: \"json\" })`,\n },\n };\n\n const dbTypeMap = typeMap[type as string];\n if (!dbTypeMap) {\n throw new Error(\n `Unsupported field type '${field.type}' for field '${name}'.`,\n );\n }\n return dbTypeMap[dialect];\n};\n\n// ---------------------------------------------------------------------------\n// Generator\n// ---------------------------------------------------------------------------\n\nexport const generateDrizzleSchema: SchemaGenerator = async ({\n schema,\n dialect,\n file,\n}) => {\n const filePath = file || \"./executor-schema.ts\";\n const fileExist = existsSync(filePath);\n\n let code = generateImport({ dialect, schema });\n\n for (const [tableKey, tableDef] of Object.entries(schema)) {\n const modelName = getModelName(tableKey, tableDef);\n const fields = tableDef.fields;\n\n // Scoped tables get a composite `(scope_id, id)` primary key so two\n // tenants can register rows with the same user-facing id without\n // colliding on a globally-unique PK. Single-column PK stays for\n // unscoped tables (conformance fixtures, the blob store, etc.).\n const hasScopeId = Object.prototype.hasOwnProperty.call(fields, \"scope_id\");\n const id = hasScopeId ? `text('id').notNull()` : `text('id').primaryKey()`;\n\n type TableExtra =\n | { kind: \"uniqueIndex\" | \"index\"; name: string; on: string }\n | { kind: \"primaryKey\"; columns: readonly string[] };\n const extras: TableExtra[] = [];\n\n const assignExtras = (items: TableExtra[]): string => {\n if (!items.length) return \"\";\n const lines: string[] = [`, (table) => [`];\n for (const item of items) {\n if (item.kind === \"primaryKey\") {\n const cols = item.columns.map((c) => `table.${c}`).join(\", \");\n lines.push(` primaryKey({ columns: [${cols}] }),`);\n } else {\n lines.push(` ${item.kind}(\"${item.name}\").on(table.${item.on}),`);\n }\n }\n lines.push(`]`);\n return lines.join(\"\\n\");\n };\n\n if (hasScopeId) {\n extras.push({ kind: \"primaryKey\", columns: [\"scope_id\", \"id\"] });\n }\n\n const tableSchema = `export const ${tableKey} = ${dialect}Table(\"${modelName}\", {\n id: ${id},\n ${Object.entries(fields)\n .filter(([fieldName]) => fieldName !== \"id\")\n .map(([fieldName, attr]) => {\n const physical = attr.fieldName ?? fieldName;\n\n if (attr.index && !attr.unique) {\n extras.push({\n kind: \"index\",\n name: `${tableKey}_${physical}_idx`,\n on: physical,\n });\n } else if (attr.index && attr.unique) {\n extras.push({\n kind: \"uniqueIndex\",\n name: `${tableKey}_${physical}_uidx`,\n on: physical,\n });\n }\n\n let col = getType(physical, attr, dialect);\n\n if (\n attr.defaultValue !== null &&\n typeof attr.defaultValue !== \"undefined\"\n ) {\n if (typeof attr.defaultValue === \"function\") {\n if (\n attr.type === \"date\" &&\n attr.defaultValue.toString().includes(\"new Date()\")\n ) {\n if (dialect === \"sqlite\") {\n col += `.default(sql\\`(cast(unixepoch('subsecond') * 1000 as integer))\\`)`;\n } else {\n col += `.defaultNow()`;\n }\n }\n } else if (typeof attr.defaultValue === \"string\") {\n col += `.default(\"${attr.defaultValue}\")`;\n } else {\n col += `.default(${attr.defaultValue})`;\n }\n }\n\n if (attr.onUpdate && attr.type === \"date\") {\n if (typeof attr.onUpdate === \"function\") {\n col += `.$onUpdate(${attr.onUpdate})`;\n }\n }\n\n return `${physical}: ${col}${attr.required !== false ? \".notNull()\" : \"\"}${\n attr.unique ? \".unique()\" : \"\"\n }${\n attr.references\n ? `.references(()=> ${attr.references.model}.${attr.references.field ?? \"id\"}, { onDelete: '${\n attr.references.onDelete || \"cascade\"\n }' })`\n : \"\"\n }`;\n })\n .join(\",\\n \")}\n}${assignExtras(extras)});`;\n\n code += `\\n${tableSchema}\\n`;\n }\n\n // ---------------------------------------------------------------------------\n // Relations — scan FKs in both directions\n // ---------------------------------------------------------------------------\n\n let relationsString = \"\";\n for (const [tableKey, tableDef] of Object.entries(schema)) {\n const modelName = tableKey;\n\n type Relation = {\n key: string;\n model: string;\n type: \"one\" | \"many\";\n reference?: {\n field: string;\n references: string;\n fieldName: string;\n };\n };\n\n const oneRelations: Relation[] = [];\n const manyRelations: Relation[] = [];\n const manyRelationsSet = new Set<string>();\n\n // Find all FKs in THIS table → \"one\" relations\n for (const [fieldName, field] of Object.entries(tableDef.fields)) {\n if (!field.references) continue;\n const referencedModel = field.references.model;\n const physical = field.fieldName ?? fieldName;\n const fieldRef = `${tableKey}.${physical}`;\n const referenceRef = `${referencedModel}.${field.references.field || \"id\"}`;\n\n oneRelations.push({\n key: referencedModel,\n model: referencedModel,\n type: \"one\",\n reference: {\n field: fieldRef,\n references: referenceRef,\n fieldName,\n },\n });\n }\n\n // Find all OTHER tables that reference THIS table → \"many\" relations\n for (const [otherKey, otherDef] of Object.entries(schema)) {\n if (otherKey === tableKey) continue;\n const hasFK = Object.values(otherDef.fields).some(\n (field) => field.references?.model === tableKey,\n );\n if (!hasFK) continue;\n\n const relationKey = `${otherKey}s`;\n if (!manyRelationsSet.has(relationKey)) {\n manyRelationsSet.add(relationKey);\n manyRelations.push({\n key: relationKey,\n model: otherKey,\n type: \"many\",\n });\n }\n }\n\n // Detect duplicates\n const relationsByModel = new Map<string, Relation[]>();\n for (const rel of oneRelations) {\n if (!rel.reference) continue;\n const arr = relationsByModel.get(rel.key) ?? [];\n arr.push(rel);\n relationsByModel.set(rel.key, arr);\n }\n\n const duplicateRelations: Relation[] = [];\n const singleRelations: Relation[] = [];\n\n for (const [, rels] of relationsByModel.entries()) {\n if (rels.length > 1) {\n duplicateRelations.push(...rels);\n } else {\n singleRelations.push(rels[0]!);\n }\n }\n\n // Duplicate relations get field-specific exports\n for (const rel of duplicateRelations) {\n if (!rel.reference) continue;\n const relExportName = `${modelName}${rel.reference.fieldName.charAt(0).toUpperCase() + rel.reference.fieldName.slice(1)}Relations`;\n const block = `export const ${relExportName} = relations(${modelName}, ({ one }) => ({\n ${rel.key}: one(${rel.model}, {\n fields: [${rel.reference.field}],\n references: [${rel.reference.references}],\n })\n}))`;\n relationsString += `\\n${block}\\n`;\n }\n\n // Combined single relations\n const hasOne = singleRelations.length > 0;\n const hasMany = manyRelations.length > 0;\n\n if (hasOne || hasMany) {\n const destructured = [\n hasOne ? \"one\" : \"\",\n hasMany ? \"many\" : \"\",\n ]\n .filter(Boolean)\n .join(\", \");\n\n const body = [\n ...singleRelations\n .filter((r) => r.reference)\n .map(\n (r) =>\n ` ${r.key}: one(${r.model}, {\\n fields: [${r.reference!.field}],\\n references: [${r.reference!.references}],\\n })`,\n ),\n ...manyRelations.map(\n ({ key, model }) => ` ${key}: many(${model})`,\n ),\n ].join(\",\\n\");\n\n const block = `export const ${modelName}Relations = relations(${modelName}, ({ ${destructured} }) => ({\n${body}\n}))`;\n relationsString += `\\n${block}\\n`;\n }\n }\n\n code += `\\n${relationsString}`;\n\n return {\n code,\n fileName: filePath,\n overwrite: fileExist,\n };\n};\n\n// ---------------------------------------------------------------------------\n// Import generation — only emit what's actually used\n// ---------------------------------------------------------------------------\n\nfunction generateImport({\n dialect,\n schema,\n}: {\n dialect: Dialect;\n schema: DBSchema;\n}) {\n const rootImports: string[] = [];\n const coreImports: string[] = [];\n\n let hasBigint = false;\n let hasJson = false;\n let hasBoolean = false;\n let hasNumber = false;\n let hasDate = false;\n let hasIndex = false;\n let hasUniqueIndex = false;\n let hasReferences = false;\n let hasCompositePrimaryKey = false;\n\n for (const [tableKey, table] of Object.entries(schema)) {\n for (const field of Object.values(table.fields)) {\n if (field.bigint) hasBigint = true;\n if (field.type === \"json\") hasJson = true;\n if (field.type === \"boolean\") hasBoolean = true;\n if (field.type === \"number\" || field.type === \"number[]\") hasNumber = true;\n if (field.type === \"date\") hasDate = true;\n if (field.index && !field.unique) hasIndex = true;\n if (field.index && field.unique) hasUniqueIndex = true;\n if (field.references) hasReferences = true;\n }\n // Scoped tables get a composite (scope_id, id) PK — see generator\n // body where `primaryKey({ columns: [...] })` is emitted.\n if (Object.prototype.hasOwnProperty.call(table.fields, \"scope_id\")) {\n hasCompositePrimaryKey = true;\n }\n // Keep the generator silent about tableKey in this pass — we only\n // need the existence check above. Referenced here to satisfy lint.\n void tableKey;\n }\n\n coreImports.push(`${dialect}Table`);\n coreImports.push(\"text\");\n\n if (hasBoolean && dialect !== \"sqlite\") coreImports.push(\"boolean\");\n if (hasDate) {\n if (dialect === \"pg\") coreImports.push(\"timestamp\");\n // sqlite uses integer for timestamps, pg uses timestamp\n }\n if (hasNumber || dialect === \"sqlite\") {\n if (dialect === \"pg\") coreImports.push(\"integer\");\n else if (dialect === \"mysql\") coreImports.push(\"int\");\n else coreImports.push(\"integer\");\n }\n if (hasBigint && dialect !== \"sqlite\") coreImports.push(\"bigint\");\n if (hasJson) {\n if (dialect === \"pg\") coreImports.push(\"jsonb\");\n else if (dialect === \"mysql\") coreImports.push(\"json\");\n // sqlite uses text for JSON\n }\n if (hasIndex) coreImports.push(\"index\");\n if (hasUniqueIndex) coreImports.push(\"uniqueIndex\");\n if (hasCompositePrimaryKey) coreImports.push(\"primaryKey\");\n\n // sqlite needs integer for boolean + date\n if (dialect === \"sqlite\" && (hasBoolean || hasDate)) {\n if (!coreImports.includes(\"integer\")) coreImports.push(\"integer\");\n }\n // sqlite needs real for number\n if (dialect === \"sqlite\" && hasNumber) {\n // better-auth uses integer for numbers on sqlite; we use real()\n // for floating-point fidelity.\n }\n\n // Has any timestamp with defaultNow function?\n const hasSqliteTimestamp =\n dialect === \"sqlite\" &&\n Object.values(schema).some((table) =>\n Object.values(table.fields).some(\n (field) =>\n field.type === \"date\" &&\n field.defaultValue &&\n typeof field.defaultValue === \"function\" &&\n field.defaultValue.toString().includes(\"new Date()\"),\n ),\n );\n\n if (hasSqliteTimestamp) {\n rootImports.push(\"sql\");\n }\n\n if (hasReferences || dialect === \"mysql\") {\n // mysql might need varchar for FK fields\n }\n\n // `relations` is only imported when the schema has any references that\n // produce relation blocks (see relationsString generation).\n if (hasReferences) rootImports.push(\"relations\");\n\n const filteredCore = coreImports\n .map((x) => x.trim())\n .filter((x) => x !== \"\");\n\n // Deduplicate\n const uniqueCore = [...new Set(filteredCore)];\n const uniqueRoot = [...new Set(rootImports)];\n\n return `${uniqueRoot.length > 0 ? `import { ${uniqueRoot.join(\", \")} } from \"drizzle-orm\";\\n` : \"\"}import { ${uniqueCore.join(\", \")} } from \"drizzle-orm/${dialect}-core\";\\n`;\n}\n"],"mappings":"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","names":["Command","existsSync","path","Schema","Schema","Schema","Schema","Schema","Schema","Schema","Schema","Schema","Schema","Effect","Effect","Effect","existsSync","path","existsSync","Command"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/generate.ts","../../storage-core/src/factory.ts","../../storage-core/src/errors.ts","../../sdk/src/ids.ts","../../sdk/src/scope.ts","../../sdk/src/errors.ts","../../sdk/src/types.ts","../../sdk/src/core-schema.ts","../../sdk/src/policies.ts","../../sdk/src/secrets.ts","../../sdk/src/secret-backed-value.ts","../../sdk/src/connections.ts","../../sdk/src/elicitation.ts","../../sdk/src/blob.ts","../../sdk/src/oauth.ts","../../sdk/src/oauth-helpers.ts","../../sdk/src/oauth-service.ts","../../sdk/src/oauth-discovery.ts","../../sdk/src/executor.ts","../../sdk/src/scoped-adapter.ts","../../storage-core/src/testing/memory.ts","../src/utils/get-config.ts","../src/generators/drizzle.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from \"commander\";\nimport { generate } from \"./commands/generate.js\";\n\nprocess.on(\"SIGINT\", () => process.exit(0));\nprocess.on(\"SIGTERM\", () => process.exit(0));\n\nconst program = new Command(\"executor\")\n .version(\"0.0.1\")\n .description(\"Executor CLI\")\n .addCommand(generate)\n .action(() => program.help());\n\nprogram.parse();\n","import { existsSync } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\nimport { collectSchemas } from \"@executor-js/sdk/core\";\nimport { getConfig } from \"../utils/get-config.js\";\nimport { generateDrizzleSchema } from \"../generators/drizzle.js\";\n\nasync function generateAction(opts: {\n cwd: string;\n config?: string;\n output?: string;\n}) {\n const cwd = path.resolve(opts.cwd);\n if (!existsSync(cwd)) {\n console.error(`The directory \"${cwd}\" does not exist.`);\n process.exit(1);\n }\n\n const config = await getConfig({ cwd, configPath: opts.config });\n if (!config) {\n console.error(\n \"No configuration file found. Add an `executor.config.ts` file to \" +\n \"your project or pass the path using the `--config` flag.\",\n );\n process.exit(1);\n }\n\n const schema = collectSchemas(config.plugins);\n\n const result = await generateDrizzleSchema({\n schema,\n dialect: config.dialect,\n file: opts.output,\n });\n\n if (!result.code) {\n console.log(\"Schema is already up to date.\");\n process.exit(0);\n }\n\n const outPath = path.resolve(cwd, result.fileName);\n const outDir = path.dirname(outPath);\n if (!existsSync(outDir)) {\n await fs.mkdir(outDir, { recursive: true });\n }\n\n await fs.writeFile(outPath, result.code);\n console.log(`Schema generated: ${path.relative(cwd, outPath)}`);\n}\n\nexport const generate = new Command(\"generate\")\n .description(\"Generate a drizzle schema file from the executor config\")\n .option(\n \"-c, --cwd <cwd>\",\n \"the working directory\",\n process.cwd(),\n )\n .option(\n \"--config <config>\",\n \"path to the executor config file\",\n )\n .option(\n \"--output <output>\",\n \"output file path for the generated schema\",\n )\n .action(generateAction);\n","// ---------------------------------------------------------------------------\n// createAdapter — factory that wraps a CustomAdapter into a DBAdapter.\n//\n// Vendored from better-auth (packages/core/src/db/adapter/factory.ts) under\n// MIT. Adapted for executor:\n// - Promise/async → Effect.Effect<T, StorageFailure>\n// - Stripped auth-specific concerns (numeric serial ids, joins, telemetry\n// spans, logger, plural model name resolution, BetterAuthOptions)\n// - Contract matches our CustomAdapter + DBAdapterFactoryConfig in\n// ./adapter.ts (simpler than better-auth's equivalents)\n//\n// Responsibilities:\n// - id generation (auto + customIdGenerator + forceAllowId)\n// - transformInput: apply defaultValue / onUpdate / transform.input,\n// map logical field names → physical column names, serialize JSON /\n// dates / booleans / arrays based on supports* flags\n// - transformOutput: map physical column names → logical field names,\n// deserialize JSON / dates / booleans / arrays, apply transform.output,\n// filter by `returned: false`\n// - transformWhereClause: fill in CleanedWhere defaults, rename field,\n// re-encode RHS to match the write path\n// - createMany fallback: loop create when the CustomAdapter doesn't\n// implement it natively\n// - transaction: delegate to config.transaction when provided; fall back\n// to running the callback against the current adapter\n// ---------------------------------------------------------------------------\n\nimport { Effect } from \"effect\";\n\nimport type {\n CleanedWhere,\n CustomAdapter,\n DBAdapter,\n DBAdapterFactoryConfig,\n DBTransactionAdapter,\n JoinConfig,\n JoinOption,\n StorageFailure,\n Where,\n} from \"./adapter\";\nimport { StorageError } from \"./errors\";\nimport type { DBFieldAttribute, DBSchema, DBPrimitive } from \"./schema\";\n\n// ---------------------------------------------------------------------------\n// Id generation\n// ---------------------------------------------------------------------------\n\nconst defaultGenerateId = (): string =>\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `id_${Math.random().toString(36).slice(2)}_${Date.now().toString(36)}`;\n\n// ---------------------------------------------------------------------------\n// Default value helpers — mirrors better-auth's `withApplyDefault`.\n// ---------------------------------------------------------------------------\n\nconst withApplyDefault = (\n value: unknown,\n field: DBFieldAttribute,\n action: \"create\" | \"update\",\n): unknown => {\n if (action === \"update\") {\n // Only apply onUpdate when the caller DID NOT supply a value. An explicit\n // `updatedAt: someDate` in the update payload should win over the\n // plugin's onUpdate hook — matches upstream.\n if (value === undefined && field.onUpdate !== undefined) {\n return field.onUpdate();\n }\n return value;\n }\n // Create: apply defaultValue only when the caller omitted the field, OR\n // when they passed null for a required field (upstream convention —\n // explicit null on an optional/nullable field is preserved). Without the\n // `required` gate we'd silently overwrite legitimate null writes.\n const triggerDefault =\n value === undefined || (field.required === true && value === null);\n if (triggerDefault && field.defaultValue !== undefined) {\n return typeof field.defaultValue === \"function\"\n ? (field.defaultValue as () => DBPrimitive)()\n : field.defaultValue;\n }\n return value;\n};\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\nexport interface CreateAdapterOptions {\n readonly schema: DBSchema;\n readonly config: DBAdapterFactoryConfig;\n readonly adapter: CustomAdapter;\n}\n\n/**\n * Wrap a CustomAdapter into a full DBAdapter that applies schema-driven\n * transforms. This is the single codepath every backend shares.\n */\nexport const createAdapter = (\n options: CreateAdapterOptions,\n): DBAdapter => {\n const { schema, adapter: inner } = options;\n const typedOutput = <T>(value: unknown): T => value as T;\n const config: Required<\n Pick<\n DBAdapterFactoryConfig,\n | \"adapterId\"\n | \"supportsJSON\"\n | \"supportsDates\"\n | \"supportsBooleans\"\n | \"supportsArrays\"\n | \"disableIdGeneration\"\n >\n > & DBAdapterFactoryConfig = {\n ...options.config,\n supportsJSON: options.config.supportsJSON ?? false,\n supportsDates: options.config.supportsDates ?? true,\n supportsBooleans: options.config.supportsBooleans ?? true,\n supportsArrays: options.config.supportsArrays ?? false,\n disableIdGeneration: options.config.disableIdGeneration ?? false,\n };\n\n const idGen = (model: string): string => {\n if (config.customIdGenerator) {\n return config.customIdGenerator({ model });\n }\n return defaultGenerateId();\n };\n\n const getModelDef = (\n model: string,\n ): Effect.Effect<DBSchema[string], StorageError> =>\n Effect.gen(function* () {\n const def = schema[model];\n if (!def) {\n return yield* Effect.fail(\n new StorageError({\n message: `[storage-core] unknown model \"${model}\"`,\n cause: undefined,\n }),\n );\n }\n return def;\n });\n\n // Sync accessor for call sites that can't sit inside Effect.gen (cleanWhere,\n // getModelName, getPhysicalField). These are all fed model names that have\n // already been validated upstream by the typed API, so unknown-model throws\n // here are a caller bug, not a runtime failure channel.\n const getModelDefSync = (model: string): DBSchema[string] => {\n const def = schema[model];\n if (!def) throw new Error(`[storage-core] unknown model \"${model}\"`);\n return def;\n };\n\n // Map physical table name → logical model key, for renaming incoming model\n // arg in mapKeysTransformInput/Output when callers pass physical names.\n // We deliberately *don't* support plural or physical-name inputs — our\n // plugins always pass the logical key — so getModelName is identity.\n const getModelName = (model: string): string =>\n getModelDefSync(model).modelName ?? model;\n\n // Field name (logical → physical). Honors mapKeysTransformInput override.\n const getPhysicalField = (model: string, logical: string): string => {\n if (logical === \"id\") return config.mapKeysTransformInput?.[\"id\"] ?? \"id\";\n const override = config.mapKeysTransformInput?.[logical];\n if (override) return override;\n const attr = getModelDefSync(model).fields[logical];\n return attr?.fieldName ?? logical;\n };\n\n // Inverse of mapKeysTransformOutput: on the output path we may need to\n // rename a logical field to a different output key for the caller (symmetric\n // to mapKeysTransformInput on the write path). Upstream better-auth wires\n // this in the same place.\n const getOutputKey = (logical: string): string =>\n config.mapKeysTransformOutput?.[logical] ?? logical;\n\n // ---------------------------------------------------------------------------\n // Value encode / decode based on supports* flags.\n // ---------------------------------------------------------------------------\n\n const encodeValue = (\n attr: DBFieldAttribute | undefined,\n value: unknown,\n ): unknown => {\n if (value === undefined) return undefined;\n if (value === null) return null;\n if (!attr) return value;\n const type = attr.type;\n if (type === \"json\") {\n if (!config.supportsJSON) return JSON.stringify(value);\n return value;\n }\n if (type === \"date\") {\n if (value instanceof Date) {\n return config.supportsDates ? value : value.toISOString();\n }\n if (typeof value === \"string\" && !config.supportsDates) {\n // Keep ISO strings as-is\n return value;\n }\n return value;\n }\n if (type === \"boolean\") {\n if (config.supportsBooleans) return value;\n return value ? 1 : 0;\n }\n if (\n (type === \"string[]\" || type === \"number[]\") &&\n Array.isArray(value) &&\n !config.supportsArrays\n ) {\n return JSON.stringify(value);\n }\n return value;\n };\n\n const decodeValue = (\n attr: DBFieldAttribute | undefined,\n value: unknown,\n ): unknown => {\n if (value === undefined || value === null) return value;\n if (!attr) return value;\n const type = attr.type;\n if (type === \"json\" && typeof value === \"string\") {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n if (type === \"date\") {\n if (value instanceof Date) return value;\n if (typeof value === \"string\" || typeof value === \"number\") {\n return new Date(value);\n }\n return value;\n }\n if (type === \"boolean\" && typeof value === \"number\") {\n return value === 1;\n }\n if (\n (type === \"string[]\" || type === \"number[]\") &&\n typeof value === \"string\"\n ) {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n if (type === \"number\" && typeof value === \"string\") {\n const n = Number(value);\n return Number.isNaN(n) ? value : n;\n }\n return value;\n };\n\n // ---------------------------------------------------------------------------\n // transformInput — logical row → physical row, with defaults + id gen\n // ---------------------------------------------------------------------------\n\n const transformInput = (\n model: string,\n data: Record<string, unknown>,\n action: \"create\" | \"update\",\n forceAllowId: boolean,\n ): Effect.Effect<Record<string, unknown>, StorageFailure> =>\n Effect.gen(function* () {\n const def = yield* getModelDef(model);\n const out: Record<string, unknown> = {};\n\n // id handling on create\n if (action === \"create\") {\n if (forceAllowId && \"id\" in data && data.id !== undefined && data.id !== null) {\n out[getPhysicalField(model, \"id\")] = data.id;\n } else if (!config.disableIdGeneration) {\n out[getPhysicalField(model, \"id\")] = idGen(model);\n }\n }\n\n for (const [logical, attr] of Object.entries(def.fields)) {\n if (logical === \"id\") continue;\n if (attr.input === false) continue;\n let value: unknown = data[logical];\n\n // Date coercion from string\n if (\n attr.type === \"date\" &&\n value !== undefined &&\n value !== null &&\n !(value instanceof Date) &&\n typeof value === \"string\"\n ) {\n try {\n value = new Date(value);\n } catch {\n // leave as-is\n }\n }\n\n // defaultValue / onUpdate\n value = withApplyDefault(value, attr, action);\n\n // transform.input\n if (attr.transform?.input) {\n const res = attr.transform.input(value as DBPrimitive);\n // Sync only in executor path; if a plugin returns a Promise we\n // await it via tryPromise to keep the Effect pure.\n if (res && typeof (res as { then?: unknown }).then === \"function\") {\n value = yield* Effect.tryPromise({\n try: () => res as Promise<DBPrimitive>,\n catch: (cause) =>\n new StorageError({\n message: `[storage-core] transform.input for \"${model}.${logical}\" failed: ${cause instanceof Error ? cause.message : String(cause)}`,\n cause,\n }),\n });\n } else {\n value = res;\n }\n }\n\n if (value === undefined) continue;\n\n const physical = getPhysicalField(model, logical);\n let encoded = encodeValue(attr, value);\n\n // customTransformInput — user-land per-field hook, runs after the\n // built-in encode step. Effect-ified from upstream's sync `any`\n // return — plugins that don't need async work can wrap with\n // `Effect.succeed`.\n if (config.customTransformInput) {\n encoded = yield* config.customTransformInput({\n data: encoded,\n fieldAttributes: attr,\n field: physical,\n action,\n model: getModelName(model),\n schema,\n });\n }\n\n out[physical] = encoded;\n }\n\n return out;\n });\n\n // ---------------------------------------------------------------------------\n // transformOutput — physical row → logical row, filter `returned: false`\n // ---------------------------------------------------------------------------\n\n const transformOutput = (\n model: string,\n row: Record<string, unknown> | null,\n select?: string[],\n ): Effect.Effect<Record<string, unknown> | null, StorageFailure> =>\n Effect.gen(function* () {\n if (row === null || row === undefined) return null;\n const def = yield* getModelDef(model);\n const out: Record<string, unknown> = {};\n\n // id always returned\n const idPhysical = getPhysicalField(model, \"id\");\n const idOutputKey = getOutputKey(\"id\");\n if (idPhysical in row && row[idPhysical] !== undefined) {\n out[idOutputKey] = row[idPhysical];\n } else if (\"id\" in row) {\n out[idOutputKey] = row[\"id\"];\n }\n\n for (const [logical, attr] of Object.entries(def.fields)) {\n if (logical === \"id\") continue;\n if (attr.returned === false) continue;\n if (select && select.length > 0 && !select.includes(logical)) continue;\n\n const physical = getPhysicalField(model, logical);\n if (!(physical in row)) continue;\n\n let value: unknown = decodeValue(attr, row[physical]);\n\n if (attr.transform?.output) {\n const res = attr.transform.output(value as DBPrimitive);\n if (res && typeof (res as { then?: unknown }).then === \"function\") {\n value = yield* Effect.tryPromise({\n try: () => res as Promise<DBPrimitive>,\n catch: (cause) =>\n new StorageError({\n message: `[storage-core] transform.output for \"${model}.${logical}\" failed: ${cause instanceof Error ? cause.message : String(cause)}`,\n cause,\n }),\n });\n } else {\n value = res;\n }\n }\n\n // customTransformOutput — user-land per-field hook, runs after the\n // built-in decode step. Mirrors upstream threading.\n if (config.customTransformOutput) {\n value = yield* config.customTransformOutput({\n data: value,\n fieldAttributes: attr,\n field: logical,\n select: select ?? [],\n model: getModelName(model),\n schema,\n });\n }\n\n out[getOutputKey(logical)] = value;\n }\n return out;\n });\n\n // ---------------------------------------------------------------------------\n // transformWhereClause — Where[] → CleanedWhere[]\n //\n // Fills in defaults, renames logical → physical, re-encodes RHS so that\n // filter comparisons line up with the wire representation produced by\n // transformInput.\n // ---------------------------------------------------------------------------\n\n // ---------------------------------------------------------------------------\n // Join resolver — JoinOption (caller) → JoinConfig (custom adapter)\n //\n // For each requested join target `T` (keyed by logical model name), we\n // look at both sides of the schema:\n //\n // - If the *base* model has a field whose `references.model === T`,\n // this is a \"child → parent\" lookup: relation = one-to-one,\n // `on.from` = the base field, `on.to` = the referenced field on T.\n // - Otherwise, if model T has a field whose `references.model === base`,\n // this is a \"parent → children\" lookup: relation = one-to-many,\n // `on.from` = the referenced field on base (usually \"id\"),\n // `on.to` = the field on T carrying the FK.\n //\n // If neither side declares a reference we throw — a caller asking for a\n // join that the schema can't resolve is a bug, not a runtime state.\n // ---------------------------------------------------------------------------\n\n const resolveJoin = (base: string, join: JoinOption): JoinConfig => {\n const baseDef = getModelDefSync(base);\n const out: JoinConfig = {};\n for (const [target, raw] of Object.entries(join)) {\n if (raw === false) continue;\n const targetDef = getModelDefSync(target);\n const limit =\n typeof raw === \"object\" && raw.limit !== undefined ? raw.limit : undefined;\n\n // child → parent\n let found: JoinConfig[string] | undefined;\n for (const [fieldName, attr] of Object.entries(baseDef.fields)) {\n if (attr.references?.model === target) {\n found = {\n on: {\n from: getPhysicalField(base, fieldName),\n to:\n getPhysicalField(target, attr.references.field) ||\n attr.references.field,\n },\n relation: \"one-to-one\",\n ...(limit !== undefined ? { limit } : {}),\n };\n break;\n }\n }\n // parent → children\n if (!found) {\n for (const [fieldName, attr] of Object.entries(targetDef.fields)) {\n if (attr.references?.model === base) {\n found = {\n on: {\n from:\n getPhysicalField(base, attr.references.field) ||\n attr.references.field,\n to: getPhysicalField(target, fieldName),\n },\n relation: \"one-to-many\",\n ...(limit !== undefined ? { limit } : {}),\n };\n break;\n }\n }\n }\n if (!found) {\n throw new Error(\n `[storage-core] cannot resolve join \"${base}\" → \"${target}\": neither model declares a \\`references\\` for the other`,\n );\n }\n out[target] = found;\n }\n return out;\n };\n\n const cleanWhere = (\n model: string,\n where: readonly Where[] | undefined,\n ): CleanedWhere[] | undefined => {\n if (!where) return undefined;\n const def = getModelDefSync(model);\n return where.map((w) => {\n const operator = w.operator ?? \"eq\";\n const connector = w.connector ?? \"AND\";\n const mode = w.mode ?? \"sensitive\";\n const logical = w.field;\n const attr =\n logical === \"id\" ? undefined : def.fields[logical];\n const physical = getPhysicalField(model, logical);\n\n let value: Where[\"value\"] = w.value;\n if (attr) {\n if (Array.isArray(value)) {\n value = (value as unknown[]).map((v) =>\n encodeValue(attr, v),\n ) as typeof value;\n } else {\n value = encodeValue(attr, value) as typeof value;\n }\n }\n\n return {\n operator,\n connector,\n mode,\n field: physical,\n value,\n } satisfies CleanedWhere;\n });\n };\n\n // ---------------------------------------------------------------------------\n // Transform skip helpers — disableTransformInput/Output let backend authors\n // bypass the factory's built-in transform when they want the raw shape\n // passed through. Matches upstream's `if (!config.disableTransform*)`.\n // ---------------------------------------------------------------------------\n\n const maybeTransformInput = (\n model: string,\n data: Record<string, unknown>,\n action: \"create\" | \"update\",\n forceAllowId: boolean,\n ): Effect.Effect<Record<string, unknown>, StorageFailure> =>\n config.disableTransformInput\n ? Effect.succeed(data)\n : transformInput(model, data, action, forceAllowId);\n\n // ---------------------------------------------------------------------------\n // attachJoinedRows — re-decode nested join payloads.\n //\n // `transformOutput` only knows about the base model's fields and drops\n // anything else. If the caller asked for `join: { tag: true }` we need\n // to run the nested rows through `transformOutput` keyed on the target\n // model and then stick the decoded payload onto the base row under the\n // logical join key. Mirrors the upstream factory's nested-result path.\n // ---------------------------------------------------------------------------\n\n const attachJoinedRows = (\n base: Record<string, unknown> | null,\n raw: Record<string, unknown> | null,\n join: JoinOption | undefined,\n ): Effect.Effect<Record<string, unknown> | null, StorageFailure> =>\n Effect.gen(function* () {\n if (!base || !raw || !join) return base;\n const merged: Record<string, unknown> = { ...base };\n for (const [target, flag] of Object.entries(join)) {\n if (flag === false) continue;\n const nested = raw[target];\n if (nested === undefined) continue;\n if (nested === null) {\n merged[target] = null;\n continue;\n }\n if (Array.isArray(nested)) {\n const decoded: unknown[] = [];\n for (const n of nested) {\n if (n && typeof n === \"object\") {\n const t = yield* transformOutput(\n target,\n n as Record<string, unknown>,\n );\n decoded.push(t);\n } else {\n decoded.push(n);\n }\n }\n merged[target] = decoded;\n } else if (typeof nested === \"object\") {\n merged[target] = yield* transformOutput(\n target,\n nested as Record<string, unknown>,\n );\n } else {\n merged[target] = nested;\n }\n }\n return merged;\n });\n\n const maybeTransformOutput = (\n model: string,\n row: Record<string, unknown> | null,\n select?: string[],\n ): Effect.Effect<Record<string, unknown> | null, StorageFailure> =>\n config.disableTransformOutput\n ? Effect.succeed(row)\n : transformOutput(model, row, select);\n\n // ---------------------------------------------------------------------------\n // DBAdapter surface\n // ---------------------------------------------------------------------------\n\n const self: DBAdapter = {\n id: config.adapterId,\n\n create: <T extends Record<string, unknown>, R = T>(data: {\n model: string;\n data: Omit<T, \"id\">;\n select?: string[] | undefined;\n forceAllowId?: boolean | undefined;\n }) =>\n Effect.gen(function* () {\n const input = yield* maybeTransformInput(\n data.model,\n data.data as Record<string, unknown>,\n \"create\",\n data.forceAllowId === true,\n );\n const res = yield* inner.create({\n model: getModelName(data.model),\n data: input,\n select: data.select,\n });\n const out = yield* maybeTransformOutput(\n data.model,\n res as Record<string, unknown>,\n data.select,\n );\n return typedOutput<R>(out);\n }).pipe(\n Effect.withSpan(\"executor.storage.create\", {\n attributes: {\n \"executor.storage.adapter_id\": config.adapterId,\n \"executor.storage.table\": data.model,\n },\n }),\n ),\n\n createMany: <T extends Record<string, unknown>, R = T>(data: {\n model: string;\n data: ReadonlyArray<Omit<T, \"id\">>;\n forceAllowId?: boolean | undefined;\n }) =>\n Effect.gen(function* () {\n // Delegates straight to the backend's native bulk insert — no\n // per-row fallback, because over a real network connection\n // (Hyperdrive, etc.) N round-trips would blow the request\n // budget for specs with thousands of operations. Transforms\n // still run per-row so JSON / dates / booleans serialize the\n // same as single `create`.\n const inputs: Record<string, unknown>[] = [];\n for (const row of data.data) {\n inputs.push(\n yield* maybeTransformInput(\n data.model,\n row as Record<string, unknown>,\n \"create\",\n data.forceAllowId === true,\n ),\n );\n }\n const res = yield* inner.createMany({\n model: getModelName(data.model),\n data: inputs,\n });\n const out: R[] = [];\n for (const row of res) {\n out.push(\n typedOutput<R>(\n yield* maybeTransformOutput(\n data.model,\n row as Record<string, unknown>,\n undefined,\n ),\n ),\n );\n }\n return typedOutput<readonly R[]>(out);\n }).pipe(\n Effect.withSpan(\"executor.storage.create_many\", {\n attributes: {\n \"executor.storage.adapter_id\": config.adapterId,\n \"executor.storage.table\": data.model,\n \"executor.storage.row_count\": data.data.length,\n },\n }),\n ),\n\n findOne: <T>(data: {\n model: string;\n where: Where[];\n select?: string[] | undefined;\n join?: JoinOption | undefined;\n }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where) ?? [];\n const join = data.join ? resolveJoin(data.model, data.join) : undefined;\n const res = yield* inner.findOne<Record<string, unknown>>({\n model: getModelName(data.model),\n where,\n select: data.select,\n join,\n });\n const out = yield* maybeTransformOutput(data.model, res, data.select);\n const merged = yield* attachJoinedRows(out, res, data.join);\n return typedOutput<T | null>(merged);\n }).pipe(\n Effect.withSpan(\"executor.storage.find_one\", {\n attributes: {\n \"executor.storage.adapter_id\": config.adapterId,\n \"executor.storage.table\": data.model,\n },\n }),\n ),\n\n findMany: <T>(data: {\n model: string;\n where?: Where[] | undefined;\n limit?: number | undefined;\n select?: string[] | undefined;\n sortBy?: { field: string; direction: \"asc\" | \"desc\" } | undefined;\n offset?: number | undefined;\n join?: JoinOption | undefined;\n }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where);\n const sortBy = data.sortBy\n ? {\n field: getPhysicalField(data.model, data.sortBy.field),\n direction: data.sortBy.direction,\n }\n : undefined;\n const join = data.join ? resolveJoin(data.model, data.join) : undefined;\n const res = yield* inner.findMany<Record<string, unknown>>({\n model: getModelName(data.model),\n where,\n limit: data.limit,\n select: data.select,\n sortBy,\n offset: data.offset,\n join,\n });\n const out: unknown[] = [];\n for (const r of res) {\n const t = yield* maybeTransformOutput(data.model, r, data.select);\n const merged = yield* attachJoinedRows(t, r, data.join);\n out.push(merged);\n }\n return out as readonly T[];\n }).pipe(\n Effect.withSpan(\"executor.storage.find_many\", {\n attributes: {\n \"executor.storage.adapter_id\": config.adapterId,\n \"executor.storage.table\": data.model,\n },\n }),\n ),\n\n count: (data: { model: string; where?: Where[] | undefined }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where);\n return yield* inner.count({\n model: getModelName(data.model),\n where,\n });\n }).pipe(\n Effect.withSpan(\"executor.storage.count\", {\n attributes: {\n \"executor.storage.adapter_id\": config.adapterId,\n \"executor.storage.table\": data.model,\n },\n }),\n ),\n\n update: <T>(data: {\n model: string;\n where: Where[];\n update: Record<string, unknown>;\n }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where) ?? [];\n const update = yield* maybeTransformInput(\n data.model,\n data.update,\n \"update\",\n false,\n );\n const res = yield* inner.update<Record<string, unknown>>({\n model: getModelName(data.model),\n where,\n update,\n });\n const out = yield* maybeTransformOutput(data.model, res);\n return typedOutput<T | null>(out);\n }).pipe(\n Effect.withSpan(\"executor.storage.update\", {\n attributes: {\n \"executor.storage.adapter_id\": config.adapterId,\n \"executor.storage.table\": data.model,\n },\n }),\n ),\n\n updateMany: (data: {\n model: string;\n where: Where[];\n update: Record<string, unknown>;\n }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where) ?? [];\n const update = yield* maybeTransformInput(\n data.model,\n data.update,\n \"update\",\n false,\n );\n return yield* inner.updateMany({\n model: getModelName(data.model),\n where,\n update,\n });\n }).pipe(\n Effect.withSpan(\"executor.storage.update_many\", {\n attributes: {\n \"executor.storage.adapter_id\": config.adapterId,\n \"executor.storage.table\": data.model,\n },\n }),\n ),\n\n delete: (data: { model: string; where: Where[] }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where) ?? [];\n yield* inner.delete({\n model: getModelName(data.model),\n where,\n });\n }).pipe(\n Effect.withSpan(\"executor.storage.delete\", {\n attributes: {\n \"executor.storage.adapter_id\": config.adapterId,\n \"executor.storage.table\": data.model,\n },\n }),\n ),\n\n deleteMany: (data: { model: string; where: Where[] }) =>\n Effect.gen(function* () {\n const where = cleanWhere(data.model, data.where) ?? [];\n return yield* inner.deleteMany({\n model: getModelName(data.model),\n where,\n });\n }).pipe(\n Effect.withSpan(\"executor.storage.delete_many\", {\n attributes: {\n \"executor.storage.adapter_id\": config.adapterId,\n \"executor.storage.table\": data.model,\n },\n }),\n ),\n\n transaction: <R, E>(\n callback: (trx: DBTransactionAdapter) => Effect.Effect<R, E>,\n ) => {\n const txFn = config.transaction;\n const ran = !txFn ? callback(self) : txFn(callback);\n return ran.pipe(\n Effect.withSpan(\"executor.storage.transaction\", {\n attributes: {\n \"executor.storage.adapter_id\": config.adapterId,\n \"executor.storage.transaction.native\": txFn !== undefined,\n },\n }),\n );\n },\n\n // Forward the backend's createSchema verbatim. Upstream better-auth\n // mutates the `tables` set here to drop session when secondaryStorage\n // is set; we intentionally don't replicate that auth-specific concern.\n createSchema: inner.createSchema\n ? (props) => inner.createSchema!(props)\n : undefined,\n\n // Expose the full factory config + the inner adapter's own options to\n // plugin authors at runtime. Mirrors upstream's `options` field on\n // DBAdapter.\n options: {\n adapterConfig: options.config,\n ...(inner.options ?? {}),\n },\n };\n\n return self;\n};\n","// ---------------------------------------------------------------------------\n// Storage-layer typed errors.\n//\n// Both are `Data.TaggedError` — runtime values, not wire schemas.\n// Storage-core stays out of HTTP / serialisation / telemetry concerns;\n// the HTTP edge (`@executor-js/api` `withCapture`) is the one place\n// that translates `StorageError` into the opaque public\n// `InternalError({ traceId })`. Plugins `Effect.catchTag(\"UniqueViolationError\")`\n// and re-fail with their own schema'd error (e.g. `McpSourceAlreadyExistsError`).\n//\n// The `Data` choice (vs `Schema.TaggedError`) is enforcement: it's\n// physically impossible to `addError(...)` these on an HttpApi group, so\n// nobody can accidentally leak storage-layer details to clients by\n// letting them serialize through.\n// ---------------------------------------------------------------------------\n\nimport { Data } from \"effect\";\n\n/**\n * Catch-all for non-recoverable backend failures (driver crash, network\n * gone, transaction abort the backend can't classify, etc.). The cause\n * travels as runtime data so the HTTP edge can capture it via\n * `ErrorCapture` before translating to the public `InternalError`.\n */\nexport class StorageError extends Data.TaggedError(\"StorageError\")<{\n readonly message: string;\n readonly cause: unknown;\n}> {}\n\n/**\n * Typed unique-constraint violation. Plugins are expected to\n * `Effect.catchTag` this and translate to their own user-facing error.\n * Carries an optional `model` so a plugin doing a batch insert across\n * tables can disambiguate; everything else (constraint name, raw\n * driver message) stays internal because plugin code rarely needs it\n * and surfacing it leaks backend specifics.\n */\nexport class UniqueViolationError extends Data.TaggedError(\n \"UniqueViolationError\",\n)<{\n readonly model?: string;\n}> {}\n","import { Schema } from \"effect\";\n\nexport const ScopeId = Schema.String.pipe(Schema.brand(\"ScopeId\"));\nexport type ScopeId = typeof ScopeId.Type;\n\nexport const ToolId = Schema.String.pipe(Schema.brand(\"ToolId\"));\nexport type ToolId = typeof ToolId.Type;\n\nexport const SecretId = Schema.String.pipe(Schema.brand(\"SecretId\"));\nexport type SecretId = typeof SecretId.Type;\n\nexport const PolicyId = Schema.String.pipe(Schema.brand(\"PolicyId\"));\nexport type PolicyId = typeof PolicyId.Type;\n\nexport const ConnectionId = Schema.String.pipe(Schema.brand(\"ConnectionId\"));\nexport type ConnectionId = typeof ConnectionId.Type;\n","import { Schema } from \"effect\";\n\nimport { ScopeId } from \"./ids\";\n\nexport class Scope extends Schema.Class<Scope>(\"Scope\")({\n id: ScopeId,\n name: Schema.String,\n createdAt: Schema.Date,\n}) {}\n","import { Data, Schema } from \"effect\";\n\nimport { ConnectionId, ToolId, SecretId } from \"./ids\";\n\n// ---------------------------------------------------------------------------\n// Tool lifecycle\n// ---------------------------------------------------------------------------\n\nexport class ToolNotFoundError extends Schema.TaggedErrorClass<ToolNotFoundError>()(\n \"ToolNotFoundError\",\n { toolId: ToolId },\n) {}\n\nexport class ToolInvocationError extends Data.TaggedError(\"ToolInvocationError\")<{\n readonly toolId: ToolId;\n readonly message: string;\n readonly cause?: unknown;\n}> {}\n\n/** Tool row exists in the DB but its owning plugin isn't loaded. Means\n * the tool was registered by a plugin that's no longer present in the\n * current executor config — usually a stale row from an older session. */\nexport class PluginNotLoadedError extends Schema.TaggedErrorClass<PluginNotLoadedError>()(\n \"PluginNotLoadedError\",\n {\n pluginId: Schema.String,\n toolId: ToolId,\n },\n) {}\n\n/** Tool was found but its owning plugin has no `invokeTool` handler —\n * the plugin only declares static tools and this one's id matched\n * dynamically somehow. Shouldn't happen in practice; guards against\n * programmer error. */\nexport class NoHandlerError extends Schema.TaggedErrorClass<NoHandlerError>()(\n \"NoHandlerError\",\n {\n toolId: ToolId,\n pluginId: Schema.String,\n },\n) {}\n\n/** Tool invocation was rejected because a workspace `tool_policy` rule\n * with `action: \"block\"` matched. `pattern` is the matched policy\n * pattern so callers / agents can render a useful \"this is blocked\n * by your `vercel.dns.*` rule\" message. */\nexport class ToolBlockedError extends Schema.TaggedErrorClass<ToolBlockedError>()(\n \"ToolBlockedError\",\n {\n toolId: ToolId,\n pattern: Schema.String,\n },\n) {}\n\n// ---------------------------------------------------------------------------\n// Source lifecycle\n// ---------------------------------------------------------------------------\n\nexport class SourceNotFoundError extends Schema.TaggedErrorClass<SourceNotFoundError>()(\n \"SourceNotFoundError\",\n { sourceId: Schema.String },\n) {}\n\n/** `executor.sources.remove(id)` was called on a source with\n * `canRemove: false` — typically a static source declared by a plugin\n * at startup. Removing static sources is a bug in the caller. */\nexport class SourceRemovalNotAllowedError extends Schema.TaggedErrorClass<SourceRemovalNotAllowedError>()(\n \"SourceRemovalNotAllowedError\",\n { sourceId: Schema.String },\n) {}\n\n// ---------------------------------------------------------------------------\n// Secrets\n// ---------------------------------------------------------------------------\n\nexport class SecretNotFoundError extends Schema.TaggedErrorClass<SecretNotFoundError>()(\n \"SecretNotFoundError\",\n { secretId: SecretId },\n) {}\n\nexport class SecretResolutionError extends Schema.TaggedErrorClass<SecretResolutionError>()(\n \"SecretResolutionError\",\n {\n secretId: SecretId,\n message: Schema.String,\n },\n) {}\n\n/** Raised when `secrets.remove(id)` is called on a secret whose row has\n * `owned_by_connection_id` set. The connection owns the lifecycle —\n * callers must go through `connections.remove(connectionId)` to\n * delete it along with its siblings. */\nexport class SecretOwnedByConnectionError extends Schema.TaggedErrorClass<SecretOwnedByConnectionError>()(\n \"SecretOwnedByConnectionError\",\n {\n secretId: SecretId,\n connectionId: ConnectionId,\n },\n) {}\n\n// ---------------------------------------------------------------------------\n// Connections\n// ---------------------------------------------------------------------------\n\nexport class ConnectionNotFoundError extends Schema.TaggedErrorClass<ConnectionNotFoundError>()(\n \"ConnectionNotFoundError\",\n { connectionId: ConnectionId },\n) {}\n\nexport class ConnectionProviderNotRegisteredError extends Schema.TaggedErrorClass<ConnectionProviderNotRegisteredError>()(\n \"ConnectionProviderNotRegisteredError\",\n {\n provider: Schema.String,\n connectionId: Schema.optional(ConnectionId),\n },\n) {}\n\nexport class ConnectionRefreshNotSupportedError extends Schema.TaggedErrorClass<ConnectionRefreshNotSupportedError>()(\n \"ConnectionRefreshNotSupportedError\",\n {\n connectionId: ConnectionId,\n provider: Schema.String,\n },\n) {}\n\n/**\n * Raised by `connections.accessToken(id)` when the provider's refresh\n * handler reported that the stored refresh token is permanently\n * invalid (RFC 6749 §5.2 `invalid_grant` and friends). The caller —\n * typically a tool invocation — surfaces this so the UI can prompt the\n * user to sign in again. Distinct from `ConnectionRefreshError` so\n * \"the network flaked, retry later\" and \"the grant is dead, re-auth\"\n * don't collapse into one error tag at the plugin boundary.\n */\nexport class ConnectionReauthRequiredError extends Schema.TaggedErrorClass<ConnectionReauthRequiredError>()(\n \"ConnectionReauthRequiredError\",\n {\n connectionId: ConnectionId,\n provider: Schema.String,\n message: Schema.String,\n },\n) {}\n\n// ---------------------------------------------------------------------------\n// Union type for convenience in signatures.\n// ---------------------------------------------------------------------------\n\nexport type ExecutorError =\n | ToolNotFoundError\n | ToolInvocationError\n | PluginNotLoadedError\n | NoHandlerError\n | ToolBlockedError\n | SourceNotFoundError\n | SourceRemovalNotAllowedError\n | SecretNotFoundError\n | SecretResolutionError\n | SecretOwnedByConnectionError\n | ConnectionNotFoundError\n | ConnectionProviderNotRegisteredError\n | ConnectionRefreshNotSupportedError\n | ConnectionReauthRequiredError;\n","// ---------------------------------------------------------------------------\n// Public projections — what consumers see when they call\n// `executor.sources.list()` / `executor.tools.list()`. Deliberately leaner\n// than the row shapes in core-schema.ts: no audit columns, no raw JSON.\n// ---------------------------------------------------------------------------\n\nimport { Schema } from \"effect\";\n\nimport type { ToolAnnotations } from \"./core-schema\";\nimport { ToolId } from \"./ids\";\n\nexport interface Source {\n readonly id: string;\n /** Owning scope of the visible source row. Present for dynamic\n * sources; static sources omit it. */\n readonly scopeId?: string;\n readonly kind: string;\n readonly name: string;\n readonly url?: string;\n /** Which plugin owns this source. */\n readonly pluginId: string;\n /** Whether the user can remove this source via\n * `executor.sources.remove(id)`. `false` for static / built-in\n * sources declared by plugins at startup. */\n readonly canRemove: boolean;\n /** Whether the plugin supports `executor.sources.refresh(id)`. */\n readonly canRefresh: boolean;\n /** Whether the source has editable config (headers, base url, etc.).\n * Editing is done via plugin-specific extension methods\n * (`executor.openapi.updateSource(id, patch)` etc.) — this flag is\n * just a UI signal. */\n readonly canEdit: boolean;\n /** True if the source was declared statically by a plugin at startup\n * (in-memory only, no DB row). False if it was added at runtime via\n * `ctx.core.sources.register(...)`. UI differentiates built-in vs\n * user-added with this. */\n readonly runtime: boolean;\n}\n\nexport interface Tool {\n readonly id: string;\n readonly sourceId: string;\n /** Which plugin owns this tool. Matches the owning source's `pluginId`. */\n readonly pluginId: string;\n readonly name: string;\n readonly description: string;\n readonly inputSchema?: unknown;\n readonly outputSchema?: unknown;\n readonly annotations?: ToolAnnotations;\n}\n\n// ---------------------------------------------------------------------------\n// ToolSchema — the full schema-side view of a tool, returned by\n// `executor.tools.schema(toolId)`. Includes JSON schemas with `$defs`\n// attached at read time AND TypeScript preview strings rendered from\n// them via `schemaToTypeScriptPreview`. The UI uses the TS previews to\n// show \"calling this tool looks like this\" code samples.\n// ---------------------------------------------------------------------------\n\nexport class ToolSchema extends Schema.Class<ToolSchema>(\"ToolSchema\")({\n id: ToolId,\n name: Schema.optional(Schema.String),\n description: Schema.optional(Schema.String),\n inputSchema: Schema.optional(Schema.Unknown),\n outputSchema: Schema.optional(Schema.Unknown),\n inputTypeScript: Schema.optional(Schema.String),\n outputTypeScript: Schema.optional(Schema.String),\n typeScriptDefinitions: Schema.optional(\n Schema.Record(Schema.String, Schema.String),\n ),\n}) {}\n\n// ---------------------------------------------------------------------------\n// Source detection — optional capability on `PluginSpec.detect`. When a\n// user pastes a URL in the onboarding UI, `executor.sources.detect(url)`\n// asks every plugin \"is this yours?\" and returns the best-confidence\n// match so the UI can auto-fill the onboarding form for the right\n// plugin.\n// ---------------------------------------------------------------------------\n\nexport class SourceDetectionResult extends Schema.Class<SourceDetectionResult>(\n \"SourceDetectionResult\",\n)({\n /** Plugin id that recognized the URL (e.g. \"openapi\", \"graphql\"). */\n kind: Schema.String,\n /** Confidence tier — UI uses this to pick a winner when multiple\n * plugins claim a URL. */\n confidence: Schema.Literals([\"high\", \"medium\", \"low\"]),\n /** The (possibly normalized) endpoint the plugin will use. */\n endpoint: Schema.String,\n /** Human-readable name suggestion, typically derived from spec title\n * or URL hostname. */\n name: Schema.String,\n /** Namespace suggestion — the plugin's recommendation for the source\n * id. UI may override. */\n namespace: Schema.String,\n}) {}\n\n// ---------------------------------------------------------------------------\n// Filter passed to `executor.tools.list(...)`. Empty filter = all tools.\n// ---------------------------------------------------------------------------\n\nexport interface ToolListFilter {\n /** Only tools under this source id. */\n readonly sourceId?: string;\n /** Case-insensitive substring match against `name` OR `description`. */\n readonly query?: string;\n /** Resolve plugin-derived annotations. Defaults to true. */\n readonly includeAnnotations?: boolean;\n /** Include tools whose effective `tool_policy` is `block`. Defaults to\n * `false` so the agent-facing surfaces (`searchTools`, sandbox `tools.list`)\n * silently omit blocked tools. The settings UI for managing policies\n * should pass `true` so users can author rules against blocked tools. */\n readonly includeBlocked?: boolean;\n}\n","// ---------------------------------------------------------------------------\n// Core data model — the SDK owns these tables. Plugins write into them via\n// `ctx.core.sources.register(...)` and `ctx.core.definitions.register(...)`;\n// the executor reads from them directly on every list / invoke / schema\n// call. There is no in-memory registry layered on top.\n//\n// Static (code-declared) sources and tools are NOT in these tables — they\n// live in an in-memory map built at executor startup from each plugin's\n// `staticSources` declaration. See executor.ts. The DB only holds\n// dynamic (runtime-registered) rows.\n// ---------------------------------------------------------------------------\n\nimport type {\n DBSchema,\n InferDBFieldsOutput,\n} from \"@executor-js/storage-core\";\n\nexport const coreSchema = {\n source: {\n fields: {\n id: { type: \"string\", required: true },\n scope_id: { type: \"string\", required: true, index: true },\n plugin_id: { type: \"string\", required: true, index: true },\n kind: { type: \"string\", required: true },\n name: { type: \"string\", required: true },\n url: { type: \"string\", required: false },\n can_remove: {\n type: \"boolean\",\n required: true,\n defaultValue: true,\n },\n can_refresh: {\n type: \"boolean\",\n required: true,\n defaultValue: false,\n },\n can_edit: {\n type: \"boolean\",\n required: true,\n defaultValue: false,\n },\n created_at: { type: \"date\", required: true },\n updated_at: { type: \"date\", required: true },\n },\n },\n tool: {\n fields: {\n id: { type: \"string\", required: true },\n scope_id: { type: \"string\", required: true, index: true },\n source_id: { type: \"string\", required: true, index: true },\n plugin_id: { type: \"string\", required: true, index: true },\n name: { type: \"string\", required: true },\n description: { type: \"string\", required: true },\n input_schema: { type: \"json\", required: false },\n output_schema: { type: \"json\", required: false },\n // NOTE: tool annotations (requiresApproval, approvalDescription,\n // mayElicit) are NOT stored on this row. They're derived at read\n // time from plugin-owned data via `plugin.resolveAnnotations`,\n // because the source of truth already lives in each plugin's own\n // storage (openapi's OperationBinding, etc.) and duplicating it\n // here would just mean bulk-rewriting rows every time the\n // derivation logic changes.\n created_at: { type: \"date\", required: true },\n updated_at: { type: \"date\", required: true },\n },\n },\n // Shared JSON-schema `$defs` stored once per source. Tool input/output\n // schemas carry `$ref: \"#/$defs/X\"` pointers; the read path attaches\n // matching defs under `$defs` before returning. Keyed by synthetic id\n // `${source_id}.${name}` so cleanup on source removal is a single\n // deleteMany by source_id.\n definition: {\n fields: {\n id: { type: \"string\", required: true },\n scope_id: { type: \"string\", required: true, index: true },\n source_id: { type: \"string\", required: true, index: true },\n plugin_id: { type: \"string\", required: true, index: true },\n name: { type: \"string\", required: true },\n schema: { type: \"json\", required: true },\n created_at: { type: \"date\", required: true },\n },\n },\n // Secrets live in the core surface as metadata (id, display name,\n // provider key). Actual values never touch this table — they live in\n // the secret provider (keychain, 1password, file, etc.) and are\n // resolved on demand via `ctx.secrets.get(id)`.\n //\n // `owned_by_connection_id` ties the row to a connection. Connection-\n // owned secrets are plumbing, not user-facing values: `ctx.secrets.list`\n // filters them out (the user sees the Connection instead), and\n // `ctx.secrets.remove` refuses to delete them (Connection.remove is\n // the single owner of the lifecycle). The FK is nullable so existing\n // \"bare\" secrets (API keys entered by the user, pre-connection OAuth\n // rows during migration) remain visible and removable unchanged.\n secret: {\n fields: {\n id: { type: \"string\", required: true },\n scope_id: { type: \"string\", required: true, index: true },\n name: { type: \"string\", required: true },\n provider: { type: \"string\", required: true, index: true },\n owned_by_connection_id: {\n type: \"string\",\n required: false,\n index: true,\n },\n created_at: { type: \"date\", required: true },\n },\n },\n // Connections — sign-in state for one identity against one remote\n // provider. A Connection owns one or more `secret` rows (access +\n // refresh tokens, etc.) via `secret.owned_by_connection_id`, and the\n // SDK exposes `ctx.connections.accessToken(id)` which transparently\n // refreshes the backing secrets when they're near expiry. Plugins\n // contribute refresh behavior via `plugin.connectionProviders[].refresh`\n // keyed by `provider`, same pattern as `secretProviders`.\n //\n // `provider_state` is plugin-owned opaque JSON — token endpoint URL,\n // scopes, issuer, auth-server metadata — whatever the provider's\n // refresh handler needs to re-hit the token endpoint. It's NOT\n // sensitive (all secrets go through the provider-backed secret rows);\n // it's just enough metadata to drive a refresh without re-running\n // discovery.\n connection: {\n fields: {\n id: { type: \"string\", required: true },\n scope_id: { type: \"string\", required: true, index: true },\n /** Routing key into `plugin.connectionProviders`. Typical shape\n * is `${pluginId}:${kind}` (e.g. `openapi:oauth2`, `mcp:oauth2`,\n * `google-discovery:google`). Mirrors `secret.provider`. */\n provider: { type: \"string\", required: true, index: true },\n /** Display label shown in the Connections UI. Usually the account\n * email / handle / org name the user signed in as. */\n identity_label: { type: \"string\", required: false },\n /** Stable id of the access-token secret. Always present. */\n access_token_secret_id: { type: \"string\", required: true },\n /** Stable id of the refresh-token secret. Null for flows that\n * don't mint a refresh token (client_credentials, etc.). */\n refresh_token_secret_id: { type: \"string\", required: false },\n /** Epoch ms when the access token expires. Null if the provider\n * didn't declare an expiry. Used as the refresh trigger. Stored as\n * `bigint` because `Date.now()` overflows int32. */\n expires_at: { type: \"number\", required: false, bigint: true },\n /** Scope string as returned by the token endpoint. */\n scope: { type: \"string\", required: false },\n /** Opaque plugin-owned JSON — token endpoint URL, scopes list,\n * discovery hints, etc. Never sensitive. */\n provider_state: { type: \"json\", required: false },\n created_at: { type: \"date\", required: true },\n updated_at: { type: \"date\", required: true },\n },\n },\n // Pending OAuth authorization rows shared by every OAuth-capable plugin.\n // Rows are short-lived and deleted after completion/cancel; the resulting\n // `connection` row is the durable sign-in state.\n oauth2_session: {\n fields: {\n id: { type: \"string\", required: true },\n scope_id: { type: \"string\", required: true, index: true },\n plugin_id: { type: \"string\", required: true, index: true },\n strategy: { type: \"string\", required: true },\n connection_id: { type: \"string\", required: true, index: true },\n token_scope: { type: \"string\", required: true },\n redirect_url: { type: \"string\", required: true },\n payload: { type: \"json\", required: true },\n expires_at: { type: \"number\", required: true, bigint: true },\n created_at: { type: \"date\", required: true },\n },\n },\n // User-authored overrides for tool permissions. Each row is one rule:\n // a glob-ish pattern + an action (approve / require_approval / block).\n // Resolution walks the scope stack innermost-first, then `position`\n // ascending within each scope; first match wins. Plugin-derived\n // annotations from `resolveAnnotations` apply only when no rule\n // matches.\n //\n // Pattern grammar (v1):\n // - `*` every tool id (universal)\n // - `vercel.dns.create` exact tool id\n // - `vercel.dns.*` any tool whose id starts with `vercel.dns.`\n // - `vercel.*` plugin-wide\n // No `**`, no brace expansion, no leading-`*` prefixes (`*foo`, `*.foo`).\n tool_policy: {\n fields: {\n id: { type: \"string\", required: true },\n scope_id: { type: \"string\", required: true, index: true },\n pattern: { type: \"string\", required: true },\n /** \"approve\" | \"require_approval\" | \"block\". */\n action: { type: \"string\", required: true },\n /** Fractional-indexing key (Jira lexorank style). Lower lex order =\n * higher precedence. New rules default to a key generated above\n * the current minimum. Strings instead of numbers so we can\n * always lengthen the key to insert between two adjacent rows\n * without precision loss; see `fractional-indexing` in\n * `policies.ts`. */\n position: { type: \"string\", required: true, index: true },\n created_at: { type: \"date\", required: true },\n updated_at: { type: \"date\", required: true },\n },\n },\n} as const satisfies DBSchema;\n\nexport type CoreSchema = typeof coreSchema;\n\n// ---------------------------------------------------------------------------\n// Row types — derived from the schema. Adding a field to coreSchema.fields\n// adds it to the row type automatically.\n// ---------------------------------------------------------------------------\n\nexport type SourceRow = InferDBFieldsOutput<CoreSchema[\"source\"][\"fields\"]> &\n Record<string, unknown>;\n\nexport type ToolRow = InferDBFieldsOutput<CoreSchema[\"tool\"][\"fields\"]> &\n Record<string, unknown>;\n\nexport type DefinitionRow = InferDBFieldsOutput<\n CoreSchema[\"definition\"][\"fields\"]\n> &\n Record<string, unknown>;\n\nexport type SecretRow = InferDBFieldsOutput<CoreSchema[\"secret\"][\"fields\"]> &\n Record<string, unknown>;\n\nexport type ConnectionRow = InferDBFieldsOutput<\n CoreSchema[\"connection\"][\"fields\"]\n> &\n Record<string, unknown>;\n\nexport type ToolPolicyRow = InferDBFieldsOutput<\n CoreSchema[\"tool_policy\"][\"fields\"]\n> &\n Record<string, unknown>;\n\n// ---------------------------------------------------------------------------\n// Tool policy — user-authored override of the default approval behavior.\n// `action` tells the executor what to do at invoke time and at search /\n// list time:\n// - approve : skip the upfront approval prompt, just run.\n// - require_approval : force an approval prompt even if the plugin's\n// annotations would have skipped it.\n// - block : invisible to search / list, hard-fail at invoke\n// with `ToolBlockedError`.\n// Mid-invocation elicitations (`mayElicit`) are NOT affected by policies.\n// ---------------------------------------------------------------------------\n\nexport type ToolPolicyAction = \"approve\" | \"require_approval\" | \"block\";\n\nexport const TOOL_POLICY_ACTIONS = [\n \"approve\",\n \"require_approval\",\n \"block\",\n] as const satisfies readonly ToolPolicyAction[];\n\nexport const isToolPolicyAction = (value: unknown): value is ToolPolicyAction =>\n typeof value === \"string\" &&\n (TOOL_POLICY_ACTIONS as readonly string[]).includes(value);\n\n// ---------------------------------------------------------------------------\n// Tool annotations — default-policy metadata the executor consults\n// before invocation. Returned by `plugin.resolveAnnotations` (dynamic\n// tools) or declared inline on `StaticToolDecl` (static tools). Never\n// stored on `tool` rows — every field here is derived at read time\n// from plugin-owned data.\n//\n// OpenAPI derives from HTTP method:\n// - GET / HEAD / OPTIONS → {} (auto-approved)\n// - POST / PUT / PATCH / DELETE → { requiresApproval: true,\n// approvalDescription: \"DELETE /users/:id\" }\n//\n// MCP derives from the server's tool declaration (mcp has its own\n// may-elicit and approval signals).\n// ---------------------------------------------------------------------------\n\nexport interface ToolAnnotations {\n /** If true, the executor will call the invoke-time elicitation handler\n * before running the tool and abort if the user declines. */\n readonly requiresApproval?: boolean;\n /** Free-text message shown in the approval prompt. Falls back to the\n * tool's id / description if unset. */\n readonly approvalDescription?: string;\n /** Hint for UI — tool may suspend to ask the user for input mid-invocation.\n * Not enforced by the executor; purely a UI signal. */\n readonly mayElicit?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// SourceInput — what a plugin passes to `ctx.core.sources.register(...)`.\n// Writes both the source row and all its tool rows in one transaction.\n// Annotations are NOT part of this input — they're computed from\n// plugin-owned data via `plugin.resolveAnnotations` when the executor\n// needs them.\n// ---------------------------------------------------------------------------\n\nexport interface SourceInputTool {\n readonly name: string;\n readonly description: string;\n readonly inputSchema?: unknown;\n readonly outputSchema?: unknown;\n}\n\nexport interface SourceInput {\n readonly id: string;\n /** Scope id this source belongs to. Must be one of the executor's\n * configured scopes. Callers (plugins) pick the target scope\n * explicitly — typically the scope the source was authored against. */\n readonly scope: string;\n readonly kind: string;\n readonly name: string;\n readonly url?: string;\n readonly canRemove?: boolean;\n readonly canRefresh?: boolean;\n readonly canEdit?: boolean;\n readonly tools: readonly SourceInputTool[];\n}\n\n// ---------------------------------------------------------------------------\n// DefinitionsInput — paired with SourceInput when a plugin registers\n// shared JSON-schema `$defs` alongside a source. Usually called inside\n// the same `ctx.transaction` as `sources.register` so a failure rolls\n// back both the source rows and the def rows.\n// ---------------------------------------------------------------------------\n\nexport interface DefinitionsInput {\n readonly sourceId: string;\n /** Scope id these definitions belong to — should match the scope of\n * the source they're registered under. */\n readonly scope: string;\n readonly definitions: Record<string, unknown>;\n}\n","// ---------------------------------------------------------------------------\n// Tool policies — pattern matcher + policy resolution. Pure functions; the\n// executor stitches them into `tools.list`, `tools.invoke`, and the public\n// `executor.policies` CRUD surface. Plugins consume the same surface.\n// ---------------------------------------------------------------------------\n\nimport { Schema } from \"effect\";\n\nimport type { ToolPolicyAction, ToolPolicyRow } from \"./core-schema\";\nimport { PolicyId, ScopeId } from \"./ids\";\n\n// ---------------------------------------------------------------------------\n// Public projection — what callers see when they list policies. Strips the\n// raw `scope_id` to a readable `scopeId`, hides `created_at` typing\n// inconsistencies between adapters, and re-tags `id` as a `PolicyId`.\n// ---------------------------------------------------------------------------\n\nexport interface ToolPolicy {\n readonly id: PolicyId;\n readonly scopeId: ScopeId;\n readonly pattern: string;\n readonly action: ToolPolicyAction;\n /** Fractional-indexing key. Lower lex order = higher precedence.\n * Use `generateKeyBetween(a, b)` from the `fractional-indexing`\n * package to produce a key that sits between two existing rows. */\n readonly position: string;\n readonly createdAt: Date;\n readonly updatedAt: Date;\n}\n\nexport interface CreateToolPolicyInput {\n readonly scope: string;\n readonly pattern: string;\n readonly action: ToolPolicyAction;\n /** Optional explicit position. Defaults to a key above the current\n * minimum (top of the scope's list; highest precedence). */\n readonly position?: string;\n}\n\nexport interface UpdateToolPolicyInput {\n readonly id: string;\n readonly pattern?: string;\n readonly action?: ToolPolicyAction;\n readonly position?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Match result — what `resolveToolPolicy` returns when a rule fires. Carries\n// the matched pattern so error messages and approval prompts can show the\n// user *which* rule produced the gate (\"matched policy: vercel.dns.*\").\n// ---------------------------------------------------------------------------\n\nexport interface PolicyMatch {\n readonly action: ToolPolicyAction;\n readonly pattern: string;\n readonly policyId: string;\n}\n\n// ---------------------------------------------------------------------------\n// Effective policy — the single answer to \"what happens when this tool is\n// invoked?\". Combines the user policy layer with the plugin's default\n// `requiresApproval` annotation. Callers (UI, agents, telemetry) shouldn't\n// need to know the layering — they ask once and render one thing.\n//\n// `source` distinguishes user-authored rules from plugin-derived defaults\n// purely for display (\"Matched: vercel.*\" vs \"Plugin default\"). The\n// `action` is what actually drives behavior at invoke time.\n// ---------------------------------------------------------------------------\n\nexport type PolicySource = \"user\" | \"plugin-default\";\n\nexport interface EffectivePolicy {\n readonly action: ToolPolicyAction;\n readonly source: PolicySource;\n /** Matched pattern; populated only when `source === \"user\"`. */\n readonly pattern?: string;\n /** Policy row id; populated only when `source === \"user\"`. */\n readonly policyId?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Pattern matching. v1 grammar:\n// - universal: `*` matches every tool id\n// - exact: `vercel.dns.create` matches only that id\n// - subtree: `vercel.dns.*` matches anything starting with `vercel.dns.`\n// - plugin-wide: `vercel.*` matches anything starting with `vercel.`\n// `*` is only meaningful as a complete trailing segment (or as the\n// entire pattern). Patterns without a wildcard are exact-id matches.\n// ---------------------------------------------------------------------------\n\nexport const matchPattern = (pattern: string, toolId: string): boolean => {\n if (pattern === \"*\") return true;\n if (pattern === toolId) return true;\n if (pattern.endsWith(\".*\")) {\n const prefix = pattern.slice(0, -2);\n if (prefix.length === 0) return false;\n return toolId === prefix || toolId.startsWith(`${prefix}.`);\n }\n return false;\n};\n\n// ---------------------------------------------------------------------------\n// Pattern validation — rejects shapes the matcher can't handle. Used by the\n// CRUD path so a malformed rule never lands in the table.\n// ---------------------------------------------------------------------------\n\nexport const isValidPattern = (pattern: string): boolean => {\n if (pattern.length === 0) return false;\n if (pattern === \"*\") return true;\n if (pattern.startsWith(\".\") || pattern.endsWith(\".\")) return false;\n if (pattern.includes(\"..\")) return false;\n if (pattern.startsWith(\"*\")) return false;\n // `*` is only valid as the entire trailing segment.\n const segments = pattern.split(\".\");\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i]!;\n if (seg.length === 0) return false;\n if (seg.includes(\"*\") && seg !== \"*\") return false;\n if (seg === \"*\" && i !== segments.length - 1) return false;\n }\n return true;\n};\n\n// ---------------------------------------------------------------------------\n// Resolution — given a tool id and the policy rows visible across the\n// executor's scope stack, return the first matching rule under the\n// (innermost-scope-first, position-ascending) ordering. Caller passes a\n// `scopeRank` function so the resolver doesn't need to know the executor's\n// scope stack shape.\n// ---------------------------------------------------------------------------\n\n// Lex compare on fractional-indexing key, then id as a stable tiebreak.\n// Two rows with identical `position` (racing inserts that picked the same\n// `generateKeyBetween(null, min)` from independent clients) would otherwise\n// flip on every refetch.\nexport const comparePolicyRow = (\n a: { position: unknown; id: unknown },\n b: { position: unknown; id: unknown },\n): number => {\n const pa = a.position as string;\n const pb = b.position as string;\n if (pa < pb) return -1;\n if (pa > pb) return 1;\n const ia = a.id as string;\n const ib = b.id as string;\n return ia < ib ? -1 : ia > ib ? 1 : 0;\n};\n\nexport const resolveToolPolicy = (\n toolId: string,\n policies: readonly ToolPolicyRow[],\n scopeRank: (row: { scope_id: unknown }) => number,\n): PolicyMatch | undefined => {\n if (policies.length === 0) return undefined;\n const sorted = [...policies].sort((a, b) => {\n const sa = scopeRank(a);\n const sb = scopeRank(b);\n if (sa !== sb) return sa - sb;\n return comparePolicyRow(a, b);\n });\n for (const row of sorted) {\n if (matchPattern(row.pattern as string, toolId)) {\n return {\n action: row.action as ToolPolicyAction,\n pattern: row.pattern as string,\n policyId: row.id as string,\n };\n }\n }\n return undefined;\n};\n\n// ---------------------------------------------------------------------------\n// Layered resolution — one call returns the effective policy combining\n// user-authored rules and the plugin's default `requiresApproval`\n// annotation. Use this anywhere a UI / agent / log needs the final answer\n// without knowing about the layering.\n//\n// Two flavors:\n// - `resolveEffectivePolicy` takes raw rows + a scopeRank, mirrors\n// `resolveToolPolicy`. Used server-side.\n// - `effectivePolicyFromSorted` takes a pre-sorted list of public\n// `ToolPolicy` projections; for clients that already received\n// policies in evaluation order from the API.\n// ---------------------------------------------------------------------------\n\nconst liftPlugin = (\n defaultRequiresApproval: boolean | undefined,\n): EffectivePolicy =>\n defaultRequiresApproval\n ? { action: \"require_approval\", source: \"plugin-default\" }\n : { action: \"approve\", source: \"plugin-default\" };\n\nconst liftUser = (match: PolicyMatch): EffectivePolicy => ({\n action: match.action,\n source: \"user\",\n pattern: match.pattern,\n policyId: match.policyId,\n});\n\nexport const resolveEffectivePolicy = (\n toolId: string,\n policies: readonly ToolPolicyRow[],\n scopeRank: (row: { scope_id: unknown }) => number,\n defaultRequiresApproval?: boolean,\n): EffectivePolicy => {\n const match = resolveToolPolicy(toolId, policies, scopeRank);\n return match ? liftUser(match) : liftPlugin(defaultRequiresApproval);\n};\n\nexport const effectivePolicyFromSorted = (\n toolId: string,\n sortedPolicies: readonly Pick<ToolPolicy, \"pattern\" | \"action\" | \"id\">[],\n defaultRequiresApproval?: boolean,\n): EffectivePolicy => {\n for (const p of sortedPolicies) {\n if (matchPattern(p.pattern, toolId)) {\n return {\n action: p.action,\n source: \"user\",\n pattern: p.pattern,\n policyId: p.id,\n };\n }\n }\n return liftPlugin(defaultRequiresApproval);\n};\n\n// ---------------------------------------------------------------------------\n// Row → public projection.\n// ---------------------------------------------------------------------------\n\nexport const rowToToolPolicy = (row: ToolPolicyRow): ToolPolicy => ({\n id: PolicyId.make(row.id as string),\n scopeId: ScopeId.make(row.scope_id as string),\n pattern: row.pattern as string,\n action: row.action as ToolPolicyAction,\n position: row.position as string,\n createdAt: row.created_at as Date,\n updatedAt: row.updated_at as Date,\n});\n\n// ---------------------------------------------------------------------------\n// Schema for the action enum — useful for HTTP edges that want to validate\n// inputs with effect/Schema.\n// ---------------------------------------------------------------------------\n\nexport const ToolPolicyActionSchema = Schema.Literals([\n \"approve\",\n \"require_approval\",\n \"block\",\n]);\n","import { Effect, Schema } from \"effect\";\n\nimport type { StorageFailure } from \"@executor-js/storage-core\";\n\nimport { SecretId, ScopeId } from \"./ids\";\n\n// ---------------------------------------------------------------------------\n// SecretProvider — what a concrete backend (keychain, 1password, file,\n// memory, workos-vault, …) implements. Providers are contributed by\n// plugins via `plugin.secretProviders` and registered in the executor\n// at startup; there's no runtime registration.\n//\n// The `key` field is the provider's identifier in the secret table's\n// `provider` column and in `executor.secrets.set({ provider, ... })`.\n// Unique per executor.\n// ---------------------------------------------------------------------------\n\nexport interface SecretProvider {\n /** Unique key (e.g. \"keychain\", \"env\", \"1password\", \"memory\"). */\n readonly key: string;\n /** If false, `set` and `delete` are never called. The executor\n * honours this before routing writes — trying to write to a\n * read-only provider is an error, not a silent drop. */\n readonly writable: boolean;\n /** Get a secret value. `scope` is the executor scope the lookup is\n * being made on behalf of — providers that partition their storage\n * by scope (memory, keychain via service name, per-vault in\n * 1password) use it; providers without tenancy ignore it and fall\n * back to a flat lookup. Failures (provider unreachable, decryption\n * failed, etc.) surface as `StorageFailure` — the executor treats\n * a provider call the same as a DB call; `StorageError` is captured\n * at the HTTP edge to `InternalError`, `UniqueViolationError` dies. */\n readonly get: (\n id: string,\n scope: string,\n ) => Effect.Effect<string | null, StorageFailure>;\n /** Check whether a provider has a backing value without returning it.\n * Providers that can answer this cheaply should implement it so\n * stale core routing rows don't appear as selectable secrets. */\n readonly has?: (\n id: string,\n scope: string,\n ) => Effect.Effect<boolean, StorageFailure>;\n /** Set a secret value at a named scope. Only called on writable\n * providers. Providers that partition by scope use this arg to\n * decide where to write; flat providers ignore it. */\n readonly set?: (\n id: string,\n value: string,\n scope: string,\n ) => Effect.Effect<void, StorageFailure>;\n /** Delete a secret at a named scope. Only called on writable providers.\n * Returns true if something was deleted. */\n readonly delete?: (\n id: string,\n scope: string,\n ) => Effect.Effect<boolean, StorageFailure>;\n /** Enumerate known secret entries. Optional — not all backends can\n * enumerate (env-backed providers, for example). */\n readonly list?: () => Effect.Effect<\n readonly { readonly id: string; readonly name: string }[],\n StorageFailure\n >;\n}\n\n// ---------------------------------------------------------------------------\n// SecretRef — metadata about a stored secret. Returned from\n// `executor.secrets.list()`. The actual value lives in the provider\n// and is only reachable via `executor.secrets.get(id)`.\n// ---------------------------------------------------------------------------\n\nexport class SecretRef extends Schema.Class<SecretRef>(\"SecretRef\")({\n id: SecretId,\n scopeId: ScopeId,\n /** Human-readable label (e.g. \"Cloudflare API Token\") */\n name: Schema.String,\n /** Which provider holds the value */\n provider: Schema.String,\n createdAt: Schema.Date,\n}) {}\n\n// ---------------------------------------------------------------------------\n// SetSecretInput — all the metadata to write a secret in one call.\n// `executor.secrets.set(input)` takes this and writes both the\n// value (to the provider) and the ref (to the `secret` table).\n//\n// `scope` is required — there's no default write target. Callers name\n// which scope in the executor's stack should own the secret. Typical\n// pattern: UI wiring up org-level API keys writes to the org scope;\n// OAuth token exchange writes to the innermost per-user scope.\n// ---------------------------------------------------------------------------\n\nexport class SetSecretInput extends Schema.Class<SetSecretInput>(\n \"SetSecretInput\",\n)({\n id: SecretId,\n /** Scope id to own this secret. Must be one of the executor's\n * configured scopes. */\n scope: ScopeId,\n /** Display name shown in secret-list UI. */\n name: Schema.String,\n /** The secret value itself — never persisted outside the provider. */\n value: Schema.String,\n /** Optional provider routing. If unset the executor picks the first\n * writable provider in registration order. */\n provider: Schema.optional(Schema.String),\n}) {}\n","import { Effect, Schema } from \"effect\";\n\nexport const SecretBackedValue = Schema.Union([\n Schema.String,\n Schema.Struct({\n secretId: Schema.String,\n prefix: Schema.optional(Schema.String),\n }),\n]);\nexport type SecretBackedValue = typeof SecretBackedValue.Type;\n\nexport const SecretBackedMap = Schema.Record(Schema.String, SecretBackedValue);\nexport type SecretBackedMap = typeof SecretBackedMap.Type;\n\nexport const isSecretBackedRef = (\n value: SecretBackedValue,\n): value is Extract<SecretBackedValue, { readonly secretId: string }> => typeof value !== \"string\";\n\nexport type ResolveSecretBackedMapOptions<E, E2> = {\n readonly values: Record<string, SecretBackedValue> | undefined;\n readonly getSecret: (secretId: string) => Effect.Effect<string | null, E>;\n readonly onMissing: (\n name: string,\n value: Extract<SecretBackedValue, { readonly secretId: string }>,\n ) => E2;\n readonly onError?: (\n error: E,\n name: string,\n value: Extract<SecretBackedValue, { readonly secretId: string }>,\n ) => E | E2;\n readonly missing?: \"fail\" | \"drop\";\n};\n\nexport const resolveSecretBackedMap = <E, E2 = E>({\n values,\n getSecret,\n onMissing,\n onError,\n missing = \"fail\",\n}: ResolveSecretBackedMapOptions<E, E2>): Effect.Effect<\n Record<string, string> | undefined,\n E | E2\n> => {\n const entries = Object.entries(values ?? {});\n if (entries.length === 0) return Effect.succeed(undefined);\n\n return Effect.gen(function* () {\n const resolved: Record<string, string> = {};\n\n for (const [name, value] of entries) {\n if (typeof value === \"string\") {\n resolved[name] = value;\n continue;\n }\n\n const secret = yield* getSecret(value.secretId).pipe(\n Effect.mapError((error) => onError?.(error, name, value) ?? error),\n );\n if (secret === null) {\n if (missing === \"drop\") continue;\n return yield* Effect.fail(onMissing(name, value));\n }\n\n resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret;\n }\n\n return Object.keys(resolved).length > 0 ? resolved : undefined;\n });\n};\n","import { Data, Effect, Schema } from \"effect\";\n\nimport { ConnectionId, ScopeId, SecretId } from \"./ids\";\n\n// ---------------------------------------------------------------------------\n// Connections — the product-level \"sign-in state\" primitive. A Connection\n// owns one or more backing `secret` rows (access + refresh tokens) via\n// `secret.owned_by_connection_id`; the user sees the Connection, the SDK\n// handles every refresh internally. Plugins register a refresh handler\n// per provider via `plugin.connectionProviders`, mirroring the shape of\n// `plugin.secretProviders` for ordinary secret backends.\n// ---------------------------------------------------------------------------\n\n/** Minimal JSON-object carrier for the plugin-owned `providerState`\n * blob. The SDK never inspects its shape; plugins encode/decode their\n * own structure. Never sensitive — that's what the secret rows are\n * for. */\nexport const ConnectionProviderState = Schema.Record(Schema.String, Schema.Unknown);\nexport type ConnectionProviderState = typeof ConnectionProviderState.Type;\n\n// ---------------------------------------------------------------------------\n// ConnectionRef — metadata projection returned from `ctx.connections.list`\n// / `executor.connections.list`. Holds token secret ids (so a plugin can\n// reference them from its source config) but not token values.\n// ---------------------------------------------------------------------------\n\nexport class ConnectionRef extends Schema.Class<ConnectionRef>(\"ConnectionRef\")({\n id: ConnectionId,\n scopeId: ScopeId,\n provider: Schema.String,\n identityLabel: Schema.NullOr(Schema.String),\n accessTokenSecretId: SecretId,\n refreshTokenSecretId: Schema.NullOr(SecretId),\n /** Epoch ms when the access token expires; null if not declared. */\n expiresAt: Schema.NullOr(Schema.Number),\n /** OAuth-style scope string as returned by the token endpoint. Named\n * `oauthScope` to avoid collision with the executor scope id. */\n oauthScope: Schema.NullOr(Schema.String),\n providerState: Schema.NullOr(ConnectionProviderState),\n createdAt: Schema.Date,\n updatedAt: Schema.Date,\n}) {}\n\n// ---------------------------------------------------------------------------\n// CreateConnectionInput — what a plugin passes to create a fresh\n// Connection after a successful OAuth exchange. The SDK writes the\n// backing secret rows and the connection row in one transaction, and\n// stamps `owned_by_connection_id` so `ctx.secrets.list` automatically\n// hides them from the bare-secrets UI.\n//\n// `provider` must match a registered `ConnectionProvider.key`. The SDK\n// validates this at create time so a typo surfaces immediately instead\n// of the first time a refresh is attempted.\n// ---------------------------------------------------------------------------\n\nexport class TokenMaterial extends Schema.Class<TokenMaterial>(\"TokenMaterial\")({\n /** Target secret id. Plugins typically derive this from the source id\n * + a stable suffix (e.g. `${sourceId}.access_token`). */\n secretId: SecretId,\n /** Display name stamped on the secret row. Only visible to code — the\n * Connections UI hides connection-owned secrets. */\n name: Schema.String,\n value: Schema.String,\n}) {}\n\nexport class CreateConnectionInput extends Schema.Class<CreateConnectionInput>(\n \"CreateConnectionInput\",\n)({\n id: ConnectionId,\n /** Executor scope id that will own this connection + its backing\n * secrets. This is the sharing boundary: a user scope is personal,\n * an org/workspace scope is shared with descendants. */\n scope: ScopeId,\n provider: Schema.String,\n identityLabel: Schema.NullOr(Schema.String),\n accessToken: TokenMaterial,\n refreshToken: Schema.NullOr(TokenMaterial),\n expiresAt: Schema.NullOr(Schema.Number),\n /** OAuth-style scope string. Distinct from the executor scope above. */\n oauthScope: Schema.NullOr(Schema.String),\n providerState: Schema.NullOr(ConnectionProviderState),\n}) {}\n\n// ---------------------------------------------------------------------------\n// ConnectionRefreshError — typed error surface for refresh handlers.\n// Plugins either return a fresh token envelope or fail with this error;\n// the SDK rethrows it from `ctx.connections.accessToken` callers.\n// ---------------------------------------------------------------------------\n\nexport class ConnectionRefreshError extends Data.TaggedError(\n \"ConnectionRefreshError\",\n)<{\n readonly connectionId: ConnectionId;\n readonly message: string;\n /**\n * Set by providers when the refresh failed in a way that the stored\n * refresh token cannot recover from (RFC 6749 §5.2 `invalid_grant`\n * — the AS has revoked the grant, the user changed their password,\n * the refresh token rotated out from under us, ...). The SDK\n * translates this into a `ConnectionReauthRequiredError` so callers\n * can prompt the user to sign in again instead of silently retrying.\n */\n readonly reauthRequired?: boolean;\n readonly cause?: unknown;\n}> {}\n\n// ---------------------------------------------------------------------------\n// ConnectionRefreshInput — what the SDK hands to a provider's `refresh`\n// callback. Includes the current refresh-token value (already resolved\n// from the secret row) and the opaque providerState blob so handlers\n// don't need to hit secrets themselves.\n// ---------------------------------------------------------------------------\n\nexport interface ConnectionRefreshInput {\n readonly connectionId: ConnectionId;\n readonly scopeId: ScopeId;\n readonly identityLabel: string | null;\n /** Resolved refresh token value, or null if the connection has none. */\n readonly refreshToken: string | null;\n /** Plugin-owned blob persisted at create / previous refresh. */\n readonly providerState: ConnectionProviderState | null;\n /** OAuth scope string from the last token issuance. */\n readonly oauthScope: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// ConnectionRefreshResult — what a provider's `refresh` callback returns\n// on success. The SDK writes the new token values through the secret\n// providers, updates `expires_at` / `scope` / `provider_state` on the\n// connection row, and returns the fresh access token to the caller.\n//\n// `refreshToken` is optional: if the AS rotates refresh tokens it's\n// present, if the AS issues long-lived refresh tokens it's absent. The\n// SDK updates the refresh secret only when a new value is supplied.\n// ---------------------------------------------------------------------------\n\nexport interface ConnectionRefreshResult {\n readonly accessToken: string;\n readonly refreshToken?: string | null;\n readonly expiresAt?: number | null;\n readonly oauthScope?: string | null;\n readonly providerState?: ConnectionProviderState | null;\n}\n\n// ---------------------------------------------------------------------------\n// ConnectionProvider — plugin contribution. Registered via\n// `plugin.connectionProviders`. One per refresh strategy (oauth2\n// authorization-code, oauth2 client-credentials, per-provider custom,\n// etc). Keyed by `key`; the connection row's `provider` column\n// references this key.\n//\n// Omitting `refresh` means \"tokens minted by this provider never\n// refresh\" — accessToken(id) just returns the stored value. Useful for\n// long-lived API tokens wrapped as connections for UX consistency.\n// ---------------------------------------------------------------------------\n\nexport interface ConnectionProvider {\n readonly key: string;\n readonly refresh?: (\n input: ConnectionRefreshInput,\n ) => Effect.Effect<ConnectionRefreshResult, ConnectionRefreshError>;\n}\n\n// ---------------------------------------------------------------------------\n// UpdateConnectionTokensInput — for flows that re-exchange tokens out\n// of band (e.g. an OAuth re-auth where the user signs in again). The\n// SDK overwrites the backing secrets and updates the connection row in\n// one transaction.\n// ---------------------------------------------------------------------------\n\nexport class UpdateConnectionTokensInput extends Schema.Class<UpdateConnectionTokensInput>(\n \"UpdateConnectionTokensInput\",\n)({\n id: ConnectionId,\n accessToken: Schema.String,\n refreshToken: Schema.optional(Schema.NullOr(Schema.String)),\n expiresAt: Schema.optional(Schema.NullOr(Schema.Number)),\n oauthScope: Schema.optional(Schema.NullOr(Schema.String)),\n providerState: Schema.optional(Schema.NullOr(ConnectionProviderState)),\n identityLabel: Schema.optional(Schema.NullOr(Schema.String)),\n}) {}\n","import { Effect, Schema } from \"effect\";\n\nimport { ToolId } from \"./ids\";\n\n// ---------------------------------------------------------------------------\n// Elicitation request — what a tool sends when it needs user input\n// ---------------------------------------------------------------------------\n\n/** Tool needs structured input from the user (render a form) */\nexport class FormElicitation extends Schema.TaggedClass<FormElicitation>()(\"FormElicitation\", {\n message: Schema.String,\n /** JSON Schema describing the fields to collect */\n requestedSchema: Schema.Record(Schema.String, Schema.Unknown),\n}) {}\n\n/** Tool needs the user to visit a URL (OAuth, approval page, etc.) */\nexport class UrlElicitation extends Schema.TaggedClass<UrlElicitation>()(\"UrlElicitation\", {\n message: Schema.String,\n url: Schema.String,\n /** Unique ID so the host can correlate the callback */\n elicitationId: Schema.String,\n}) {}\n\nexport type ElicitationRequest = FormElicitation | UrlElicitation;\n\n// ---------------------------------------------------------------------------\n// Elicitation response — what the host sends back\n// ---------------------------------------------------------------------------\n\nexport const ElicitationAction = Schema.Literals([\"accept\", \"decline\", \"cancel\"]);\nexport type ElicitationAction = typeof ElicitationAction.Type;\n\nexport class ElicitationResponse extends Schema.Class<ElicitationResponse>(\"ElicitationResponse\")({\n action: ElicitationAction,\n /** Present when action is \"accept\" — the data the user provided */\n content: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),\n}) {}\n\n// ---------------------------------------------------------------------------\n// Elicitation handler — the host provides this to handle requests\n// ---------------------------------------------------------------------------\n\nexport interface ElicitationContext {\n readonly toolId: ToolId;\n readonly args: unknown;\n readonly request: ElicitationRequest;\n}\n\n/**\n * A function the host provides to handle elicitation.\n * The SDK calls this when a tool suspends to ask for user input.\n * The host renders UI / prompts the user / does OAuth / etc.\n */\nexport type ElicitationHandler = (ctx: ElicitationContext) => Effect.Effect<ElicitationResponse>;\n\n// ---------------------------------------------------------------------------\n// Elicitation error — tool was declined or cancelled\n// ---------------------------------------------------------------------------\n\nexport class ElicitationDeclinedError extends Schema.TaggedErrorClass<ElicitationDeclinedError>()(\n \"ElicitationDeclinedError\",\n {\n toolId: ToolId,\n action: Schema.Literals([\"decline\", \"cancel\"]),\n },\n) {}\n","// ---------------------------------------------------------------------------\n// BlobStore — the seam for large, opaque, write-once data. Separate from\n// the relational adapter on purpose: blobs want different lifecycle,\n// durability, and placement (think S3/R2 in cloud, flat files locally)\n// than the metadata that indexes them.\n//\n// Plugins see a `PluginBlobStore` that's already namespaced to the\n// plugin id and bound to the executor's scope stack. Reads fall through\n// the stack in order (innermost first, first hit wins); writes and\n// deletes require an explicit scope id naming where the operation\n// should land. That mirrors the secrets API — shadowing by key on\n// read, explicit target on write.\n//\n// Error channel is `StorageError` — blobs only do read/write/delete, so\n// they never produce `UniqueViolationError`. The HTTP edge translates\n// `StorageError` to the opaque public `InternalError({ traceId })`.\n// ---------------------------------------------------------------------------\n\nimport { Effect } from \"effect\";\n\nimport { StorageError } from \"@executor-js/storage-core\";\n\nexport interface BlobStore {\n readonly get: (\n namespace: string,\n key: string,\n ) => Effect.Effect<string | null, StorageError>;\n /** Multi-namespace lookup for a single key. Backends issue one query\n * (`WHERE namespace IN (...) AND key = ?`) and return the hits keyed\n * by namespace — the caller applies its own precedence. Lets\n * `pluginBlobStore` walk the scope stack in O(1) round-trips instead\n * of one per scope. */\n readonly getMany: (\n namespaces: readonly string[],\n key: string,\n ) => Effect.Effect<ReadonlyMap<string, string>, StorageError>;\n readonly put: (\n namespace: string,\n key: string,\n value: string,\n ) => Effect.Effect<void, StorageError>;\n readonly delete: (\n namespace: string,\n key: string,\n ) => Effect.Effect<void, StorageError>;\n readonly has: (\n namespace: string,\n key: string,\n ) => Effect.Effect<boolean, StorageError>;\n}\n\nexport interface PluginBlobStore {\n /** Walk the scope stack (innermost first) and return the first\n * non-null value for `key`. */\n readonly get: (key: string) => Effect.Effect<string | null, StorageError>;\n /** Write `value` under `key` at the named scope. Scope must be one\n * of the executor's configured scopes. */\n readonly put: (\n key: string,\n value: string,\n options: { readonly scope: string },\n ) => Effect.Effect<void, StorageError>;\n /** Delete `key` at the named scope. */\n readonly delete: (\n key: string,\n options: { readonly scope: string },\n ) => Effect.Effect<void, StorageError>;\n /** Walk the scope stack and return true if any scope has a value for `key`. */\n readonly has: (key: string) => Effect.Effect<boolean, StorageError>;\n}\n\nconst assertScope = (\n scope: string,\n scopes: readonly string[],\n): Effect.Effect<void, StorageError> =>\n scopes.includes(scope)\n ? Effect.void\n : Effect.fail(\n new StorageError({\n message:\n `Blob write targets scope \"${scope}\" which is not in the ` +\n `executor's scope stack [${scopes.join(\", \")}].`,\n cause: undefined,\n }),\n );\n\nconst nsFor = (scope: string, pluginId: string) => `${scope}/${pluginId}`;\n\n/**\n * Bind a `BlobStore` to a specific scope stack and plugin id. Reads\n * fall through the stack; writes require an explicit scope. Used by\n * the executor to build the `blobs` field handed to each plugin's\n * `storage` factory.\n */\nexport const pluginBlobStore = (\n store: BlobStore,\n scopes: readonly string[],\n pluginId: string,\n): PluginBlobStore => ({\n get: (key) =>\n Effect.gen(function* () {\n const namespaces = scopes.map((s) => nsFor(s, pluginId));\n const hits = yield* store.getMany(namespaces, key);\n if (hits.size === 0) return null;\n for (const ns of namespaces) {\n const v = hits.get(ns);\n if (v !== undefined) return v;\n }\n return null;\n }),\n put: (key, value, options) =>\n Effect.flatMap(assertScope(options.scope, scopes), () =>\n store.put(nsFor(options.scope, pluginId), key, value),\n ),\n delete: (key, options) =>\n Effect.flatMap(assertScope(options.scope, scopes), () =>\n store.delete(nsFor(options.scope, pluginId), key),\n ),\n has: (key) =>\n store\n .getMany(\n scopes.map((s) => nsFor(s, pluginId)),\n key,\n )\n .pipe(Effect.map((hits) => hits.size > 0)),\n});\n\n/**\n * Minimal in-memory BlobStore — good for tests and trivial hosts. Real\n * backends (filesystem, S3/R2, SQLite-table-backed) implement the same\n * interface.\n *\n * Every method is `Effect<_, never>` — a pure in-memory Map can't fail.\n * `never` is assignable to `StorageError`, so the result still fits the\n * `BlobStore` interface.\n */\nexport const makeInMemoryBlobStore = (): BlobStore => {\n const store = new Map<string, string>();\n const k = (ns: string, key: string) => `${ns}::${key}`;\n return {\n get: (ns, key) => Effect.sync(() => store.get(k(ns, key)) ?? null),\n getMany: (namespaces, key) =>\n Effect.sync(() => {\n const hits = new Map<string, string>();\n for (const ns of namespaces) {\n const v = store.get(k(ns, key));\n if (v !== undefined) hits.set(ns, v);\n }\n return hits;\n }),\n put: (ns, key, value) =>\n Effect.sync(() => {\n store.set(k(ns, key), value);\n }),\n delete: (ns, key) =>\n Effect.sync(() => {\n store.delete(k(ns, key));\n }),\n has: (ns, key) => Effect.sync(() => store.has(k(ns, key))),\n };\n};\n","// ---------------------------------------------------------------------------\n// OAuth — core-level authorization service contract.\n//\n// `ctx.oauth` is the single entry point every plugin uses to mint an\n// OAuth-backed `Connection`. It owns the `oauth2_session` table (pending\n// authorizations), runs the strategy-specific code exchange at\n// completion, and writes the resulting Connection via `ctx.connections`.\n//\n// Plugins supply: the resource URL they want a token for, a\n// pre-decided `connectionId`, and a strategy descriptor. They get back\n// an authorization URL for the user's browser; the callback reaches\n// `ctx.oauth.complete`, and at invoke time the plugin calls\n// `ctx.connections.accessToken(connectionId)` for a fresh Bearer.\n//\n// This replaces four per-plugin state machines (one each in mcp,\n// openapi, google-discovery, graphql) that were all shading the same\n// lifecycle.\n// ---------------------------------------------------------------------------\n\nimport { Effect, Schema } from \"effect\";\n\nimport type { StorageFailure } from \"@executor-js/storage-core\";\n\nimport { ConnectionId } from \"./ids\";\n\n// ---------------------------------------------------------------------------\n// Strategy descriptors\n//\n// The strategy answers \"how do we turn a resource URL + optional\n// pre-configured credentials into an access token?\" — separate from\n// \"which plugin asked?\" The session row carries the strategy kind so\n// completion / refresh can route without a plugin callback.\n// ---------------------------------------------------------------------------\n\n/** RFC 9728 + RFC 8414 + RFC 7591 + PKCE: discover protected-resource\n * metadata, discover the authorization server, dynamically register a\n * client, then PKCE-encode the authorization URL. Zero pre-configured\n * credentials — the user just pastes a resource URL. */\nexport const OAuthDynamicDcrStrategy = Schema.Struct({\n kind: Schema.Literal(\"dynamic-dcr\"),\n /** Scopes to request. Defaults to whatever `scopes_supported`\n * advertises; caller can narrow or extend. */\n scopes: Schema.optional(Schema.Array(Schema.String)),\n});\nexport type OAuthDynamicDcrStrategy = typeof OAuthDynamicDcrStrategy.Type;\n\n/** RFC 6749 authorization code + PKCE with pre-configured endpoints +\n * client_id. Used when the caller has out-of-band-registered an OAuth\n * app (Google via Cloud Console, GitHub via developer portal, etc.) or\n * when the auth-server URL is declared in an OpenAPI `securityScheme`. */\nexport const OAuthAuthorizationCodeStrategy = Schema.Struct({\n kind: Schema.Literal(\"authorization-code\"),\n authorizationEndpoint: Schema.String,\n tokenEndpoint: Schema.String,\n /** Expected authorization-server issuer for ID token validation. Some\n * providers use a token endpoint host that differs from issuer, or a\n * path-scoped issuer such as Okta custom authorization servers. */\n issuerUrl: Schema.optional(Schema.NullOr(Schema.String)),\n /** Secret id holding the `client_id`. Using a secret row rather than\n * an inline string so the value lives at the scope where the caller\n * configured it and shadowing behaves consistently. */\n clientIdSecretId: Schema.String,\n /** Secret id for `client_secret`. Null for public clients using\n * PKCE without a confidential secret. */\n clientSecretSecretId: Schema.NullOr(Schema.String),\n scopes: Schema.Array(Schema.String),\n /** Separator between scopes. RFC 6749 says space; some providers\n * (GitHub classic) use comma. */\n scopeSeparator: Schema.optional(Schema.String),\n /** Provider-specific params injected at authorization URL build time\n * (Google's `access_type=offline`, `prompt=consent`, ...). */\n extraAuthorizationParams: Schema.optional(\n Schema.Record(Schema.String, Schema.String),\n ),\n /** `\"body\"` (default) sends client creds in the form body; `\"basic\"`\n * uses HTTP Basic auth. Stripe-style servers require basic. */\n clientAuth: Schema.optional(Schema.Literals([\"body\", \"basic\"])),\n});\nexport type OAuthAuthorizationCodeStrategy =\n typeof OAuthAuthorizationCodeStrategy.Type;\n\n/** RFC 6749 §4.4 client credentials — no user redirect, no PKCE. Used\n * for server-to-server integrations where the plugin has both\n * `client_id` and `client_secret` and the server will mint tokens\n * directly on the token endpoint. */\nexport const OAuthClientCredentialsStrategy = Schema.Struct({\n kind: Schema.Literal(\"client-credentials\"),\n tokenEndpoint: Schema.String,\n clientIdSecretId: Schema.String,\n clientSecretSecretId: Schema.String,\n scopes: Schema.optional(Schema.Array(Schema.String)),\n scopeSeparator: Schema.optional(Schema.String),\n clientAuth: Schema.optional(Schema.Literals([\"body\", \"basic\"])),\n});\nexport type OAuthClientCredentialsStrategy =\n typeof OAuthClientCredentialsStrategy.Type;\n\n/** Tagged union of every start-time strategy shape. A new strategy (e.g.\n * device-code) is added here; the service's start/complete routes on\n * `kind`. */\nexport const OAuthStrategy = Schema.Union([\n OAuthDynamicDcrStrategy,\n OAuthAuthorizationCodeStrategy,\n OAuthClientCredentialsStrategy,\n]);\nexport type OAuthStrategy = typeof OAuthStrategy.Type;\n\n// ---------------------------------------------------------------------------\n// Provider state — what the canonical `\"oauth2\"` refresh handler reads\n// off the Connection row. One shape regardless of originating strategy;\n// only the fields needed for refresh are persisted.\n// ---------------------------------------------------------------------------\n\n/** Discriminator mirrors `OAuthStrategy[\"kind\"]`. Refresh reads\n * `tokenEndpoint` + `clientAuth` + client id/secret refs directly and\n * never re-runs discovery. */\nexport const OAuthProviderState = Schema.Union([\n Schema.Struct({\n kind: Schema.Literal(\"dynamic-dcr\"),\n tokenEndpoint: Schema.String,\n issuerUrl: Schema.optional(Schema.NullOr(Schema.String)),\n authorizationServerUrl: Schema.optional(Schema.NullOr(Schema.String)),\n authorizationServerMetadataUrl: Schema.NullOr(Schema.String),\n idTokenSigningAlgValuesSupported: Schema.optional(\n Schema.Array(Schema.String),\n ),\n /** DCR-minted client_id. Embedded inline (not a secret) — DCR\n * clients are public-ish by design; the secret part (if the AS\n * issued one) is a separate secret row. */\n clientId: Schema.String,\n clientSecretSecretId: Schema.NullOr(Schema.String),\n clientAuth: Schema.Literals([\"body\", \"basic\"]),\n scopes: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultType(Effect.succeed([]))),\n scopeSeparator: Schema.optional(Schema.String),\n scope: Schema.NullOr(Schema.String),\n }),\n Schema.Struct({\n kind: Schema.Literal(\"authorization-code\"),\n tokenEndpoint: Schema.String,\n issuerUrl: Schema.optional(Schema.NullOr(Schema.String)),\n clientIdSecretId: Schema.String,\n clientSecretSecretId: Schema.NullOr(Schema.String),\n clientAuth: Schema.Literals([\"body\", \"basic\"]),\n scopes: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultType(Effect.succeed([]))),\n scopeSeparator: Schema.optional(Schema.String),\n scope: Schema.NullOr(Schema.String),\n }),\n Schema.Struct({\n kind: Schema.Literal(\"client-credentials\"),\n tokenEndpoint: Schema.String,\n clientIdSecretId: Schema.String,\n clientSecretSecretId: Schema.String,\n scopes: Schema.Array(Schema.String),\n scopeSeparator: Schema.optional(Schema.String),\n clientAuth: Schema.Literals([\"body\", \"basic\"]),\n scope: Schema.NullOr(Schema.String),\n }),\n]);\nexport type OAuthProviderState = typeof OAuthProviderState.Type;\n\n/** The canonical refresh handler key. Every OAuth2-minted connection\n * registers under this single value; the handler switches on\n * `providerState.kind`. Historical per-plugin keys (`mcp:oauth2`,\n * `openapi:oauth2`, `google-discovery:google`) are aliased to this\n * during migration. */\nexport const OAUTH2_PROVIDER_KEY = \"oauth2\" as const;\n\n// ---------------------------------------------------------------------------\n// Probe — \"does this URL use OAuth, and if so how?\"\n// ---------------------------------------------------------------------------\n\nexport interface OAuthProbeInput {\n readonly endpoint: string;\n readonly headers?: Record<string, string>;\n readonly queryParams?: Record<string, string>;\n}\n\nexport interface OAuthProbeResult {\n /** RFC 9728 resource metadata the server advertises, if any. */\n readonly resourceMetadata: Record<string, unknown> | null;\n readonly resourceMetadataUrl: string | null;\n /** RFC 8414 / OIDC metadata for the authorization server tied to the\n * resource, if the server advertised one and we could fetch it. */\n readonly authorizationServerMetadata: Record<string, unknown> | null;\n readonly authorizationServerMetadataUrl: string | null;\n readonly authorizationServerUrl: string | null;\n /** True iff the AS advertises `registration_endpoint` and\n * `token_endpoint_auth_methods_supported` includes `\"none\"` (public\n * client + PKCE). A `false` value here doesn't mean OAuth is\n * unavailable — just that the dynamic-DCR strategy can't run and the\n * caller must fall back to `authorization-code` with user-supplied\n * client credentials. */\n readonly supportsDynamicRegistration: boolean;\n /** True iff an unauth POST to the endpoint responded with `401` and\n * an MCP-shaped `WWW-Authenticate: Bearer` challenge (RFC 6750).\n * MCP-only signal; non-MCP OAuth-protected APIs usually encode auth\n * failures inside their own protocol envelope and never surface\n * this flag. */\n readonly isBearerChallengeEndpoint: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Start / complete\n// ---------------------------------------------------------------------------\n\nexport interface OAuthStartInput {\n /** Resource URL the caller wants a token for. For `dynamic-dcr` this\n * is the probe target; for `authorization-code` it's stored only so\n * the UI can display \"signed in to X.\" */\n readonly endpoint: string;\n readonly headers?: Record<string, string>;\n readonly queryParams?: Record<string, string>;\n /** Pre-decided `Connection.id`. Writing it before the flow starts\n * lets callers stamp `{kind:\"oauth2\", connectionId}` onto a source\n * row atomically with the start call. Convention:\n * `${pluginId}-oauth2-${namespace}`. */\n readonly connectionId: string;\n /** Scope where the resulting `Connection` + its backing secrets\n * land. Innermost scope for per-user sign-ins. */\n readonly tokenScope: string;\n /** Redirect URL the authorization server will bounce back to. For\n * strategies that don't redirect (`client-credentials`) pass a\n * placeholder; it's persisted but unused. */\n readonly redirectUrl: string;\n readonly strategy: OAuthStrategy;\n /** Which plugin is initiating the flow. Persisted on the session +\n * stamped on the minted Connection's identity label for UI. */\n readonly pluginId: string;\n /** Optional human label for the minted Connection, e.g. \"Spotify OAuth\". */\n readonly identityLabel?: string;\n}\n\nexport interface OAuthStartResult {\n readonly sessionId: string;\n /** Present for user-interactive strategies. `null` for\n * `client-credentials`, which skips straight to a Connection write\n * inside `start`. */\n readonly authorizationUrl: string | null;\n /** For strategies that don't redirect, the Connection has already\n * been minted. Surfaced so callers can stamp the source row\n * immediately without waiting on a completion callback. */\n readonly completedConnection: { readonly connectionId: string } | null;\n}\n\nexport interface OAuthCompleteInput {\n /** RFC 6749 `state` parameter — maps to a session row id. */\n readonly state: string;\n readonly code?: string;\n /** RFC 6749 `error` parameter — set when the AS redirected back with\n * a failure. The service surfaces this as a tagged error. */\n readonly error?: string;\n}\n\nexport interface OAuthCompleteResult {\n readonly connectionId: string;\n readonly expiresAt: number | null;\n readonly scope: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n// Errors use `Schema.TaggedError` (rather than `Data.TaggedError`) so\n// they can be encoded as HTTP 4xx payloads directly — every OAuth-\n// capable plugin group `.addError(OAuthStartError)` etc. and the HTTP\n// edge renders them with the annotated status.\n\nexport class OAuthProbeError extends Schema.TaggedErrorClass<OAuthProbeError>()(\n \"OAuthProbeError\",\n {\n message: Schema.String,\n },\n) {\n static annotations = { httpApiStatus: 400 };\n}\n\nexport class OAuthStartError extends Schema.TaggedErrorClass<OAuthStartError>()(\n \"OAuthStartError\",\n {\n message: Schema.String,\n },\n) {\n static annotations = { httpApiStatus: 400 };\n}\n\nexport class OAuthCompleteError extends Schema.TaggedErrorClass<OAuthCompleteError>()(\n \"OAuthCompleteError\",\n {\n message: Schema.String,\n /** RFC 6749 §5.2 error code, when the token endpoint returned one.\n * Callers distinguish terminal failures (`invalid_grant` ⇒\n * re-auth required) from transient ones. */\n code: Schema.optional(Schema.String),\n },\n) {\n static annotations = { httpApiStatus: 400 };\n}\n\nexport class OAuthSessionNotFoundError extends Schema.TaggedErrorClass<OAuthSessionNotFoundError>()(\n \"OAuthSessionNotFoundError\",\n {\n sessionId: Schema.String,\n },\n) {\n static annotations = { httpApiStatus: 404 };\n}\n\n// ---------------------------------------------------------------------------\n// Contract — what `ctx.oauth` exposes. Implementation lives in\n// `oauth-service.ts`; this file owns the stable public shape.\n// ---------------------------------------------------------------------------\n\nexport interface OAuthService {\n readonly probe: (\n input: OAuthProbeInput,\n ) => Effect.Effect<OAuthProbeResult, OAuthProbeError>;\n readonly start: (\n input: OAuthStartInput,\n ) => Effect.Effect<OAuthStartResult, OAuthStartError | StorageFailure>;\n readonly complete: (\n input: OAuthCompleteInput,\n ) => Effect.Effect<\n OAuthCompleteResult,\n OAuthCompleteError | OAuthSessionNotFoundError | StorageFailure\n >;\n /** Drop an in-flight session without completing — used when the\n * user cancels the popup or the source is deleted mid-onboarding. */\n readonly cancel: (\n sessionId: string,\n tokenScope?: string,\n ) => Effect.Effect<void, StorageFailure>;\n}\n\n// ---------------------------------------------------------------------------\n// Session TTL — how long a pending authorization stays redeemable.\n// 15 minutes matches every existing per-plugin implementation.\n// ---------------------------------------------------------------------------\n\nexport const OAUTH2_SESSION_TTL_MS = 15 * 60 * 1000;\n\n// Re-export ConnectionId for ergonomics — callers constructing start\n// input shouldn't need to import it from two places.\nexport { ConnectionId };\n","// ---------------------------------------------------------------------------\n// OAuth 2.0 helpers — generic, isomorphic building blocks.\n//\n// Thin wrappers around `oauth4webapi` (stateless; pure Web Crypto +\n// `fetch`, no deps; runs unchanged in Node, CF Workers, and browsers).\n// Each public helper is a single `Effect.tryPromise` call that delegates\n// the RFC work to the library and normalises the failure surface into\n// `OAuth2Error`.\n//\n// What stays hand-rolled:\n// - `OAuth2Error` — our tagged error; we want a stable shape across\n// every token-endpoint call\n// - `shouldRefreshToken` — skew check, trivial\n// - `buildAuthorizationUrl` — the library doesn't expose a raw\n// authorization-URL builder (it prefers PAR); a 30-line manual\n// construction keeps the call sync and lets callers opt out of PAR\n// ---------------------------------------------------------------------------\n\nimport { Data, Effect } from \"effect\";\nimport * as oauth from \"oauth4webapi\";\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\nexport class OAuth2Error extends Data.TaggedError(\"OAuth2Error\")<{\n readonly message: string;\n /**\n * RFC 6749 §5.2 error code, when the token endpoint returned one\n * (`invalid_grant`, `invalid_client`, `unauthorized_client`, ...).\n * Callers use this to distinguish terminal failures (a refresh token\n * the AS no longer honours → re-auth required) from transient ones.\n */\n readonly error?: string;\n readonly cause?: unknown;\n}> {}\n\n// ---------------------------------------------------------------------------\n// Token response shape (RFC 6749 §5.1)\n// ---------------------------------------------------------------------------\n\nexport type OAuth2TokenResponse = {\n readonly access_token: string;\n readonly token_type?: string;\n readonly refresh_token?: string;\n readonly expires_in?: number;\n readonly scope?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Refresh tokens this many ms before expiry to avoid mid-request expiration. */\nexport const OAUTH2_REFRESH_SKEW_MS = 60_000;\n\n/** Default token-endpoint timeout. */\nexport const OAUTH2_DEFAULT_TIMEOUT_MS = 20_000;\n\n// ---------------------------------------------------------------------------\n// PKCE (RFC 7636) — straight delegation to `oauth4webapi`\n// ---------------------------------------------------------------------------\n\nexport const createPkceCodeVerifier = (): string =>\n oauth.generateRandomCodeVerifier();\n\nexport const createPkceCodeChallenge = (verifier: string): Promise<string> =>\n oauth.calculatePKCECodeChallenge(verifier);\n\n// ---------------------------------------------------------------------------\n// Authorization URL builder\n// ---------------------------------------------------------------------------\n\nexport type BuildAuthorizationUrlInput = {\n readonly authorizationUrl: string;\n readonly clientId: string;\n readonly redirectUrl: string;\n readonly scopes: readonly string[];\n readonly state: string;\n /** Pre-computed base64url S256 challenge (from `createPkceCodeChallenge`). */\n readonly codeChallenge: string;\n /** Separator between scopes. RFC 6749 says space; some providers use comma. */\n readonly scopeSeparator?: string;\n /** Provider-specific extras (e.g. Google's `access_type=offline`). */\n readonly extraParams?: Readonly<Record<string, string>>;\n};\n\n/** Build an RFC 6749 §4.1.1 authorization URL. Sync; pre-computed\n * challenge lets this stay out of the Promise world. */\nexport const buildAuthorizationUrl = (input: BuildAuthorizationUrlInput): string => {\n const url = new URL(input.authorizationUrl);\n const separator = input.scopeSeparator ?? \" \";\n url.searchParams.set(\"client_id\", input.clientId);\n url.searchParams.set(\"redirect_uri\", input.redirectUrl);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", input.scopes.join(separator));\n url.searchParams.set(\"state\", input.state);\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n url.searchParams.set(\"code_challenge\", input.codeChallenge);\n if (input.extraParams) {\n for (const [k, v] of Object.entries(input.extraParams)) {\n url.searchParams.set(k, v);\n }\n }\n return url.toString();\n};\n\n// ---------------------------------------------------------------------------\n// Error mapping — `oauth4webapi`'s `process*Response` failure shapes are\n// either a WWW-Authenticate challenge or an RFC 6749 §5.2 error body,\n// both exposed via `.error` / `.error_description`. Probing the envelope\n// preserves RFC 6749 error-code semantics (e.g., mapping `invalid_grant`\n// to reauth-required) across wrappers.\n// ---------------------------------------------------------------------------\n\nconst toOAuth2Error = (cause: unknown): OAuth2Error => {\n if (typeof cause === \"object\" && cause !== null) {\n const c = cause as {\n error?: unknown;\n error_description?: unknown;\n message?: unknown;\n };\n const code = typeof c.error === \"string\" ? c.error : undefined;\n const description =\n typeof c.error_description === \"string\"\n ? c.error_description\n : typeof c.message === \"string\"\n ? c.message\n : undefined;\n return new OAuth2Error({\n message: `OAuth token exchange failed: ${description ?? code ?? \"unknown error\"}`,\n error: code,\n cause,\n });\n }\n return new OAuth2Error({\n message: `OAuth token exchange failed: ${String(cause)}`,\n cause,\n });\n};\n\n// ---------------------------------------------------------------------------\n// oauth4webapi adapter helpers\n// ---------------------------------------------------------------------------\n\nexport type ClientAuthMethod = \"body\" | \"basic\";\n\nconst asFromTokenUrl = (tokenUrl: string): oauth.AuthorizationServer => {\n const url = new URL(tokenUrl);\n return {\n issuer: `${url.protocol}//${url.host}`,\n token_endpoint: tokenUrl,\n };\n};\n\nconst asFromTokenUrlAndIssuer = (\n tokenUrl: string,\n issuerUrl: string | null | undefined,\n options: {\n readonly idTokenSigningAlgValuesSupported?: readonly string[];\n } = {},\n): oauth.AuthorizationServer => {\n const as = asFromTokenUrl(tokenUrl);\n const withIssuer = issuerUrl ? { ...as, issuer: issuerUrl } : as;\n return options.idTokenSigningAlgValuesSupported\n ? {\n ...withIssuer,\n id_token_signing_alg_values_supported: [\n ...options.idTokenSigningAlgValuesSupported,\n ],\n }\n : withIssuer;\n};\n\nconst isLoopbackHttpUrl = (value: string): boolean => {\n try {\n const url = new URL(value);\n if (url.protocol !== \"http:\") return false;\n const hostname = url.hostname.toLowerCase();\n return (\n hostname === \"localhost\" ||\n hostname === \"0.0.0.0\" ||\n hostname === \"::1\" ||\n hostname === \"[::1]\" ||\n hostname.startsWith(\"127.\")\n );\n } catch {\n return false;\n }\n};\n\nconst oauth4webapiRequestOptions = (\n targetUrl: string,\n timeoutMs: number | undefined,\n): Record<string, unknown> => {\n const options: Record<string, unknown> = {\n signal: AbortSignal.timeout(timeoutMs ?? OAUTH2_DEFAULT_TIMEOUT_MS),\n };\n if (isLoopbackHttpUrl(targetUrl)) {\n (options as { [oauth.allowInsecureRequests]?: boolean })[\n oauth.allowInsecureRequests\n ] = true;\n }\n return options;\n};\n\nconst pickClientAuth = (\n clientSecret: string | null | undefined,\n method: ClientAuthMethod,\n): oauth.ClientAuth => {\n if (!clientSecret) return oauth.None();\n return method === \"basic\"\n ? oauth.ClientSecretBasic(clientSecret)\n : oauth.ClientSecretPost(clientSecret);\n};\n\nconst tokenResponseFrom = (\n r: oauth.TokenEndpointResponse,\n): OAuth2TokenResponse => ({\n access_token: r.access_token,\n token_type: r.token_type,\n refresh_token: r.refresh_token,\n expires_in: typeof r.expires_in === \"number\" ? r.expires_in : undefined,\n scope: r.scope,\n});\n\nconst isUnexpectedIdTokenAlg = (cause: unknown): boolean =>\n cause instanceof Error &&\n cause.message.includes('unexpected JWT \"alg\" header parameter');\n\nconst looseTokenResponseFrom = (body: unknown): OAuth2TokenResponse => {\n if (typeof body !== \"object\" || body === null) {\n throw new Error(\"token endpoint response body is not an object\");\n }\n const record = body as Record<string, unknown>;\n if (typeof record.access_token !== \"string\" || !record.access_token) {\n throw new Error('token endpoint response is missing \"access_token\"');\n }\n const expiresIn =\n typeof record.expires_in === \"number\"\n ? record.expires_in\n : typeof record.expires_in === \"string\"\n ? Number.parseFloat(record.expires_in)\n : undefined;\n if (expiresIn !== undefined && !Number.isFinite(expiresIn)) {\n throw new Error('token endpoint response has invalid \"expires_in\"');\n }\n return {\n access_token: record.access_token,\n token_type:\n typeof record.token_type === \"string\" ? record.token_type : undefined,\n refresh_token:\n typeof record.refresh_token === \"string\"\n ? record.refresh_token\n : undefined,\n expires_in: expiresIn,\n scope: typeof record.scope === \"string\" ? record.scope : undefined,\n };\n};\n\nconst processTokenEndpointResponse = async (\n as: oauth.AuthorizationServer,\n client: oauth.Client,\n response: Response,\n): Promise<OAuth2TokenResponse> => {\n const fallback = response.clone();\n try {\n return tokenResponseFrom(\n await oauth.processGenericTokenEndpointResponse(as, client, response),\n );\n } catch (cause) {\n if (!isUnexpectedIdTokenAlg(cause)) throw cause;\n // Some OAuth-only providers include an ID token even when their metadata\n // does not advertise enough OIDC signing metadata for strict validation.\n // Executor stores only access/refresh tokens for source auth, so tolerate\n // an unusable ID token after the access token response itself is valid.\n return looseTokenResponseFrom(await fallback.json());\n }\n};\n\n// ---------------------------------------------------------------------------\n// Exchange authorization code → tokens\n// ---------------------------------------------------------------------------\n\nexport type ExchangeAuthorizationCodeInput = {\n readonly tokenUrl: string;\n readonly issuerUrl?: string | null;\n readonly clientId: string;\n readonly clientSecret?: string | null;\n readonly redirectUrl: string;\n readonly codeVerifier: string;\n readonly code: string;\n readonly clientAuth?: ClientAuthMethod;\n readonly idTokenSigningAlgValuesSupported?: readonly string[];\n readonly timeoutMs?: number;\n};\n\nexport const exchangeAuthorizationCode = (\n input: ExchangeAuthorizationCodeInput,\n): Effect.Effect<OAuth2TokenResponse, OAuth2Error> =>\n Effect.tryPromise({\n try: async () => {\n const as = asFromTokenUrlAndIssuer(input.tokenUrl, input.issuerUrl, {\n idTokenSigningAlgValuesSupported:\n input.idTokenSigningAlgValuesSupported,\n });\n const client: oauth.Client = { client_id: input.clientId };\n const clientAuth = pickClientAuth(\n input.clientSecret,\n input.clientAuth ?? \"body\",\n );\n // `authorizationCodeGrantRequest` requires its `callbackParameters`\n // to have been returned from `validateAuthResponse`. Our public API\n // takes the `code` directly (the UI already validated `state` by\n // looking up the session), so skip the library's state-validation\n // rail and go through the generic grant request instead.\n const params = new URLSearchParams({\n code: input.code,\n redirect_uri: input.redirectUrl,\n code_verifier: input.codeVerifier,\n });\n const response = await oauth.genericTokenEndpointRequest(\n as,\n client,\n clientAuth,\n \"authorization_code\",\n params,\n oauth4webapiRequestOptions(input.tokenUrl, input.timeoutMs),\n );\n return processTokenEndpointResponse(as, client, response);\n },\n catch: toOAuth2Error,\n });\n\n// ---------------------------------------------------------------------------\n// Exchange client credentials → tokens (RFC 6749 §4.4)\n// ---------------------------------------------------------------------------\n\nexport type ExchangeClientCredentialsInput = {\n readonly tokenUrl: string;\n readonly clientId: string;\n readonly clientSecret: string;\n readonly scopes?: readonly string[];\n readonly scopeSeparator?: string;\n readonly clientAuth?: ClientAuthMethod;\n readonly timeoutMs?: number;\n};\n\nexport const exchangeClientCredentials = (\n input: ExchangeClientCredentialsInput,\n): Effect.Effect<OAuth2TokenResponse, OAuth2Error> =>\n Effect.tryPromise({\n try: async () => {\n const as = asFromTokenUrl(input.tokenUrl);\n const client: oauth.Client = { client_id: input.clientId };\n const clientAuth = pickClientAuth(\n input.clientSecret,\n input.clientAuth ?? \"body\",\n );\n const params = new URLSearchParams();\n if (input.scopes && input.scopes.length > 0) {\n params.set(\"scope\", input.scopes.join(input.scopeSeparator ?? \" \"));\n }\n const response = await oauth.clientCredentialsGrantRequest(\n as,\n client,\n clientAuth,\n params,\n oauth4webapiRequestOptions(input.tokenUrl, input.timeoutMs),\n );\n const result = await oauth.processClientCredentialsResponse(\n as,\n client,\n response,\n );\n return tokenResponseFrom(result);\n },\n catch: toOAuth2Error,\n });\n\n// ---------------------------------------------------------------------------\n// Refresh access token\n// ---------------------------------------------------------------------------\n\nexport type RefreshAccessTokenInput = {\n readonly tokenUrl: string;\n readonly issuerUrl?: string | null;\n readonly clientId: string;\n readonly clientSecret?: string | null;\n readonly refreshToken: string;\n readonly scopes?: readonly string[];\n readonly scopeSeparator?: string;\n readonly clientAuth?: ClientAuthMethod;\n readonly idTokenSigningAlgValuesSupported?: readonly string[];\n readonly timeoutMs?: number;\n};\n\nexport const refreshAccessToken = (\n input: RefreshAccessTokenInput,\n): Effect.Effect<OAuth2TokenResponse, OAuth2Error> =>\n Effect.tryPromise({\n try: async () => {\n const as = asFromTokenUrlAndIssuer(input.tokenUrl, input.issuerUrl, {\n idTokenSigningAlgValuesSupported:\n input.idTokenSigningAlgValuesSupported,\n });\n const client: oauth.Client = { client_id: input.clientId };\n const clientAuth = pickClientAuth(\n input.clientSecret,\n input.clientAuth ?? \"body\",\n );\n const additionalParameters =\n input.scopes && input.scopes.length > 0\n ? new URLSearchParams({\n scope: input.scopes.join(input.scopeSeparator ?? \" \"),\n })\n : undefined;\n const response = await oauth.refreshTokenGrantRequest(\n as,\n client,\n clientAuth,\n input.refreshToken,\n {\n ...oauth4webapiRequestOptions(input.tokenUrl, input.timeoutMs),\n additionalParameters,\n },\n );\n const fallback = response.clone();\n try {\n const result = await oauth.processRefreshTokenResponse(\n as,\n client,\n response,\n );\n return tokenResponseFrom(result);\n } catch (cause) {\n if (!isUnexpectedIdTokenAlg(cause)) throw cause;\n return looseTokenResponseFrom(await fallback.json());\n }\n },\n catch: toOAuth2Error,\n });\n\n// ---------------------------------------------------------------------------\n// Refresh-needed predicate\n// ---------------------------------------------------------------------------\n\nexport const shouldRefreshToken = (input: {\n readonly expiresAt: number | null;\n readonly now?: number;\n readonly skewMs?: number;\n}): boolean => {\n if (input.expiresAt === null) return false;\n const now = input.now ?? Date.now();\n const skew = input.skewMs ?? OAUTH2_REFRESH_SKEW_MS;\n return input.expiresAt <= now + skew;\n};\n","// ---------------------------------------------------------------------------\n// OAuth service implementation — the runtime behind `ctx.oauth`.\n//\n// Owns three flows, all on one codepath:\n//\n// - probe(endpoint) RFC 9728 + 8414 metadata lookup without\n// starting a flow. Used by onboarding UI\n// to decide between dynamic-DCR and\n// paste-your-credentials strategies.\n//\n// - start({strategy, ...}) Persists an `oauth2_session` row.\n// * `dynamic-dcr` runs discovery +\n// DCR + PKCE, emits\n// an authorization URL.\n// * `authorization-code`\n// uses pre-configured\n// client_id (secret)\n// + endpoints + PKCE.\n// * `client-credentials`\n// no user step —\n// mints the Connection\n// inline, returns\n// authorizationUrl=null.\n//\n// - complete({state, code}) Looks up the session, exchanges code\n// for tokens, creates the Connection via\n// `ctx.connections.create`, deletes the\n// session. Idempotent-ish in the sense\n// that a retried code past TTL fails\n// clean rather than draining the AS.\n//\n// The service also exposes a canonical `\"oauth2\"` `ConnectionProvider`\n// for refresh. The provider reads `providerState.kind` to pick which\n// token endpoint + client credentials to present; one handler covers\n// every strategy because refresh semantics are strategy-independent.\n// ---------------------------------------------------------------------------\n\nimport { Effect, Schema } from \"effect\";\n\nimport type {\n DBAdapter,\n StorageFailure,\n TypedAdapter,\n} from \"@executor-js/storage-core\";\n\nimport {\n ConnectionRefreshError,\n CreateConnectionInput,\n TokenMaterial,\n type ConnectionProvider,\n type ConnectionRefreshInput,\n type ConnectionRefreshResult,\n type ConnectionRef,\n} from \"./connections\";\nimport type {\n ConnectionProviderNotRegisteredError,\n} from \"./errors\";\nimport type { CoreSchema } from \"./core-schema\";\nimport { ConnectionId, ScopeId, SecretId } from \"./ids\";\nimport { SetSecretInput, type SecretRef } from \"./secrets\";\nimport {\n OAUTH2_PROVIDER_KEY,\n OAUTH2_SESSION_TTL_MS,\n OAuthCompleteError,\n OAuthProbeError,\n OAuthProviderState as OAuthProviderStateSchema,\n OAuthSessionNotFoundError,\n OAuthStartError,\n type OAuthAuthorizationCodeStrategy,\n type OAuthClientCredentialsStrategy,\n type OAuthCompleteInput,\n type OAuthCompleteResult,\n type OAuthDynamicDcrStrategy,\n type OAuthProbeInput,\n type OAuthProbeResult,\n type OAuthProviderState,\n type OAuthService,\n type OAuthStartInput,\n type OAuthStartResult,\n} from \"./oauth\";\nimport {\n beginDynamicAuthorization,\n discoverAuthorizationServerMetadata,\n discoverProtectedResourceMetadata,\n} from \"./oauth-discovery\";\nimport {\n buildAuthorizationUrl,\n createPkceCodeChallenge,\n createPkceCodeVerifier,\n exchangeAuthorizationCode,\n exchangeClientCredentials,\n refreshAccessToken,\n} from \"./oauth-helpers\";\n\n// ---------------------------------------------------------------------------\n// Session payload — persisted under `oauth2_session.payload` as opaque\n// JSON. Shape is strategy-specific; the discriminator matches\n// `OAuthStrategy[\"kind\"]` so completion picks the right exchange path.\n// ---------------------------------------------------------------------------\n\nconst OAuthAuthorizationServerMetadataJson = Schema.Record(Schema.String, Schema.Unknown);\nconst OAuthClientInformationJson = Schema.Record(Schema.String, Schema.Unknown);\n\nconst DynamicDcrSessionPayload = Schema.Struct({\n kind: Schema.Literal(\"dynamic-dcr\"),\n identityLabel: Schema.NullOr(Schema.String),\n codeVerifier: Schema.String,\n authorizationServerUrl: Schema.String,\n authorizationServerMetadataUrl: Schema.String,\n authorizationServerMetadata: OAuthAuthorizationServerMetadataJson,\n clientInformation: OAuthClientInformationJson,\n resourceMetadataUrl: Schema.NullOr(Schema.String),\n resourceMetadata: Schema.NullOr(\n Schema.Record(Schema.String, Schema.Unknown),\n ),\n scopes: Schema.Array(Schema.String),\n});\n\nconst AuthorizationCodeSessionPayload = Schema.Struct({\n kind: Schema.Literal(\"authorization-code\"),\n identityLabel: Schema.NullOr(Schema.String),\n codeVerifier: Schema.String,\n authorizationEndpoint: Schema.String,\n tokenEndpoint: Schema.String,\n issuerUrl: Schema.NullOr(Schema.String).pipe(Schema.withDecodingDefaultType(Effect.succeed(null))),\n clientIdSecretId: Schema.String,\n clientSecretSecretId: Schema.NullOr(Schema.String),\n scopes: Schema.Array(Schema.String),\n scopeSeparator: Schema.optional(Schema.String),\n clientAuth: Schema.Literals([\"body\", \"basic\"]),\n});\n\n/** `client-credentials` doesn't produce a session row — it mints the\n * Connection inline during `start`. The shape is included here for\n * completeness / future device-code use. */\nconst OAuthSessionPayload = Schema.Union([\n DynamicDcrSessionPayload,\n AuthorizationCodeSessionPayload,\n]);\ntype OAuthSessionPayload = typeof OAuthSessionPayload.Type;\n\nconst decodeSessionPayload = Schema.decodeUnknownSync(OAuthSessionPayload);\nconst encodeSessionPayload = Schema.encodeSync(OAuthSessionPayload);\n\nconst coerceJson = (value: unknown): unknown => {\n if (typeof value !== \"string\") return value;\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n};\n\nconst stringArray = (value: unknown): readonly string[] =>\n Array.isArray(value)\n ? value.filter((scope): scope is string => typeof scope === \"string\")\n : [];\n\nconst originOrNull = (value: unknown): string | null => {\n if (typeof value !== \"string\") return null;\n try {\n return new URL(value).origin;\n } catch {\n return null;\n }\n};\n\nconst decodeProviderState = (value: unknown): OAuthProviderState => {\n const raw = coerceJson(value);\n const record =\n raw && typeof raw === \"object\" ? (raw as Record<string, unknown>) : null;\n\n if (record && !(\"kind\" in record) && \"flow\" in record && \"tokenUrl\" in record) {\n const flow = record.flow;\n if (flow === \"authorizationCode\") {\n return Schema.decodeUnknownSync(OAuthProviderStateSchema)({\n kind: \"authorization-code\",\n tokenEndpoint: record.tokenUrl,\n issuerUrl: originOrNull(record.authorizationEndpoint),\n clientIdSecretId: record.clientIdSecretId,\n clientSecretSecretId: record.clientSecretSecretId ?? null,\n clientAuth: \"body\",\n scope: stringArray(record.scopes).join(\" \") || null,\n });\n }\n if (flow === \"clientCredentials\") {\n return Schema.decodeUnknownSync(OAuthProviderStateSchema)({\n kind: \"client-credentials\",\n tokenEndpoint: record.tokenUrl,\n clientIdSecretId: record.clientIdSecretId,\n clientSecretSecretId: record.clientSecretSecretId,\n scopes: stringArray(record.scopes),\n clientAuth: \"body\",\n scope: stringArray(record.scopes).join(\" \") || null,\n });\n }\n }\n\n if (\n record &&\n !(\"kind\" in record) &&\n \"clientIdSecretId\" in record &&\n \"scopes\" in record\n ) {\n const scopes = stringArray(record.scopes);\n return Schema.decodeUnknownSync(OAuthProviderStateSchema)({\n kind: \"authorization-code\",\n tokenEndpoint: \"https://oauth2.googleapis.com/token\",\n issuerUrl: \"https://accounts.google.com\",\n clientIdSecretId: record.clientIdSecretId,\n clientSecretSecretId: record.clientSecretSecretId ?? null,\n clientAuth: \"body\",\n scope: scopes.join(\" \") || null,\n });\n }\n\n if (\n record &&\n !(\"kind\" in record) &&\n \"clientInformation\" in record &&\n \"endpoint\" in record\n ) {\n const clientInformation =\n record.clientInformation && typeof record.clientInformation === \"object\"\n ? (record.clientInformation as Record<string, unknown>)\n : null;\n return Schema.decodeUnknownSync(OAuthProviderStateSchema)({\n kind: \"dynamic-dcr\",\n tokenEndpoint:\n typeof record.tokenEndpoint === \"string\"\n ? record.tokenEndpoint\n : record.authorizationServerMetadata &&\n typeof record.authorizationServerMetadata === \"object\" &&\n typeof (record.authorizationServerMetadata as Record<string, unknown>)\n .token_endpoint === \"string\"\n ? ((record.authorizationServerMetadata as Record<string, unknown>)\n .token_endpoint as string)\n : \"\",\n issuerUrl:\n record.authorizationServerMetadata &&\n typeof record.authorizationServerMetadata === \"object\" &&\n typeof (record.authorizationServerMetadata as Record<string, unknown>).issuer ===\n \"string\"\n ? ((record.authorizationServerMetadata as Record<string, unknown>)\n .issuer as string)\n : null,\n authorizationServerUrl:\n typeof record.authorizationServerUrl === \"string\"\n ? record.authorizationServerUrl\n : null,\n authorizationServerMetadataUrl:\n typeof record.authorizationServerMetadataUrl === \"string\"\n ? record.authorizationServerMetadataUrl\n : null,\n clientId:\n typeof clientInformation?.client_id === \"string\"\n ? clientInformation.client_id\n : \"\",\n clientSecretSecretId: null,\n clientAuth: \"body\",\n scope: null,\n });\n }\n\n return Schema.decodeUnknownSync(OAuthProviderStateSchema)(raw);\n};\n\n// ---------------------------------------------------------------------------\n// Service dependencies — the executor wires these up when it constructs\n// the service. Every dep is a narrow surface so the service stays\n// testable: point to an in-memory adapter + a secrets stub and every\n// code path is exercisable.\n// ---------------------------------------------------------------------------\n\nexport interface OAuthServiceDeps {\n /** Typed core-schema adapter. Already scope-wrapped upstream so reads\n * fall through the scope stack; writes stamp the scope the caller\n * named (`tokenScope` on start input). */\n readonly adapter: TypedAdapter<CoreSchema>;\n /** Raw adapter for opening transactions — the typed one doesn't expose\n * `.transaction` directly. */\n readonly rawAdapter: DBAdapter;\n /** Resolves client-id / client-secret refs at start + refresh time.\n * A `null` return means \"secret row is gone\" and aborts the flow. */\n readonly secretsGet: (id: string) => Effect.Effect<string | null, StorageFailure>;\n readonly secretsSet: (input: SetSecretInput) => Effect.Effect<SecretRef, StorageFailure>;\n /** Mints the Connection row + backing secret rows. Called from\n * `complete` (and from `start` for `client-credentials`). */\n readonly connectionsCreate: (\n input: CreateConnectionInput,\n ) => Effect.Effect<\n ConnectionRef,\n ConnectionProviderNotRegisteredError | StorageFailure\n >;\n /** Random session id generator. Tests override to make outputs\n * deterministic. */\n readonly newSessionId?: () => string;\n /** `Date.now()` substitute — tests override to drive TTL behavior. */\n readonly now?: () => number;\n}\n\nconst defaultSessionId = (): string => {\n const crypto = globalThis.crypto;\n if (crypto?.randomUUID) return `oauth2_session_${crypto.randomUUID()}`;\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n return `oauth2_session_${Array.from(bytes, (byte) =>\n byte.toString(16).padStart(2, \"0\"),\n ).join(\"\")}`;\n};\n\nconst secretIdPart = (value: string): string =>\n value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\") || \"oauth\";\n\nconst oauthSecretId = (\n connectionId: string,\n suffix: \"access-token\" | \"refresh-token\" | \"client-secret\",\n): string => {\n const base = secretIdPart(connectionId);\n const readable = base.length <= 48 ? base : base.slice(0, 40);\n return `oauth2-${readable}-${suffix}`;\n};\n\nconst scopedSessionId = (scopeId: string, sessionId: string): string =>\n `${sessionId}_${secretIdPart(scopeId).slice(0, 24)}`;\n\nconst terminalRefreshErrors = new Set([\n \"invalid_grant\",\n \"invalid_client\",\n \"unauthorized_client\",\n]);\n\n// ---------------------------------------------------------------------------\n// Service factory\n// ---------------------------------------------------------------------------\n\nexport const makeOAuth2Service = (\n deps: OAuthServiceDeps,\n): { readonly service: OAuthService; readonly connectionProvider: ConnectionProvider } => {\n const now = deps.now ?? (() => Date.now());\n const newSessionId = deps.newSessionId ?? defaultSessionId;\n\n // -------------------------------------------------------------------\n // probe\n // -------------------------------------------------------------------\n const probe = (\n input: OAuthProbeInput,\n ): Effect.Effect<OAuthProbeResult, OAuthProbeError> =>\n Effect.gen(function* () {\n const resource = yield* discoverProtectedResourceMetadata(\n input.endpoint,\n { resourceHeaders: input.headers, resourceQueryParams: input.queryParams },\n ).pipe(\n Effect.catchTag(\"OAuthDiscoveryError\", (err) =>\n Effect.fail(\n new OAuthProbeError({\n message: `Protected resource metadata probe failed: ${err.message}`,\n\n }),\n ),\n ),\n );\n\n const authorizationServerUrl = (() => {\n const fromResource = resource?.metadata.authorization_servers?.[0];\n if (fromResource) return fromResource;\n try {\n const u = new URL(input.endpoint);\n return `${u.protocol}//${u.host}`;\n } catch {\n return null;\n }\n })();\n\n const authServer = authorizationServerUrl\n ? yield* discoverAuthorizationServerMetadata(\n authorizationServerUrl,\n ).pipe(\n Effect.catchTag(\"OAuthDiscoveryError\", () =>\n Effect.succeed(null),\n ),\n )\n : null;\n\n const supportsDynamicRegistration = !!(\n authServer?.metadata.registration_endpoint &&\n (authServer.metadata.token_endpoint_auth_methods_supported ?? []).includes(\n \"none\",\n )\n );\n\n // Bearer challenge probe — POST the endpoint unauth, look for\n // 401 + WWW-Authenticate: Bearer. Harmless against non-MCP\n // endpoints (Railway/GraphQL endpoints respond 200 or 400 with\n // protocol-specific bodies that we simply read as \"not a bearer\n // challenge\").\n const isBearerChallengeEndpoint = yield* Effect.tryPromise({\n try: async (): Promise<boolean> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 6_000);\n try {\n const probeUrl = new URL(input.endpoint);\n for (const [key, value] of Object.entries(input.queryParams ?? {})) {\n probeUrl.searchParams.set(key, value);\n }\n const response = await fetch(probeUrl.toString(), {\n method: \"POST\",\n headers: {\n ...(input.headers ?? {}),\n \"content-type\": \"application/json\",\n accept: \"application/json, text/event-stream\",\n },\n body: JSON.stringify({\n jsonrpc: \"2.0\",\n id: 1,\n method: \"initialize\",\n params: {\n protocolVersion: \"2025-06-18\",\n capabilities: {},\n clientInfo: { name: \"executor-probe\", version: \"0\" },\n },\n }),\n signal: controller.signal,\n });\n if (response.status !== 401) return false;\n const wwwAuth =\n response.headers.get(\"www-authenticate\") ??\n response.headers.get(\"WWW-Authenticate\");\n return !!wwwAuth && /^\\s*bearer\\b/i.test(wwwAuth);\n } finally {\n clearTimeout(timer);\n }\n },\n catch: () => null,\n }).pipe(Effect.catch(() => Effect.succeed(false)));\n\n return {\n resourceMetadata:\n (resource?.metadata as Record<string, unknown> | undefined) ?? null,\n resourceMetadataUrl: resource?.metadataUrl ?? null,\n authorizationServerMetadata:\n (authServer?.metadata as Record<string, unknown> | undefined) ?? null,\n authorizationServerMetadataUrl: authServer?.metadataUrl ?? null,\n authorizationServerUrl: authorizationServerUrl ?? null,\n supportsDynamicRegistration,\n isBearerChallengeEndpoint,\n };\n });\n\n // -------------------------------------------------------------------\n // start — branches on strategy.kind\n // -------------------------------------------------------------------\n const startDynamicDcr = (\n input: OAuthStartInput,\n strategy: OAuthDynamicDcrStrategy,\n ): Effect.Effect<OAuthStartResult, OAuthStartError | StorageFailure> =>\n Effect.gen(function* () {\n const started = yield* beginDynamicAuthorization({\n endpoint: input.endpoint,\n redirectUrl: input.redirectUrl,\n state: \"\",\n scopes: strategy.scopes,\n }, {\n resourceHeaders: input.headers,\n resourceQueryParams: input.queryParams,\n }).pipe(\n Effect.catchTag(\"OAuthDiscoveryError\", (err) =>\n Effect.fail(\n new OAuthStartError({\n message: `Dynamic authorization setup failed: ${err.message}`,\n\n }),\n ),\n ),\n );\n\n const sessionId = scopedSessionId(input.tokenScope, newSessionId());\n\n // beginDynamicAuthorization returns an authorizationUrl already\n // signed with whatever `state` we passed. We need the session id\n // to be the state parameter so completion can look up the row.\n // Re-build the URL with the corrected state — cheap, one SHA-256\n // for the PKCE challenge, no network calls.\n const codeChallenge = yield* Effect.promise(() =>\n createPkceCodeChallenge(started.codeVerifier),\n );\n const authorizationUrl = buildAuthorizationUrl({\n authorizationUrl: started.state.authorizationServerMetadata.authorization_endpoint,\n clientId: started.state.clientInformation.client_id,\n redirectUrl: input.redirectUrl,\n scopes:\n strategy.scopes ??\n started.state.authorizationServerMetadata.scopes_supported ??\n [],\n state: sessionId,\n codeChallenge,\n });\n\n const payload: OAuthSessionPayload = {\n kind: \"dynamic-dcr\",\n identityLabel: input.identityLabel ?? null,\n codeVerifier: started.codeVerifier,\n authorizationServerUrl: started.state.authorizationServerUrl,\n authorizationServerMetadataUrl:\n started.state.authorizationServerMetadataUrl,\n authorizationServerMetadata:\n started.state.authorizationServerMetadata as Record<string, unknown>,\n clientInformation: (() => {\n const value: unknown = started.state.clientInformation;\n return value as Record<string, unknown>;\n })(),\n resourceMetadataUrl: started.state.resourceMetadataUrl,\n resourceMetadata:\n (started.state.resourceMetadata as Record<string, unknown> | null) ??\n null,\n scopes: [\n ...(strategy.scopes ??\n started.state.authorizationServerMetadata.scopes_supported ??\n []),\n ],\n };\n\n yield* writeSession({\n sessionId,\n input,\n payload,\n strategyKind: \"dynamic-dcr\",\n });\n\n return {\n sessionId,\n authorizationUrl,\n completedConnection: null,\n };\n });\n\n const startAuthorizationCode = (\n input: OAuthStartInput,\n strategy: OAuthAuthorizationCodeStrategy,\n ): Effect.Effect<OAuthStartResult, OAuthStartError | StorageFailure> =>\n Effect.gen(function* () {\n const clientId = yield* deps.secretsGet(strategy.clientIdSecretId).pipe(\n Effect.mapError((err) =>\n // Storage failure propagates; null returns aren't errors — the\n // branch below handles them.\n err,\n ),\n );\n if (clientId === null) {\n return yield* Effect.fail(\n new OAuthStartError({\n message: `client_id secret \"${strategy.clientIdSecretId}\" not found`,\n }),\n );\n }\n\n const sessionId = scopedSessionId(input.tokenScope, newSessionId());\n const codeVerifier = createPkceCodeVerifier();\n const codeChallenge = yield* Effect.promise(() =>\n createPkceCodeChallenge(codeVerifier),\n );\n\n const authorizationUrl = buildAuthorizationUrl({\n authorizationUrl: strategy.authorizationEndpoint,\n clientId,\n redirectUrl: input.redirectUrl,\n scopes: strategy.scopes,\n state: sessionId,\n codeChallenge,\n scopeSeparator: strategy.scopeSeparator,\n extraParams: strategy.extraAuthorizationParams,\n });\n\n const payload: OAuthSessionPayload = {\n kind: \"authorization-code\",\n identityLabel: input.identityLabel ?? null,\n codeVerifier,\n authorizationEndpoint: strategy.authorizationEndpoint,\n tokenEndpoint: strategy.tokenEndpoint,\n issuerUrl: strategy.issuerUrl ?? new URL(strategy.authorizationEndpoint).origin,\n clientIdSecretId: strategy.clientIdSecretId,\n clientSecretSecretId: strategy.clientSecretSecretId,\n scopes: [...strategy.scopes],\n scopeSeparator: strategy.scopeSeparator,\n clientAuth: strategy.clientAuth ?? \"body\",\n };\n\n yield* writeSession({\n sessionId,\n input,\n payload,\n strategyKind: \"authorization-code\",\n });\n\n return {\n sessionId,\n authorizationUrl,\n completedConnection: null,\n };\n });\n\n const startClientCredentials = (\n input: OAuthStartInput,\n strategy: OAuthClientCredentialsStrategy,\n ): Effect.Effect<OAuthStartResult, OAuthStartError | StorageFailure> =>\n Effect.gen(function* () {\n const clientId = yield* deps.secretsGet(strategy.clientIdSecretId);\n const clientSecret = yield* deps.secretsGet(strategy.clientSecretSecretId);\n if (clientId === null || clientSecret === null) {\n return yield* Effect.fail(\n new OAuthStartError({\n message: \"client_id / client_secret secret not found\",\n }),\n );\n }\n\n const tokens = yield* exchangeClientCredentials({\n tokenUrl: strategy.tokenEndpoint,\n clientId,\n clientSecret,\n scopes: strategy.scopes,\n scopeSeparator: strategy.scopeSeparator,\n clientAuth: strategy.clientAuth ?? \"body\",\n }).pipe(\n Effect.mapError(\n (err) =>\n new OAuthStartError({\n message: `Client credentials exchange failed: ${err.message}`,\n\n }),\n ),\n );\n\n const expiresAt =\n typeof tokens.expires_in === \"number\"\n ? now() + tokens.expires_in * 1000\n : null;\n\n const providerState: OAuthProviderState = {\n kind: \"client-credentials\",\n tokenEndpoint: strategy.tokenEndpoint,\n clientIdSecretId: strategy.clientIdSecretId,\n clientSecretSecretId: strategy.clientSecretSecretId,\n scopes: [...(strategy.scopes ?? [])],\n scopeSeparator: strategy.scopeSeparator,\n clientAuth: strategy.clientAuth ?? \"body\",\n scope: tokens.scope ?? null,\n };\n\n yield* deps\n .connectionsCreate(\n new CreateConnectionInput({\n id: ConnectionId.make(input.connectionId),\n scope: ScopeId.make(input.tokenScope),\n provider: OAUTH2_PROVIDER_KEY,\n identityLabel: input.identityLabel ?? safeHostname(input.endpoint),\n accessToken: new TokenMaterial({\n secretId: SecretId.make(oauthSecretId(input.connectionId, \"access-token\")),\n name: \"OAuth Access Token\",\n value: tokens.access_token,\n }),\n refreshToken: null,\n expiresAt,\n oauthScope: tokens.scope ?? null,\n providerState: Schema.encodeSync(OAuthProviderStateSchema)(\n providerState,\n ) as Record<string, unknown>,\n }),\n )\n .pipe(\n Effect.mapError(\n (err) =>\n new OAuthStartError({\n message: `Failed to mint connection: ${\n err instanceof Error ? err.message : String(err)\n }`,\n\n }),\n ),\n );\n\n return {\n sessionId: \"\",\n authorizationUrl: null,\n completedConnection: { connectionId: input.connectionId },\n };\n });\n\n const start = (\n input: OAuthStartInput,\n ): Effect.Effect<OAuthStartResult, OAuthStartError | StorageFailure> => {\n switch (input.strategy.kind) {\n case \"dynamic-dcr\":\n return startDynamicDcr(input, input.strategy);\n case \"authorization-code\":\n return startAuthorizationCode(input, input.strategy);\n case \"client-credentials\":\n return startClientCredentials(input, input.strategy);\n }\n };\n\n const writeSession = (args: {\n sessionId: string;\n input: OAuthStartInput;\n payload: OAuthSessionPayload;\n strategyKind: string;\n }): Effect.Effect<void, StorageFailure> =>\n deps.adapter.create({\n model: \"oauth2_session\",\n data: {\n id: args.sessionId,\n scope_id: args.input.tokenScope,\n plugin_id: args.input.pluginId,\n strategy: args.strategyKind,\n connection_id: args.input.connectionId,\n token_scope: args.input.tokenScope,\n redirect_url: args.input.redirectUrl,\n payload: encodeSessionPayload(args.payload) as Record<string, unknown>,\n expires_at: now() + OAUTH2_SESSION_TTL_MS,\n created_at: new Date(),\n },\n forceAllowId: true,\n }).pipe(Effect.asVoid);\n\n // -------------------------------------------------------------------\n // complete — exchange the code, mint the Connection, delete the session\n // -------------------------------------------------------------------\n const complete = (\n input: OAuthCompleteInput,\n ): Effect.Effect<\n OAuthCompleteResult,\n OAuthCompleteError | OAuthSessionNotFoundError | StorageFailure\n > =>\n Effect.gen(function* () {\n const row = yield* deps.adapter.findOne({\n model: \"oauth2_session\",\n where: [{ field: \"id\", value: input.state }],\n });\n if (!row) {\n return yield* Effect.fail(\n new OAuthSessionNotFoundError({ sessionId: input.state }),\n );\n }\n\n const deleteSession = deps.adapter.delete({\n model: \"oauth2_session\",\n where: [\n { field: \"id\", value: input.state },\n { field: \"scope_id\", value: row.scope_id as string },\n ],\n });\n\n if (input.error) {\n yield* deleteSession;\n return yield* Effect.fail(\n new OAuthCompleteError({\n message: `Authorization server returned error: ${input.error}`,\n code: input.error,\n }),\n );\n }\n if (!input.code) {\n yield* deleteSession;\n return yield* Effect.fail(\n new OAuthCompleteError({\n message: \"Missing authorization code\",\n }),\n );\n }\n const expiresAt = Number(row.expires_at as number | bigint);\n if (expiresAt <= now()) {\n yield* deleteSession;\n return yield* Effect.fail(\n new OAuthCompleteError({\n message: \"OAuth session expired\",\n }),\n );\n }\n\n const payload = decodeSessionPayload(coerceJson(row.payload));\n const endpoint = \"\"; // not stored on the row — the payload's own\n // endpoint fields drive exchange; we just need\n // a display string for the identity label.\n const connectionId = row.connection_id as string;\n const tokenScope = row.token_scope as string;\n const redirectUrl = row.redirect_url as string;\n\n // Dispatch to the strategy-specific exchange.\n const exchangeResult = yield* (() => {\n switch (payload.kind) {\n case \"dynamic-dcr\":\n return exchangeDynamicDcr(payload, input.code, redirectUrl);\n case \"authorization-code\":\n return exchangeAuthorizationCodeStrategy(\n payload,\n input.code,\n redirectUrl,\n );\n }\n })().pipe(Effect.tapError(() => deleteSession));\n\n const connectionExpiresAt =\n typeof exchangeResult.tokens.expires_in === \"number\"\n ? now() + exchangeResult.tokens.expires_in * 1000\n : null;\n\n const dynamicClientSecretSecretId = yield* (() => {\n if (payload.kind !== \"dynamic-dcr\") return Effect.succeed(null);\n const clientSecret = (payload.clientInformation as { client_secret?: unknown })\n .client_secret;\n if (typeof clientSecret !== \"string\" || clientSecret.length === 0) {\n return Effect.succeed(null);\n }\n const secretId = oauthSecretId(connectionId, \"client-secret\");\n return deps\n .secretsSet(\n new SetSecretInput({\n id: SecretId.make(secretId),\n scope: ScopeId.make(tokenScope),\n name: \"OAuth Client Secret\",\n value: clientSecret,\n }),\n )\n .pipe(\n Effect.as(secretId),\n Effect.mapError(\n (err) =>\n new OAuthCompleteError({\n message: `Failed to persist DCR client_secret: ${\n err instanceof Error ? err.message : String(err)\n }`,\n }),\n ),\n );\n })();\n\n const providerState: OAuthProviderState =\n payload.kind === \"dynamic-dcr\"\n ? {\n kind: \"dynamic-dcr\",\n tokenEndpoint: (payload.authorizationServerMetadata as {\n token_endpoint: string;\n }).token_endpoint,\n issuerUrl:\n (payload.authorizationServerMetadata as { issuer?: string }).issuer ??\n null,\n authorizationServerUrl: payload.authorizationServerUrl,\n authorizationServerMetadataUrl:\n payload.authorizationServerMetadataUrl,\n idTokenSigningAlgValuesSupported:\n (payload.authorizationServerMetadata as {\n id_token_signing_alg_values_supported?: string[];\n }).id_token_signing_alg_values_supported,\n clientId: (payload.clientInformation as { client_id: string })\n .client_id,\n clientSecretSecretId: dynamicClientSecretSecretId,\n clientAuth:\n (payload.clientInformation as { token_endpoint_auth_method?: string })\n .token_endpoint_auth_method === \"client_secret_basic\"\n ? \"basic\"\n : \"body\",\n scopes: [...payload.scopes],\n scope: exchangeResult.tokens.scope ?? null,\n }\n : {\n kind: \"authorization-code\",\n tokenEndpoint: payload.tokenEndpoint,\n issuerUrl: payload.issuerUrl,\n clientIdSecretId: payload.clientIdSecretId,\n clientSecretSecretId: payload.clientSecretSecretId,\n clientAuth: payload.clientAuth,\n scopes: [...payload.scopes],\n scopeSeparator: payload.scopeSeparator,\n scope: exchangeResult.tokens.scope ?? null,\n };\n\n yield* deps\n .connectionsCreate(\n new CreateConnectionInput({\n id: ConnectionId.make(connectionId),\n scope: ScopeId.make(tokenScope),\n provider: OAUTH2_PROVIDER_KEY,\n identityLabel: safeHostname(\n payload.identityLabel ?? exchangeResult.endpointForDisplay ?? endpoint,\n ),\n accessToken: new TokenMaterial({\n secretId: SecretId.make(oauthSecretId(connectionId, \"access-token\")),\n name: \"OAuth Access Token\",\n value: exchangeResult.tokens.access_token,\n }),\n refreshToken: exchangeResult.tokens.refresh_token\n ? new TokenMaterial({\n secretId: SecretId.make(oauthSecretId(connectionId, \"refresh-token\")),\n name: \"OAuth Refresh Token\",\n value: exchangeResult.tokens.refresh_token,\n })\n : null,\n expiresAt: connectionExpiresAt,\n oauthScope: exchangeResult.tokens.scope ?? null,\n providerState: Schema.encodeSync(OAuthProviderStateSchema)(\n providerState,\n ) as Record<string, unknown>,\n }),\n )\n .pipe(\n Effect.mapError(\n (err) =>\n new OAuthCompleteError({\n message: `Failed to mint connection: ${\n err instanceof Error ? err.message : String(err)\n }`,\n\n }),\n ),\n );\n\n yield* deleteSession;\n\n return {\n connectionId,\n expiresAt: connectionExpiresAt,\n scope: exchangeResult.tokens.scope ?? null,\n };\n });\n\n interface ExchangeResult {\n readonly tokens: {\n readonly access_token: string;\n readonly refresh_token?: string;\n readonly expires_in?: number;\n readonly scope?: string;\n readonly token_type?: string;\n };\n readonly endpointForDisplay: string | null;\n }\n\n const exchangeDynamicDcr = (\n payload: Extract<OAuthSessionPayload, { kind: \"dynamic-dcr\" }>,\n code: string,\n redirectUrl: string,\n ): Effect.Effect<ExchangeResult, OAuthCompleteError> =>\n Effect.gen(function* () {\n const md = payload.authorizationServerMetadata as {\n token_endpoint: string;\n issuer?: string;\n id_token_signing_alg_values_supported?: string[];\n };\n const ci = payload.clientInformation as {\n client_id: string;\n client_secret?: string;\n token_endpoint_auth_method?: string;\n };\n const tokens = yield* exchangeAuthorizationCode({\n tokenUrl: md.token_endpoint,\n issuerUrl: md.issuer,\n clientId: ci.client_id,\n clientSecret: ci.client_secret ?? undefined,\n redirectUrl,\n codeVerifier: payload.codeVerifier,\n code,\n idTokenSigningAlgValuesSupported:\n md.id_token_signing_alg_values_supported,\n clientAuth:\n ci.token_endpoint_auth_method === \"client_secret_basic\"\n ? \"basic\"\n : \"body\",\n }).pipe(\n Effect.mapError(\n (err) =>\n new OAuthCompleteError({\n message: `Token exchange failed: ${err.message}`,\n code: err.error,\n\n }),\n ),\n );\n return {\n tokens,\n endpointForDisplay: payload.authorizationServerUrl,\n };\n });\n\n const exchangeAuthorizationCodeStrategy = (\n payload: Extract<OAuthSessionPayload, { kind: \"authorization-code\" }>,\n code: string,\n redirectUrl: string,\n ): Effect.Effect<\n ExchangeResult,\n OAuthCompleteError | StorageFailure\n > =>\n Effect.gen(function* () {\n const clientId = yield* deps.secretsGet(payload.clientIdSecretId);\n if (clientId === null) {\n return yield* Effect.fail(\n new OAuthCompleteError({\n message: `client_id secret \"${payload.clientIdSecretId}\" not found`,\n }),\n );\n }\n const clientSecret = payload.clientSecretSecretId\n ? yield* deps.secretsGet(payload.clientSecretSecretId)\n : null;\n if (payload.clientSecretSecretId && clientSecret === null) {\n return yield* Effect.fail(\n new OAuthCompleteError({\n message: `client_secret secret \"${payload.clientSecretSecretId}\" not found`,\n }),\n );\n }\n\n const tokens = yield* exchangeAuthorizationCode({\n tokenUrl: payload.tokenEndpoint,\n issuerUrl: payload.issuerUrl,\n clientId,\n clientSecret: clientSecret ?? undefined,\n redirectUrl,\n codeVerifier: payload.codeVerifier,\n code,\n clientAuth: payload.clientAuth,\n }).pipe(\n Effect.mapError(\n (err) =>\n new OAuthCompleteError({\n message: `Token exchange failed: ${err.message}`,\n code: err.error,\n\n }),\n ),\n );\n return {\n tokens,\n endpointForDisplay: null,\n };\n });\n\n const cancel = (sessionId: string): Effect.Effect<void, StorageFailure> =>\n Effect.gen(function* () {\n const row = yield* deps.adapter.findOne({\n model: \"oauth2_session\",\n where: [{ field: \"id\", value: sessionId }],\n });\n if (!row) return;\n yield* deps.adapter.delete({\n model: \"oauth2_session\",\n where: [\n { field: \"id\", value: sessionId },\n { field: \"scope_id\", value: row.scope_id as string },\n ],\n });\n });\n\n // -------------------------------------------------------------------\n // Canonical connection provider — refresh handler\n // -------------------------------------------------------------------\n const connectionProvider: ConnectionProvider = {\n key: OAUTH2_PROVIDER_KEY,\n refresh: (input: ConnectionRefreshInput) =>\n Effect.gen(function* () {\n if (!input.providerState) {\n return yield* new ConnectionRefreshError({\n connectionId: input.connectionId,\n message: \"oauth2 connection missing providerState\",\n });\n }\n const state = yield* Effect.try({\n try: () => decodeProviderState(input.providerState),\n catch: (cause) =>\n new ConnectionRefreshError({\n connectionId: input.connectionId,\n message: `oauth2 providerState is malformed: ${\n cause instanceof Error ? cause.message : String(cause)\n }`,\n cause,\n }),\n });\n\n if (state.kind !== \"client-credentials\" && !input.refreshToken) {\n return yield* new ConnectionRefreshError({\n connectionId: input.connectionId,\n message: \"oauth2 connection has no refresh token\",\n reauthRequired: true,\n });\n }\n\n // Resolve client credentials depending on strategy. Dynamic-DCR\n // embeds `client_id` inline (DCR-minted public client);\n // authorization-code reads it off a secret; client-credentials\n // reads both id + secret.\n const { clientId, clientSecret } = yield* (() => {\n switch (state.kind) {\n case \"dynamic-dcr\":\n return Effect.gen(function* () {\n const csec = state.clientSecretSecretId\n ? yield* deps\n .secretsGet(state.clientSecretSecretId)\n .pipe(\n Effect.mapError(\n (cause) =>\n new ConnectionRefreshError({\n connectionId: input.connectionId,\n message: `Failed to resolve DCR client_secret: ${\n cause instanceof Error\n ? cause.message\n : String(cause)\n }`,\n cause,\n }),\n ),\n )\n : null;\n if (state.clientSecretSecretId && csec === null) {\n return yield* new ConnectionRefreshError({\n connectionId: input.connectionId,\n message: `client_secret secret \"${state.clientSecretSecretId}\" not found`,\n reauthRequired: true,\n });\n }\n return { clientId: state.clientId, clientSecret: csec };\n });\n case \"authorization-code\":\n case \"client-credentials\":\n return Effect.gen(function* () {\n const cid = yield* deps\n .secretsGet(state.clientIdSecretId)\n .pipe(\n Effect.mapError(\n (cause) =>\n new ConnectionRefreshError({\n connectionId: input.connectionId,\n message: `Failed to resolve client_id secret: ${\n cause instanceof Error\n ? cause.message\n : String(cause)\n }`,\n cause,\n }),\n ),\n );\n if (cid === null) {\n return yield* new ConnectionRefreshError({\n connectionId: input.connectionId,\n message: `client_id secret \"${state.clientIdSecretId}\" not found`,\n reauthRequired: true,\n });\n }\n const csec = state.clientSecretSecretId\n ? yield* deps\n .secretsGet(state.clientSecretSecretId)\n .pipe(\n Effect.mapError(\n (cause) =>\n new ConnectionRefreshError({\n connectionId: input.connectionId,\n message: `Failed to resolve client_secret: ${\n cause instanceof Error\n ? cause.message\n : String(cause)\n }`,\n cause,\n }),\n ),\n )\n : null;\n if (state.clientSecretSecretId && csec === null) {\n return yield* new ConnectionRefreshError({\n connectionId: input.connectionId,\n message: `client_secret secret \"${state.clientSecretSecretId}\" not found`,\n reauthRequired: true,\n });\n }\n return { clientId: cid, clientSecret: csec };\n });\n }\n })();\n\n const tokenEndpoint = yield* (() => {\n if (state.tokenEndpoint) return Effect.succeed(state.tokenEndpoint);\n if (\n state.kind === \"dynamic-dcr\" &&\n state.authorizationServerUrl\n ) {\n return discoverAuthorizationServerMetadata(\n state.authorizationServerUrl,\n ).pipe(\n Effect.flatMap((metadata) =>\n metadata?.metadata.token_endpoint\n ? Effect.succeed(metadata.metadata.token_endpoint)\n : Effect.fail(\n new ConnectionRefreshError({\n connectionId: input.connectionId,\n message:\n \"oauth2 legacy MCP providerState is missing token endpoint\",\n reauthRequired: true,\n }),\n ),\n ),\n Effect.mapError((cause) =>\n cause instanceof ConnectionRefreshError\n ? cause\n : new ConnectionRefreshError({\n connectionId: input.connectionId,\n message:\n \"Failed to discover token endpoint for legacy MCP OAuth connection\",\n reauthRequired: true,\n cause,\n }),\n ),\n );\n }\n return Effect.fail(\n new ConnectionRefreshError({\n connectionId: input.connectionId,\n message: \"oauth2 providerState is missing token endpoint\",\n reauthRequired: true,\n }),\n );\n })();\n\n const tokens = yield* (state.kind === \"client-credentials\"\n ? exchangeClientCredentials({\n tokenUrl: tokenEndpoint,\n clientId,\n clientSecret: clientSecret ?? \"\",\n scopes: state.scopes,\n scopeSeparator: state.scopeSeparator,\n clientAuth: state.clientAuth,\n })\n : refreshAccessToken({\n tokenUrl: tokenEndpoint,\n issuerUrl:\n state.kind === \"dynamic-dcr\" || state.kind === \"authorization-code\"\n ? (state.issuerUrl ?? undefined)\n : undefined,\n clientId,\n clientSecret: clientSecret ?? undefined,\n refreshToken: input.refreshToken!,\n scopes:\n state.kind === \"dynamic-dcr\" || state.kind === \"authorization-code\"\n ? state.scopes\n : undefined,\n scopeSeparator:\n state.kind === \"dynamic-dcr\" || state.kind === \"authorization-code\"\n ? state.scopeSeparator\n : undefined,\n clientAuth: state.clientAuth,\n idTokenSigningAlgValuesSupported:\n state.kind === \"dynamic-dcr\"\n ? state.idTokenSigningAlgValuesSupported\n : undefined,\n })).pipe(\n Effect.mapError(\n (err) =>\n new ConnectionRefreshError({\n connectionId: input.connectionId,\n message: `OAuth refresh failed: ${err.message}`,\n // Terminal RFC 6749 §5.2 errors mean retrying won't heal it.\n reauthRequired: err.error\n ? terminalRefreshErrors.has(err.error)\n : false,\n\n }),\n ),\n );\n\n const expiresAt =\n typeof tokens.expires_in === \"number\"\n ? now() + tokens.expires_in * 1000\n : null;\n\n const result: ConnectionRefreshResult = {\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt,\n oauthScope: tokens.scope ?? input.oauthScope,\n providerState: Schema.encodeSync(OAuthProviderStateSchema)({\n ...state,\n tokenEndpoint,\n scope: tokens.scope ?? state.scope,\n }) as Record<string, unknown>,\n };\n return result;\n }),\n };\n\n const service: OAuthService = { probe, start, complete, cancel };\n\n return { service, connectionProvider };\n};\n\nconst safeHostname = (value: string | null): string | null => {\n if (!value) return null;\n try {\n return new URL(value).host;\n } catch {\n return value;\n }\n};\n","// ---------------------------------------------------------------------------\n// OAuth 2.0 metadata discovery + DCR.\n//\n// The token-endpoint helpers in `./oauth-helpers.ts` assume the caller\n// already knows the authorization/token URLs and client_id — that's\n// fine for static integrations (Google, a specific OpenAPI server).\n// The zero-config case — user pastes an arbitrary endpoint URL and we\n// figure out its OAuth configuration — needs three more building blocks:\n//\n// - RFC 9728 Protected Resource Metadata (/.well-known/oauth-protected-resource)\n// - RFC 8414 Authorization Server Metadata (/.well-known/oauth-authorization-server,\n// with OIDC /.well-known/openid-configuration as fallback)\n// - RFC 7591 Dynamic Client Registration (POST `registration_endpoint`)\n//\n// `oauth4webapi` covers (2) and (3); (1) is MCP-spec-only and not yet in\n// the library, so we keep a 30-line hand-rolled probe. A convenience\n// `beginDynamicAuthorization` chains all three into the single call\n// callers actually need.\n// ---------------------------------------------------------------------------\n\nimport { Data, Effect, Result, Schema } from \"effect\";\nimport * as oauth from \"oauth4webapi\";\n\nimport {\n OAUTH2_DEFAULT_TIMEOUT_MS,\n buildAuthorizationUrl,\n createPkceCodeChallenge,\n createPkceCodeVerifier,\n} from \"./oauth-helpers\";\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/** Separate tag from `OAuth2Error` so callers can distinguish discovery\n * / DCR failures (happen once, before any token round-trips) from\n * token-endpoint failures. A plugin's refresh path should never have\n * to inspect error messages to tell \"metadata drifted, re-discover\"\n * apart from \"refresh token is no longer honoured\". */\nexport class OAuthDiscoveryError extends Data.TaggedError(\n \"OAuthDiscoveryError\",\n)<{\n readonly message: string;\n readonly status?: number;\n readonly cause?: unknown;\n}> {}\n\nconst discoveryError = (\n message: string,\n options: { status?: number; cause?: unknown } = {},\n): OAuthDiscoveryError =>\n new OAuthDiscoveryError({\n message,\n status: options.status,\n cause: options.cause,\n });\n\n// ---------------------------------------------------------------------------\n// Schemas (narrow structural parsing — the RFCs leave many fields\n// optional; we validate only the subset consumers read)\n// ---------------------------------------------------------------------------\n\nconst StringArray = Schema.Array(Schema.String);\n\nexport const OAuthProtectedResourceMetadataSchema = Schema.Struct({\n resource: Schema.optional(Schema.String),\n authorization_servers: Schema.optional(StringArray),\n scopes_supported: Schema.optional(StringArray),\n bearer_methods_supported: Schema.optional(StringArray),\n resource_documentation: Schema.optional(Schema.String),\n}).annotate({ identifier: \"OAuthProtectedResourceMetadata\" });\nexport type OAuthProtectedResourceMetadata =\n typeof OAuthProtectedResourceMetadataSchema.Type;\n\nexport const OAuthAuthorizationServerMetadataSchema = Schema.Struct({\n issuer: Schema.String,\n authorization_endpoint: Schema.String,\n token_endpoint: Schema.String,\n registration_endpoint: Schema.optional(Schema.String),\n scopes_supported: Schema.optional(StringArray),\n response_types_supported: Schema.optional(StringArray),\n grant_types_supported: Schema.optional(StringArray),\n code_challenge_methods_supported: Schema.optional(StringArray),\n token_endpoint_auth_methods_supported: Schema.optional(StringArray),\n revocation_endpoint: Schema.optional(Schema.String),\n introspection_endpoint: Schema.optional(Schema.String),\n userinfo_endpoint: Schema.optional(Schema.String),\n id_token_signing_alg_values_supported: Schema.optional(StringArray),\n}).annotate({ identifier: \"OAuthAuthorizationServerMetadata\" });\nexport type OAuthAuthorizationServerMetadata =\n typeof OAuthAuthorizationServerMetadataSchema.Type;\n\nexport type DynamicClientMetadata = {\n readonly client_name?: string;\n readonly redirect_uris: readonly string[];\n readonly grant_types?: readonly string[];\n readonly response_types?: readonly string[];\n readonly token_endpoint_auth_method?:\n | \"none\"\n | \"client_secret_basic\"\n | \"client_secret_post\"\n | \"private_key_jwt\";\n readonly scope?: string;\n readonly application_type?: \"web\" | \"native\";\n readonly client_uri?: string;\n readonly logo_uri?: string;\n readonly contacts?: readonly string[];\n readonly software_id?: string;\n readonly software_version?: string;\n /** Escape hatch for provider-specific extensions; merged last. */\n readonly extra?: Readonly<Record<string, unknown>>;\n};\n\nexport const OAuthClientInformationSchema = Schema.Struct({\n client_id: Schema.String,\n client_secret: Schema.optional(Schema.String),\n client_id_issued_at: Schema.optional(Schema.Number),\n client_secret_expires_at: Schema.optional(Schema.Number),\n registration_access_token: Schema.optional(Schema.String),\n registration_client_uri: Schema.optional(Schema.String),\n token_endpoint_auth_method: Schema.optional(Schema.String),\n grant_types: Schema.optional(StringArray),\n response_types: Schema.optional(StringArray),\n redirect_uris: Schema.optional(StringArray),\n client_name: Schema.optional(Schema.String),\n scope: Schema.optional(Schema.String),\n}).annotate({ identifier: \"OAuthClientInformation\" });\nexport type OAuthClientInformation = typeof OAuthClientInformationSchema.Type;\n\nconst decodeResourceMetadata = Schema.decodeUnknownEffect(\n OAuthProtectedResourceMetadataSchema,\n);\nconst decodeAuthServerMetadata = Schema.decodeUnknownEffect(\n OAuthAuthorizationServerMetadataSchema,\n);\nconst decodeClientInformation = Schema.decodeUnknownEffect(\n OAuthClientInformationSchema,\n);\n\nexport interface DiscoveryRequestOptions {\n /** Injected for tests. Defaults to the global `fetch`. */\n readonly fetch?: typeof fetch;\n /** Abort the request after this many ms. Default 20000. */\n readonly timeoutMs?: number;\n /** Send `MCP-Protocol-Version: <value>` on every request. Harmless\n * for non-MCP servers; required by the MCP authorization spec. */\n readonly mcpProtocolVersion?: string;\n /** Credentials needed to reach the protected resource itself. These\n * are intentionally used only for resource-side probes, never for\n * authorization-server metadata, DCR, authorization, or token calls. */\n readonly resourceHeaders?: Readonly<Record<string, string>>;\n readonly resourceQueryParams?: Readonly<Record<string, string>>;\n}\n\nconst MCP_PROTOCOL_VERSION_HEADER = \"mcp-protocol-version\";\n\nconst isLoopbackHttpUrl = (value: string): boolean => {\n try {\n const url = new URL(value);\n if (url.protocol !== \"http:\") return false;\n const hostname = url.hostname.toLowerCase();\n return (\n hostname === \"localhost\" ||\n hostname === \"0.0.0.0\" ||\n hostname === \"::1\" ||\n hostname === \"[::1]\" ||\n hostname.startsWith(\"127.\")\n );\n } catch {\n return false;\n }\n};\n\nconst oauth4webapiOptions = (\n options: DiscoveryRequestOptions,\n targetUrl?: string,\n): Record<string, unknown> => {\n const out: Record<string, unknown> = {};\n if (options.fetch) (out as { [customFetch]?: typeof fetch })[customFetch] = options.fetch;\n if (targetUrl && isLoopbackHttpUrl(targetUrl)) {\n (out as { [oauth.allowInsecureRequests]?: boolean })[\n oauth.allowInsecureRequests\n ] = true;\n }\n const signal = AbortSignal.timeout(options.timeoutMs ?? OAUTH2_DEFAULT_TIMEOUT_MS);\n out.signal = signal;\n if (options.mcpProtocolVersion) {\n out.headers = new Headers({\n [MCP_PROTOCOL_VERSION_HEADER]: options.mcpProtocolVersion,\n });\n }\n return out;\n};\n\n// oauth4webapi's custom-fetch symbol — imported lazily so dropping the\n// library (unlikely but fine) doesn't leave a dangling symbol reference.\nconst customFetch = Symbol.for(\"oauth4webapi.customFetch\");\n\n// ---------------------------------------------------------------------------\n// RFC 9728 — Protected Resource Metadata\n//\n// Not covered by `oauth4webapi`. Hand-rolled probe: try the path-scoped\n// well-known first, then the origin-scoped fallback.\n// ---------------------------------------------------------------------------\n\nconst buildResourceMetadataUrls = (resourceUrl: string): string[] => {\n const url = new URL(resourceUrl);\n const origin = `${url.protocol}//${url.host}`;\n const path = url.pathname.replace(/\\/+$/, \"\");\n const urls: string[] = [];\n if (path && path !== \"/\") {\n urls.push(`${origin}/.well-known/oauth-protected-resource${path}`);\n }\n urls.push(`${origin}/.well-known/oauth-protected-resource`);\n return urls;\n};\n\nconst withResourceQueryParams = (\n url: string,\n queryParams: Readonly<Record<string, string>> | undefined,\n): string => {\n if (!queryParams || Object.keys(queryParams).length === 0) return url;\n const parsed = new URL(url);\n for (const [key, value] of Object.entries(queryParams)) {\n parsed.searchParams.set(key, value);\n }\n return parsed.toString();\n};\n\nexport const discoverProtectedResourceMetadata = (\n resourceUrl: string,\n options: DiscoveryRequestOptions = {},\n): Effect.Effect<\n | { readonly metadataUrl: string; readonly metadata: OAuthProtectedResourceMetadata }\n | null,\n OAuthDiscoveryError\n> =>\n Effect.gen(function* () {\n const fetchImpl = options.fetch ?? globalThis.fetch;\n const timeoutMs = options.timeoutMs ?? OAUTH2_DEFAULT_TIMEOUT_MS;\n for (const url of buildResourceMetadataUrls(resourceUrl)) {\n const result = yield* Effect.tryPromise({\n try: async () => {\n const requestUrl = withResourceQueryParams(url, options.resourceQueryParams);\n const headers: Record<string, string> = {\n ...options.resourceHeaders,\n accept: \"application/json\",\n };\n if (options.mcpProtocolVersion) {\n headers[MCP_PROTOCOL_VERSION_HEADER] = options.mcpProtocolVersion;\n }\n const response = await fetchImpl(requestUrl, {\n method: \"GET\",\n headers,\n signal: AbortSignal.timeout(timeoutMs),\n });\n if (response.status === 404 || response.status === 405) return \"skip\" as const;\n if (response.status < 200 || response.status >= 300) {\n return { status: response.status } as const;\n }\n const text = await response.text();\n if (text.length === 0) return \"skip\" as const;\n return { status: response.status, body: JSON.parse(text) } as const;\n },\n catch: (cause) =>\n discoveryError(\n `Failed to fetch ${url}: ${cause instanceof Error ? cause.message : String(cause)}`,\n { cause },\n ),\n });\n if (result === \"skip\") continue;\n if (!(\"body\" in result)) {\n return yield* Effect.fail(\n discoveryError(\n `Protected resource metadata returned status ${result.status}`,\n { status: result.status },\n ),\n );\n }\n const metadata = yield* decodeResourceMetadata(result.body).pipe(\n Effect.mapError(\n (err) =>\n new OAuthDiscoveryError({\n message: `Protected resource metadata is malformed: ${\n Schema.isSchemaError(err) ? err.message : String(err)\n }`,\n cause: err,\n }),\n ),\n );\n return { metadataUrl: url, metadata };\n }\n return null;\n });\n\n// ---------------------------------------------------------------------------\n// RFC 8414 + OIDC Discovery — Authorization Server Metadata\n//\n// Delegates to `oauth4webapi.discoveryRequest` + `processDiscoveryResponse`.\n// The library only probes one `.well-known` variant per call; we try\n// RFC 8414 (`oauth2`) first and fall back to OIDC Discovery.\n// ---------------------------------------------------------------------------\n\nconst wellKnownUrlFor = (\n issuerOrigin: string,\n algorithm: \"oauth2\" | \"oidc\",\n issuerPath: string,\n): string => {\n // Mirrors the library's own well-known composition so the URL we\n // surface matches what was actually fetched.\n const suffix = algorithm === \"oauth2\"\n ? \"oauth-authorization-server\"\n : \"openid-configuration\";\n return issuerPath && issuerPath !== \"/\"\n ? `${issuerOrigin}/.well-known/${suffix}${issuerPath}`\n : `${issuerOrigin}/.well-known/${suffix}`;\n};\n\nexport const discoverAuthorizationServerMetadata = (\n issuer: string,\n options: DiscoveryRequestOptions = {},\n): Effect.Effect<\n | {\n readonly metadataUrl: string;\n readonly metadata: OAuthAuthorizationServerMetadata;\n }\n | null,\n OAuthDiscoveryError\n> =>\n Effect.gen(function* () {\n const issuerUrl = new URL(issuer);\n const issuerOrigin = `${issuerUrl.protocol}//${issuerUrl.host}`;\n const issuerPath = issuerUrl.pathname.replace(/\\/+$/, \"\");\n\n for (const algorithm of [\"oauth2\", \"oidc\"] as const) {\n const result = yield* Effect.tryPromise({\n try: async () => {\n const response = await oauth.discoveryRequest(issuerUrl, {\n algorithm,\n ...oauth4webapiOptions(options, issuer),\n });\n if (response.status === 404 || response.status === 405) {\n return null;\n }\n const as = await oauth.processDiscoveryResponse(issuerUrl, response);\n return {\n metadataUrl: wellKnownUrlFor(issuerOrigin, algorithm, issuerPath),\n raw: as,\n };\n },\n catch: (cause) => {\n if (cause instanceof OAuthDiscoveryError) return cause;\n return discoveryError(\n `Discovery (${algorithm}) failed for ${issuer}: ${\n cause instanceof Error ? cause.message : String(cause)\n }`,\n { cause },\n );\n },\n }).pipe(\n // If one algorithm fails mid-roundtrip (network, parse, issuer\n // mismatch) we still want to try the other before giving up.\n Effect.result,\n );\n\n if (Result.isFailure(result)) continue;\n if (result.success === null) continue;\n\n const metadata = yield* decodeAuthServerMetadata(result.success.raw).pipe(\n Effect.mapError(\n (err) =>\n new OAuthDiscoveryError({\n message: `Authorization server metadata is malformed: ${\n Schema.isSchemaError(err) ? err.message : String(err)\n }`,\n cause: err,\n }),\n ),\n );\n return { metadataUrl: result.success.metadataUrl, metadata };\n }\n return null;\n });\n\n// ---------------------------------------------------------------------------\n// RFC 7591 — Dynamic Client Registration\n//\n// Delegates to `oauth4webapi.dynamicClientRegistrationRequest` +\n// `processDynamicClientRegistrationResponse`.\n// ---------------------------------------------------------------------------\n\nexport interface RegisterDynamicClientInput {\n readonly registrationEndpoint: string;\n readonly metadata: DynamicClientMetadata;\n readonly initialAccessToken?: string | null;\n}\n\nconst asForDcr = (\n registrationEndpoint: string,\n): oauth.AuthorizationServer => {\n const url = new URL(registrationEndpoint);\n return {\n issuer: `${url.protocol}//${url.host}`,\n registration_endpoint: registrationEndpoint,\n };\n};\n\nexport const registerDynamicClient = (\n input: RegisterDynamicClientInput,\n options: DiscoveryRequestOptions = {},\n): Effect.Effect<OAuthClientInformation, OAuthDiscoveryError> =>\n Effect.tryPromise({\n try: async () => {\n const as = asForDcr(input.registrationEndpoint);\n const m = input.metadata;\n const clientMetadata: Record<string, unknown> = {\n redirect_uris: [...m.redirect_uris],\n };\n if (m.client_name !== undefined) clientMetadata.client_name = m.client_name;\n if (m.grant_types !== undefined) clientMetadata.grant_types = [...m.grant_types];\n if (m.response_types !== undefined) clientMetadata.response_types = [...m.response_types];\n if (m.token_endpoint_auth_method !== undefined) {\n clientMetadata.token_endpoint_auth_method = m.token_endpoint_auth_method;\n }\n if (m.scope !== undefined) clientMetadata.scope = m.scope;\n if (m.application_type !== undefined) clientMetadata.application_type = m.application_type;\n if (m.client_uri !== undefined) clientMetadata.client_uri = m.client_uri;\n if (m.logo_uri !== undefined) clientMetadata.logo_uri = m.logo_uri;\n if (m.contacts !== undefined) clientMetadata.contacts = [...m.contacts];\n if (m.software_id !== undefined) clientMetadata.software_id = m.software_id;\n if (m.software_version !== undefined) clientMetadata.software_version = m.software_version;\n if (m.extra) for (const [k, v] of Object.entries(m.extra)) clientMetadata[k] = v;\n\n const reqOptions: Record<string, unknown> = oauth4webapiOptions(options);\n if (isLoopbackHttpUrl(input.registrationEndpoint)) {\n (reqOptions as { [oauth.allowInsecureRequests]?: boolean })[\n oauth.allowInsecureRequests\n ] = true;\n }\n if (input.initialAccessToken) {\n reqOptions.initialAccessToken = input.initialAccessToken;\n }\n\n const response = await oauth.dynamicClientRegistrationRequest(\n as,\n clientMetadata as Partial<oauth.Client>,\n reqOptions,\n );\n const client = await oauth.processDynamicClientRegistrationResponse(\n response,\n );\n return client as OAuthClientInformation;\n },\n catch: (cause) => {\n // `oauth4webapi` throws `ResponseBodyError` for RFC 6749 error\n // envelopes (carrying `.status`, `.error`, `.error_description`).\n // Preserve the HTTP status + error code so the plugin layer can\n // surface specific failures (`invalid_client_metadata` →\n // `client_metadata is wrong`) without regex-ing the message.\n if (cause instanceof oauth.ResponseBodyError) {\n return discoveryError(\n `Dynamic Client Registration failed: ${cause.error}${\n cause.error_description ? ` — ${cause.error_description}` : \"\"\n }`,\n { status: cause.status, cause },\n );\n }\n return discoveryError(\n `Dynamic Client Registration failed: ${cause instanceof Error ? cause.message : String(cause)}`,\n { cause },\n );\n },\n }).pipe(\n Effect.flatMap((raw) =>\n decodeClientInformation(raw).pipe(\n Effect.mapError(\n (err) =>\n new OAuthDiscoveryError({\n message: `Dynamic Client Registration response is malformed: ${\n Schema.isSchemaError(err) ? err.message : String(err)\n }`,\n cause: err,\n }),\n ),\n ),\n ),\n );\n\n// ---------------------------------------------------------------------------\n// Convenience: begin the full dynamic flow in one call\n// ---------------------------------------------------------------------------\n\nexport interface DynamicAuthorizationState {\n readonly resourceMetadata: OAuthProtectedResourceMetadata | null;\n readonly resourceMetadataUrl: string | null;\n readonly authorizationServerUrl: string;\n readonly authorizationServerMetadataUrl: string;\n readonly authorizationServerMetadata: OAuthAuthorizationServerMetadata;\n readonly clientInformation: OAuthClientInformation;\n}\n\nexport interface DynamicAuthorizationStartResult {\n readonly authorizationUrl: string;\n readonly codeVerifier: string;\n readonly state: DynamicAuthorizationState;\n}\n\nexport interface BeginDynamicAuthorizationInput {\n readonly endpoint: string;\n readonly redirectUrl: string;\n /** RFC 6749 `state` — callers typically pass a per-session random id. */\n readonly state: string;\n /** Defaults: `redirect_uris=[redirectUrl]`, `token_endpoint_auth_method=\"none\"`\n * (public client + PKCE). */\n readonly clientMetadata?: Partial<DynamicClientMetadata>;\n /** Scopes to request. Defaults to `scopes_supported`; omitted if\n * neither is set. */\n readonly scopes?: readonly string[];\n /** Pre-existing state from a previous flow. When provided, the\n * matching discovery / DCR step is skipped so multi-user sign-ins\n * against the same source don't re-pay those costs. */\n readonly previousState?: {\n readonly authorizationServerUrl?: string | null;\n readonly authorizationServerMetadata?: OAuthAuthorizationServerMetadata | null;\n readonly authorizationServerMetadataUrl?: string | null;\n readonly resourceMetadata?: OAuthProtectedResourceMetadata | null;\n readonly resourceMetadataUrl?: string | null;\n readonly clientInformation?: OAuthClientInformation | null;\n };\n}\n\nexport const beginDynamicAuthorization = (\n input: BeginDynamicAuthorizationInput,\n options: DiscoveryRequestOptions = {},\n): Effect.Effect<DynamicAuthorizationStartResult, OAuthDiscoveryError> =>\n Effect.gen(function* () {\n const prior = input.previousState ?? {};\n\n // Skip the resource-metadata probe when we already know (or can\n // derive) the authorization server URL. Saves two round-trips for\n // every second-and-later user signing into the same source.\n const canSkipResourceDiscovery =\n prior.resourceMetadata !== undefined ||\n !!prior.authorizationServerUrl ||\n !!prior.authorizationServerMetadata;\n\n const resource = canSkipResourceDiscovery\n ? prior.resourceMetadata\n ? {\n metadata: prior.resourceMetadata,\n metadataUrl: prior.resourceMetadataUrl ?? null,\n }\n : null\n : yield* discoverProtectedResourceMetadata(input.endpoint, options);\n\n const authorizationServerUrl = (() => {\n if (prior.authorizationServerUrl) return prior.authorizationServerUrl;\n const fromResource =\n resource && resource.metadata.authorization_servers?.[0];\n if (fromResource) return fromResource;\n const u = new URL(input.endpoint);\n return `${u.protocol}//${u.host}`;\n })();\n\n const authServer =\n prior.authorizationServerMetadata && prior.authorizationServerMetadataUrl\n ? {\n metadata: prior.authorizationServerMetadata,\n metadataUrl: prior.authorizationServerMetadataUrl,\n }\n : yield* discoverAuthorizationServerMetadata(\n authorizationServerUrl,\n options,\n );\n\n if (!authServer) {\n return yield* Effect.fail(\n discoveryError(\n `No OAuth authorization server metadata at ${authorizationServerUrl}`,\n ),\n );\n }\n\n const pkceMethods = authServer.metadata.code_challenge_methods_supported ?? [];\n if (pkceMethods.length > 0 && !pkceMethods.includes(\"S256\")) {\n return yield* Effect.fail(\n discoveryError(\n `Authorization server does not support PKCE S256 (advertised: ${pkceMethods.join(\", \")})`,\n ),\n );\n }\n\n const responseTypes = authServer.metadata.response_types_supported ?? [];\n if (responseTypes.length > 0 && !responseTypes.includes(\"code\")) {\n return yield* Effect.fail(\n discoveryError(\n `Authorization server does not support response_type=code (advertised: ${responseTypes.join(\", \")})`,\n ),\n );\n }\n\n const baseClientMetadata: DynamicClientMetadata = {\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n token_endpoint_auth_method: \"none\",\n client_name: \"Executor\",\n ...(input.clientMetadata ?? {}),\n redirect_uris: input.clientMetadata?.redirect_uris ?? [input.redirectUrl],\n };\n\n const clientInformation =\n prior.clientInformation ??\n (yield* (() => {\n const reg = authServer.metadata.registration_endpoint;\n if (!reg) {\n return Effect.fail(\n discoveryError(\n \"Authorization server does not advertise registration_endpoint — cannot auto-register a client\",\n ),\n );\n }\n return registerDynamicClient(\n { registrationEndpoint: reg, metadata: baseClientMetadata },\n options,\n );\n })());\n\n const codeVerifier = createPkceCodeVerifier();\n const codeChallenge = yield* Effect.promise(() =>\n createPkceCodeChallenge(codeVerifier),\n );\n const scopes = input.scopes ?? authServer.metadata.scopes_supported ?? [];\n\n const authorizationUrl = buildAuthorizationUrl({\n authorizationUrl: authServer.metadata.authorization_endpoint,\n clientId: clientInformation.client_id,\n redirectUrl: input.redirectUrl,\n scopes,\n state: input.state,\n codeChallenge,\n });\n\n return {\n authorizationUrl,\n codeVerifier,\n state: {\n resourceMetadata: resource?.metadata ?? null,\n resourceMetadataUrl: resource?.metadataUrl ?? null,\n authorizationServerUrl,\n authorizationServerMetadataUrl: authServer.metadataUrl,\n authorizationServerMetadata: authServer.metadata,\n clientInformation,\n },\n };\n });\n\nexport { createPkceCodeChallenge };\n","import { Context, Deferred, Effect, Option, Result, Schema, Semaphore } from \"effect\";\nimport { generateKeyBetween } from \"fractional-indexing\";\nimport {\n StorageError,\n typedAdapter,\n type DBAdapter,\n type DBSchema,\n type DBTransactionAdapter,\n type StorageFailure,\n type TypedAdapter,\n} from \"@executor-js/storage-core\";\n\nimport {\n pluginBlobStore,\n type BlobStore,\n} from \"./blob\";\nimport {\n ConnectionProviderState,\n ConnectionRef,\n ConnectionRefreshError,\n type ConnectionProvider,\n type ConnectionRefreshResult,\n type CreateConnectionInput,\n type UpdateConnectionTokensInput,\n} from \"./connections\";\nimport {\n coreSchema,\n isToolPolicyAction,\n type ConnectionRow,\n type CoreSchema,\n type DefinitionsInput,\n type SecretRow,\n type SourceInput,\n type SourceRow,\n type ToolAnnotations,\n type ToolPolicyRow,\n type ToolRow,\n} from \"./core-schema\";\nimport {\n ElicitationDeclinedError,\n ElicitationResponse,\n FormElicitation,\n type ElicitationHandler,\n type ElicitationRequest,\n} from \"./elicitation\";\nimport {\n ConnectionNotFoundError,\n ConnectionProviderNotRegisteredError,\n ConnectionReauthRequiredError,\n ConnectionRefreshNotSupportedError,\n NoHandlerError,\n PluginNotLoadedError,\n SecretOwnedByConnectionError,\n SourceRemovalNotAllowedError,\n ToolBlockedError,\n ToolInvocationError,\n ToolNotFoundError,\n} from \"./errors\";\nimport { ConnectionId, ScopeId, SecretId, ToolId } from \"./ids\";\nimport { makeOAuth2Service } from \"./oauth-service\";\nimport type { OAuthService } from \"./oauth\";\nimport {\n comparePolicyRow,\n isValidPattern,\n resolveToolPolicy,\n rowToToolPolicy,\n type CreateToolPolicyInput,\n type PolicyMatch,\n type ToolPolicy,\n type UpdateToolPolicyInput,\n} from \"./policies\";\nimport type {\n AnyPlugin,\n Elicit,\n PluginCtx,\n PluginExtensions,\n StaticSourceDecl,\n StaticToolDecl,\n StorageDeps,\n} from \"./plugin\";\nimport type { Scope } from \"./scope\";\nimport {\n SecretRef,\n SetSecretInput,\n type SecretProvider,\n} from \"./secrets\";\nimport {\n ToolSchema,\n type Source,\n type SourceDetectionResult,\n type Tool,\n type ToolListFilter,\n} from \"./types\";\nimport { buildToolTypeScriptPreview } from \"./schema-types\";\nimport { scopeAdapter } from \"./scoped-adapter\";\n\n// ---------------------------------------------------------------------------\n// Elicitation handler — set once at `createExecutor({ onElicitation })`\n// and threaded into every tool invocation. A tool that requests user\n// input mid-execution suspends the fiber and the handler decides how to\n// respond. Tools that never elicit simply don't trigger the handler.\n//\n// The \"accept-all\" sentinel is convenient for tests and CLI automation —\n// every elicitation request gets auto-accepted with an empty content\n// payload. For real interactive hosts, pass a real handler.\n//\n// Required at the executor level rather than per-invoke, so the\n// \"what if a caller forgot to pass a handler\" branch is structurally\n// impossible. Higher layers that need per-invocation handler control\n// (an MCP server bridging different per-client handlers, the execution\n// engine threading agent-loop callbacks) can pass an override via\n// `tools.invoke(id, args, { onElicitation })` — the executor-level\n// handler is the fallback, never null.\n// ---------------------------------------------------------------------------\n\nexport type OnElicitation = ElicitationHandler | \"accept-all\";\n\nexport interface InvokeOptions {\n /** Override the executor-level handler for this single call. */\n readonly onElicitation?: OnElicitation;\n}\n\nconst acceptAllHandler: ElicitationHandler = () =>\n Effect.succeed(new ElicitationResponse({ action: \"accept\" }));\n\nconst resolveElicitationHandler = (\n onElicitation: OnElicitation,\n): ElicitationHandler =>\n onElicitation === \"accept-all\" ? acceptAllHandler : onElicitation;\n\n// ---------------------------------------------------------------------------\n// Executor — public surface. Every list/invoke/schema call is a direct\n// core-table query (for dynamic rows) unioned with the in-memory static\n// pool. No ToolRegistry, no SourceRegistry, no SecretStore services.\n// ---------------------------------------------------------------------------\n\nexport type Executor<TPlugins extends readonly AnyPlugin[] = []> = {\n /**\n * Precedence-ordered scope stack this executor was configured with.\n * Innermost first. Consumers that need \"the display scope\" typically\n * pick `scopes.at(-1)` (outermost, e.g. the organization) or\n * `scopes[0]` (innermost, e.g. the current user-in-org) depending on\n * what they're rendering.\n */\n readonly scopes: readonly Scope[];\n\n readonly tools: {\n readonly list: (\n filter?: ToolListFilter,\n ) => Effect.Effect<readonly Tool[], StorageFailure>;\n /** Fetch a tool's full schema view: JSON schemas with `$defs`\n * attached from the core `definition` table, plus TypeScript\n * preview strings rendered from them. Returns `null` for unknown\n * tool ids. */\n readonly schema: (\n toolId: string,\n ) => Effect.Effect<ToolSchema | null, StorageFailure>;\n /** Every `$defs` entry across every source, grouped by source id.\n * Used for bulk schema export and downstream TypeScript rendering. */\n readonly definitions: () => Effect.Effect<\n Record<string, Record<string, unknown>>,\n StorageFailure\n >;\n readonly invoke: (\n toolId: string,\n args: unknown,\n options?: InvokeOptions,\n ) => Effect.Effect<\n unknown,\n | ToolNotFoundError\n | ToolBlockedError\n | PluginNotLoadedError\n | NoHandlerError\n | ToolInvocationError\n | ElicitationDeclinedError\n | StorageFailure\n >;\n };\n\n readonly sources: {\n readonly list: () => Effect.Effect<readonly Source[], StorageFailure>;\n readonly remove: (\n sourceId: string,\n ) => Effect.Effect<void, SourceRemovalNotAllowedError | StorageFailure>;\n readonly refresh: (sourceId: string) => Effect.Effect<void, StorageFailure>;\n /** URL autodetection — fans out to every plugin's `detect` hook\n * (if declared), returns every high/medium/low-confidence match.\n * UI picks a winner from the list. */\n readonly detect: (\n url: string,\n ) => Effect.Effect<readonly SourceDetectionResult[], StorageFailure>;\n /** All `$defs` registered for a single source, keyed by def name. */\n readonly definitions: (\n sourceId: string,\n ) => Effect.Effect<Record<string, unknown>, StorageFailure>;\n };\n\n readonly secrets: {\n readonly get: (\n id: string,\n ) => Effect.Effect<string | null, SecretOwnedByConnectionError | StorageFailure>;\n /** Fast-path existence check — hits the core `secret` routing table\n * only, never calls the provider. Use this for UI state (\"secret\n * missing, prompt to add\") to avoid keychain permission prompts\n * or 1password IPC roundtrips on a pre-flight check. */\n readonly status: (\n id: string,\n ) => Effect.Effect<\"resolved\" | \"missing\", StorageFailure>;\n readonly set: (\n input: SetSecretInput,\n ) => Effect.Effect<SecretRef, StorageFailure>;\n /** Delete a bare (non-connection-owned) secret. Connection-owned\n * secrets are rejected with `SecretOwnedByConnectionError` — use\n * `connections.remove` instead. */\n readonly remove: (\n id: string,\n ) => Effect.Effect<void, SecretOwnedByConnectionError | StorageFailure>;\n readonly list: () => Effect.Effect<readonly SecretRef[], StorageFailure>;\n readonly providers: () => Effect.Effect<readonly string[]>;\n };\n\n readonly connections: {\n readonly get: (\n id: string,\n ) => Effect.Effect<ConnectionRef | null, StorageFailure>;\n readonly list: () => Effect.Effect<readonly ConnectionRef[], StorageFailure>;\n readonly create: (\n input: CreateConnectionInput,\n ) => Effect.Effect<\n ConnectionRef,\n ConnectionProviderNotRegisteredError | StorageFailure\n >;\n readonly updateTokens: (\n input: UpdateConnectionTokensInput,\n ) => Effect.Effect<\n ConnectionRef,\n ConnectionNotFoundError | StorageFailure\n >;\n readonly setIdentityLabel: (\n id: string,\n label: string | null,\n ) => Effect.Effect<void, ConnectionNotFoundError | StorageFailure>;\n readonly accessToken: (\n id: string,\n ) => Effect.Effect<\n string,\n | ConnectionNotFoundError\n | ConnectionProviderNotRegisteredError\n | ConnectionRefreshNotSupportedError\n | ConnectionReauthRequiredError\n | ConnectionRefreshError\n | StorageFailure\n >;\n readonly remove: (id: string) => Effect.Effect<void, StorageFailure>;\n readonly providers: () => Effect.Effect<readonly string[]>;\n };\n\n /** Shared OAuth service. Hosts use this through the core HTTP OAuth group;\n * plugins see the same service as `ctx.oauth`. */\n readonly oauth: OAuthService;\n\n readonly policies: {\n /** All policies visible across the executor's scope stack, sorted\n * by (innermost-scope-first, position ascending) — i.e. the order\n * in which they're evaluated by first-match-wins. */\n readonly list: () => Effect.Effect<readonly ToolPolicy[], StorageFailure>;\n /** Create a new policy. Defaults to the top of the target scope's\n * list (highest precedence) when `position` is omitted. */\n readonly create: (\n input: CreateToolPolicyInput,\n ) => Effect.Effect<ToolPolicy, StorageFailure>;\n readonly update: (\n input: UpdateToolPolicyInput,\n ) => Effect.Effect<ToolPolicy, StorageFailure>;\n readonly remove: (id: string) => Effect.Effect<void, StorageFailure>;\n /** Resolve the effective policy for a tool id by walking the scope-\n * stacked policy list with first-match-wins semantics. Returns\n * `undefined` when no rule matches (caller falls back to the\n * plugin's `resolveAnnotations` output). */\n readonly resolve: (\n toolId: string,\n ) => Effect.Effect<PolicyMatch | undefined, StorageFailure>;\n };\n\n readonly close: () => Effect.Effect<void, StorageFailure>;\n} & PluginExtensions<TPlugins>;\n\nexport interface ExecutorConfig<\n TPlugins extends readonly AnyPlugin[] = [],\n> {\n /**\n * Precedence-ordered scope stack. Innermost first; typical shape is\n * `[userInOrgScope, orgScope]`. Reads on scoped tables walk the\n * stack (first hit wins for shadow-by-id consumers like secrets and\n * blobs); writes require callers to name an explicit target scope.\n * Must be non-empty.\n */\n readonly scopes: readonly Scope[];\n readonly adapter: DBAdapter;\n readonly blobs: BlobStore;\n readonly plugins?: TPlugins;\n /**\n * How to respond when a tool requests user input mid-invocation. Pass\n * `\"accept-all\"` for tests / non-interactive hosts, or a handler\n * `(ctx) => Effect<ElicitationResponse>` for interactive ones.\n * Required at construction so per-invoke calls don't have to thread\n * an options arg.\n */\n readonly onElicitation: OnElicitation;\n}\n\n// ---------------------------------------------------------------------------\n// collectSchemas — merge coreSchema with every plugin's declared schema.\n// Hosts call this and pass the result to the migration runner (or to\n// the adapter factory for backends that auto-migrate from a schema\n// manifest) before constructing the executor.\n// ---------------------------------------------------------------------------\n\nexport const collectSchemas = (\n plugins: readonly AnyPlugin[],\n): DBSchema => {\n const merged: Record<string, DBSchema[string]> = { ...coreSchema };\n for (const plugin of plugins) {\n if (!plugin.schema) continue;\n for (const [modelKey, model] of Object.entries(plugin.schema)) {\n if (merged[modelKey]) {\n throw new Error(\n `Duplicate model \"${modelKey}\" contributed by plugin \"${plugin.id}\"` +\n ` (reserved by core or another plugin)`,\n );\n }\n merged[modelKey] = model as DBSchema[string];\n }\n }\n return merged;\n};\n\n// ---------------------------------------------------------------------------\n// Row → public projection conversions\n// ---------------------------------------------------------------------------\n\nconst rowToSource = (row: SourceRow): Source => ({\n id: row.id,\n scopeId: row.scope_id,\n kind: row.kind,\n name: row.name,\n url: row.url ?? undefined,\n pluginId: row.plugin_id,\n canRemove: Boolean(row.can_remove),\n canRefresh: Boolean(row.can_refresh),\n canEdit: Boolean(row.can_edit),\n runtime: false,\n});\n\nconst staticDeclToSource = (\n decl: StaticSourceDecl,\n pluginId: string,\n): Source => ({\n id: decl.id,\n scopeId: undefined,\n kind: decl.kind,\n name: decl.name,\n url: decl.url,\n pluginId,\n canRemove: decl.canRemove ?? false,\n canRefresh: decl.canRefresh ?? false,\n canEdit: decl.canEdit ?? false,\n runtime: true,\n});\n\nconst decodeJsonColumn = (value: unknown): unknown => {\n if (value === null || value === undefined) return undefined;\n if (typeof value !== \"string\") return value;\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n};\n\nconst rowToTool = (\n row: ToolRow,\n annotations?: ToolAnnotations,\n): Tool => ({\n id: row.id,\n sourceId: row.source_id,\n pluginId: row.plugin_id,\n name: row.name,\n description: row.description,\n inputSchema: decodeJsonColumn(row.input_schema),\n outputSchema: decodeJsonColumn(row.output_schema),\n annotations,\n});\n\nconst staticDeclToTool = (\n source: StaticSourceDecl,\n tool: StaticToolDecl,\n pluginId: string,\n): Tool => ({\n id: `${source.id}.${tool.name}`,\n sourceId: source.id,\n pluginId,\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n outputSchema: tool.outputSchema,\n annotations: tool.annotations,\n});\n\n// ---------------------------------------------------------------------------\n// Dynamic-row writers — used by ctx.core.sources.register. Static sources\n// never touch these functions.\n// ---------------------------------------------------------------------------\n\n// Upsert shape: delete any existing source + tools + definitions for\n// `input.id` before creating fresh rows. Keeps replayable — boot-time\n// sync from executor.jsonc can call register() on rows that already\n// exist without tripping a UNIQUE constraint.\nconst writeSourceInput = (\n core: TypedAdapter<CoreSchema>,\n pluginId: string,\n input: SourceInput,\n): Effect.Effect<void, StorageFailure> =>\n Effect.gen(function* () {\n yield* deleteSourceById(core, input.id, input.scope);\n\n const now = new Date();\n yield* core.create({\n model: \"source\",\n data: {\n id: input.id,\n scope_id: input.scope,\n plugin_id: pluginId,\n kind: input.kind,\n name: input.name,\n url: input.url ?? undefined,\n can_remove: input.canRemove ?? true,\n can_refresh: input.canRefresh ?? false,\n can_edit: input.canEdit ?? false,\n created_at: now,\n updated_at: now,\n },\n forceAllowId: true,\n });\n\n if (input.tools.length > 0) {\n yield* core.createMany({\n model: \"tool\",\n data: input.tools.map((tool) => ({\n id: `${input.id}.${tool.name}`,\n scope_id: input.scope,\n source_id: input.id,\n plugin_id: pluginId,\n name: tool.name,\n description: tool.description,\n input_schema: tool.inputSchema ?? undefined,\n output_schema: tool.outputSchema ?? undefined,\n created_at: now,\n updated_at: now,\n })),\n forceAllowId: true,\n });\n }\n });\n\n// Delete a source and its tools + definitions at ONE specific scope.\n// The scoped adapter already narrows reads/writes to the executor's\n// stack via `scope_id IN (...)`, but we pin `scope_id = scopeId` here\n// so this helper never widens into a stack-wide wipe — a bystander\n// scope's rows with a colliding `source_id` must survive.\nconst deleteSourceById = (\n core: TypedAdapter<CoreSchema>,\n sourceId: string,\n scopeId: string,\n): Effect.Effect<void, StorageFailure> =>\n Effect.gen(function* () {\n yield* core.deleteMany({\n model: \"tool\",\n where: [\n { field: \"source_id\", value: sourceId },\n { field: \"scope_id\", value: scopeId },\n ],\n });\n yield* core.deleteMany({\n model: \"definition\",\n where: [\n { field: \"source_id\", value: sourceId },\n { field: \"scope_id\", value: scopeId },\n ],\n });\n yield* core.delete({\n model: \"source\",\n where: [\n { field: \"id\", value: sourceId },\n { field: \"scope_id\", value: scopeId },\n ],\n });\n });\n\nconst writeDefinitions = (\n core: TypedAdapter<CoreSchema>,\n pluginId: string,\n input: DefinitionsInput,\n): Effect.Effect<void, StorageFailure> =>\n Effect.gen(function* () {\n // Pin the delete to `input.scope` — without this, the scoped\n // adapter's `scope_id IN (stack)` injection would nuke definitions\n // at outer scopes whenever an inner-scope writer re-registers\n // definitions for the same source id.\n yield* core.deleteMany({\n model: \"definition\",\n where: [\n { field: \"source_id\", value: input.sourceId },\n { field: \"scope_id\", value: input.scope },\n ],\n });\n const entries = Object.entries(input.definitions);\n if (entries.length === 0) return;\n const now = new Date();\n yield* core.createMany({\n model: \"definition\",\n data: entries.map(([name, schema]) => ({\n id: `${input.sourceId}.${name}`,\n scope_id: input.scope,\n source_id: input.sourceId,\n plugin_id: pluginId,\n name,\n schema: schema as Record<string, unknown>,\n created_at: now,\n })),\n forceAllowId: true,\n });\n });\n\n// ---------------------------------------------------------------------------\n// Filtering — shared between dynamic (DB) and static (in-memory) pools\n// so `tools.list({ query, sourceId })` matches across both.\n// ---------------------------------------------------------------------------\n\nconst toolMatchesFilter = (tool: Tool, filter: ToolListFilter): boolean => {\n if (filter.sourceId && tool.sourceId !== filter.sourceId) return false;\n if (filter.query) {\n const q = filter.query.toLowerCase();\n const hay = `${tool.name} ${tool.description}`.toLowerCase();\n if (!hay.includes(q)) return false;\n }\n return true;\n};\n\n// ---------------------------------------------------------------------------\n// Active-adapter FiberRef. Nested plugin writes read this ref so that\n// `ctx.transaction` can swap in a tx-bound adapter handle without changing\n// the ctx shape — the root adapter returned by `buildAdapterRouter` below\n// resolves its target per call, so any Effect running inside\n// `Effect.locally(_, activeAdapterRef, trx)` automatically routes every\n// query through the same sql.begin connection. This is what makes nested\n// writes atomic on postgres + Hyperdrive without deadlocking a pool of 1.\n// ---------------------------------------------------------------------------\nconst activeAdapterRef = Context.Reference<DBTransactionAdapter | null>(\n \"executor/ActiveAdapter\",\n { defaultValue: () => null },\n);\n\n// A `DBAdapter` whose methods dispatch to the active adapter (tx handle or\n// root) on every call. Stable identity for consumers (plugin storage,\n// `typedAdapter`, etc.) — they see one adapter object, but the routing is\n// decided at call time via the FiberRef above.\nconst buildAdapterRouter = (root: DBAdapter): DBAdapter => {\n const pick = <A, E>(\n use: (active: DBTransactionAdapter) => Effect.Effect<A, E>,\n ): Effect.Effect<A, E> =>\n Effect.flatMap(Effect.service(activeAdapterRef), (active) =>\n use((active ?? (root as DBTransactionAdapter))),\n );\n\n return {\n id: root.id,\n create: (data) => pick((a) => a.create(data)),\n createMany: (data) => pick((a) => a.createMany(data)),\n findOne: (data) => pick((a) => a.findOne(data)),\n findMany: (data) => pick((a) => a.findMany(data)),\n count: (data) => pick((a) => a.count(data)),\n update: (data) => pick((a) => a.update(data)),\n updateMany: (data) => pick((a) => a.updateMany(data)),\n delete: (data) => pick((a) => a.delete(data)),\n deleteMany: (data) => pick((a) => a.deleteMany(data)),\n // transaction() always opens a real boundary on the ROOT adapter so the\n // tx uses one real connection from the pool. If we're already inside a\n // parent tx (FiberRef set), skip opening a nested sql.begin — that's\n // the postgres.js + Hyperdrive deadlock path — and just run the\n // callback with the existing tx handle. In both cases the callback\n // sees a FiberRef-substituted adapter so further nested writes thread\n // through.\n transaction: (callback) =>\n Effect.flatMap(Effect.service(activeAdapterRef), (active) => {\n if (active) return callback(active);\n return root.transaction((trx) =>\n Effect.provideService(callback(trx), activeAdapterRef, trx),\n );\n }),\n } as DBAdapter;\n};\n\n// ---------------------------------------------------------------------------\n// createExecutor\n// ---------------------------------------------------------------------------\n\ninterface StaticTools {\n readonly source: StaticSourceDecl;\n readonly tool: StaticToolDecl;\n readonly pluginId: string;\n readonly ctx: PluginCtx<unknown>;\n}\n\ninterface StaticSources {\n readonly source: StaticSourceDecl;\n readonly pluginId: string;\n}\n\ninterface PluginRuntime {\n readonly plugin: AnyPlugin;\n readonly storage: unknown;\n readonly ctx: PluginCtx<unknown>;\n}\n\nexport const createExecutor = <\n const TPlugins extends readonly AnyPlugin[] = [],\n>(\n config: ExecutorConfig<TPlugins>,\n): Effect.Effect<Executor<TPlugins>, Error> =>\n Effect.gen(function* () {\n const defaultPlugins = (): TPlugins => {\n const empty: readonly AnyPlugin[] = [];\n return empty as TPlugins;\n };\n const {\n scopes,\n adapter: rootAdapter,\n blobs,\n plugins = defaultPlugins(),\n } = config;\n\n if (scopes.length === 0) {\n return yield* Effect.fail(\n new Error(\"createExecutor requires a non-empty scopes array\"),\n );\n }\n\n // Scope-wrap the root adapter so every read on a tenant-scoped\n // table filters by `scope_id IN (scopes)` and every write's\n // `scope_id` payload is validated to be in the stack. Reads walk\n // the scope array in order at the consumer layer (secrets,\n // blobs) — the adapter itself just bounds the set of rows\n // visible. Only tables whose schema declares `scope_id` are\n // scoped.\n const schema = collectSchemas(plugins);\n const scopeIds = scopes.map((s) => s.id as string);\n const scopedRoot = scopeAdapter(\n rootAdapter,\n { scopes: scopeIds },\n schema,\n );\n const adapter = buildAdapterRouter(scopedRoot);\n const core = typedAdapter<CoreSchema>(adapter);\n\n // Populated once, never mutated after startup.\n const staticTools = new Map<string, StaticTools>();\n const staticSources = new Map<string, StaticSources>();\n\n // Per-plugin runtime state.\n const runtimes = new Map<string, PluginRuntime>();\n // Secret providers keyed by `provider.key`.\n const secretProviders = new Map<string, SecretProvider>();\n // Connection providers keyed by `provider.key` — drive the refresh\n // lifecycle for connection-owned tokens.\n const connectionProviders = new Map<string, ConnectionProvider>();\n const connectionProviderAliases = new Map<string, string>([\n [\"mcp:oauth2\", \"oauth2\"],\n [\"openapi:oauth2\", \"oauth2\"],\n [\"google-discovery:google\", \"oauth2\"],\n [\"google-discovery:oauth2\", \"oauth2\"],\n ]);\n const resolveConnectionProvider = (\n key: string,\n ): ConnectionProvider | undefined => {\n const direct = connectionProviders.get(key);\n if (direct) return direct;\n const canonical = connectionProviderAliases.get(key);\n return canonical ? connectionProviders.get(canonical) : undefined;\n };\n // In-flight refresh dedup. `connectionsAccessToken` stamps a\n // `Deferred` here before calling the provider's `refresh`; parallel\n // callers that walk in while a refresh is still running observe\n // the same Deferred and await its resolution instead of hitting\n // the AS a second time. The map is mutated under a semaphore so\n // check-or-register is atomic under fiber interleavings.\n const refreshInFlight = new Map<\n string,\n Deferred.Deferred<\n string,\n | ConnectionNotFoundError\n | ConnectionProviderNotRegisteredError\n | ConnectionRefreshNotSupportedError\n | ConnectionReauthRequiredError\n | ConnectionRefreshError\n | StorageFailure\n >\n >();\n const refreshInFlightLock = Semaphore.makeUnsafe(1);\n const extensions: Record<string, object> = {};\n\n // ------------------------------------------------------------------\n // Secrets facade — fast path is the core `secret` routing table\n // (explicit set()s, keychain entries, etc). Fallback is a walk\n // across providers that implement `list()`, because those are the\n // providers that own their own inventories (1password, file-secrets,\n // workos-vault, env) and enumerate-without-register. Providers\n // without a list() implementation (keychain) never hit the fallback\n // walk because their secrets must be registered through set() to\n // be known at all.\n //\n // Multi-scope behavior: the routing-table lookup pulls every row\n // for this id across the scope stack in a single `IN (...)` query,\n // then sorts innermost-first so a secret registered in a deeper\n // scope shadows one with the same id at a shallower scope (e.g. a\n // user's personal OAuth token wins over an org-wide one). Provider\n // calls stay sequential — scope-partitioning providers (workos-vault,\n // 1password-per-vault) have to be asked per scope because the object\n // name includes the scope — but they're bounded by the number of\n // registered rows for this id, not by scope-stack depth. The\n // provider-enumeration fallback is scope-agnostic: providers like\n // env or 1password don't partition their inventory by executor scope.\n const scopePrecedence = new Map<string, number>();\n scopeIds.forEach((s, i) => scopePrecedence.set(s, i));\n\n // Rank a row by how close its `scope_id` sits to the innermost scope.\n // Rows whose scope isn't in the stack get pushed to the end (they\n // shouldn't reach us — the adapter filters by `scope_id IN (stack)` —\n // but guarding here means a stray row can't silently win).\n const scopeRank = (row: { scope_id: unknown }) =>\n scopePrecedence.get(row.scope_id as string) ?? Infinity;\n\n // Pick the innermost-scope row on a findOne-by-id against a scoped\n // model. The scope-wrapped adapter returns rows from every scope in\n // the stack, so a bare `findOne({ id })` picks whichever one the\n // storage backend iterates first — non-deterministic across backends,\n // and wrong when a user has shadowed an outer default. Callers that\n // need a single logical row (invoke, tool schema, source removal)\n // must go through this path so the innermost write always wins.\n const findInnermost = <T extends { scope_id: unknown }>(\n rows: readonly T[],\n ): T | null => {\n if (rows.length === 0) return null;\n let winner: T | undefined;\n let best = Infinity;\n for (const row of rows) {\n const rank = scopeRank(row);\n if (rank < best) {\n best = rank;\n winner = row;\n }\n }\n return winner ?? null;\n };\n\n const secretRowsForId = (\n id: string,\n ): Effect.Effect<readonly SecretRow[], StorageFailure> =>\n core.findMany({\n model: \"secret\",\n where: [{ field: \"id\", value: id }],\n }) as Effect.Effect<readonly SecretRow[], StorageFailure>;\n\n const resolveSecretValueFromRows = (\n id: string,\n rows: readonly SecretRow[],\n ): Effect.Effect<string | null, StorageFailure> =>\n Effect.gen(function* () {\n const ordered = [...rows].sort(\n (a, b) =>\n (scopePrecedence.get(a.scope_id as string) ?? Infinity) -\n (scopePrecedence.get(b.scope_id as string) ?? Infinity),\n );\n for (const row of ordered) {\n const provider = secretProviders.get(row.provider as string);\n if (!provider) continue;\n const value = yield* provider.get(id, row.scope_id as string);\n if (value !== null) return value;\n }\n\n // Fallback: ask every enumerating provider in parallel. First\n // non-null in registration order wins. Providers that throw\n // are treated as \"don't have it\" so one flaky provider can't\n // block resolution via others. Scope-partitioning providers\n // get asked at the innermost scope as a display default — the\n // enumeration fallback doesn't know which scope the value\n // lives in; flat providers ignore the arg.\n const fallbackScope = scopeIds[0]!;\n const candidates = [...secretProviders.values()].filter(\n (p) => p.list,\n );\n const values = yield* Effect.all(\n candidates.map((p) =>\n p\n .get(id, fallbackScope)\n .pipe(Effect.catch(() => Effect.succeed(null))),\n ),\n { concurrency: \"unbounded\" },\n );\n for (const value of values) if (value !== null) return value;\n return null;\n });\n\n const secretsGet = (\n id: string,\n ): Effect.Effect<string | null, SecretOwnedByConnectionError | StorageFailure> =>\n Effect.gen(function* () {\n // The scope-wrapped adapter injects `scope_id IN (scopeIds)`\n // automatically, so we only filter by id here. Connection-owned\n // token rows are internal plumbing; public secret resolution\n // must not expose them even if a token secret id is leaked.\n const rows = yield* secretRowsForId(id);\n const owned = rows.find((row) => row.owned_by_connection_id);\n if (owned) {\n return yield* Effect.fail(\n new SecretOwnedByConnectionError({\n secretId: SecretId.make(id),\n connectionId: ConnectionId.make(\n owned.owned_by_connection_id as string,\n ),\n }),\n );\n }\n return yield* resolveSecretValueFromRows(id, rows);\n });\n\n const connectionSecretGet = (\n id: string,\n ): Effect.Effect<string | null, StorageFailure> =>\n Effect.gen(function* () {\n const rows = yield* secretRowsForId(id);\n return yield* resolveSecretValueFromRows(id, rows);\n });\n\n const secretRouteHasBackingValue = (row: SecretRow) => {\n const provider = secretProviders.get(row.provider as string);\n if (!provider?.has) return Effect.succeed(true);\n return provider\n .has(row.id as string, row.scope_id as string)\n .pipe(Effect.catch(() => Effect.succeed(false)));\n };\n\n const secretsSet = (\n input: SetSecretInput,\n ): Effect.Effect<SecretRef, StorageFailure> =>\n Effect.gen(function* () {\n // Validate the write target up front. The adapter would reject\n // an out-of-stack scope too, but catching it here gives a\n // clearer error before we touch the provider.\n if (!scopeIds.includes(input.scope as string)) {\n return yield* Effect.fail(\n new StorageError({\n message:\n `secrets.set targets scope \"${input.scope}\" which is not ` +\n `in the executor's scope stack [${scopeIds.join(\", \")}].`,\n cause: undefined,\n }),\n );\n }\n\n // Pick provider: explicit or first-writable. Misconfiguration\n // (unknown provider, no writable provider, read-only provider)\n // is a host setup bug — surface as `StorageError` so it lands\n // as a captured InternalError(traceId) at the SDK boundary.\n let target: SecretProvider | undefined;\n if (input.provider) {\n target = secretProviders.get(input.provider);\n if (!target) {\n return yield* Effect.fail(\n new StorageError({\n message: `Unknown secret provider: ${input.provider}`,\n cause: undefined,\n }),\n );\n }\n } else {\n for (const provider of secretProviders.values()) {\n if (provider.writable && provider.set) {\n target = provider;\n break;\n }\n }\n if (!target) {\n return yield* Effect.fail(\n new StorageError({\n message: \"No writable secret providers registered\",\n cause: undefined,\n }),\n );\n }\n }\n if (!target.writable || !target.set) {\n return yield* Effect.fail(\n new StorageError({\n message: `Secret provider \"${target.key}\" is read-only`,\n cause: undefined,\n }),\n );\n }\n\n yield* target.set(input.id, input.value, input.scope as string);\n\n // Upsert metadata row in the core `secret` table at the\n // caller-named scope. Pin the delete to `scope_id = input.scope`\n // — without it, the scoped adapter's `scope_id IN (stack)`\n // injection would wipe rows at outer scopes too, so any member\n // writing a personal override could delete admin-written\n // org-wide secrets with the same id.\n const now = new Date();\n yield* core.delete({\n model: \"secret\",\n where: [\n { field: \"id\", value: input.id },\n { field: \"scope_id\", value: input.scope },\n ],\n });\n yield* core.create({\n model: \"secret\",\n data: {\n id: input.id,\n scope_id: input.scope,\n name: input.name,\n provider: target.key,\n created_at: now,\n },\n forceAllowId: true,\n });\n\n return new SecretRef({\n id: input.id,\n scopeId: input.scope,\n name: input.name,\n provider: target.key,\n createdAt: now,\n });\n });\n\n const secretsRemove = (\n id: string,\n ): Effect.Effect<void, SecretOwnedByConnectionError | StorageFailure> =>\n Effect.gen(function* () {\n // Remove is shadowing-aware: drop only the innermost-scope row.\n // Removing a user-scope override on a secret that also has an\n // org-scope default should reveal the org default, not wipe it.\n //\n // Without this, a regular member calling `secrets.remove(\"api_key\")`\n // at their inner scope would cascade through `scope_id IN (stack)`\n // and delete the admin-written org row too.\n const rows = yield* core.findMany({\n model: \"secret\",\n where: [{ field: \"id\", value: id }],\n });\n const target = findInnermost(rows);\n // Refuse to delete connection-owned secrets. The connection owns\n // the lifecycle — callers must go through connections.remove.\n if (target && target.owned_by_connection_id) {\n return yield* Effect.fail(\n new SecretOwnedByConnectionError({\n secretId: SecretId.make(id),\n connectionId: ConnectionId.make(\n target.owned_by_connection_id as string,\n ),\n }),\n );\n }\n const targetScope = (target?.scope_id as string | undefined) ??\n scopeIds[0]!;\n\n const deleters = [...secretProviders.values()].filter(\n (p): p is typeof p & { delete: NonNullable<typeof p.delete> } =>\n !!(p.writable && p.delete),\n );\n yield* Effect.all(\n deleters.map((p) => p.delete(id, targetScope)),\n { concurrency: \"unbounded\" },\n );\n\n if (target) {\n yield* core.delete({\n model: \"secret\",\n where: [\n { field: \"id\", value: id },\n { field: \"scope_id\", value: targetScope },\n ],\n });\n }\n });\n\n // List is a union of two sources of truth:\n //\n // 1. Core `secret` rows — secrets explicitly registered via\n // executor.secrets.set(...). These carry their pinned provider\n // and are authoritative for routing (get() uses them).\n // 2. Each provider's own `list()` — for read-only or\n // already-populated providers (1password, file-secrets,\n // workos-vault, env), the provider enumerates what's actually\n // in its backend. These show up in the list even if the user\n // never called set() through the executor.\n //\n // Dedupe by secret id; core rows win over provider-enumerated ones\n // so that routing information in the core table is authoritative.\n // Providers without a list() method (e.g. keychain) contribute\n // only via the core table path.\n //\n // Multi-scope: core rows from any scope in the stack show up\n // (adapter filters by `scope_id IN`), each tagged with its own\n // `scope_id`. When the same id appears in multiple scopes, the\n // innermost wins — same rule as `secretsGet`. Provider-enumerated\n // entries don't know what scope they belong to and are attributed\n // to the innermost scope as a display default.\n const secretsList = (): Effect.Effect<readonly SecretRef[], StorageFailure> =>\n Effect.gen(function* () {\n const byId = new Map<string, SecretRef>();\n\n // Core routing rows first. Adapter returns rows from every\n // scope in the stack; resolve collisions using the caller's\n // precedence order (innermost first). Rows owned by a\n // connection are filtered out — the user sees the Connection\n // entry, not its backing token secrets. Their ids go in a\n // deny-set so provider `list()` results for the same id can't\n // leak them back in below.\n const allRows = yield* core.findMany({ model: \"secret\" });\n const connectionOwnedIds = new Set(\n allRows\n .filter((r) => r.owned_by_connection_id)\n .map((r) => r.id as string),\n );\n const rows = allRows.filter((r) => !r.owned_by_connection_id);\n const precedence = new Map<string, number>();\n scopeIds.forEach((id, index) => precedence.set(id, index));\n const pick = (row: typeof rows[number]) => {\n const existing = byId.get(row.id);\n const incomingScope = row.scope_id as string;\n const incomingRank = precedence.get(incomingScope) ?? Number.MAX_SAFE_INTEGER;\n if (existing) {\n const existingRank = precedence.get(existing.scopeId as string) ?? Number.MAX_SAFE_INTEGER;\n if (existingRank <= incomingRank) return;\n }\n byId.set(\n row.id,\n new SecretRef({\n id: SecretId.make(row.id),\n scopeId: ScopeId.make(incomingScope),\n name: row.name,\n provider: row.provider,\n createdAt:\n row.created_at instanceof Date\n ? row.created_at\n : new Date(row.created_at as string),\n }),\n );\n };\n for (const row of rows) {\n const hasBackingValue = yield* secretRouteHasBackingValue(row);\n if (hasBackingValue) pick(row);\n }\n\n // Then every provider that can enumerate itself, in parallel.\n // If a provider fails to list (unlocked vault, network error),\n // swallow the failure so one flaky provider can't block the\n // whole list. Merge in registration order afterwards so the\n // \"first provider wins\" precedence stays deterministic.\n const attribution = scopes[0]!.id;\n const listers = [...secretProviders.entries()].filter(\n ([, p]) => p.list,\n );\n const lists = yield* Effect.all(\n listers.map(([key, p]) =>\n p\n .list!()\n .pipe(\n Effect.catch(() => Effect.succeed([] as const)),\n Effect.map((entries) => ({ key, entries })),\n ),\n ),\n { concurrency: \"unbounded\" },\n );\n for (const { key, entries } of lists) {\n for (const entry of entries) {\n if (byId.has(entry.id)) continue; // core row wins\n if (connectionOwnedIds.has(entry.id)) continue; // hidden by connection\n byId.set(\n entry.id,\n new SecretRef({\n id: SecretId.make(entry.id),\n scopeId: attribution,\n name: entry.name,\n provider: key,\n createdAt: new Date(),\n }),\n );\n }\n }\n\n return Array.from(byId.values());\n });\n\n // Same union shape as secretsList but projected to the leaner\n // SecretListEntry shape that plugins get via ctx.secrets.list().\n const secretsListForCtx = () =>\n Effect.gen(function* () {\n const list = yield* secretsList();\n return list.map((ref) => ({\n id: String(ref.id),\n name: ref.name,\n provider: ref.provider,\n }));\n });\n\n // ------------------------------------------------------------------\n // Connections facade — sign-in state as a first-class primitive.\n // Connection rows own one or more backing `secret` rows via\n // `secret.owned_by_connection_id`; the SDK orchestrates refresh via\n // the registered provider keyed by `connection.provider`.\n // ------------------------------------------------------------------\n\n // Refresh skew: treat the access token as \"about to expire\" when\n // we're within this many ms of the expiry the AS declared.\n // Matches the value the old per-plugin refresh code used, so\n // behavior under the new SDK orchestration stays identical.\n const CONNECTION_REFRESH_SKEW_MS = 60_000;\n\n const decodeProviderState = Schema.decodeUnknownOption(\n ConnectionProviderState,\n );\n\n const rowToConnection = (row: ConnectionRow): ConnectionRef =>\n new ConnectionRef({\n id: ConnectionId.make(row.id as string),\n scopeId: ScopeId.make(row.scope_id as string),\n provider: row.provider as string,\n identityLabel: (row.identity_label as string | null | undefined) ?? null,\n accessTokenSecretId: SecretId.make(row.access_token_secret_id as string),\n refreshTokenSecretId:\n row.refresh_token_secret_id != null\n ? SecretId.make(row.refresh_token_secret_id as string)\n : null,\n expiresAt:\n row.expires_at != null ? Number(row.expires_at as number) : null,\n oauthScope: (row.scope as string | null | undefined) ?? null,\n providerState: Option.getOrNull(\n decodeProviderState(decodeJsonColumn(row.provider_state)),\n ),\n createdAt:\n row.created_at instanceof Date\n ? row.created_at\n : new Date(row.created_at as string),\n updatedAt:\n row.updated_at instanceof Date\n ? row.updated_at\n : new Date(row.updated_at as string),\n });\n\n const findInnermostConnectionRow = (\n id: string,\n ): Effect.Effect<ConnectionRow | null, StorageFailure> =>\n Effect.gen(function* () {\n const rows = yield* core.findMany({\n model: \"connection\",\n where: [{ field: \"id\", value: id }],\n });\n return findInnermost(rows as readonly ConnectionRow[]);\n });\n\n const connectionsGet = (\n id: string,\n ): Effect.Effect<ConnectionRef | null, StorageFailure> =>\n Effect.gen(function* () {\n const row = yield* findInnermostConnectionRow(id);\n return row ? rowToConnection(row) : null;\n });\n\n const connectionsList = (): Effect.Effect<\n readonly ConnectionRef[],\n StorageFailure\n > =>\n Effect.gen(function* () {\n const rows = yield* core.findMany({ model: \"connection\" });\n // Dedup by id, innermost scope wins — same rule as sources/tools.\n const byId = new Map<string, ConnectionRow>();\n const byIdRank = new Map<string, number>();\n for (const row of rows as readonly ConnectionRow[]) {\n const rank = scopeRank(row as { scope_id: unknown });\n const existing = byIdRank.get(row.id as string);\n if (existing === undefined || rank < existing) {\n byId.set(row.id as string, row);\n byIdRank.set(row.id as string, rank);\n }\n }\n return [...byId.values()].map(rowToConnection);\n });\n\n // Write a secret value through a specific provider, bypassing the\n // bare-secrets ownership check so the SDK can stamp\n // `owned_by_connection_id` atomically alongside a connection row.\n const writeOwnedSecret = (\n params: {\n id: string;\n scope: string;\n name: string;\n value: string;\n provider: string;\n ownedByConnectionId: string;\n },\n ): Effect.Effect<void, StorageFailure> =>\n Effect.gen(function* () {\n const target = secretProviders.get(params.provider);\n if (!target) {\n return yield* Effect.fail(\n new StorageError({\n message: `Unknown secret provider: ${params.provider}`,\n cause: undefined,\n }),\n );\n }\n if (!target.writable || !target.set) {\n return yield* Effect.fail(\n new StorageError({\n message: `Secret provider \"${target.key}\" is read-only`,\n cause: undefined,\n }),\n );\n }\n yield* target.set(params.id, params.value, params.scope);\n\n const now = new Date();\n yield* core.delete({\n model: \"secret\",\n where: [\n { field: \"id\", value: params.id },\n { field: \"scope_id\", value: params.scope },\n ],\n });\n yield* core.create({\n model: \"secret\",\n data: {\n id: params.id,\n scope_id: params.scope,\n name: params.name,\n provider: target.key,\n owned_by_connection_id: params.ownedByConnectionId,\n created_at: now,\n },\n forceAllowId: true,\n });\n });\n\n const pickWritableProvider = (\n requested?: string,\n ): Effect.Effect<SecretProvider, StorageFailure> =>\n Effect.gen(function* () {\n if (requested) {\n const p = secretProviders.get(requested);\n if (!p) {\n return yield* Effect.fail(\n new StorageError({\n message: `Unknown secret provider: ${requested}`,\n cause: undefined,\n }),\n );\n }\n return p;\n }\n for (const p of secretProviders.values()) {\n if (p.writable && p.set) return p;\n }\n return yield* Effect.fail(\n new StorageError({\n message: \"No writable secret providers registered\",\n cause: undefined,\n }),\n );\n });\n\n const connectionsCreate = (\n input: CreateConnectionInput,\n ): Effect.Effect<\n ConnectionRef,\n ConnectionProviderNotRegisteredError | StorageFailure\n > =>\n Effect.gen(function* () {\n if (!scopeIds.includes(input.scope as string)) {\n return yield* Effect.fail(\n new StorageError({\n message:\n `connections.create targets scope \"${input.scope}\" which is not ` +\n `in the executor's scope stack [${scopeIds.join(\", \")}].`,\n cause: undefined,\n }),\n );\n }\n if (!resolveConnectionProvider(input.provider)) {\n return yield* Effect.fail(\n new ConnectionProviderNotRegisteredError({\n provider: input.provider,\n connectionId: input.id,\n }),\n );\n }\n\n const writable = yield* pickWritableProvider();\n const now = new Date();\n\n return yield* adapter.transaction(() =>\n Effect.gen(function* () {\n // Drop any existing connection row at this scope first so a\n // re-auth replaces cleanly. Owned-secret rows for the old\n // connection are removed by the cascade below (we delete\n // both old + new token secret ids explicitly).\n yield* core.delete({\n model: \"connection\",\n where: [\n { field: \"id\", value: input.id as string },\n { field: \"scope_id\", value: input.scope as string },\n ],\n });\n\n yield* writeOwnedSecret({\n id: input.accessToken.secretId as string,\n scope: input.scope as string,\n name: input.accessToken.name,\n value: input.accessToken.value,\n provider: writable.key,\n ownedByConnectionId: input.id as string,\n });\n if (input.refreshToken) {\n yield* writeOwnedSecret({\n id: input.refreshToken.secretId as string,\n scope: input.scope as string,\n name: input.refreshToken.name,\n value: input.refreshToken.value,\n provider: writable.key,\n ownedByConnectionId: input.id as string,\n });\n }\n\n yield* core.create({\n model: \"connection\",\n data: {\n id: input.id as string,\n scope_id: input.scope as string,\n provider: input.provider,\n identity_label: input.identityLabel ?? undefined,\n access_token_secret_id: input.accessToken.secretId as string,\n refresh_token_secret_id:\n input.refreshToken?.secretId ?? undefined,\n expires_at: input.expiresAt ?? undefined,\n scope: input.oauthScope ?? undefined,\n provider_state: input.providerState ?? undefined,\n created_at: now,\n updated_at: now,\n },\n forceAllowId: true,\n });\n\n return new ConnectionRef({\n id: input.id,\n scopeId: input.scope,\n provider: input.provider,\n identityLabel: input.identityLabel,\n accessTokenSecretId: input.accessToken.secretId,\n refreshTokenSecretId:\n input.refreshToken?.secretId ?? null,\n expiresAt: input.expiresAt,\n oauthScope: input.oauthScope,\n providerState: input.providerState,\n createdAt: now,\n updatedAt: now,\n });\n }),\n );\n });\n\n // Write new token material into the existing secret rows and bump\n // the connection row's expiry / scope / providerState. Never\n // mutates `access_token_secret_id` or `refresh_token_secret_id` —\n // those stay pinned so consumers that stashed them in source\n // configs still resolve.\n const connectionsUpdateTokens = (\n input: UpdateConnectionTokensInput,\n ): Effect.Effect<\n ConnectionRef,\n ConnectionNotFoundError | StorageFailure\n > =>\n Effect.gen(function* () {\n const row = yield* findInnermostConnectionRow(input.id as string);\n if (!row) {\n return yield* Effect.fail(\n new ConnectionNotFoundError({ connectionId: input.id }),\n );\n }\n const writable = yield* pickWritableProvider();\n const accessName =\n `Connection ${input.id as string} access token`;\n const refreshName =\n `Connection ${input.id as string} refresh token`;\n\n return yield* adapter.transaction(() =>\n Effect.gen(function* () {\n yield* writeOwnedSecret({\n id: row.access_token_secret_id as string,\n scope: row.scope_id as string,\n name: accessName,\n value: input.accessToken,\n provider: writable.key,\n ownedByConnectionId: row.id as string,\n });\n const rotatedRefresh = input.refreshToken ?? undefined;\n if (\n rotatedRefresh &&\n row.refresh_token_secret_id\n ) {\n yield* writeOwnedSecret({\n id: row.refresh_token_secret_id as string,\n scope: row.scope_id as string,\n name: refreshName,\n value: rotatedRefresh,\n provider: writable.key,\n ownedByConnectionId: row.id as string,\n });\n }\n const now = new Date();\n const patch: Record<string, unknown> = { updated_at: now };\n if (input.expiresAt !== undefined)\n patch.expires_at = input.expiresAt ?? undefined;\n if (input.oauthScope !== undefined)\n patch.scope = input.oauthScope ?? undefined;\n if (input.providerState !== undefined)\n patch.provider_state = input.providerState ?? undefined;\n if (input.identityLabel !== undefined)\n patch.identity_label = input.identityLabel ?? undefined;\n yield* core.update({\n model: \"connection\",\n where: [\n { field: \"id\", value: row.id as string },\n { field: \"scope_id\", value: row.scope_id as string },\n ],\n update: patch,\n });\n const updated = yield* findInnermostConnectionRow(\n row.id as string,\n );\n if (!updated) {\n return yield* Effect.fail(\n new ConnectionNotFoundError({ connectionId: input.id }),\n );\n }\n return rowToConnection(updated);\n }),\n );\n });\n\n const connectionsSetIdentityLabel = (\n id: string,\n label: string | null,\n ): Effect.Effect<void, ConnectionNotFoundError | StorageFailure> =>\n Effect.gen(function* () {\n const row = yield* findInnermostConnectionRow(id);\n if (!row) {\n return yield* Effect.fail(\n new ConnectionNotFoundError({\n connectionId: ConnectionId.make(id),\n }),\n );\n }\n yield* core.update({\n model: \"connection\",\n where: [\n { field: \"id\", value: id },\n { field: \"scope_id\", value: row.scope_id as string },\n ],\n update: {\n identity_label: label ?? undefined,\n updated_at: new Date(),\n },\n });\n });\n\n const connectionsRemove = (\n id: string,\n ): Effect.Effect<void, StorageFailure> =>\n Effect.gen(function* () {\n const row = yield* findInnermostConnectionRow(id);\n if (!row) return;\n const scope = row.scope_id as string;\n yield* adapter.transaction(() =>\n Effect.gen(function* () {\n // Find every owned secret at this scope and drop through\n // its provider + the core row. We look up by\n // `owned_by_connection_id` rather than just the two ids on\n // the connection row so any accidentally-orphaned siblings\n // get cleaned up too.\n const owned = yield* core.findMany({\n model: \"secret\",\n where: [\n { field: \"owned_by_connection_id\", value: id },\n { field: \"scope_id\", value: scope },\n ],\n });\n const deleters = [...secretProviders.values()].filter(\n (p): p is typeof p & { delete: NonNullable<typeof p.delete> } =>\n !!(p.writable && p.delete),\n );\n for (const secret of owned) {\n yield* Effect.all(\n deleters.map((p) =>\n p.delete(secret.id as string, scope).pipe(\n Effect.catchCause((cause) =>\n Effect.logWarning(\n `Failed to delete connection-owned secret from provider ${p.key}`,\n cause,\n ).pipe(Effect.as(false)),\n ),\n ),\n ),\n { concurrency: \"unbounded\" },\n );\n }\n yield* core.deleteMany({\n model: \"secret\",\n where: [\n { field: \"owned_by_connection_id\", value: id },\n { field: \"scope_id\", value: scope },\n ],\n });\n yield* core.delete({\n model: \"connection\",\n where: [\n { field: \"id\", value: id },\n { field: \"scope_id\", value: scope },\n ],\n });\n }),\n );\n });\n\n // Typed error union that `connectionsAccessToken` and every helper\n // that participates in a refresh returns. Pulled out into a type\n // alias because it has to match the Deferred's channel exactly —\n // otherwise concurrent waiters and the leader diverge on the error\n // type.\n type AccessTokenError =\n | ConnectionNotFoundError\n | ConnectionProviderNotRegisteredError\n | ConnectionRefreshNotSupportedError\n | ConnectionReauthRequiredError\n | ConnectionRefreshError\n | StorageFailure;\n\n // The actual work of a single refresh cycle, factored out so the\n // concurrency gate (`connectionsAccessToken`) stays readable. Runs\n // for the fiber that wins the `refreshInFlight` race.\n const performRefresh = (\n ref: ConnectionRef,\n ): Effect.Effect<string, AccessTokenError> =>\n Effect.gen(function* () {\n const provider = resolveConnectionProvider(ref.provider);\n if (!provider) {\n return yield* Effect.fail(\n new ConnectionProviderNotRegisteredError({\n provider: ref.provider,\n connectionId: ref.id,\n }),\n );\n }\n if (!provider.refresh) {\n return yield* Effect.fail(\n new ConnectionRefreshNotSupportedError({\n connectionId: ref.id,\n provider: ref.provider,\n }),\n );\n }\n\n const refreshTokenValue = ref.refreshTokenSecretId\n ? yield* connectionSecretGet(ref.refreshTokenSecretId)\n : null;\n\n // RFC 6749 §5.2 `invalid_grant` (and anything else the\n // provider tags with `reauthRequired`) is terminal — the\n // stored refresh token can't recover. Translate into the\n // caller-visible \"re-authenticate\" error so the UI can\n // prompt sign-in instead of silently retrying.\n const rawResult: Result.Result<\n ConnectionRefreshResult,\n ConnectionRefreshError\n > = yield* Effect.result(\n provider.refresh({\n connectionId: ref.id,\n scopeId: ref.scopeId,\n identityLabel: ref.identityLabel,\n refreshToken: refreshTokenValue,\n providerState: ref.providerState,\n oauthScope: ref.oauthScope,\n }),\n );\n if (Result.isFailure(rawResult)) {\n const err = rawResult.failure;\n if (err.reauthRequired) {\n return yield* Effect.fail(\n new ConnectionReauthRequiredError({\n connectionId: err.connectionId,\n provider: ref.provider,\n message: err.message,\n }),\n );\n }\n return yield* Effect.fail(err);\n }\n const result = rawResult.success;\n\n yield* connectionsUpdateTokens({\n id: ref.id,\n accessToken: result.accessToken,\n refreshToken: result.refreshToken,\n expiresAt: result.expiresAt,\n oauthScope: result.oauthScope,\n providerState: result.providerState,\n } as UpdateConnectionTokensInput);\n\n return result.accessToken;\n });\n\n // accessToken(id) — the single surface plugins use at invoke time.\n // Resolves the backing secret, checks expiry, calls the provider's\n // refresh handler if we're inside the skew window. New tokens are\n // written back through the same provider and the connection row is\n // patched with the new expiry.\n //\n // Concurrent invokes on an expired token all share one refresh.\n // The fiber that wins the `refreshInFlightLock` race registers a\n // Deferred and performs the refresh; every other concurrent caller\n // observes the Deferred and awaits its completion. The Deferred is\n // pulled out of the map before the refresh result resolves so\n // later invokes don't reuse a completed slot.\n const connectionsAccessToken = (\n id: string,\n ): Effect.Effect<string, AccessTokenError> =>\n Effect.gen(function* () {\n const row = yield* findInnermostConnectionRow(id);\n if (!row) {\n return yield* Effect.fail(\n new ConnectionNotFoundError({\n connectionId: ConnectionId.make(id),\n }),\n );\n }\n const ref = rowToConnection(row);\n const now = Date.now();\n const needsRefresh =\n ref.expiresAt !== null &&\n ref.expiresAt - CONNECTION_REFRESH_SKEW_MS <= now;\n\n if (!needsRefresh) {\n const current = yield* connectionSecretGet(\n ref.accessTokenSecretId,\n );\n if (current !== null) return current;\n // Fall through to refresh if the stored token vanished — a\n // genuinely-missing secret with no way to refresh is a\n // hard-failure, same behavior as if `expires_at` had passed.\n }\n\n // Concurrency gate. `action` either returns the fresh access\n // token (this fiber did the refresh) or the already-running\n // Deferred that another fiber stamped into the map (this fiber\n // piggybacks on their refresh).\n const action = yield* refreshInFlightLock.withPermits(1)(\n Effect.gen(function* () {\n const existing = refreshInFlight.get(id);\n if (existing) {\n return {\n kind: \"await\" as const,\n deferred: existing,\n };\n }\n const deferred = yield* Deferred.make<string, AccessTokenError>();\n refreshInFlight.set(id, deferred);\n return { kind: \"lead\" as const, deferred };\n }),\n );\n\n if (action.kind === \"await\") {\n return yield* Deferred.await(action.deferred);\n }\n\n // Leader path: run the refresh, pipe the outcome into the\n // Deferred (so waiters wake up), and then clear the map slot\n // regardless of success or failure. Completing before delete\n // ensures a caller that arrives during cleanup can still observe\n // the settled leader result instead of starting a second refresh.\n return yield* performRefresh(ref).pipe(\n Effect.onExit((exit) =>\n refreshInFlightLock.withPermits(1)(\n Effect.gen(function* () {\n yield* Deferred.done(action.deferred, exit);\n refreshInFlight.delete(id);\n }),\n ),\n ),\n );\n });\n\n const connectionsListForCtx = () => connectionsList();\n\n const oauthBundle = makeOAuth2Service({\n adapter: core,\n rawAdapter: adapter,\n secretsGet: (id) =>\n secretsGet(id).pipe(\n Effect.catchTag(\"SecretOwnedByConnectionError\", () =>\n Effect.succeed(null),\n ),\n ),\n secretsSet: (input) => secretsSet(input),\n connectionsCreate: (input) => connectionsCreate(input),\n });\n connectionProviders.set(\n oauthBundle.connectionProvider.key,\n oauthBundle.connectionProvider,\n );\n\n // ------------------------------------------------------------------\n // Plugin wiring — build ctx, run extension, populate static pools,\n // register secret providers. No adapter reads here.\n // ------------------------------------------------------------------\n for (const plugin of plugins) {\n if (runtimes.has(plugin.id)) {\n return yield* Effect.fail(\n new Error(`Duplicate plugin id: ${plugin.id}`),\n );\n }\n\n // Plugin-facing typed view. `StorageError` and `UniqueViolationError`\n // flow through the typed channel unchanged — plugins can\n // `catchTag(\"UniqueViolationError\", …)` to translate to their own\n // user-facing errors; the HTTP edge (see @executor-js/api\n // `withCapture`) is responsible for translating any\n // `StorageError` that still escapes into the opaque InternalError.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const storageDeps: StorageDeps<any> = {\n scopes,\n adapter: typedAdapter(adapter),\n // Blob keys are namespaced by `<scope>/<plugin>` so two tenants\n // sharing a backing BlobStore can't collide or leak on the\n // same `(plugin, key)` pair. The store's `get`/`has` walk the\n // scope stack (innermost first); `put`/`delete` require the\n // plugin to name a target scope explicitly.\n blobs: pluginBlobStore(blobs, scopeIds, plugin.id),\n };\n const storage = plugin.storage(storageDeps);\n\n const ctx: PluginCtx<unknown> = {\n scopes,\n storage,\n core: {\n sources: {\n register: (input: SourceInput) =>\n Effect.gen(function* () {\n // Guard: reject a dynamic source whose id collides with\n // a static source id, or any of whose would-be tool ids\n // collide with a static tool id. Tool ids are\n // `${source_id}.${tool.name}` — static and dynamic\n // share the same string space. Fails as `StorageError`\n // so the HTTP edge surfaces it as `InternalError(traceId)`.\n if (staticSources.has(input.id)) {\n return yield* Effect.fail(\n new StorageError({\n message: `Source id \"${input.id}\" collides with a static source`,\n cause: undefined,\n }),\n );\n }\n for (const tool of input.tools) {\n const fqid = `${input.id}.${tool.name}`;\n if (staticTools.has(fqid)) {\n return yield* Effect.fail(\n new StorageError({\n message: `Tool id \"${fqid}\" collides with a static tool`,\n cause: undefined,\n }),\n );\n }\n }\n // Wrap in adapter.transaction so a standalone register()\n // call is atomic (source create + tools createMany group\n // together). When already inside a parent ctx.transaction,\n // the router short-circuits to the active tx handle\n // instead of opening a nested sql.begin — that nested\n // sql.begin is the postgres.js + pool=1 deadlock path.\n yield* adapter.transaction(() =>\n writeSourceInput(core, plugin.id, input),\n );\n }),\n unregister: (sourceId: string) =>\n // `unregister` is scoped to a specific source row — look up\n // its scope before deleting so the tool/definition sweep\n // only touches rows at that scope. Walk the full stack and\n // pick the innermost-scope shadow so an inner-scope caller\n // can't accidentally (via non-deterministic findOne\n // iteration order) unregister the outer-scope source and\n // wipe a bystander's data at the same time.\n adapter.transaction(() =>\n Effect.gen(function* () {\n const rows = yield* core.findMany({\n model: \"source\",\n where: [{ field: \"id\", value: sourceId }],\n });\n const row = findInnermost(rows);\n if (!row) return;\n yield* deleteSourceById(\n core,\n sourceId,\n row.scope_id as string,\n );\n }),\n ),\n update: (input) =>\n core.update({\n model: \"source\",\n where: [\n { field: \"id\", value: input.id },\n { field: \"scope_id\", value: input.scope },\n ],\n update: {\n ...(input.name !== undefined ? { name: input.name } : {}),\n ...(input.url !== undefined ? { url: input.url ?? undefined } : {}),\n updated_at: new Date(),\n },\n }).pipe(Effect.asVoid),\n },\n definitions: {\n register: (input: DefinitionsInput) =>\n adapter.transaction(() =>\n writeDefinitions(core, plugin.id, input),\n ),\n },\n },\n secrets: {\n get: (id) => secretsGet(id),\n list: () => secretsListForCtx(),\n set: (input) => secretsSet(input),\n remove: (id) => secretsRemove(id),\n },\n connections: {\n get: (id) => connectionsGet(id),\n list: () => connectionsListForCtx(),\n create: (input) => connectionsCreate(input),\n updateTokens: (input) => connectionsUpdateTokens(input),\n setIdentityLabel: (id, label) =>\n connectionsSetIdentityLabel(id, label),\n accessToken: (id) => connectionsAccessToken(id),\n remove: (id) => connectionsRemove(id),\n },\n oauth: oauthBundle.service,\n // Open one real tx boundary and route every nested write inside\n // `effect` through that same handle via the activeAdapterRef —\n // see buildAdapterRouter above. Caller-typed errors (`E`)\n // propagate unchanged; storage failures also stay typed\n // (`StorageFailure`) so the HTTP edge wrapper can translate them.\n transaction: <A, E>(effect: Effect.Effect<A, E>) =>\n adapter.transaction(() => effect) as Effect.Effect<\n A,\n E | StorageFailure\n >,\n };\n\n // Build extension FIRST so it's available as `self` when resolving\n // staticSources. Field ordering in the plugin spec matters — TS\n // infers TExtension from `extension`'s return type, then NoInfer\n // locks `self` to that inferred type on `staticSources`.\n const extension: object = plugin.extension\n ? plugin.extension(ctx)\n : {};\n if (plugin.extension) {\n extensions[plugin.id] = extension;\n }\n\n // Resolve static declarations to the in-memory pools. NO DB WRITES.\n const decls = plugin.staticSources\n ? plugin.staticSources(extension)\n : [];\n for (const source of decls) {\n if (staticSources.has(source.id)) {\n return yield* Effect.fail(\n new Error(\n `Duplicate static source id: ${source.id} (plugin ${plugin.id})`,\n ),\n );\n }\n staticSources.set(source.id, { source, pluginId: plugin.id });\n\n for (const tool of source.tools) {\n const fqid = `${source.id}.${tool.name}`;\n if (staticTools.has(fqid)) {\n return yield* Effect.fail(\n new Error(\n `Duplicate static tool id: ${fqid} (plugin ${plugin.id})`,\n ),\n );\n }\n staticTools.set(fqid, {\n source,\n tool,\n pluginId: plugin.id,\n ctx,\n });\n }\n }\n\n runtimes.set(plugin.id, { plugin, storage, ctx });\n\n if (plugin.secretProviders) {\n const raw =\n typeof plugin.secretProviders === \"function\"\n ? plugin.secretProviders(ctx)\n : plugin.secretProviders;\n const providers = Effect.isEffect(raw) ? yield* raw : raw;\n for (const provider of providers) {\n if (secretProviders.has(provider.key)) {\n return yield* Effect.fail(\n new Error(\n `Duplicate secret provider key: ${provider.key} (from plugin ${plugin.id})`,\n ),\n );\n }\n secretProviders.set(provider.key, provider);\n }\n }\n\n if (plugin.connectionProviders) {\n const raw =\n typeof plugin.connectionProviders === \"function\"\n ? plugin.connectionProviders(ctx)\n : plugin.connectionProviders;\n const providers = Effect.isEffect(raw) ? yield* raw : raw;\n for (const provider of providers) {\n if (connectionProviders.has(provider.key)) {\n return yield* Effect.fail(\n new Error(\n `Duplicate connection provider key: ${provider.key} (from plugin ${plugin.id})`,\n ),\n );\n }\n connectionProviders.set(provider.key, provider);\n }\n }\n }\n\n // ------------------------------------------------------------------\n // Executor surface\n // ------------------------------------------------------------------\n const listSources = () =>\n Effect.gen(function* () {\n const dynamic = yield* core.findMany({ model: \"source\" });\n // Dedup by id with innermost scope winning. Without this, a user\n // who shadowed an org-wide source at their inner scope would see\n // two rows — their override and the outer default — which is\n // inconsistent with how `secrets.list` and every other list\n // surface dedup shadowed entries.\n const byId = new Map<string, typeof dynamic[number]>();\n const byIdRank = new Map<string, number>();\n for (const row of dynamic) {\n const rank = scopeRank(row);\n const existing = byIdRank.get(row.id);\n if (existing === undefined || rank < existing) {\n byId.set(row.id, row);\n byIdRank.set(row.id, rank);\n }\n }\n const dynamicDeduped = [...byId.values()];\n const staticList: Source[] = [];\n for (const { source, pluginId } of staticSources.values()) {\n staticList.push(staticDeclToSource(source, pluginId));\n }\n const merged = [...staticList, ...dynamicDeduped.map(rowToSource)];\n yield* Effect.annotateCurrentSpan({\n \"executor.sources.static_count\": staticList.length,\n \"executor.sources.dynamic_count\": dynamicDeduped.length,\n });\n return merged;\n }).pipe(Effect.withSpan(\"executor.sources.list\"));\n\n // Bulk-resolve annotations across a set of dynamic tool rows by\n // grouping them under their owning plugin's resolveAnnotations\n // callback. One plugin call per (plugin_id, source_id) pair, not\n // per row. Plugins without a resolver simply contribute no\n // annotations for their rows.\n const resolveAnnotationsFor = (rows: readonly ToolRow[]) =>\n Effect.gen(function* () {\n const result = new Map<string, ToolAnnotations>();\n if (rows.length === 0) return result;\n\n // Group by (plugin_id, source_id)\n const groups = new Map<string, ToolRow[]>();\n for (const row of rows) {\n const key = `${row.plugin_id}\\u0000${row.source_id}`;\n const bucket = groups.get(key);\n if (bucket) bucket.push(row);\n else groups.set(key, [row]);\n }\n\n // Each (plugin_id, source_id) group is an independent DB read,\n // so fan them out concurrently. Yielding them serially stacks\n // ~200-300ms storage round-trips end-to-end and dominates the\n // `executor.tools.list.annotations` span.\n const maps = yield* Effect.forEach(\n [...groups],\n ([key, groupRows]) =>\n Effect.gen(function* () {\n const [pluginId, sourceId] = key.split(\"\\u0000\") as [\n string,\n string,\n ];\n const runtime = runtimes.get(pluginId);\n if (!runtime?.plugin.resolveAnnotations) return undefined;\n return yield* runtime.plugin.resolveAnnotations({\n ctx: runtime.ctx,\n sourceId,\n toolRows: groupRows,\n });\n }),\n { concurrency: \"unbounded\" },\n );\n for (const map of maps) {\n if (!map) continue;\n for (const [toolId, annotations] of Object.entries(map)) {\n result.set(toolId, annotations);\n }\n }\n return result;\n });\n\n const listTools = (filter?: ToolListFilter) =>\n Effect.gen(function* () {\n const dynamic = yield* core.findMany({\n model: \"tool\",\n where: filter?.sourceId\n ? [{ field: \"source_id\", value: filter.sourceId }]\n : undefined,\n });\n // Dedup by tool id, innermost scope winning — same reason as\n // `listSources` above: a shadowed id must surface as one entry\n // (the inner one), not two.\n const byId = new Map<string, typeof dynamic[number]>();\n const byIdRank = new Map<string, number>();\n for (const row of dynamic) {\n const rank = scopeRank(row);\n const existing = byIdRank.get(row.id);\n if (existing === undefined || rank < existing) {\n byId.set(row.id, row);\n byIdRank.set(row.id, rank);\n }\n }\n const dynamicDeduped = [...byId.values()];\n const annotations =\n filter?.includeAnnotations === false\n ? new Map<string, ToolAnnotations>()\n : yield* resolveAnnotationsFor(dynamicDeduped).pipe(\n Effect.withSpan(\"executor.tools.list.annotations\"),\n );\n\n const out: Tool[] = [];\n // Static tools — annotations from the declaration, not a resolver.\n for (const entry of staticTools.values()) {\n out.push(staticDeclToTool(entry.source, entry.tool, entry.pluginId));\n }\n for (const row of dynamicDeduped) {\n out.push(rowToTool(row, annotations.get(row.id)));\n }\n const filtered = filter\n ? out.filter((t) => toolMatchesFilter(t, filter))\n : out;\n\n // Drop tools blocked by user policy unless the caller explicitly\n // asked to see them (the settings UI does, agent surfaces don't).\n // One findMany covers the entire scope stack; resolution per\n // tool is in-memory.\n let result = filtered;\n let blockedCount = 0;\n if (filter?.includeBlocked !== true) {\n const policies = yield* loadAllPolicies();\n if (policies.length > 0) {\n const kept: Tool[] = [];\n for (const tool of filtered) {\n const match = resolveToolPolicy(tool.id, policies, scopeRank);\n if (match?.action === \"block\") {\n blockedCount++;\n continue;\n }\n kept.push(tool);\n }\n result = kept;\n }\n }\n\n yield* Effect.annotateCurrentSpan({\n \"executor.tools.static_count\": staticTools.size,\n \"executor.tools.dynamic_count\": dynamicDeduped.length,\n \"executor.tools.result_count\": result.length,\n \"executor.tools.blocked_count\": blockedCount,\n });\n return result;\n }).pipe(Effect.withSpan(\"executor.tools.list\"));\n\n // Load all definitions for a single source as a plain map. Defs\n // for the same name can exist at multiple scopes (an admin registers\n // a default, a user overrides one entry with a tighter schema) —\n // dedup by name keeping the innermost-scope row.\n const loadDefinitionsForSource = (sourceId: string) =>\n Effect.gen(function* () {\n const defRows = yield* core.findMany({\n model: \"definition\",\n where: [{ field: \"source_id\", value: sourceId }],\n });\n const winners = new Map<string, { row: typeof defRows[number]; rank: number }>();\n for (const row of defRows) {\n const rank = scopeRank(row);\n const existing = winners.get(row.name);\n if (!existing || rank < existing.rank) {\n winners.set(row.name, { row, rank });\n }\n }\n const out: Record<string, unknown> = {};\n for (const [name, { row }] of winners) out[name] = row.schema;\n return out;\n });\n\n // Render the ToolSchema view for a tool — wraps the raw JSON schemas\n // with attached `$defs` and runs them through the TypeScript preview\n // helpers so the UI gets ready-to-display code samples.\n const buildToolSchemaView = (opts: {\n toolId: string;\n name?: string;\n description?: string;\n sourceId: string | undefined;\n rawInput: unknown;\n rawOutput: unknown;\n }) =>\n Effect.gen(function* () {\n const defs: Record<string, unknown> = opts.sourceId\n ? yield* loadDefinitionsForSource(opts.sourceId).pipe(\n Effect.withSpan(\"executor.tool.schema.load_defs\"),\n )\n : {};\n\n const attachDefs = (schema: unknown): unknown => {\n if (schema == null || typeof schema !== \"object\") return schema;\n if (Object.keys(defs).length === 0) return schema;\n return { ...(schema as Record<string, unknown>), $defs: defs };\n };\n\n const inputSchema = attachDefs(opts.rawInput);\n const outputSchema = attachDefs(opts.rawOutput);\n\n const defsMap = new Map<string, unknown>(Object.entries(defs));\n const preview = yield* Effect.sync(() =>\n buildToolTypeScriptPreview({ inputSchema, outputSchema, defs: defsMap }),\n ).pipe(\n Effect.withSpan(\"schema.compile.preview\", {\n attributes: {\n \"schema.kind\": \"tool.preview\",\n \"schema.has_input\": inputSchema !== undefined,\n \"schema.has_output\": outputSchema !== undefined,\n \"schema.def_count\": defsMap.size,\n },\n }),\n );\n\n return new ToolSchema({\n id: ToolId.make(opts.toolId),\n name: opts.name,\n description: opts.description,\n inputSchema,\n outputSchema,\n inputTypeScript: preview.inputTypeScript ?? undefined,\n outputTypeScript: preview.outputTypeScript ?? undefined,\n typeScriptDefinitions: preview.typeScriptDefinitions ?? undefined,\n });\n });\n\n const toolSchema = (toolId: string) =>\n Effect.gen(function* () {\n // Static pool first — static tools have no source in the DB so\n // no `$defs` attach; just wrap the declared schemas.\n const staticEntry = staticTools.get(toolId);\n if (staticEntry) {\n yield* Effect.annotateCurrentSpan({\n \"executor.tool.dispatch_path\": \"static\",\n \"executor.source_id\": staticEntry.source.id,\n \"executor.source_kind\": staticEntry.source.kind,\n });\n return yield* buildToolSchemaView({\n toolId,\n name: staticEntry.tool.name,\n description: staticEntry.tool.description,\n sourceId: undefined,\n rawInput: staticEntry.tool.inputSchema,\n rawOutput: staticEntry.tool.outputSchema,\n });\n }\n // Innermost-wins lookup: the scope-wrapped adapter returns rows\n // from every scope in the stack, so a bare findOne would pick the\n // first row the backend iterates. That's wrong when a user has\n // shadowed an outer-scope tool — they'd get the outer schema\n // back instead of their override.\n const rows = yield* core\n .findMany({\n model: \"tool\",\n where: [{ field: \"id\", value: toolId }],\n })\n .pipe(Effect.withSpan(\"executor.tool.resolve\"));\n const row = findInnermost(rows);\n if (!row) return null;\n yield* Effect.annotateCurrentSpan({\n \"executor.tool.dispatch_path\": \"dynamic\",\n \"executor.source_id\": row.source_id,\n \"executor.plugin_id\": row.plugin_id,\n });\n return yield* buildToolSchemaView({\n toolId,\n name: row.name,\n description: row.description,\n sourceId: row.source_id,\n rawInput: decodeJsonColumn(row.input_schema),\n rawOutput: decodeJsonColumn(row.output_schema),\n });\n }).pipe(\n Effect.withSpan(\"executor.tool.schema\", {\n attributes: { \"mcp.tool.name\": toolId },\n }),\n );\n\n // Bulk definitions accessor — every source's $defs, grouped by\n // source id. One query against the definition table, plus an\n // in-memory group-by with innermost-scope dedup: if the same\n // (source_id, name) pair exists at multiple scopes, the inner\n // scope's schema wins.\n const toolsDefinitions = () =>\n Effect.gen(function* () {\n const rows = yield* core.findMany({ model: \"definition\" });\n const winners = new Map<string, { row: typeof rows[number]; rank: number }>();\n for (const row of rows) {\n const key = `${row.source_id}\\u0000${row.name}`;\n const rank = scopeRank(row);\n const existing = winners.get(key);\n if (!existing || rank < existing.rank) {\n winners.set(key, { row, rank });\n }\n }\n const out: Record<string, Record<string, unknown>> = {};\n for (const { row } of winners.values()) {\n let bucket = out[row.source_id];\n if (!bucket) {\n bucket = {};\n out[row.source_id] = bucket;\n }\n bucket[row.name] = row.schema;\n }\n return out;\n });\n\n const defaultElicitationHandler = resolveElicitationHandler(\n config.onElicitation,\n );\n const pickHandler = (options: InvokeOptions | undefined): ElicitationHandler =>\n options?.onElicitation\n ? resolveElicitationHandler(options.onElicitation)\n : defaultElicitationHandler;\n\n const buildElicit = (\n toolId: string,\n args: unknown,\n handler: ElicitationHandler,\n ): Elicit => {\n return (request: ElicitationRequest) =>\n Effect.gen(function* () {\n const tid = ToolId.make(toolId);\n const response: ElicitationResponse = yield* handler({\n toolId: tid,\n args,\n request,\n });\n if (response.action !== \"accept\") {\n return yield* new ElicitationDeclinedError({\n toolId: tid,\n action: response.action,\n });\n }\n return response;\n });\n };\n\n // ------------------------------------------------------------------\n // Tool policies — user-authored overrides of the plugin-derived\n // approval annotations. Resolution walks the scope-stacked policy\n // table with first-match-wins ordering (innermost scope first, then\n // `position` ascending). The result either short-circuits invoke\n // (`block`), forces approval (`require_approval`), skips approval\n // (`approve`), or returns `undefined` so the plugin annotation is\n // used as today.\n // ------------------------------------------------------------------\n\n const loadAllPolicies = () =>\n core.findMany({ model: \"tool_policy\" });\n\n const resolveToolPolicyForId = (toolId: string) =>\n Effect.gen(function* () {\n const policies = yield* loadAllPolicies();\n return resolveToolPolicy(toolId, policies, scopeRank);\n });\n\n const enforceApproval = (\n annotations: ToolAnnotations | undefined,\n toolId: string,\n args: unknown,\n policy: PolicyMatch | undefined,\n handler: ElicitationHandler,\n ) =>\n Effect.gen(function* () {\n // approve → never prompt regardless of plugin annotation.\n if (policy?.action === \"approve\") return;\n\n // require_approval → always prompt. If the plugin already had a\n // description, prefer it; otherwise show the matched pattern so\n // the user can see *why* the prompt fired.\n const policyForcesApproval = policy?.action === \"require_approval\";\n if (!policyForcesApproval && !annotations?.requiresApproval) return;\n\n const tid = ToolId.make(toolId);\n const message = annotations?.approvalDescription\n ? annotations.approvalDescription\n : policyForcesApproval && policy\n ? `Approve ${toolId}? (matched policy: ${policy.pattern})`\n : `Approve ${toolId}?`;\n const request = new FormElicitation({\n message,\n requestedSchema: {},\n });\n const response = yield* handler({ toolId: tid, args, request });\n if (response.action !== \"accept\") {\n return yield* new ElicitationDeclinedError({\n toolId: tid,\n action: response.action,\n });\n }\n });\n\n const invokeTool = (\n toolId: string,\n args: unknown,\n options?: InvokeOptions,\n ) => {\n const handler = pickHandler(options);\n return Effect.gen(function* () {\n const wrapInvocationError = <A, E>(\n effect: Effect.Effect<A, E>,\n ): Effect.Effect<A, ToolInvocationError> =>\n effect.pipe(\n Effect.mapError(\n (cause) =>\n new ToolInvocationError({\n toolId: ToolId.make(toolId),\n message:\n cause instanceof Error ? cause.message : String(cause),\n cause,\n }),\n ),\n );\n\n // Resolve the user-authored policy first. A `block` rule\n // short-circuits both the static and dynamic paths before any\n // plugin code runs.\n const policy = yield* resolveToolPolicyForId(toolId).pipe(\n Effect.withSpan(\"executor.tool.resolve_policy\"),\n );\n if (policy?.action === \"block\") {\n return yield* new ToolBlockedError({\n toolId: ToolId.make(toolId),\n pattern: policy.pattern,\n });\n }\n\n // Static path — O(1) map lookup, no DB hit.\n const staticEntry = staticTools.get(toolId);\n if (staticEntry) {\n yield* Effect.annotateCurrentSpan({\n \"executor.tool.dispatch_path\": \"static\",\n \"executor.source_id\": staticEntry.source.id,\n \"executor.source_kind\": staticEntry.source.kind,\n \"executor.plugin_id\": staticEntry.pluginId,\n });\n yield* enforceApproval(\n staticEntry.tool.annotations,\n toolId,\n args,\n policy,\n handler,\n ).pipe(Effect.withSpan(\"executor.tool.enforce_approval\"));\n return yield* wrapInvocationError(\n staticEntry.tool.handler({\n ctx: staticEntry.ctx,\n args,\n elicit: buildElicit(toolId, args, handler),\n }),\n ).pipe(Effect.withSpan(\"executor.tool.handler\"));\n }\n\n // Dynamic path — DB lookup + delegate to owning plugin. Walk\n // the whole scope stack and pick the innermost-scope row so a\n // user's shadow of an outer tool actually wins on invoke (a bare\n // findOne would pick whatever row the backend iterated first).\n const toolRows = yield* core\n .findMany({\n model: \"tool\",\n where: [{ field: \"id\", value: toolId }],\n })\n .pipe(Effect.withSpan(\"executor.tool.resolve\"));\n const row = findInnermost(toolRows);\n if (!row) {\n return yield* new ToolNotFoundError({\n toolId: ToolId.make(toolId),\n });\n }\n yield* Effect.annotateCurrentSpan({\n \"executor.tool.dispatch_path\": \"dynamic\",\n \"executor.source_id\": row.source_id,\n \"executor.plugin_id\": row.plugin_id,\n });\n const runtime = runtimes.get(row.plugin_id);\n if (!runtime) {\n return yield* new PluginNotLoadedError({\n pluginId: row.plugin_id,\n toolId: ToolId.make(toolId),\n });\n }\n if (!runtime.plugin.invokeTool) {\n return yield* new NoHandlerError({\n toolId: ToolId.make(toolId),\n pluginId: row.plugin_id,\n });\n }\n\n // Ask the plugin to derive annotations for this one row, if it\n // has a resolver. Cheap because the plugin typically already\n // needs to load its enrichment data to invoke the tool —\n // implementations should structure their resolver + invokeTool\n // around a single storage read. Skipped entirely when the user\n // policy is `approve` — the prompt is going to be skipped no\n // matter what the plugin says, so don't pay for the lookup.\n let annotations: ToolAnnotations | undefined;\n if (policy?.action !== \"approve\" && runtime.plugin.resolveAnnotations) {\n const map = yield* runtime.plugin\n .resolveAnnotations({\n ctx: runtime.ctx,\n sourceId: row.source_id,\n toolRows: [row],\n })\n .pipe(Effect.withSpan(\"executor.tool.resolve_annotations\"));\n annotations = map[toolId];\n }\n yield* enforceApproval(annotations, toolId, args, policy, handler).pipe(\n Effect.withSpan(\"executor.tool.enforce_approval\"),\n );\n\n return yield* wrapInvocationError(\n runtime.plugin.invokeTool({\n ctx: runtime.ctx,\n toolRow: row,\n args,\n elicit: buildElicit(toolId, args, handler),\n }),\n ).pipe(Effect.withSpan(\"executor.tool.handler\"));\n }).pipe(\n Effect.withSpan(\"executor.tool.invoke\", {\n attributes: {\n \"mcp.tool.name\": toolId,\n },\n }),\n );\n };\n\n const removeSource = (sourceId: string) =>\n Effect.gen(function* () {\n // Block removal of static sources structurally.\n if (staticSources.has(sourceId)) {\n return yield* new SourceRemovalNotAllowedError({ sourceId });\n }\n // Innermost-wins lookup — same reason as ctx.sources.unregister:\n // a caller with a stack that straddles two scopes must target\n // their own shadow, not the outer scope's row.\n const sourceRows = yield* core.findMany({\n model: \"source\",\n where: [{ field: \"id\", value: sourceId }],\n });\n const sourceRow = findInnermost(sourceRows);\n if (!sourceRow) return;\n if (!sourceRow.can_remove) {\n return yield* new SourceRemovalNotAllowedError({ sourceId });\n }\n const runtime = runtimes.get(sourceRow.plugin_id);\n // Group the plugin's own cleanup + the core row delete into one\n // tx so a removeSource never leaves orphan rows on failure. The\n // router short-circuits on nested calls when the caller is\n // already inside a parent ctx.transaction.\n yield* adapter.transaction(() =>\n Effect.gen(function* () {\n if (runtime?.plugin.removeSource) {\n yield* runtime.plugin.removeSource({\n ctx: runtime.ctx,\n sourceId,\n scope: sourceRow.scope_id as string,\n });\n }\n yield* deleteSourceById(\n core,\n sourceId,\n sourceRow.scope_id as string,\n );\n }),\n );\n });\n\n const refreshSource = (sourceId: string) =>\n Effect.gen(function* () {\n if (staticSources.has(sourceId)) return;\n // Innermost-wins: refresh the caller's shadow, not an outer-scope\n // source that happens to share an id.\n const sourceRows = yield* core.findMany({\n model: \"source\",\n where: [{ field: \"id\", value: sourceId }],\n });\n const sourceRow = findInnermost(sourceRows);\n if (!sourceRow) return;\n const runtime = runtimes.get(sourceRow.plugin_id);\n if (runtime?.plugin.refreshSource) {\n yield* runtime.plugin.refreshSource({\n ctx: runtime.ctx,\n sourceId,\n scope: sourceRow.scope_id as string,\n });\n }\n });\n\n // URL autodetection — fan out across every plugin that declared a\n // `detect` hook. Collect all non-null results. Plugin-level detect\n // implementations should swallow fetch errors and return null, so\n // one flaky plugin doesn't block the whole dispatch.\n const detectionConfidenceScore = (\n confidence: SourceDetectionResult[\"confidence\"],\n ) => {\n switch (confidence) {\n case \"high\":\n return 3;\n case \"medium\":\n return 2;\n case \"low\":\n return 1;\n }\n };\n\n const detectSource = (url: string) =>\n Effect.gen(function* () {\n const results: SourceDetectionResult[] = [];\n for (const runtime of runtimes.values()) {\n if (!runtime.plugin.detect) continue;\n const result = yield* runtime.plugin\n .detect({ ctx: runtime.ctx, url })\n .pipe(Effect.catch(() => Effect.succeed(null)));\n if (result) results.push(result);\n }\n return results.sort(\n (a, b) =>\n detectionConfidenceScore(b.confidence) -\n detectionConfidenceScore(a.confidence),\n );\n });\n\n // Per-source definitions accessor — one query, one mapping pass.\n const sourceDefinitions = (sourceId: string) =>\n loadDefinitionsForSource(sourceId);\n\n // Existence check for user-facing secret pickers. Core `secret`\n // rows are routing metadata; when a provider can answer `has()`,\n // confirm the backing value still exists. Providers without `has()`\n // remain conservative so keychain/1password don't need to return\n // the value or prompt just to populate picker/status UI.\n const secretsStatus = (\n id: string,\n ): Effect.Effect<\"resolved\" | \"missing\", StorageFailure> =>\n Effect.gen(function* () {\n const rows = yield* secretRowsForId(id);\n if (rows.some((row) => row.owned_by_connection_id)) return \"missing\";\n for (const row of rows) {\n if (yield* secretRouteHasBackingValue(row)) return \"resolved\";\n }\n\n for (const provider of secretProviders.values()) {\n if (!provider.list) continue;\n const entries = yield* provider\n .list()\n .pipe(Effect.catch(() => Effect.succeed([] as const)));\n if (entries.some((e) => e.id === id)) return \"resolved\";\n }\n return \"missing\";\n });\n\n // ------------------------------------------------------------------\n // Policies — CRUD surface backed by the `tool_policy` core table.\n // The cloud settings UI is one consumer; plugins call the same API\n // when they programmatically manage policies.\n //\n // `list` orders rows the same way resolution does — innermost scope\n // first, then position ascending — so the UI can render the\n // evaluation order without re-sorting.\n // ------------------------------------------------------------------\n const policiesList = () =>\n Effect.gen(function* () {\n const rows = yield* loadAllPolicies();\n const sorted = [...rows].sort((a, b) => {\n const sa = scopeRank(a);\n const sb = scopeRank(b);\n if (sa !== sb) return sa - sb;\n return comparePolicyRow(a, b);\n });\n return sorted.map((row) => rowToToolPolicy(row));\n }).pipe(Effect.withSpan(\"executor.policies.list\"));\n\n const policiesCreate = (input: CreateToolPolicyInput) =>\n Effect.gen(function* () {\n if (!isValidPattern(input.pattern)) {\n return yield* new StorageError({\n message:\n `Invalid tool policy pattern \"${input.pattern}\". ` +\n `Patterns must be \"*\" (every tool), an exact tool id (\"a.b.c\"), ` +\n `or a trailing wildcard (\"a.b.*\"). Leading \"*\" prefixes ` +\n `(\"*foo\", \"*.foo\") and \"**\" are not supported.`,\n cause: undefined,\n });\n }\n if (!isToolPolicyAction(input.action)) {\n return yield* new StorageError({\n message:\n `Invalid tool policy action \"${String(input.action)}\". ` +\n `Expected \"approve\" | \"require_approval\" | \"block\".`,\n cause: undefined,\n });\n }\n\n // Default position: a fractional-indexing key above the\n // current minimum. Lets newly-created rules win against\n // existing ones, which matches the v1 design — users typically\n // add a rule to override behavior they're seeing right now,\n // not as a background fallback.\n let position = input.position;\n if (position === undefined) {\n const existing = yield* core.findMany({\n model: \"tool_policy\",\n where: [{ field: \"scope_id\", value: input.scope }],\n });\n let min: string | null = null;\n for (const row of existing) {\n const p = row.position as string;\n if (min === null || p < min) min = p;\n }\n position = generateKeyBetween(null, min);\n }\n\n const id = `pol_${Math.random().toString(36).slice(2, 10)}_${Date.now().toString(36)}`;\n const now = new Date();\n yield* core.create({\n model: \"tool_policy\",\n data: {\n id,\n scope_id: input.scope,\n pattern: input.pattern,\n action: input.action,\n position,\n created_at: now,\n updated_at: now,\n },\n forceAllowId: true,\n });\n return rowToToolPolicy({\n id,\n scope_id: input.scope,\n pattern: input.pattern,\n action: input.action,\n position,\n created_at: now,\n updated_at: now,\n } as ToolPolicyRow);\n }).pipe(Effect.withSpan(\"executor.policies.create\"));\n\n const policiesUpdate = (input: UpdateToolPolicyInput) =>\n Effect.gen(function* () {\n if (input.pattern !== undefined && !isValidPattern(input.pattern)) {\n return yield* new StorageError({\n message: `Invalid tool policy pattern \"${input.pattern}\".`,\n cause: undefined,\n });\n }\n if (input.action !== undefined && !isToolPolicyAction(input.action)) {\n return yield* new StorageError({\n message: `Invalid tool policy action \"${String(input.action)}\".`,\n cause: undefined,\n });\n }\n\n const rows = yield* core.findMany({\n model: \"tool_policy\",\n where: [{ field: \"id\", value: input.id }],\n });\n const row = findInnermost(rows);\n if (!row) {\n return yield* new StorageError({\n message: `Tool policy \"${input.id}\" not found.`,\n cause: undefined,\n });\n }\n\n const updated: ToolPolicyRow = {\n ...row,\n pattern: input.pattern ?? row.pattern,\n action: input.action ?? row.action,\n position: input.position ?? row.position,\n updated_at: new Date(),\n };\n yield* core.update({\n model: \"tool_policy\",\n where: [\n { field: \"id\", value: input.id },\n { field: \"scope_id\", value: row.scope_id as string },\n ],\n update: {\n pattern: updated.pattern as string,\n action: updated.action as string,\n position: updated.position as string,\n updated_at: updated.updated_at as Date,\n },\n });\n return rowToToolPolicy(updated);\n }).pipe(Effect.withSpan(\"executor.policies.update\"));\n\n const policiesRemove = (id: string) =>\n core\n .deleteMany({\n model: \"tool_policy\",\n where: [{ field: \"id\", value: id }],\n })\n .pipe(Effect.asVoid, Effect.withSpan(\"executor.policies.remove\"));\n\n const policiesResolve = (toolId: string) =>\n resolveToolPolicyForId(toolId).pipe(\n Effect.withSpan(\"executor.policies.resolve\"),\n );\n\n const close = () =>\n Effect.gen(function* () {\n for (const runtime of runtimes.values()) {\n if (runtime.plugin.close) {\n yield* runtime.plugin.close();\n }\n }\n });\n\n // Public Executor surface — storage-backed methods surface\n // `StorageFailure` (StorageError | UniqueViolationError) raw. The\n // HTTP edge wraps this surface with `withCapture` to\n // translate `StorageError` → `InternalError({ traceId })`; non-HTTP\n // consumers (CLI, Promise SDK, tests) see the raw typed channel.\n const base = {\n scopes,\n tools: {\n list: listTools,\n schema: toolSchema,\n definitions: toolsDefinitions,\n invoke: invokeTool,\n },\n sources: {\n list: listSources,\n remove: removeSource,\n refresh: refreshSource,\n detect: detectSource,\n definitions: sourceDefinitions,\n },\n secrets: {\n get: secretsGet,\n status: secretsStatus,\n set: secretsSet,\n remove: secretsRemove,\n list: secretsList,\n providers: () =>\n Effect.sync(\n () => Array.from(secretProviders.keys()) as readonly string[],\n ),\n },\n connections: {\n get: connectionsGet,\n list: connectionsList,\n create: connectionsCreate,\n updateTokens: connectionsUpdateTokens,\n setIdentityLabel: connectionsSetIdentityLabel,\n accessToken: connectionsAccessToken,\n remove: connectionsRemove,\n providers: () =>\n Effect.sync(\n () =>\n Array.from(connectionProviders.keys()) as readonly string[],\n ),\n },\n oauth: oauthBundle.service,\n policies: {\n list: policiesList,\n create: policiesCreate,\n update: policiesUpdate,\n remove: policiesRemove,\n resolve: policiesResolve,\n },\n close,\n };\n\n // Cast through `unknown` because the impl effects can include\n // `Error` from plugin-supplied callbacks (resolveAnnotations etc.) —\n // those leak via the helper functions and won't be cleaned until\n // every plugin tightens its surface to typed errors. The runtime\n // shape matches `Executor<TPlugins>`.\n const toExecutor = (value: unknown): Executor<TPlugins> =>\n value as Executor<TPlugins>;\n return toExecutor(Object.assign(base, extensions));\n });\n","// Scoped adapter — wraps a DBAdapter so every read on a tenant-scoped\n// table filters by `scope_id IN (scopes)` and every write validates that\n// its `scope_id` payload is one of the allowed scopes. Tables the\n// schema doesn't declare a `scope_id` field on pass through untouched —\n// the wrapper doesn't invent columns that aren't there.\n//\n// Writes are explicit: the caller must include `scope_id` in every\n// create/update payload for scoped tables. The adapter does not pick a\n// default. A missing `scope_id` on a scoped write, or a value outside\n// the allowed `scopes` array, is a `StorageError`.\n//\n// The SDK's `createExecutor` wraps the root adapter (and every tx\n// handle passed back into transaction callbacks) with this before\n// handing it to the core table writers or to plugin storage via\n// `typedAdapter(...)`. Plugins see a stable DBAdapter; they learn their\n// scope from `ctx.scopes` and stamp it explicitly on every write.\n//\n// Contract: every multi-tenant table's schema must include\n// `scope_id: { type: \"string\", required: true, index: true }`. Tables\n// without it are shared across scopes by construction.\n\nimport { Effect } from \"effect\";\n\nimport {\n StorageError,\n type DBAdapter,\n type DBSchema,\n type DBTransactionAdapter,\n type Where,\n} from \"@executor-js/storage-core\";\n\nconst SCOPE_FIELD = \"scope_id\";\n\nexport interface ScopeContext {\n /**\n * Precedence-ordered list of scope ids the wrapper accepts on reads\n * and writes. Innermost first. Reads walk every scope in the list\n * (via `scope_id IN (...)`); writes must name one of them explicitly\n * via `scope_id` in the payload.\n */\n readonly scopes: readonly string[];\n}\n\nconst collectScopedModels = (schema: DBSchema): Set<string> => {\n const out = new Set<string>();\n for (const [model, def] of Object.entries(schema)) {\n if (def.fields[SCOPE_FIELD]) out.add(model);\n }\n return out;\n};\n\nconst withScopeRead = (\n where: readonly Where[] | undefined,\n ctx: ScopeContext,\n): Where[] => {\n const base = (where ?? []).filter((w) => w.field !== SCOPE_FIELD);\n const callerScope = (where ?? []).find((w) => w.field === SCOPE_FIELD);\n\n // Honor a caller-supplied scope filter IF it names a single scope\n // that lives in the executor's stack. This turns a stack-wide read\n // (default) into a single-scope read — and, for delete/update, pins\n // mutations to the named scope instead of letting the `IN (stack)`\n // injection silently widen them. An out-of-stack value is treated\n // as an isolation bypass attempt and discarded; the stack-wide\n // filter applies instead, so the caller sees nothing outside their\n // stack regardless of what they asked for.\n if (\n callerScope &&\n typeof callerScope.value === \"string\" &&\n ctx.scopes.includes(callerScope.value)\n ) {\n return [...base, { field: SCOPE_FIELD, value: callerScope.value }];\n }\n\n const scope: Where =\n ctx.scopes.length === 1\n ? { field: SCOPE_FIELD, value: ctx.scopes[0]! }\n : { field: SCOPE_FIELD, value: [...ctx.scopes], operator: \"in\" };\n return [...base, scope];\n};\n\nconst assertScopedWrite = (\n model: string,\n data: Record<string, unknown>,\n ctx: ScopeContext,\n): Effect.Effect<void, StorageError> => {\n const value = data[SCOPE_FIELD];\n if (typeof value !== \"string\" || value.length === 0) {\n return Effect.fail(\n new StorageError({\n message:\n `Write to scoped table \"${model}\" missing required \\`scope_id\\`. ` +\n `Callers must name the target scope explicitly.`,\n cause: undefined,\n }),\n );\n }\n if (!ctx.scopes.includes(value)) {\n return Effect.fail(\n new StorageError({\n message:\n `Write to scoped table \"${model}\" targets scope \"${value}\" ` +\n `which is not in the executor's scope stack ` +\n `[${ctx.scopes.join(\", \")}].`,\n cause: undefined,\n }),\n );\n }\n return Effect.void;\n};\n\ntype TxMethods = Omit<DBAdapter, \"transaction\" | \"createSchema\" | \"options\">;\n\nconst wrapTxMethods = (\n inner: TxMethods,\n ctx: ScopeContext,\n scopedModels: Set<string>,\n): TxMethods => {\n const isScoped = (model: string) => scopedModels.has(model);\n\n return {\n id: inner.id,\n create: (data) =>\n isScoped(data.model)\n ? Effect.flatMap(\n assertScopedWrite(\n data.model,\n data.data as Record<string, unknown>,\n ctx,\n ),\n () => inner.create(data),\n )\n : inner.create(data),\n createMany: (data) =>\n isScoped(data.model)\n ? Effect.flatMap(\n Effect.all(\n data.data.map((row) =>\n assertScopedWrite(\n data.model,\n row as Record<string, unknown>,\n ctx,\n ),\n ),\n ),\n () => inner.createMany(data),\n )\n : inner.createMany(data),\n findOne: (data) =>\n isScoped(data.model)\n ? inner.findOne({ ...data, where: withScopeRead(data.where, ctx) })\n : inner.findOne(data),\n findMany: (data) =>\n isScoped(data.model)\n ? inner.findMany({ ...data, where: withScopeRead(data.where, ctx) })\n : inner.findMany(data),\n count: (data) =>\n isScoped(data.model)\n ? inner.count({ ...data, where: withScopeRead(data.where, ctx) })\n : inner.count(data),\n update: (data) =>\n isScoped(data.model)\n ? Effect.flatMap(\n // If the caller sets `scope_id` in the update payload, it\n // must be one of the allowed scopes. If they don't, we leave\n // the row's existing scope_id in place — updates are scoped\n // by the where filter's IN clause, so you can only mutate\n // rows you can read. That's sufficient for isolation; we\n // don't need to force-stamp on update.\n (data.update as Record<string, unknown>)[SCOPE_FIELD] !== undefined\n ? assertScopedWrite(\n data.model,\n data.update as Record<string, unknown>,\n ctx,\n )\n : Effect.void,\n () =>\n inner.update({\n ...data,\n where: withScopeRead(data.where, ctx),\n }),\n )\n : inner.update(data),\n updateMany: (data) =>\n isScoped(data.model)\n ? Effect.flatMap(\n (data.update as Record<string, unknown>)[SCOPE_FIELD] !== undefined\n ? assertScopedWrite(\n data.model,\n data.update as Record<string, unknown>,\n ctx,\n )\n : Effect.void,\n () =>\n inner.updateMany({\n ...data,\n where: withScopeRead(data.where, ctx),\n }),\n )\n : inner.updateMany(data),\n delete: (data) =>\n isScoped(data.model)\n ? inner.delete({ ...data, where: withScopeRead(data.where, ctx) })\n : inner.delete(data),\n deleteMany: (data) =>\n isScoped(data.model)\n ? inner.deleteMany({ ...data, where: withScopeRead(data.where, ctx) })\n : inner.deleteMany(data),\n };\n};\n\nexport const scopeAdapter = (\n inner: DBAdapter,\n ctx: ScopeContext,\n schema: DBSchema,\n): DBAdapter => {\n const scopedModels = collectScopedModels(schema);\n const tx = wrapTxMethods(inner, ctx, scopedModels);\n return {\n ...tx,\n transaction: (callback) =>\n inner.transaction((rawTrx) => {\n const scopedTrx: DBTransactionAdapter = wrapTxMethods(\n rawTrx,\n ctx,\n scopedModels,\n );\n return callback(scopedTrx);\n }),\n createSchema: inner.createSchema,\n options: inner.options,\n };\n};\n\nexport const __scopeField = SCOPE_FIELD;\n","// ---------------------------------------------------------------------------\n// In-memory CustomAdapter, piped through createAdapter to produce a full\n// DBAdapter. Used by the conformance suite and for unit tests that don't\n// need real persistence.\n//\n// Vendored from better-auth (packages/memory-adapter/src/memory-adapter.ts)\n// under MIT. Adapted for executor:\n// - Promise/async → Effect.Effect<T, Error>\n// - Stripped the BetterAuthOptions layering (our config is simpler)\n// - Reuses the filter logic from better-auth's memoryAdapter, minus\n// join support (join resolution is not needed for the storage-core\n// testing path — plugins in-process do their own joining)\n// - Transaction uses structuredClone snapshot/rollback, same as\n// better-auth's memory-adapter\n// ---------------------------------------------------------------------------\n\nimport { Effect } from \"effect\";\n\nimport type {\n CleanedWhere,\n CustomAdapter,\n DBAdapter,\n DBAdapterFactoryConfig,\n JoinConfig,\n StorageFailure,\n} from \"../adapter\";\nimport type { DBSchema } from \"../schema\";\nimport { createAdapter } from \"../factory\";\n\ntype Row = Record<string, unknown>;\ntype Store = Record<string, Row[]>;\ntype Comparable = string | number | boolean | Date;\n\nconst compare = (\n a: unknown,\n b: unknown,\n op: \"gt\" | \"gte\" | \"lt\" | \"lte\",\n): boolean => {\n if (\n !(\n typeof a === \"string\" ||\n typeof a === \"number\" ||\n typeof a === \"boolean\" ||\n a instanceof Date\n ) ||\n !(\n typeof b === \"string\" ||\n typeof b === \"number\" ||\n typeof b === \"boolean\" ||\n b instanceof Date\n )\n ) {\n return false;\n }\n const lhs = a as Comparable;\n const rhs = b as Comparable;\n switch (op) {\n case \"gt\":\n return lhs > rhs;\n case \"gte\":\n return lhs >= rhs;\n case \"lt\":\n return lhs < rhs;\n case \"lte\":\n return lhs <= rhs;\n }\n};\n\nconst rowAs = <T>(row: Row): T => row as T;\nconst rowsAs = <T>(rows: readonly Row[]): T[] => rows.map(rowAs<T>);\n\nconst evalClause = (record: Row, clause: CleanedWhere): boolean => {\n const { field, value, operator, mode } = clause;\n const isInsensitive =\n mode === \"insensitive\" &&\n (typeof value === \"string\" ||\n (Array.isArray(value) &&\n (value as unknown[]).every((v) => typeof v === \"string\")));\n\n const lhs = record[field];\n const lowerStr = (v: unknown) =>\n typeof v === \"string\" ? v.toLowerCase() : v;\n\n const cmp = (a: unknown, b: unknown): boolean =>\n isInsensitive ? lowerStr(a) === lowerStr(b) : a === b;\n switch (operator) {\n case \"in\":\n if (!Array.isArray(value)) throw new Error(\"Value must be an array\");\n return (value as unknown[]).some((v) => cmp(lhs, v));\n case \"not_in\":\n if (!Array.isArray(value)) throw new Error(\"Value must be an array\");\n return !(value as unknown[]).some((v) => cmp(lhs, v));\n case \"contains\": {\n if (typeof lhs !== \"string\" || typeof value !== \"string\") return false;\n return isInsensitive\n ? lhs.toLowerCase().includes(value.toLowerCase())\n : lhs.includes(value);\n }\n case \"starts_with\": {\n if (typeof lhs !== \"string\" || typeof value !== \"string\") return false;\n return isInsensitive\n ? lhs.toLowerCase().startsWith(value.toLowerCase())\n : lhs.startsWith(value);\n }\n case \"ends_with\": {\n if (typeof lhs !== \"string\" || typeof value !== \"string\") return false;\n return isInsensitive\n ? lhs.toLowerCase().endsWith(value.toLowerCase())\n : lhs.endsWith(value);\n }\n case \"ne\":\n return !cmp(lhs, value);\n case \"gt\":\n return value != null && compare(lhs, value, \"gt\");\n case \"gte\":\n return value != null && compare(lhs, value, \"gte\");\n case \"lt\":\n return value != null && compare(lhs, value, \"lt\");\n case \"lte\":\n return value != null && compare(lhs, value, \"lte\");\n case \"eq\":\n default:\n return cmp(lhs, value);\n }\n};\n\n// Split-group AND/OR grouping: clauses with `connector: \"AND\"` (or no\n// connector) are conjoined, clauses with `connector: \"OR\"` are disjoined,\n// and the two groups are ANDed together. Mirrors the upstream better-auth\n// drizzle adapter's `convertWhereClause` so every backend (memory, sqlite,\n// postgres) observes the same mixed-connector semantics under the shared\n// conformance suite. This diverges from upstream's *memory* adapter, which\n// still uses a left-to-right fold; we prefer drizzle parity so that a\n// plugin that works against memory always works against SQL.\nconst matchAll = (record: Row, where: readonly CleanedWhere[]): boolean => {\n if (where.length === 0) return true;\n if (where.length === 1) return evalClause(record, where[0]!);\n const andGroup = where.filter(\n (w) => w.connector === \"AND\" || !w.connector,\n );\n const orGroup = where.filter((w) => w.connector === \"OR\");\n const andResult =\n andGroup.length === 0 ? true : andGroup.every((w) => evalClause(record, w));\n const orResult =\n orGroup.length === 0 ? true : orGroup.some((w) => evalClause(record, w));\n return andResult && orResult;\n};\n\nconst filterWhere = (rows: Row[], where: readonly CleanedWhere[]): Row[] =>\n rows.filter((r) => matchAll(r, where));\n\nconst cloneStore = (s: Store): Store => {\n const out: Store = {};\n for (const [k, v] of Object.entries(s)) {\n out[k] = v.map((r) => ({ ...r }));\n }\n return out;\n};\n\n// ---------------------------------------------------------------------------\n// makeMemoryAdapter — builds a DBAdapter wired up through createAdapter.\n// ---------------------------------------------------------------------------\n\nexport interface MakeMemoryAdapterOptions {\n readonly schema: DBSchema;\n readonly adapterId?: string;\n readonly generateId?: () => string;\n}\n\nexport const makeMemoryAdapter = (\n options: MakeMemoryAdapterOptions,\n): DBAdapter => {\n let store: Store = {};\n\n const tableFor = (model: string): Row[] => {\n if (!store[model]) store[model] = [];\n return store[model]!;\n };\n\n // Join resolver — mirrors the upstream memory adapter's path. Given a\n // base row and a resolved JoinConfig, look up matching rows in the\n // target model's table and attach them under the target's logical name.\n // For one-to-one we attach a single row (or null), otherwise we attach\n // an array capped at `limit`.\n const attachJoins = (base: Row, join: JoinConfig): Row => {\n const out: Row = { ...base };\n for (const [target, cfg] of Object.entries(join)) {\n const targetRows = tableFor(target);\n const matches = targetRows.filter(\n (r) => r[cfg.on.to] === base[cfg.on.from],\n );\n if (cfg.relation === \"one-to-one\") {\n out[target] = matches[0] ?? null;\n } else {\n const limit = cfg.limit ?? 100;\n out[target] = matches.slice(0, limit);\n }\n }\n return out;\n };\n\n const findOne: CustomAdapter[\"findOne\"] = <T>({\n model,\n where,\n join,\n }: {\n model: string;\n where: CleanedWhere[];\n select?: string[] | undefined;\n join?: JoinConfig | undefined;\n }) =>\n Effect.sync<T | null>(() => {\n const rows = filterWhere(tableFor(model), where);\n const first = rows[0];\n if (!first) return null;\n return rowAs<T>(join ? attachJoins(first, join) : first);\n });\n\n const findMany: CustomAdapter[\"findMany\"] = <T>({\n model,\n where,\n limit,\n sortBy,\n offset,\n join,\n }: {\n model: string;\n where?: CleanedWhere[] | undefined;\n limit?: number | undefined;\n select?: string[] | undefined;\n sortBy?: { field: string; direction: \"asc\" | \"desc\" } | undefined;\n offset?: number | undefined;\n join?: JoinConfig | undefined;\n }) =>\n Effect.sync<T[]>(() => {\n let rows = filterWhere(tableFor(model), where ?? []);\n if (sortBy) {\n const { field, direction } = sortBy;\n const sign = direction === \"asc\" ? 1 : -1;\n rows = rows.slice().sort((a, b) => {\n const av = a[field];\n const bv = b[field];\n if (av === bv) return 0;\n return compare(av, bv, \"lt\") ? -sign : sign;\n });\n }\n if (offset !== undefined) rows = rows.slice(offset);\n if (limit !== undefined && limit > 0) rows = rows.slice(0, limit);\n if (join) {\n return rowsAs<T>(rows.map((r) => attachJoins(r, join)));\n }\n return rowsAs<T>(rows);\n });\n\n const updateOne: CustomAdapter[\"update\"] = <T>({\n model,\n where,\n update,\n }: {\n model: string;\n where: CleanedWhere[];\n update: T;\n }) =>\n Effect.sync<T | null>(() => {\n const rows = filterWhere(tableFor(model), where);\n const first = rows[0];\n if (!first) return null;\n Object.assign(first, update as Row);\n return rowAs<T>(first);\n });\n\n const custom: CustomAdapter = {\n create: ({ model, data }) =>\n Effect.sync(() => {\n const table = tableFor(model);\n table.push(data as Row);\n return data;\n }),\n\n createMany: ({ model, data }) =>\n Effect.sync(() => {\n const table = tableFor(model);\n for (const row of data) table.push(row as Row);\n return data.slice();\n }),\n\n findOne,\n\n findMany,\n\n count: ({ model, where }) =>\n Effect.sync(() => filterWhere(tableFor(model), where ?? []).length),\n\n update: updateOne,\n\n updateMany: ({ model, where, update }) =>\n Effect.sync(() => {\n const rows = filterWhere(tableFor(model), where);\n for (const r of rows) Object.assign(r, update);\n return rows.length;\n }),\n\n delete: ({ model, where }) =>\n Effect.sync(() => {\n const table = tableFor(model);\n const matches = filterWhere(table, where);\n const first = matches[0];\n if (!first) return;\n const idx = table.indexOf(first);\n if (idx >= 0) table.splice(idx, 1);\n }),\n\n deleteMany: ({ model, where }) =>\n Effect.sync(() => {\n const table = tableFor(model);\n const matches = new Set(filterWhere(table, where));\n let count = 0;\n store[model] = table.filter((r) => {\n if (matches.has(r)) {\n count++;\n return false;\n }\n return true;\n });\n return count;\n }),\n };\n\n // Snapshot-based transaction: clone on entry, restore on failure.\n const txFn: DBAdapterFactoryConfig[\"transaction\"] = <R, E>(\n cb: (trx: Parameters<DBAdapter[\"transaction\"]>[0] extends (\n t: infer T,\n ) => unknown\n ? T\n : never) => Effect.Effect<R, E>,\n ) =>\n Effect.gen(function* () {\n const snapshot = cloneStore(store);\n const result = yield* cb(adapter).pipe(\n Effect.catch((e) => {\n store = snapshot;\n return Effect.fail(e);\n }),\n );\n return result;\n }) as Effect.Effect<R, E | StorageFailure>;\n\n const adapter: DBAdapter = createAdapter({\n schema: options.schema,\n config: {\n adapterId: options.adapterId ?? \"memory\",\n supportsJSON: true,\n supportsDates: true,\n supportsBooleans: true,\n supportsArrays: true,\n customIdGenerator: options.generateId\n ? () => options.generateId!()\n : undefined,\n transaction: txFn,\n },\n adapter: custom,\n });\n\n return adapter;\n};\n","import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { createJiti } from \"jiti\";\nimport type { ExecutorCliConfig } from \"@executor-js/sdk/core\";\n\nconst defaultPaths = [\n \"executor.config.ts\",\n \"executor.config.js\",\n \"src/executor.config.ts\",\n \"src/executor.config.js\",\n];\n\nexport const getConfig = async (opts: {\n cwd: string;\n configPath?: string;\n}): Promise<ExecutorCliConfig | null> => {\n const { cwd, configPath } = opts;\n\n let resolvedPath: string | undefined;\n\n if (configPath) {\n resolvedPath = path.resolve(cwd, configPath);\n if (!existsSync(resolvedPath)) {\n console.error(`Config file not found: ${resolvedPath}`);\n return null;\n }\n } else {\n for (const p of defaultPaths) {\n const candidate = path.resolve(cwd, p);\n if (existsSync(candidate)) {\n resolvedPath = candidate;\n break;\n }\n }\n }\n\n if (!resolvedPath) return null;\n\n const jiti = createJiti(cwd, {\n interopDefault: true,\n moduleCache: false,\n });\n\n const mod = await jiti.import(resolvedPath);\n const config = (mod as { default?: ExecutorCliConfig }).default ?? mod;\n return config as ExecutorCliConfig;\n};\n","// ---------------------------------------------------------------------------\n// Drizzle schema generator — DBSchema → drizzle-orm TS source.\n//\n// Ported from better-auth (packages/cli/src/generators/drizzle.ts) under\n// MIT. Adapted for executor:\n// - Reads our DBSchema shape (modelName optional, key = default)\n// - No auth-specific logic (uuid/serial id modes, usePlural, camelCase)\n// - Always emits text primary keys\n// - Dialect from ExecutorCliConfig, not from adapter.options.provider\n// ---------------------------------------------------------------------------\n\nimport { existsSync } from \"node:fs\";\nimport type { DBSchema, DBFieldAttribute } from \"@executor-js/storage-core\";\nimport type { SchemaGenerator } from \"./types.js\";\n\ntype Dialect = \"pg\" | \"sqlite\" | \"mysql\";\n\nconst getModelName = (key: string, def: DBSchema[string]): string =>\n def.modelName ?? key;\n\nconst getType = (\n name: string,\n field: DBFieldAttribute,\n dialect: Dialect,\n): string => {\n if (field.references?.field === \"id\") {\n return `text('${name}')`;\n }\n\n const type = field.type;\n\n if (typeof type !== \"string\") {\n // Enum array — e.g. [\"active\", \"inactive\"]\n if (Array.isArray(type) && type.every((x) => typeof x === \"string\")) {\n return {\n sqlite: `text({ enum: [${type.map((x) => `'${x}'`).join(\", \")}] })`,\n pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(\", \")}] })`,\n mysql: `mysqlEnum([${type.map((x) => `'${x}'`).join(\", \")}])`,\n }[dialect];\n }\n throw new TypeError(\n `Invalid field type for field ${name}`,\n );\n }\n\n const typeMap: Record<string, Record<Dialect, string>> = {\n string: {\n sqlite: `text('${name}')`,\n pg: `text('${name}')`,\n mysql: field.unique\n ? `varchar('${name}', { length: 255 })`\n : field.references\n ? `varchar('${name}', { length: 36 })`\n : field.sortable\n ? `varchar('${name}', { length: 255 })`\n : field.index\n ? `varchar('${name}', { length: 255 })`\n : `text('${name}')`,\n },\n boolean: {\n sqlite: `integer('${name}', { mode: 'boolean' })`,\n pg: `boolean('${name}')`,\n mysql: `boolean('${name}')`,\n },\n number: {\n sqlite: `integer('${name}')`,\n pg: field.bigint\n ? `bigint('${name}', { mode: 'number' })`\n : `integer('${name}')`,\n mysql: field.bigint\n ? `bigint('${name}', { mode: 'number' })`\n : `int('${name}')`,\n },\n date: {\n sqlite: `integer('${name}', { mode: 'timestamp_ms' })`,\n pg: `timestamp('${name}')`,\n mysql: `timestamp('${name}', { fsp: 3 })`,\n },\n \"number[]\": {\n sqlite: `text('${name}', { mode: \"json\" })`,\n pg: field.bigint\n ? `bigint('${name}', { mode: 'number' }).array()`\n : `integer('${name}').array()`,\n mysql: `text('${name}', { mode: 'json' })`,\n },\n \"string[]\": {\n sqlite: `text('${name}', { mode: \"json\" })`,\n pg: `text('${name}').array()`,\n mysql: `text('${name}', { mode: \"json\" })`,\n },\n json: {\n sqlite: `text('${name}', { mode: \"json\" })`,\n pg: `jsonb('${name}')`,\n mysql: `json('${name}', { mode: \"json\" })`,\n },\n };\n\n const dbTypeMap = typeMap[type as string];\n if (!dbTypeMap) {\n throw new Error(\n `Unsupported field type '${field.type}' for field '${name}'.`,\n );\n }\n return dbTypeMap[dialect];\n};\n\n// ---------------------------------------------------------------------------\n// Generator\n// ---------------------------------------------------------------------------\n\nexport const generateDrizzleSchema: SchemaGenerator = async ({\n schema,\n dialect,\n file,\n}) => {\n const filePath = file || \"./executor-schema.ts\";\n const fileExist = existsSync(filePath);\n\n let code = generateImport({ dialect, schema });\n\n for (const [tableKey, tableDef] of Object.entries(schema)) {\n const modelName = getModelName(tableKey, tableDef);\n const fields = tableDef.fields;\n\n // Scoped tables get a composite `(scope_id, id)` primary key so two\n // tenants can register rows with the same user-facing id without\n // colliding on a globally-unique PK. Single-column PK stays for\n // unscoped tables (conformance fixtures, the blob store, etc.).\n const hasScopeId = Object.prototype.hasOwnProperty.call(fields, \"scope_id\");\n const id = hasScopeId ? `text('id').notNull()` : `text('id').primaryKey()`;\n\n type TableExtra =\n | { kind: \"uniqueIndex\" | \"index\"; name: string; on: string | readonly string[] }\n | { kind: \"primaryKey\"; columns: readonly string[] };\n const extras: TableExtra[] = [];\n\n const assignExtras = (items: TableExtra[]): string => {\n if (!items.length) return \"\";\n const lines: string[] = [`, (table) => [`];\n for (const item of items) {\n if (item.kind === \"primaryKey\") {\n const cols = item.columns.map((c) => `table.${c}`).join(\", \");\n lines.push(` primaryKey({ columns: [${cols}] }),`);\n } else {\n const cols = Array.isArray(item.on)\n ? item.on.map((c) => `table.${c}`).join(\", \")\n : `table.${item.on}`;\n lines.push(` ${item.kind}(\"${item.name}\").on(${cols}),`);\n }\n }\n lines.push(`]`);\n return lines.join(\"\\n\");\n };\n\n if (hasScopeId) {\n extras.push({ kind: \"primaryKey\", columns: [\"scope_id\", \"id\"] });\n }\n\n const fieldLines = Object.entries(fields)\n .filter(([fieldName]) => fieldName !== \"id\")\n .map(([fieldName, attr]) => {\n const physical = attr.fieldName ?? fieldName;\n\n const isToolPolicyCompositeField =\n tableKey === \"tool_policy\" &&\n (physical === \"scope_id\" || physical === \"position\");\n\n if (attr.index && !attr.unique && !isToolPolicyCompositeField) {\n extras.push({\n kind: \"index\",\n name: `${tableKey}_${physical}_idx`,\n on: physical,\n });\n } else if (attr.index && attr.unique) {\n extras.push({\n kind: \"uniqueIndex\",\n name: `${tableKey}_${physical}_uidx`,\n on: physical,\n });\n }\n\n let col = getType(physical, attr, dialect);\n\n if (\n attr.defaultValue !== null &&\n typeof attr.defaultValue !== \"undefined\"\n ) {\n if (typeof attr.defaultValue === \"function\") {\n if (\n attr.type === \"date\" &&\n attr.defaultValue.toString().includes(\"new Date()\")\n ) {\n if (dialect === \"sqlite\") {\n col += `.default(sql\\`(cast(unixepoch('subsecond') * 1000 as integer))\\`)`;\n } else {\n col += `.defaultNow()`;\n }\n }\n } else if (typeof attr.defaultValue === \"string\") {\n col += `.default(\"${attr.defaultValue}\")`;\n } else {\n col += `.default(${attr.defaultValue})`;\n }\n }\n\n if (attr.onUpdate && attr.type === \"date\") {\n if (typeof attr.onUpdate === \"function\") {\n col += `.$onUpdate(${attr.onUpdate})`;\n }\n }\n\n return `${physical}: ${col}${attr.required !== false ? \".notNull()\" : \"\"}${\n attr.unique ? \".unique()\" : \"\"\n }${\n attr.references\n ? `.references(()=> ${attr.references.model}.${attr.references.field ?? \"id\"}, { onDelete: '${\n attr.references.onDelete || \"cascade\"\n }' })`\n : \"\"\n }`;\n })\n .join(\",\\n \");\n\n if (tableKey === \"tool_policy\") {\n extras.push({\n kind: \"index\",\n name: \"tool_policy_scope_id_position_idx\",\n on: [\"scope_id\", \"position\"],\n });\n }\n\n const tableSchema = `export const ${tableKey} = ${dialect}Table(\"${modelName}\", {\n id: ${id},\n ${fieldLines}\n}${assignExtras(extras)});`;\n\n code += `\\n${tableSchema}\\n`;\n }\n\n // ---------------------------------------------------------------------------\n // Relations — scan FKs in both directions\n // ---------------------------------------------------------------------------\n\n let relationsString = \"\";\n for (const [tableKey, tableDef] of Object.entries(schema)) {\n const modelName = tableKey;\n\n type Relation = {\n key: string;\n model: string;\n type: \"one\" | \"many\";\n reference?: {\n field: string;\n references: string;\n fieldName: string;\n };\n };\n\n const oneRelations: Relation[] = [];\n const manyRelations: Relation[] = [];\n const manyRelationsSet = new Set<string>();\n\n // Find all FKs in THIS table → \"one\" relations\n for (const [fieldName, field] of Object.entries(tableDef.fields)) {\n if (!field.references) continue;\n const referencedModel = field.references.model;\n const physical = field.fieldName ?? fieldName;\n const fieldRef = `${tableKey}.${physical}`;\n const referenceRef = `${referencedModel}.${field.references.field || \"id\"}`;\n\n oneRelations.push({\n key: referencedModel,\n model: referencedModel,\n type: \"one\",\n reference: {\n field: fieldRef,\n references: referenceRef,\n fieldName,\n },\n });\n }\n\n // Find all OTHER tables that reference THIS table → \"many\" relations\n for (const [otherKey, otherDef] of Object.entries(schema)) {\n if (otherKey === tableKey) continue;\n const hasFK = Object.values(otherDef.fields).some(\n (field) => field.references?.model === tableKey,\n );\n if (!hasFK) continue;\n\n const relationKey = `${otherKey}s`;\n if (!manyRelationsSet.has(relationKey)) {\n manyRelationsSet.add(relationKey);\n manyRelations.push({\n key: relationKey,\n model: otherKey,\n type: \"many\",\n });\n }\n }\n\n // Detect duplicates\n const relationsByModel = new Map<string, Relation[]>();\n for (const rel of oneRelations) {\n if (!rel.reference) continue;\n const arr = relationsByModel.get(rel.key) ?? [];\n arr.push(rel);\n relationsByModel.set(rel.key, arr);\n }\n\n const duplicateRelations: Relation[] = [];\n const singleRelations: Relation[] = [];\n\n for (const [, rels] of relationsByModel.entries()) {\n if (rels.length > 1) {\n duplicateRelations.push(...rels);\n } else {\n singleRelations.push(rels[0]!);\n }\n }\n\n // Duplicate relations get field-specific exports\n for (const rel of duplicateRelations) {\n if (!rel.reference) continue;\n const relExportName = `${modelName}${rel.reference.fieldName.charAt(0).toUpperCase() + rel.reference.fieldName.slice(1)}Relations`;\n const block = `export const ${relExportName} = relations(${modelName}, ({ one }) => ({\n ${rel.key}: one(${rel.model}, {\n fields: [${rel.reference.field}],\n references: [${rel.reference.references}],\n })\n}))`;\n relationsString += `\\n${block}\\n`;\n }\n\n // Combined single relations\n const hasOne = singleRelations.length > 0;\n const hasMany = manyRelations.length > 0;\n\n if (hasOne || hasMany) {\n const destructured = [\n hasOne ? \"one\" : \"\",\n hasMany ? \"many\" : \"\",\n ]\n .filter(Boolean)\n .join(\", \");\n\n const body = [\n ...singleRelations\n .filter((r) => r.reference)\n .map(\n (r) =>\n ` ${r.key}: one(${r.model}, {\\n fields: [${r.reference!.field}],\\n references: [${r.reference!.references}],\\n })`,\n ),\n ...manyRelations.map(\n ({ key, model }) => ` ${key}: many(${model})`,\n ),\n ].join(\",\\n\");\n\n const block = `export const ${modelName}Relations = relations(${modelName}, ({ ${destructured} }) => ({\n${body}\n}))`;\n relationsString += `\\n${block}\\n`;\n }\n }\n\n code += `\\n${relationsString}`;\n\n return {\n code,\n fileName: filePath,\n overwrite: fileExist,\n };\n};\n\n// ---------------------------------------------------------------------------\n// Import generation — only emit what's actually used\n// ---------------------------------------------------------------------------\n\nfunction generateImport({\n dialect,\n schema,\n}: {\n dialect: Dialect;\n schema: DBSchema;\n}) {\n const rootImports: string[] = [];\n const coreImports: string[] = [];\n\n let hasBigint = false;\n let hasJson = false;\n let hasBoolean = false;\n let hasNumber = false;\n let hasDate = false;\n let hasIndex = false;\n let hasUniqueIndex = false;\n let hasReferences = false;\n let hasCompositePrimaryKey = false;\n\n for (const [tableKey, table] of Object.entries(schema)) {\n for (const field of Object.values(table.fields)) {\n if (field.bigint) hasBigint = true;\n if (field.type === \"json\") hasJson = true;\n if (field.type === \"boolean\") hasBoolean = true;\n if (\n (field.type === \"number\" && !field.bigint) ||\n field.type === \"number[]\"\n ) {\n hasNumber = true;\n }\n if (field.type === \"date\") hasDate = true;\n if (field.index && !field.unique) hasIndex = true;\n if (field.index && field.unique) hasUniqueIndex = true;\n if (field.references) hasReferences = true;\n }\n // Scoped tables get a composite (scope_id, id) PK — see generator\n // body where `primaryKey({ columns: [...] })` is emitted.\n if (Object.prototype.hasOwnProperty.call(table.fields, \"scope_id\")) {\n hasCompositePrimaryKey = true;\n }\n // Keep the generator silent about tableKey in this pass — we only\n // need the existence check above. Referenced here to satisfy lint.\n void tableKey;\n }\n\n coreImports.push(`${dialect}Table`);\n coreImports.push(\"text\");\n\n if (hasBoolean && dialect !== \"sqlite\") coreImports.push(\"boolean\");\n if (hasDate) {\n if (dialect === \"pg\") coreImports.push(\"timestamp\");\n // sqlite uses integer for timestamps, pg uses timestamp\n }\n if (hasNumber || dialect === \"sqlite\") {\n if (dialect === \"pg\") coreImports.push(\"integer\");\n else if (dialect === \"mysql\") coreImports.push(\"int\");\n else coreImports.push(\"integer\");\n }\n if (hasBigint && dialect !== \"sqlite\") coreImports.push(\"bigint\");\n if (hasJson) {\n if (dialect === \"pg\") coreImports.push(\"jsonb\");\n else if (dialect === \"mysql\") coreImports.push(\"json\");\n // sqlite uses text for JSON\n }\n if (hasIndex) coreImports.push(\"index\");\n if (hasUniqueIndex) coreImports.push(\"uniqueIndex\");\n if (hasCompositePrimaryKey) coreImports.push(\"primaryKey\");\n\n // sqlite needs integer for boolean + date\n if (dialect === \"sqlite\" && (hasBoolean || hasDate)) {\n if (!coreImports.includes(\"integer\")) coreImports.push(\"integer\");\n }\n // sqlite needs real for number\n if (dialect === \"sqlite\" && hasNumber) {\n // better-auth uses integer for numbers on sqlite; we use real()\n // for floating-point fidelity.\n }\n\n // Has any timestamp with defaultNow function?\n const hasSqliteTimestamp =\n dialect === \"sqlite\" &&\n Object.values(schema).some((table) =>\n Object.values(table.fields).some(\n (field) =>\n field.type === \"date\" &&\n field.defaultValue &&\n typeof field.defaultValue === \"function\" &&\n field.defaultValue.toString().includes(\"new Date()\"),\n ),\n );\n\n if (hasSqliteTimestamp) {\n rootImports.push(\"sql\");\n }\n\n if (hasReferences || dialect === \"mysql\") {\n // mysql might need varchar for FK fields\n }\n\n // `relations` is only imported when the schema has any references that\n // produce relation blocks (see relationsString generation).\n if (hasReferences) rootImports.push(\"relations\");\n\n const filteredCore = coreImports\n .map((x) => x.trim())\n .filter((x) => x !== \"\");\n\n // Deduplicate\n const uniqueCore = [...new Set(filteredCore)];\n const uniqueRoot = [...new Set(rootImports)];\n\n return `${uniqueRoot.length > 0 ? `import { ${uniqueRoot.join(\", \")} } from \"drizzle-orm\";\\n` : \"\"}import { ${uniqueCore.join(\", \")} } from \"drizzle-orm/${dialect}-core\";\\n`;\n}\n"],"mappings":"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","names":["Command","existsSync","path","Schema","Schema","Data","Schema","Schema","Data","Schema","Schema","Schema","Schema","Schema","Schema","Effect","Schema","Data","Schema","Schema","Data","Schema","Schema","Effect","Effect","Schema","Schema","Effect","Schema","Data","Effect","Data","Effect","Schema","Data","Effect","Schema","Data","Schema","Schema","Effect","Effect","Result","Schema","Effect","Effect","existsSync","path","existsSync","Command"]}
|