repos: backend — schema, /api/repos routes + GitHub sync provider
Migration 0034: two tables. repo_connections carries the workspace's per-provider config (owner, base_url, label, last_synced_at, last_sync_error) and points at an app_connections row for the PAT. repos is the per-connection cache with (connection_id, external_id) unique so upsert is idempotent across re-syncs. Cascading deletes clean up cleanly on connection removal. cm-secrets grows a FetchAuthorized op — GET with the stored PAT injected as bearer, returns status + JSON body without ever exposing the credential to cm-api. This is the least-privilege door for read-only provider APIs (list repos), distinct from the InvokeHttp path that still requires a single-use approval grant for outbound writes. cm-api::routes::repos wires: - POST /api/repos/connections (broker store_secret + insert both rows + initial sync + mark_synced) - GET /api/repos/connections - DELETE /api/repos/connections/:id - POST /api/repos/connections/:id/sync - GET /api/repos (500 cap, newest provider_updated first) - GET /api/repos/:id (full detail incl. clone_url + html_url) GitHub provider inline for v1 — paginated pull of /orgs/:owner/repos (when owner set) or /user/repos (when absent), 100/page, capped at 20 pages (~2k repos) to keep first-sync latency bounded. Non-2xx surface back to the caller as sync_error; parse failures are best-effort per repo (skipped, logged, don't abort the batch). Gitea + GitLab providers land in a follow-up — mostly URL swap + response-shape adapter.
This commit is contained in:
@@ -112,6 +112,28 @@ pub async fn find_provider(
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
/// Fetch a single connection by id, workspace-scoped so cross-tenant reads
|
||||
/// return NotFound. Used when another table (e.g. repo_connections) needs
|
||||
/// to resolve the secret_ref of the credential it points at.
|
||||
pub async fn get(
|
||||
pool: &PgPool,
|
||||
id: Uuid,
|
||||
workspace_id: WorkspaceId,
|
||||
) -> Result<AppConnection, DbError> {
|
||||
let row = sqlx::query_as!(
|
||||
AppConnection,
|
||||
r#"SELECT id, workspace_id, agent_id, provider, auth_type, status, secret_ref
|
||||
FROM app_connections
|
||||
WHERE id = $1 AND workspace_id = $2"#,
|
||||
id,
|
||||
workspace_id.as_uuid(),
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
.ok_or(DbError::NotFound)?;
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
pub async fn disconnect(pool: &PgPool, id: Uuid) -> Result<(), DbError> {
|
||||
let result = sqlx::query!(
|
||||
"UPDATE app_connections SET status = 'disconnected' WHERE id = $1",
|
||||
|
||||
Reference in New Issue
Block a user