diff --git a/apps/local/drizzle/0010_normalize_google_discovery.sql b/apps/local/drizzle/0010_normalize_google_discovery.sql new file mode 100644 index 00000000..abd2dcbd --- /dev/null +++ b/apps/local/drizzle/0010_normalize_google_discovery.sql @@ -0,0 +1,105 @@ +-- Normalize google-discovery plugin: lift the GoogleDiscoveryAuth and +-- the credentials.{headers,queryParams} SecretBackedMaps out of +-- google_discovery_source.config JSON. +-- +-- Old shape: +-- google_discovery_source.config (json) — GoogleDiscoveryStoredSourceData +-- with `auth: {kind:"none"} | {kind:"oauth2", connectionId, clientId..., clientSecret..., scopes}` +-- and optional `credentials: { headers?, queryParams? }` +-- +-- New shape: +-- google_discovery_source gains: auth_kind, auth_connection_id, +-- auth_client_id_secret_id, auth_client_secret_secret_id, auth_scopes. +-- google_discovery_source_credential_header / _query_param: child +-- tables for the SecretBackedMap entries. + +CREATE TABLE `google_discovery_source_credential_header` ( + `id` text NOT NULL, + `scope_id` text NOT NULL, + `source_id` text NOT NULL, + `name` text NOT NULL, + `kind` text NOT NULL, + `text_value` text, + `secret_id` text, + `secret_prefix` text, + PRIMARY KEY(`scope_id`, `id`) +); +--> statement-breakpoint +CREATE INDEX `google_discovery_source_credential_header_scope_id_idx` ON `google_discovery_source_credential_header` (`scope_id`);--> statement-breakpoint +CREATE INDEX `google_discovery_source_credential_header_source_id_idx` ON `google_discovery_source_credential_header` (`source_id`);--> statement-breakpoint +CREATE INDEX `google_discovery_source_credential_header_secret_id_idx` ON `google_discovery_source_credential_header` (`secret_id`);--> statement-breakpoint + +CREATE TABLE `google_discovery_source_credential_query_param` ( + `id` text NOT NULL, + `scope_id` text NOT NULL, + `source_id` text NOT NULL, + `name` text NOT NULL, + `kind` text NOT NULL, + `text_value` text, + `secret_id` text, + `secret_prefix` text, + PRIMARY KEY(`scope_id`, `id`) +); +--> statement-breakpoint +CREATE INDEX `google_discovery_source_credential_query_param_scope_id_idx` ON `google_discovery_source_credential_query_param` (`scope_id`);--> statement-breakpoint +CREATE INDEX `google_discovery_source_credential_query_param_source_id_idx` ON `google_discovery_source_credential_query_param` (`source_id`);--> statement-breakpoint +CREATE INDEX `google_discovery_source_credential_query_param_secret_id_idx` ON `google_discovery_source_credential_query_param` (`secret_id`);--> statement-breakpoint + +ALTER TABLE `google_discovery_source` ADD `auth_kind` text DEFAULT 'none' NOT NULL;--> statement-breakpoint +ALTER TABLE `google_discovery_source` ADD `auth_connection_id` text;--> statement-breakpoint +ALTER TABLE `google_discovery_source` ADD `auth_client_id_secret_id` text;--> statement-breakpoint +ALTER TABLE `google_discovery_source` ADD `auth_client_secret_secret_id` text;--> statement-breakpoint +ALTER TABLE `google_discovery_source` ADD `auth_scopes` text;--> statement-breakpoint +CREATE INDEX `google_discovery_source_auth_connection_id_idx` ON `google_discovery_source` (`auth_connection_id`);--> statement-breakpoint +CREATE INDEX `google_discovery_source_auth_client_id_secret_id_idx` ON `google_discovery_source` (`auth_client_id_secret_id`);--> statement-breakpoint +CREATE INDEX `google_discovery_source_auth_client_secret_secret_id_idx` ON `google_discovery_source` (`auth_client_secret_secret_id`);--> statement-breakpoint + +-- Backfill auth columns from config.auth. +UPDATE `google_discovery_source` +SET + `auth_kind` = COALESCE(json_extract(`config`, '$.auth.kind'), 'none'), + `auth_connection_id` = json_extract(`config`, '$.auth.connectionId'), + `auth_client_id_secret_id` = json_extract(`config`, '$.auth.clientIdSecretId'), + `auth_client_secret_secret_id` = json_extract(`config`, '$.auth.clientSecretSecretId'), + `auth_scopes` = json_extract(`config`, '$.auth.scopes') +WHERE `config` IS NOT NULL;--> statement-breakpoint + +-- Backfill credential header / query_param child rows. +INSERT OR IGNORE INTO `google_discovery_source_credential_header` + (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) +SELECT + s.`scope_id`, + s.`id` || ':' || h.`key`, + s.`id`, + h.`key`, + CASE + WHEN h.`type` = 'object' AND json_extract(h.`value`, '$.secretId') IS NOT NULL THEN 'secret' + ELSE 'text' + END, + CASE WHEN h.`type` = 'object' THEN NULL ELSE h.`value` END, + CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.secretId') ELSE NULL END, + CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.prefix') ELSE NULL END +FROM `google_discovery_source` s, json_each(json_extract(s.`config`, '$.credentials.headers')) h +WHERE json_extract(s.`config`, '$.credentials.headers') IS NOT NULL;--> statement-breakpoint + +INSERT OR IGNORE INTO `google_discovery_source_credential_query_param` + (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) +SELECT + s.`scope_id`, + s.`id` || ':' || q.`key`, + s.`id`, + q.`key`, + CASE + WHEN q.`type` = 'object' AND json_extract(q.`value`, '$.secretId') IS NOT NULL THEN 'secret' + ELSE 'text' + END, + CASE WHEN q.`type` = 'object' THEN NULL ELSE q.`value` END, + CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.secretId') ELSE NULL END, + CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.prefix') ELSE NULL END +FROM `google_discovery_source` s, json_each(json_extract(s.`config`, '$.credentials.queryParams')) q +WHERE json_extract(s.`config`, '$.credentials.queryParams') IS NOT NULL;--> statement-breakpoint + +-- Strip the extracted fields from the legacy config JSON. +UPDATE `google_discovery_source` +SET `config` = json_remove(`config`, '$.auth', '$.credentials') +WHERE `config` IS NOT NULL; diff --git a/apps/local/drizzle/meta/0010_snapshot.json b/apps/local/drizzle/meta/0010_snapshot.json new file mode 100644 index 00000000..469d999a --- /dev/null +++ b/apps/local/drizzle/meta/0010_snapshot.json @@ -0,0 +1,2422 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "44444444-5555-6666-7777-888888888888", + "prevId": "33333333-4444-5555-6666-777777777777", + "tables": { + "blob": { + "name": "blob", + "columns": { + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "blob_namespace_key_pk": { + "columns": [ + "namespace", + "key" + ], + "name": "blob_namespace_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "connection": { + "name": "connection", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_secret_id": { + "name": "access_token_secret_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token_secret_id": { + "name": "refresh_token_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_state": { + "name": "provider_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "connection_scope_id_idx": { + "name": "connection_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "connection_provider_idx": { + "name": "connection_provider_idx", + "columns": [ + "provider" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connection_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "connection_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "definition": { + "name": "definition", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema": { + "name": "schema", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "definition_scope_id_idx": { + "name": "definition_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "definition_source_id_idx": { + "name": "definition_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "definition_plugin_id_idx": { + "name": "definition_plugin_id_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "definition_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "definition_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "google_discovery_binding": { + "name": "google_discovery_binding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "binding": { + "name": "binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "google_discovery_binding_scope_id_idx": { + "name": "google_discovery_binding_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "google_discovery_binding_source_id_idx": { + "name": "google_discovery_binding_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "google_discovery_binding_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "google_discovery_binding_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "google_discovery_source": { + "name": "google_discovery_source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "auth_connection_id": { + "name": "auth_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_client_id_secret_id": { + "name": "auth_client_id_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_client_secret_secret_id": { + "name": "auth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_scopes": { + "name": "auth_scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "google_discovery_source_scope_id_idx": { + "name": "google_discovery_source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "google_discovery_source_auth_connection_id_idx": { + "name": "google_discovery_source_auth_connection_id_idx", + "columns": [ + "auth_connection_id" + ], + "isUnique": false + }, + "google_discovery_source_auth_client_id_secret_id_idx": { + "name": "google_discovery_source_auth_client_id_secret_id_idx", + "columns": [ + "auth_client_id_secret_id" + ], + "isUnique": false + }, + "google_discovery_source_auth_client_secret_secret_id_idx": { + "name": "google_discovery_source_auth_client_secret_secret_id_idx", + "columns": [ + "auth_client_secret_secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "google_discovery_source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "google_discovery_source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graphql_operation": { + "name": "graphql_operation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "binding": { + "name": "binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "graphql_operation_scope_id_idx": { + "name": "graphql_operation_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "graphql_operation_source_id_idx": { + "name": "graphql_operation_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_operation_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "graphql_operation_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graphql_source": { + "name": "graphql_source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "auth_connection_id": { + "name": "auth_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "graphql_source_scope_id_idx": { + "name": "graphql_source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "graphql_source_auth_connection_id_idx": { + "name": "graphql_source_auth_connection_id_idx", + "columns": [ + "auth_connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "graphql_source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_binding": { + "name": "mcp_binding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "binding": { + "name": "binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_binding_scope_id_idx": { + "name": "mcp_binding_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "mcp_binding_source_id_idx": { + "name": "mcp_binding_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_binding_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "mcp_binding_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_source": { + "name": "mcp_source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "auth_header_name": { + "name": "auth_header_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_secret_id": { + "name": "auth_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_secret_prefix": { + "name": "auth_secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_connection_id": { + "name": "auth_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_client_id_secret_id": { + "name": "auth_client_id_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_client_secret_secret_id": { + "name": "auth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "mcp_source_scope_id_idx": { + "name": "mcp_source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "mcp_source_auth_secret_id_idx": { + "name": "mcp_source_auth_secret_id_idx", + "columns": [ + "auth_secret_id" + ], + "isUnique": false + }, + "mcp_source_auth_connection_id_idx": { + "name": "mcp_source_auth_connection_id_idx", + "columns": [ + "auth_connection_id" + ], + "isUnique": false + }, + "mcp_source_auth_client_id_secret_id_idx": { + "name": "mcp_source_auth_client_id_secret_id_idx", + "columns": [ + "auth_client_id_secret_id" + ], + "isUnique": false + }, + "mcp_source_auth_client_secret_secret_id_idx": { + "name": "mcp_source_auth_client_secret_secret_id_idx", + "columns": [ + "auth_client_secret_secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "mcp_source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth2_session": { + "name": "oauth2_session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_scope": { + "name": "token_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauth2_session_scope_id_idx": { + "name": "oauth2_session_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "oauth2_session_plugin_id_idx": { + "name": "oauth2_session_plugin_id_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + }, + "oauth2_session_connection_id_idx": { + "name": "oauth2_session_connection_id_idx", + "columns": [ + "connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "oauth2_session_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "oauth2_session_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_operation": { + "name": "openapi_operation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "binding": { + "name": "binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "openapi_operation_scope_id_idx": { + "name": "openapi_operation_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "openapi_operation_source_id_idx": { + "name": "openapi_operation_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_operation_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_operation_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source": { + "name": "openapi_source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "spec": { + "name": "spec", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth2": { + "name": "oauth2", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_scope_id_idx": { + "name": "openapi_source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source_binding": { + "name": "openapi_source_binding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_scope_id": { + "name": "source_scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_scope_id": { + "name": "target_scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slot": { + "name": "slot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_binding_source_id_idx": { + "name": "openapi_source_binding_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "openapi_source_binding_source_scope_id_idx": { + "name": "openapi_source_binding_source_scope_id_idx", + "columns": [ + "source_scope_id" + ], + "isUnique": false + }, + "openapi_source_binding_target_scope_id_idx": { + "name": "openapi_source_binding_target_scope_id_idx", + "columns": [ + "target_scope_id" + ], + "isUnique": false + }, + "openapi_source_binding_slot_idx": { + "name": "openapi_source_binding_slot_idx", + "columns": [ + "slot" + ], + "isUnique": false + }, + "openapi_source_binding_secret_id_idx": { + "name": "openapi_source_binding_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + }, + "openapi_source_binding_connection_id_idx": { + "name": "openapi_source_binding_connection_id_idx", + "columns": [ + "connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "secret": { + "name": "secret", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owned_by_connection_id": { + "name": "owned_by_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "secret_scope_id_idx": { + "name": "secret_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "secret_provider_idx": { + "name": "secret_provider_idx", + "columns": [ + "provider" + ], + "isUnique": false + }, + "secret_owned_by_connection_id_idx": { + "name": "secret_owned_by_connection_id_idx", + "columns": [ + "owned_by_connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "secret_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "secret_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "source": { + "name": "source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "can_remove": { + "name": "can_remove", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "can_edit": { + "name": "can_edit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "source_scope_id_idx": { + "name": "source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "source_plugin_id_idx": { + "name": "source_plugin_id_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tool": { + "name": "tool", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_schema": { + "name": "input_schema", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_schema": { + "name": "output_schema", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tool_scope_id_idx": { + "name": "tool_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "tool_source_id_idx": { + "name": "tool_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "tool_plugin_id_idx": { + "name": "tool_plugin_id_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "tool_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tool_policy": { + "name": "tool_policy", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tool_policy_scope_id_position_idx": { + "name": "tool_policy_scope_id_position_idx", + "columns": [ + "scope_id", + "position" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_policy_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "tool_policy_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graphql_source_header": { + "name": "graphql_source_header", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "graphql_source_header_scope_id_idx": { + "name": "graphql_source_header_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "graphql_source_header_source_id_idx": { + "name": "graphql_source_header_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "graphql_source_header_secret_id_idx": { + "name": "graphql_source_header_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_header_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "graphql_source_header_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graphql_source_query_param": { + "name": "graphql_source_query_param", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "graphql_source_query_param_scope_id_idx": { + "name": "graphql_source_query_param_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "graphql_source_query_param_source_id_idx": { + "name": "graphql_source_query_param_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "graphql_source_query_param_secret_id_idx": { + "name": "graphql_source_query_param_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_query_param_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "graphql_source_query_param_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source_query_param": { + "name": "openapi_source_query_param", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_query_param_scope_id_idx": { + "name": "openapi_source_query_param_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "openapi_source_query_param_source_id_idx": { + "name": "openapi_source_query_param_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "openapi_source_query_param_secret_id_idx": { + "name": "openapi_source_query_param_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_query_param_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_source_query_param_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source_spec_fetch_header": { + "name": "openapi_source_spec_fetch_header", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_spec_fetch_header_scope_id_idx": { + "name": "openapi_source_spec_fetch_header_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "openapi_source_spec_fetch_header_source_id_idx": { + "name": "openapi_source_spec_fetch_header_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "openapi_source_spec_fetch_header_secret_id_idx": { + "name": "openapi_source_spec_fetch_header_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_spec_fetch_header_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_source_spec_fetch_header_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source_spec_fetch_query_param": { + "name": "openapi_source_spec_fetch_query_param", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_spec_fetch_query_param_scope_id_idx": { + "name": "openapi_source_spec_fetch_query_param_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "openapi_source_spec_fetch_query_param_source_id_idx": { + "name": "openapi_source_spec_fetch_query_param_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "openapi_source_spec_fetch_query_param_secret_id_idx": { + "name": "openapi_source_spec_fetch_query_param_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_spec_fetch_query_param_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_source_spec_fetch_query_param_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_source_header": { + "name": "mcp_source_header", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "mcp_source_header_scope_id_idx": { + "name": "mcp_source_header_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "mcp_source_header_source_id_idx": { + "name": "mcp_source_header_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "mcp_source_header_secret_id_idx": { + "name": "mcp_source_header_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_source_header_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "mcp_source_header_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_source_query_param": { + "name": "mcp_source_query_param", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "mcp_source_query_param_scope_id_idx": { + "name": "mcp_source_query_param_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "mcp_source_query_param_source_id_idx": { + "name": "mcp_source_query_param_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "mcp_source_query_param_secret_id_idx": { + "name": "mcp_source_query_param_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_source_query_param_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "mcp_source_query_param_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "google_discovery_source_credential_header": { + "name": "google_discovery_source_credential_header", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "google_discovery_source_credential_header_scope_id_idx": { + "name": "google_discovery_source_credential_header_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "google_discovery_source_credential_header_source_id_idx": { + "name": "google_discovery_source_credential_header_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "google_discovery_source_credential_header_secret_id_idx": { + "name": "google_discovery_source_credential_header_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "google_discovery_source_credential_header_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "google_discovery_source_credential_header_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "google_discovery_source_credential_query_param": { + "name": "google_discovery_source_credential_query_param", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "google_discovery_source_credential_query_param_scope_id_idx": { + "name": "google_discovery_source_credential_query_param_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "google_discovery_source_credential_query_param_source_id_idx": { + "name": "google_discovery_source_credential_query_param_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "google_discovery_source_credential_query_param_secret_id_idx": { + "name": "google_discovery_source_credential_query_param_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "google_discovery_source_credential_query_param_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "google_discovery_source_credential_query_param_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/apps/local/drizzle/meta/_journal.json b/apps/local/drizzle/meta/_journal.json index 704c6104..76bffcb3 100644 --- a/apps/local/drizzle/meta/_journal.json +++ b/apps/local/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1778100000002, "tag": "0009_normalize_mcp", "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1778100000003, + "tag": "0010_normalize_google_discovery", + "breakpoints": true } ] } diff --git a/apps/local/src/server/executor-schema.ts b/apps/local/src/server/executor-schema.ts index 22a91d46..1cd37372 100644 --- a/apps/local/src/server/executor-schema.ts +++ b/apps/local/src/server/executor-schema.ts @@ -279,11 +279,51 @@ export const google_discovery_source = sqliteTable("google_discovery_source", { scope_id: text('scope_id').notNull(), name: text('name').notNull(), config: text('config', { mode: "json" }).notNull(), + auth_kind: text({ enum: ['none', 'oauth2'] }).default("none").notNull(), + auth_connection_id: text('auth_connection_id'), + auth_client_id_secret_id: text('auth_client_id_secret_id'), + auth_client_secret_secret_id: text('auth_client_secret_secret_id'), + auth_scopes: text('auth_scopes', { mode: "json" }), created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(), updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull() }, (table) => [ primaryKey({ columns: [table.scope_id, table.id] }), index("google_discovery_source_scope_id_idx").on(table.scope_id), + index("google_discovery_source_auth_connection_id_idx").on(table.auth_connection_id), + index("google_discovery_source_auth_client_id_secret_id_idx").on(table.auth_client_id_secret_id), + index("google_discovery_source_auth_client_secret_secret_id_idx").on(table.auth_client_secret_secret_id), +]); + +export const google_discovery_source_credential_header = sqliteTable("google_discovery_source_credential_header", { + id: text('id').notNull(), + scope_id: text('scope_id').notNull(), + source_id: text('source_id').notNull(), + name: text('name').notNull(), + kind: text({ enum: ['text', 'secret'] }).notNull(), + text_value: text('text_value'), + secret_id: text('secret_id'), + secret_prefix: text('secret_prefix') +}, (table) => [ + primaryKey({ columns: [table.scope_id, table.id] }), + index("google_discovery_source_credential_header_scope_id_idx").on(table.scope_id), + index("google_discovery_source_credential_header_source_id_idx").on(table.source_id), + index("google_discovery_source_credential_header_secret_id_idx").on(table.secret_id), +]); + +export const google_discovery_source_credential_query_param = sqliteTable("google_discovery_source_credential_query_param", { + id: text('id').notNull(), + scope_id: text('scope_id').notNull(), + source_id: text('source_id').notNull(), + name: text('name').notNull(), + kind: text({ enum: ['text', 'secret'] }).notNull(), + text_value: text('text_value'), + secret_id: text('secret_id'), + secret_prefix: text('secret_prefix') +}, (table) => [ + primaryKey({ columns: [table.scope_id, table.id] }), + index("google_discovery_source_credential_query_param_scope_id_idx").on(table.scope_id), + index("google_discovery_source_credential_query_param_source_id_idx").on(table.source_id), + index("google_discovery_source_credential_query_param_secret_id_idx").on(table.secret_id), ]); export const google_discovery_binding = sqliteTable("google_discovery_binding", { diff --git a/apps/local/src/server/migrate-google-discovery-bindings.test.ts b/apps/local/src/server/migrate-google-discovery-bindings.test.ts new file mode 100644 index 00000000..1e8b709e --- /dev/null +++ b/apps/local/src/server/migrate-google-discovery-bindings.test.ts @@ -0,0 +1,236 @@ +// End-to-end test for `0010_normalize_google_discovery.sql`. Seeds a +// google_discovery_source row with the legacy json shape (config +// containing auth/credentials), runs the migration, asserts the new +// columns and child tables are populated. + +import { describe, expect, it } from "@effect/vitest"; +import { Database } from "bun:sqlite"; +import { mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { drizzle } from "drizzle-orm/bun-sqlite"; +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; + +const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle"); + +const PRE_0010_SQL = ` + CREATE TABLE __drizzle_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hash TEXT NOT NULL, + created_at NUMERIC + ); + + CREATE TABLE google_discovery_source ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + name TEXT NOT NULL, + config TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); + + CREATE TABLE google_discovery_binding ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + source_id TEXT NOT NULL, + binding TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); +`; + +const STAMP_BEFORE = 1778100000002; // 0009_normalize_mcp.when + +const stampPriorMigrationsApplied = (db: Database) => { + db.prepare( + "INSERT INTO __drizzle_migrations (hash, created_at) VALUES (?, ?)", + ).run("pre-0010-marker", STAMP_BEFORE); +}; + +describe("0010_normalize_google_discovery backfill", () => { + it("flattens oauth2 auth into columns", () => { + const dir = mkdtempSync(join(tmpdir(), "gd-mig-")); + const dbPath = join(dir, "test.sqlite"); + try { + const db = new Database(dbPath); + db.exec(PRE_0010_SQL); + stampPriorMigrationsApplied(db); + + db.prepare( + "INSERT INTO google_discovery_source (scope_id, id, name, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + ).run( + "default-scope", + "drive", + "Drive", + JSON.stringify({ + name: "Drive", + discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", + service: "drive", + version: "v3", + rootUrl: "https://www.googleapis.com/", + servicePath: "drive/v3/", + auth: { + kind: "oauth2", + connectionId: "conn-1", + clientIdSecretId: "client-id", + clientSecretSecretId: "client-secret", + scopes: ["https://www.googleapis.com/auth/drive"], + }, + }), + Date.now(), + Date.now(), + ); + + db.close(); + const drizzleDb = drizzle(new Database(dbPath)); + migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); + + const after = new Database(dbPath, { readonly: true }); + const row = after + .prepare( + "SELECT auth_kind, auth_connection_id, auth_client_id_secret_id, auth_client_secret_secret_id, auth_scopes, config FROM google_discovery_source WHERE id = ?", + ) + .get("drive") as Record; + expect(row.auth_kind).toBe("oauth2"); + expect(row.auth_connection_id).toBe("conn-1"); + expect(row.auth_client_id_secret_id).toBe("client-id"); + expect(row.auth_client_secret_secret_id).toBe("client-secret"); + // auth_scopes column is text-typed (string[] gets stored as JSON in sqlite). + expect(row.auth_scopes).toContain("drive"); + // The auth key should be stripped from config json. + const config = JSON.parse(row.config!); + expect(config.auth).toBeUndefined(); + expect(config.service).toBe("drive"); + after.close(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("explodes credentials.headers and queryParams into child rows", () => { + const dir = mkdtempSync(join(tmpdir(), "gd-mig-")); + const dbPath = join(dir, "test.sqlite"); + try { + const db = new Database(dbPath); + db.exec(PRE_0010_SQL); + stampPriorMigrationsApplied(db); + + db.prepare( + "INSERT INTO google_discovery_source (scope_id, id, name, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + ).run( + "default-scope", + "with-creds", + "With Creds", + JSON.stringify({ + name: "With Creds", + discoveryUrl: "https://example.com/discovery", + service: "svc", + version: "v1", + rootUrl: "https://example.com/", + servicePath: "svc/v1/", + auth: { kind: "none" }, + credentials: { + headers: { + "X-Static": "literal", + Authorization: { secretId: "tok-secret", prefix: "Bearer " }, + }, + queryParams: { + api_key: { secretId: "key-secret" }, + }, + }, + }), + Date.now(), + Date.now(), + ); + + db.close(); + const drizzleDb = drizzle(new Database(dbPath)); + migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); + + const after = new Database(dbPath, { readonly: true }); + const headers = after + .prepare( + "SELECT name, kind, text_value, secret_id, secret_prefix FROM google_discovery_source_credential_header WHERE source_id = ? ORDER BY name", + ) + .all("with-creds") as ReadonlyArray>; + expect(headers).toHaveLength(2); + const byName = new Map(headers.map((h) => [h.name!, h])); + expect(byName.get("X-Static")).toMatchObject({ + kind: "text", + text_value: "literal", + }); + expect(byName.get("Authorization")).toMatchObject({ + kind: "secret", + secret_id: "tok-secret", + secret_prefix: "Bearer ", + }); + + const params = after + .prepare( + "SELECT name, secret_id FROM google_discovery_source_credential_query_param WHERE source_id = ?", + ) + .all("with-creds") as ReadonlyArray>; + expect(params).toHaveLength(1); + expect(params[0]).toMatchObject({ name: "api_key", secret_id: "key-secret" }); + + after.close(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("survives auth.kind=none with no credentials", () => { + const dir = mkdtempSync(join(tmpdir(), "gd-mig-")); + const dbPath = join(dir, "test.sqlite"); + try { + const db = new Database(dbPath); + db.exec(PRE_0010_SQL); + stampPriorMigrationsApplied(db); + + db.prepare( + "INSERT INTO google_discovery_source (scope_id, id, name, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + ).run( + "default-scope", + "bare", + "Bare", + JSON.stringify({ + name: "Bare", + discoveryUrl: "https://example.com/discovery", + service: "svc", + version: "v1", + rootUrl: "https://example.com/", + servicePath: "svc/v1/", + auth: { kind: "none" }, + }), + Date.now(), + Date.now(), + ); + + db.close(); + const drizzleDb = drizzle(new Database(dbPath)); + migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); + + const after = new Database(dbPath, { readonly: true }); + const row = after + .prepare( + "SELECT auth_kind, auth_connection_id, auth_scopes FROM google_discovery_source WHERE id = ?", + ) + .get("bare") as Record; + expect(row.auth_kind).toBe("none"); + expect(row.auth_connection_id).toBeNull(); + + const headerCount = ( + after + .prepare( + "SELECT count(*) as n FROM google_discovery_source_credential_header WHERE source_id = ?", + ) + .get("bare") as { n: number } + ).n; + expect(headerCount).toBe(0); + after.close(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/local/src/server/migrate-graphql-bindings.test.ts b/apps/local/src/server/migrate-graphql-bindings.test.ts index a6d0de32..aebf583e 100644 --- a/apps/local/src/server/migrate-graphql-bindings.test.ts +++ b/apps/local/src/server/migrate-graphql-bindings.test.ts @@ -87,6 +87,25 @@ const PRE_0007_SQL = ` created_at INTEGER NOT NULL, PRIMARY KEY (scope_id, id) ); + + CREATE TABLE google_discovery_source ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + name TEXT NOT NULL, + config TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); + + CREATE TABLE google_discovery_binding ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + source_id TEXT NOT NULL, + binding TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); `; // drizzle's sqlite migrator picks the latest `created_at` from diff --git a/apps/local/src/server/migrate-mcp-bindings.test.ts b/apps/local/src/server/migrate-mcp-bindings.test.ts index ac8c360e..4dd74a40 100644 --- a/apps/local/src/server/migrate-mcp-bindings.test.ts +++ b/apps/local/src/server/migrate-mcp-bindings.test.ts @@ -37,6 +37,25 @@ const PRE_0009_SQL = ` created_at INTEGER NOT NULL, PRIMARY KEY (scope_id, id) ); + + CREATE TABLE google_discovery_source ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + name TEXT NOT NULL, + config TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); + + CREATE TABLE google_discovery_binding ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + source_id TEXT NOT NULL, + binding TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); `; const STAMP_BEFORE = 1778100000001; // 0008_normalize_openapi.when diff --git a/apps/local/src/server/migrate-openapi-bindings.test.ts b/apps/local/src/server/migrate-openapi-bindings.test.ts index 367a0a7d..a9e6465d 100644 --- a/apps/local/src/server/migrate-openapi-bindings.test.ts +++ b/apps/local/src/server/migrate-openapi-bindings.test.ts @@ -74,10 +74,29 @@ const PRE_0008_SQL = ` created_at INTEGER NOT NULL, PRIMARY KEY (scope_id, id) ); + + CREATE TABLE google_discovery_source ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + name TEXT NOT NULL, + config TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); + + CREATE TABLE google_discovery_binding ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + source_id TEXT NOT NULL, + binding TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); `; // Stamp 0007's folderMillis from the journal so drizzle's runner skips -// 0000..0007 and only executes 0008 against this hand-seeded DB. +// 0000..0007 and only executes 0008+ against this hand-seeded DB. const STAMP_BEFORE = 1778100000000; // 0007_normalize_graphql.when const stampPriorMigrationsApplied = (db: Database) => { diff --git a/packages/plugins/google-discovery/src/sdk/binding-store.ts b/packages/plugins/google-discovery/src/sdk/binding-store.ts index e741a116..6439f343 100644 --- a/packages/plugins/google-discovery/src/sdk/binding-store.ts +++ b/packages/plugins/google-discovery/src/sdk/binding-store.ts @@ -21,7 +21,13 @@ import { type StorageFailure, } from "@executor-js/sdk/core"; -import { GoogleDiscoveryMethodBinding, GoogleDiscoveryStoredSourceData } from "./types"; +import { + GoogleDiscoveryMethodBinding, + GoogleDiscoveryStoredSourceData, + type GoogleDiscoveryAuth, + type GoogleDiscoveryCredentialValue, + type GoogleDiscoveryFetchCredentials, +} from "./types"; // --------------------------------------------------------------------------- // OAuth session TTL @@ -39,11 +45,58 @@ export const googleDiscoverySchema = defineSchema({ id: { type: "string", required: true }, scope_id: { type: "string", required: true, index: true }, name: { type: "string", required: true }, + // Plugin-private structural config minus auth/credentials — + // discoveryUrl, service, version, rootUrl, servicePath. These + // never carry refs. config: { type: "json", required: true }, + // Flattened GoogleDiscoveryAuth. + auth_kind: { + type: ["none", "oauth2"], + required: true, + defaultValue: "none", + }, + auth_connection_id: { type: "string", required: false, index: true }, + auth_client_id_secret_id: { + type: "string", + required: false, + index: true, + }, + auth_client_secret_secret_id: { + type: "string", + required: false, + index: true, + }, + // Stored as a string[] (JSON-backed but not a ref-bearing column). + // Empty array when auth_kind is "none". + auth_scopes: { type: "string[]", required: false }, created_at: { type: "date", required: true }, updated_at: { type: "date", required: true }, }, }, + google_discovery_source_credential_header: { + fields: { + id: { type: "string", required: true }, + scope_id: { type: "string", required: true, index: true }, + source_id: { type: "string", required: true, index: true }, + name: { type: "string", required: true }, + kind: { type: ["text", "secret"], required: true }, + text_value: { type: "string", required: false }, + secret_id: { type: "string", required: false, index: true }, + secret_prefix: { type: "string", required: false }, + }, + }, + google_discovery_source_credential_query_param: { + fields: { + id: { type: "string", required: true }, + scope_id: { type: "string", required: true, index: true }, + source_id: { type: "string", required: true, index: true }, + name: { type: "string", required: true }, + kind: { type: ["text", "secret"], required: true }, + text_value: { type: "string", required: false }, + secret_id: { type: "string", required: false, index: true }, + secret_prefix: { type: "string", required: false }, + }, + }, google_discovery_binding: { fields: { id: { type: "string", required: true }, @@ -93,6 +146,112 @@ const decodeJson = (value: unknown): unknown => { } }; +// --- auth column packing/unpacking ------------------------------------------ + +interface AuthColumns { + readonly auth_kind: "none" | "oauth2"; + readonly auth_connection_id?: string; + readonly auth_client_id_secret_id?: string; + readonly auth_client_secret_secret_id?: string; + // Mutable rather than readonly so the typed adapter's RowInput shape + // (which expects `string[]`, not `readonly string[]`) is satisfied. + readonly auth_scopes?: string[]; +} + +const authToColumns = (auth: GoogleDiscoveryAuth): AuthColumns => { + if (auth.kind === "oauth2") { + return { + auth_kind: "oauth2", + auth_connection_id: auth.connectionId, + auth_client_id_secret_id: auth.clientIdSecretId, + auth_client_secret_secret_id: auth.clientSecretSecretId ?? undefined, + auth_scopes: [...auth.scopes], + }; + } + return { auth_kind: "none" }; +}; + +const columnsToAuth = (row: Record): GoogleDiscoveryAuth => { + if ( + row.auth_kind === "oauth2" && + typeof row.auth_connection_id === "string" && + typeof row.auth_client_id_secret_id === "string" + ) { + const csec = row.auth_client_secret_secret_id as string | null | undefined; + const scopes = (row.auth_scopes as readonly string[] | null | undefined) ?? []; + return { + kind: "oauth2", + connectionId: row.auth_connection_id, + clientIdSecretId: row.auth_client_id_secret_id, + clientSecretSecretId: csec ?? null, + scopes: [...scopes], + }; + } + return { kind: "none" }; +}; + +// --- SecretBackedValue maps <-> child rows ---------------------------------- + +interface CredentialRow { + readonly id: string; + readonly scope_id: string; + readonly source_id: string; + readonly name: string; + readonly kind: "text" | "secret"; + readonly text_value?: string; + readonly secret_id?: string; + readonly secret_prefix?: string; + readonly [k: string]: unknown; +} + +const valueMapToRows = ( + sourceId: string, + scope: string, + values: Record | undefined, +): readonly CredentialRow[] => { + if (!values) return []; + return Object.entries(values).map(([name, value]) => { + const id = `${sourceId}:${name}`; + if (typeof value === "string") { + return { + id, + scope_id: scope, + source_id: sourceId, + name, + kind: "text", + text_value: value, + }; + } + return { + id, + scope_id: scope, + source_id: sourceId, + name, + kind: "secret", + secret_id: value.secretId, + secret_prefix: value.prefix, + }; + }); +}; + +const rowsToValueMap = ( + rows: readonly Record[], +): Record => { + const out: Record = {}; + for (const row of rows) { + const name = row.name as string; + if (row.kind === "secret" && typeof row.secret_id === "string") { + const prefix = row.secret_prefix as string | undefined | null; + out[name] = prefix + ? { secretId: row.secret_id, prefix } + : { secretId: row.secret_id }; + } else if (row.kind === "text" && typeof row.text_value === "string") { + out[name] = row.text_value; + } + } + return out; +}; + // --------------------------------------------------------------------------- // Store interface // --------------------------------------------------------------------------- @@ -154,6 +313,52 @@ export interface GoogleDiscoveryStore { sourceId: string, scope: string, ) => Effect.Effect; + + // --------------------------------------------------------------------- + // Usage lookups — back `usagesForSecret` / `usagesForConnection`. + // --------------------------------------------------------------------- + + /** Source rows whose oauth2 auth columns reference the given secret id. + * `slot` distinguishes client_id vs client_secret. */ + readonly findSourcesBySecret: ( + secretId: string, + ) => Effect.Effect< + readonly { + readonly namespace: string; + readonly scope_id: string; + readonly name: string; + readonly slot: string; + }[], + StorageFailure + >; + + /** Source rows whose oauth2 auth points at the given connection id. */ + readonly findSourcesByConnection: ( + connectionId: string, + ) => Effect.Effect< + readonly { + readonly namespace: string; + readonly scope_id: string; + readonly name: string; + readonly slot: string; + }[], + StorageFailure + >; + + /** Credential header / query_param child rows referencing the secret. */ + readonly findCredentialRowsBySecret: (secretId: string) => Effect.Effect< + readonly { + readonly kind: "credential_header" | "credential_query_param"; + readonly source_id: string; + readonly scope_id: string; + readonly name: string; + }[], + StorageFailure + >; + + readonly lookupSourceNames: ( + keys: readonly string[], + ) => Effect.Effect, StorageFailure>; } // --------------------------------------------------------------------------- @@ -245,6 +450,8 @@ export const makeGoogleDiscoveryStore = ( putSource: (source) => Effect.gen(function* () { const now = new Date(); + // Wipe the source row + every child row before recreating — + // matches putSource's "fully replace" semantic. yield* db.delete({ model: "google_discovery_source", where: [ @@ -252,18 +459,29 @@ export const makeGoogleDiscoveryStore = ( { field: "scope_id", value: source.scope }, ], }); + yield* deleteSourceChildren(source.namespace, source.scope); + + const encoded = stripExtractedFields( + encodeStoredSourceData(source.config) as Record, + ); yield* db.create({ model: "google_discovery_source", data: { id: source.namespace, scope_id: source.scope, name: source.name, - config: toJsonRecord(encodeStoredSourceData(source.config)), + config: toJsonRecord(encoded), created_at: now, updated_at: now, + ...authToColumns(source.config.auth), }, forceAllowId: true, }); + yield* writeCredentialRows( + source.namespace, + source.scope, + source.config.credentials, + ); }), updateSourceMeta: (sourceId, scope, update) => @@ -276,12 +494,7 @@ export const makeGoogleDiscoveryStore = ( ], }); if (!row) return; - const config = decodeStoredSourceData(decodeJson(row.config)); - const nextConfig = new GoogleDiscoveryStoredSourceData({ - ...config, - name: update.name ?? config.name, - auth: update.auth ?? config.auth, - }); + const auth = update.auth ?? columnsToAuth(row); yield* db.update({ model: "google_discovery_source", where: [ @@ -290,22 +503,23 @@ export const makeGoogleDiscoveryStore = ( ], update: { name: update.name ?? (row.name as string), - config: toJsonRecord(encodeStoredSourceData(nextConfig)), updated_at: new Date(), + ...authToColumns(auth), }, }); }), removeSource: (sourceId, scope) => - db - .delete({ + Effect.gen(function* () { + yield* deleteSourceChildren(sourceId, scope); + yield* db.delete({ model: "google_discovery_source", where: [ { field: "id", value: sourceId }, { field: "scope_id", value: scope }, ], - }) - .pipe(Effect.asVoid), + }); + }), getSource: (sourceId, scope) => Effect.gen(function* () { @@ -321,7 +535,7 @@ export const makeGoogleDiscoveryStore = ( namespace: row.id as string, scope: row.scope_id as string, name: row.name as string, - config: decodeStoredSourceData(decodeJson(row.config)), + config: yield* hydrateStoredSourceData(row, sourceId, scope), }; }), @@ -335,7 +549,219 @@ export const makeGoogleDiscoveryStore = ( ], }); if (!row) return null; - return decodeStoredSourceData(decodeJson(row.config)); + return yield* hydrateStoredSourceData(row, sourceId, scope); + }), + + findSourcesBySecret: (secretId) => + Effect.gen(function* () { + const [byClientId, byClientSecret] = yield* Effect.all( + [ + db.findMany({ + model: "google_discovery_source", + where: [ + { field: "auth_client_id_secret_id", value: secretId }, + ], + }), + db.findMany({ + model: "google_discovery_source", + where: [ + { field: "auth_client_secret_secret_id", value: secretId }, + ], + }), + ], + { concurrency: "unbounded" }, + ); + const out: { + readonly namespace: string; + readonly scope_id: string; + readonly name: string; + readonly slot: string; + }[] = []; + for (const r of byClientId) { + out.push({ + namespace: r.id as string, + scope_id: r.scope_id as string, + name: r.name as string, + slot: "auth.oauth2.client_id", + }); + } + for (const r of byClientSecret) { + out.push({ + namespace: r.id as string, + scope_id: r.scope_id as string, + name: r.name as string, + slot: "auth.oauth2.client_secret", + }); + } + return out; + }), + + findSourcesByConnection: (connectionId) => + db + .findMany({ + model: "google_discovery_source", + where: [{ field: "auth_connection_id", value: connectionId }], + }) + .pipe( + Effect.map((rows) => + rows.map((r) => ({ + namespace: r.id as string, + scope_id: r.scope_id as string, + name: r.name as string, + slot: "auth.oauth2.connection", + })), + ), + ), + + findCredentialRowsBySecret: (secretId) => + Effect.gen(function* () { + const [headers, params] = yield* Effect.all( + [ + db.findMany({ + model: "google_discovery_source_credential_header", + where: [{ field: "secret_id", value: secretId }], + }), + db.findMany({ + model: "google_discovery_source_credential_query_param", + where: [{ field: "secret_id", value: secretId }], + }), + ], + { concurrency: "unbounded" }, + ); + return [ + ...headers.map((r) => ({ + kind: "credential_header" as const, + source_id: r.source_id as string, + scope_id: r.scope_id as string, + name: r.name as string, + })), + ...params.map((r) => ({ + kind: "credential_query_param" as const, + source_id: r.source_id as string, + scope_id: r.scope_id as string, + name: r.name as string, + })), + ]; + }), + + lookupSourceNames: (keys) => + Effect.gen(function* () { + if (keys.length === 0) return new Map(); + const rows = yield* db.findMany({ model: "google_discovery_source" }); + const requested = new Set(keys); + const out = new Map(); + for (const r of rows) { + const key = `${r.scope_id as string}:${r.id as string}`; + if (requested.has(key)) out.set(key, r.name as string); + } + return out; }), }; + + // --------------------------------------------------------------------- + // Closure helpers (depend on `db`). + // --------------------------------------------------------------------- + + function deleteSourceChildren(sourceId: string, scope: string) { + // Drop only credential child rows. Bindings live independently and + // are managed via putBinding / removeBindingsBySource — wiping them + // here would break putSource (which legitimately keeps existing + // bindings) and the test for "registers and invokes ... tools". + return Effect.gen(function* () { + for (const model of [ + "google_discovery_source_credential_header", + "google_discovery_source_credential_query_param", + ] as const) { + yield* db.deleteMany({ + model, + where: [ + { field: "source_id", value: sourceId }, + { field: "scope_id", value: scope }, + ], + }); + } + }); + } + + function writeCredentialRows( + sourceId: string, + scope: string, + credentials: GoogleDiscoveryFetchCredentials | undefined, + ) { + return Effect.gen(function* () { + if (!credentials) return; + const headerRows = valueMapToRows(sourceId, scope, credentials.headers); + if (headerRows.length > 0) { + yield* db.createMany({ + model: "google_discovery_source_credential_header", + data: headerRows, + forceAllowId: true, + }); + } + const paramRows = valueMapToRows( + sourceId, + scope, + credentials.queryParams, + ); + if (paramRows.length > 0) { + yield* db.createMany({ + model: "google_discovery_source_credential_query_param", + data: paramRows, + forceAllowId: true, + }); + } + }); + } + + function hydrateStoredSourceData( + row: Record, + sourceId: string, + scope: string, + ): Effect.Effect { + return Effect.gen(function* () { + const partial = decodeJson(row.config) as Record; + const headerRows = yield* db.findMany({ + model: "google_discovery_source_credential_header", + where: [ + { field: "source_id", value: sourceId }, + { field: "scope_id", value: scope }, + ], + }); + const paramRows = yield* db.findMany({ + model: "google_discovery_source_credential_query_param", + where: [ + { field: "source_id", value: sourceId }, + { field: "scope_id", value: scope }, + ], + }); + const headers = rowsToValueMap(headerRows); + const queryParams = rowsToValueMap(paramRows); + const credentials = + Object.keys(headers).length === 0 && + Object.keys(queryParams).length === 0 + ? undefined + : { + ...(Object.keys(headers).length > 0 ? { headers } : {}), + ...(Object.keys(queryParams).length > 0 ? { queryParams } : {}), + }; + const reassembled = { + ...partial, + auth: columnsToAuth(row), + ...(credentials ? { credentials } : {}), + }; + return decodeStoredSourceData(reassembled); + }); + } +}; + +// Strip auth/credentials from the encoded source-data shape. Those +// moved to columns and child tables; the remaining structural fields +// live in the `config` JSON. +const stripExtractedFields = ( + encoded: Record, +): Record => { + const { auth, credentials, ...rest } = encoded; + void auth; + void credentials; + return rest; }; diff --git a/packages/plugins/google-discovery/src/sdk/plugin.test.ts b/packages/plugins/google-discovery/src/sdk/plugin.test.ts index d5faaec9..56edde03 100644 --- a/packages/plugins/google-discovery/src/sdk/plugin.test.ts +++ b/packages/plugins/google-discovery/src/sdk/plugin.test.ts @@ -625,4 +625,82 @@ describe("Google Discovery plugin", () => { } }), ); + + // ------------------------------------------------------------------------- + // Usage tracking — refs land on auth_* columns and the credential + // child tables. `usagesForSecret` / `usagesForConnection` should + // surface them all. + // ------------------------------------------------------------------------- + + it.effect("usagesForSecret returns refs across auth + credential rows", () => + Effect.gen(function* () { + const handle = yield* Effect.promise(() => startServer()); + try { + const executor = yield* createExecutor( + makeTestConfig({ + plugins: [ + makeMemorySecretsPlugin()(), + googleDiscoveryPlugin(), + ] as const, + }), + ); + try { + const connectionId = ConnectionId.make( + "google-discovery-oauth2-usages", + ); + yield* executor.connections.create( + new CreateConnectionInput({ + id: connectionId, + scope: ScopeId.make("test-scope"), + provider: "oauth2", + identityLabel: "Drive Usages", + accessToken: new TokenMaterial({ + secretId: SecretId.make(`${connectionId}.access_token`), + name: "Drive Access Token", + value: "secret-token", + }), + refreshToken: null, + expiresAt: null, + oauthScope: null, + providerState: null, + }), + ); + + yield* executor.googleDiscovery.addSource({ + name: "Drive (Usages)", + scope: "test-scope", + discoveryUrl: handle.discoveryUrl, + namespace: "drive_u", + auth: { + kind: "oauth2", + connectionId, + clientIdSecretId: "shared-secret", + clientSecretSecretId: null, + scopes: [], + }, + }); + + // The auth.client_id_secret_id alone holds `shared-secret`. + const usages = yield* executor.secrets.usages( + SecretId.make("shared-secret"), + ); + expect(usages.length).toBe(1); + expect(usages[0]).toMatchObject({ + pluginId: "google-discovery", + ownerKind: "google-discovery-source", + ownerId: "drive_u", + slot: "auth.oauth2.client_id", + }); + + const connUsages = yield* executor.connections.usages(connectionId); + expect(connUsages.length).toBe(1); + expect(connUsages[0].slot).toBe("auth.oauth2.connection"); + } finally { + yield* executor.close(); + } + } finally { + yield* Effect.promise(() => handle.close()); + } + }), + ); }); diff --git a/packages/plugins/google-discovery/src/sdk/plugin.ts b/packages/plugins/google-discovery/src/sdk/plugin.ts index db5aea6b..26875728 100644 --- a/packages/plugins/google-discovery/src/sdk/plugin.ts +++ b/packages/plugins/google-discovery/src/sdk/plugin.ts @@ -1,7 +1,9 @@ import { Effect, Option } from "effect"; import { + ScopeId, SourceDetectionResult, + Usage, definePlugin, resolveSecretBackedMap, type PluginCtx, @@ -435,6 +437,71 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ yield* typedCtx.storage.removeSource(sourceId, scope); }), + // Aggregate usages across the auth columns and the credential child + // tables. Each is one indexed SELECT in the store; the merge plus a + // single source-name JOIN happens here. + usagesForSecret: ({ ctx, args }) => + Effect.gen(function* () { + const typedCtx = ctx as PluginCtx; + const sources = yield* typedCtx.storage.findSourcesBySecret( + args.secretId, + ); + const childRows = yield* typedCtx.storage.findCredentialRowsBySecret( + args.secretId, + ); + const sourceKeys = new Set(); + for (const s of sources) sourceKeys.add(`${s.scope_id}:${s.namespace}`); + for (const r of childRows) + sourceKeys.add(`${r.scope_id}:${r.source_id}`); + const names = yield* typedCtx.storage.lookupSourceNames([...sourceKeys]); + + const out: Usage[] = []; + for (const s of sources) { + out.push( + new Usage({ + pluginId: "google-discovery", + scopeId: ScopeId.make(s.scope_id), + ownerKind: "google-discovery-source", + ownerId: s.namespace, + ownerName: names.get(`${s.scope_id}:${s.namespace}`) ?? s.name, + slot: s.slot, + }), + ); + } + for (const r of childRows) { + out.push( + new Usage({ + pluginId: "google-discovery", + scopeId: ScopeId.make(r.scope_id), + ownerKind: `google-discovery-source-${r.kind.replace(/_/g, "-")}`, + ownerId: r.source_id, + ownerName: names.get(`${r.scope_id}:${r.source_id}`) ?? null, + slot: `${r.kind}:${r.name}`, + }), + ); + } + return out; + }), + + usagesForConnection: ({ ctx, args }) => + Effect.gen(function* () { + const typedCtx = ctx as PluginCtx; + const sources = yield* typedCtx.storage.findSourcesByConnection( + args.connectionId, + ); + return sources.map( + (s) => + new Usage({ + pluginId: "google-discovery", + scopeId: ScopeId.make(s.scope_id), + ownerKind: "google-discovery-source", + ownerId: s.namespace, + ownerName: s.name, + slot: s.slot, + }), + ); + }), + detect: ({ url }) => Effect.gen(function* () { const trimmed = url.trim();