Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
314 lines
12 KiB
Go
314 lines
12 KiB
Go
package storesqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
|
|
)
|
|
|
|
func (store *Store) AuthorizationProjection(
|
|
ctx context.Context,
|
|
accountID, connectorKey string,
|
|
) (market.AuthorizationProjection, error) {
|
|
var payload string
|
|
if err := store.db.QueryRowContext(ctx, `
|
|
SELECT projection_json FROM connector_market_authorization_projections
|
|
WHERE account_id = ? AND connector_key = ?`, accountID, connectorKey).Scan(&payload); err != nil {
|
|
return market.AuthorizationProjection{}, mapNotFound(err)
|
|
}
|
|
var projection market.AuthorizationProjection
|
|
if err := json.Unmarshal([]byte(payload), &projection); err != nil {
|
|
return market.AuthorizationProjection{}, fmt.Errorf("decode connector authorization projection: %w", err)
|
|
}
|
|
return projection, nil
|
|
}
|
|
|
|
func overlayAuthorizationProjectionsOn(
|
|
ctx context.Context,
|
|
tx *sql.Tx,
|
|
accountID string,
|
|
connectors []market.Connector,
|
|
) ([]market.Connector, error) {
|
|
rows, err := tx.QueryContext(ctx, `SELECT connector_key, projection_json FROM connector_market_authorization_projections WHERE account_id = ?`, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
projections := make(map[string]market.AuthorizationProjection)
|
|
for rows.Next() {
|
|
var connectorKey, payload string
|
|
if err := rows.Scan(&connectorKey, &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
var projection market.AuthorizationProjection
|
|
if err := json.Unmarshal([]byte(payload), &projection); err != nil {
|
|
return nil, fmt.Errorf("decode connector authorization projection: %w", err)
|
|
}
|
|
projections[connectorKey] = projection
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
for index := range connectors {
|
|
projection, ok := projections[connectors[index].Key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
connectors[index].Authorization = market.Authorization{
|
|
State: projection.State, FailureCode: projection.FailureCode,
|
|
}
|
|
}
|
|
return connectors, nil
|
|
}
|
|
|
|
func (store *Store) SaveAuthorizationProjection(
|
|
ctx context.Context,
|
|
projection market.AuthorizationProjection,
|
|
) error {
|
|
payload, err := json.Marshal(projection)
|
|
if err != nil {
|
|
return fmt.Errorf("encode connector authorization projection: %w", err)
|
|
}
|
|
tx, err := store.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
var currentPayload string
|
|
publicChanged := true
|
|
currentErr := tx.QueryRowContext(ctx, `SELECT projection_json FROM connector_market_authorization_projections
|
|
WHERE account_id = ? AND connector_key = ?`, projection.AccountID, projection.ConnectorKey).Scan(¤tPayload)
|
|
if currentErr == nil {
|
|
var current market.AuthorizationProjection
|
|
if err := json.Unmarshal([]byte(currentPayload), ¤t); err != nil {
|
|
return fmt.Errorf("decode connector authorization projection: %w", err)
|
|
}
|
|
if current.ServerSynchronized && !projection.ServerSynchronized ||
|
|
current.ServerSynchronized && projection.ServerSynchronized && current.ServerRevision > projection.ServerRevision {
|
|
return tx.Commit()
|
|
}
|
|
publicChanged = !samePublicAuthorizationProjection(current, projection)
|
|
} else if !errors.Is(currentErr, sql.ErrNoRows) {
|
|
return currentErr
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `
|
|
INSERT INTO connector_market_authorization_projections (account_id, connector_key, projection_json)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(account_id, connector_key) DO UPDATE SET projection_json = excluded.projection_json`,
|
|
projection.AccountID, projection.ConnectorKey, string(payload)); err != nil {
|
|
return err
|
|
}
|
|
if publicChanged {
|
|
if err := bumpAuthorizationProjectionRevisionOn(ctx, tx, projection.ConnectorKey); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (store *Store) ApplyAuthorizationSnapshot(
|
|
ctx context.Context,
|
|
accountID string,
|
|
snapshot market.AuthorizationSnapshot,
|
|
) (market.AuthorizationSnapshotApplyResult, error) {
|
|
accountID = strings.TrimSpace(accountID)
|
|
if accountID == "" {
|
|
return market.AuthorizationSnapshotApplyResult{}, errors.New("authorization snapshot account is required")
|
|
}
|
|
tx, err := store.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return market.AuthorizationSnapshotApplyResult{}, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
var currentRevision uint64
|
|
err = tx.QueryRowContext(ctx, `SELECT revision FROM connector_market_authorization_snapshot_revisions WHERE account_id = ?`, accountID).Scan(¤tRevision)
|
|
hasRevision := err == nil
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return market.AuthorizationSnapshotApplyResult{}, err
|
|
}
|
|
rows, err := tx.QueryContext(ctx, `SELECT connector_key, projection_json FROM connector_market_authorization_projections WHERE account_id = ?`, accountID)
|
|
if err != nil {
|
|
return market.AuthorizationSnapshotApplyResult{}, err
|
|
}
|
|
existing := make(map[string]market.AuthorizationProjection)
|
|
for rows.Next() {
|
|
var connectorKey, payload string
|
|
if err := rows.Scan(&connectorKey, &payload); err != nil {
|
|
_ = rows.Close()
|
|
return market.AuthorizationSnapshotApplyResult{}, err
|
|
}
|
|
var projection market.AuthorizationProjection
|
|
if err := json.Unmarshal([]byte(payload), &projection); err != nil {
|
|
_ = rows.Close()
|
|
return market.AuthorizationSnapshotApplyResult{}, fmt.Errorf("decode connector authorization projection: %w", err)
|
|
}
|
|
existing[connectorKey] = projection
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return market.AuthorizationSnapshotApplyResult{}, err
|
|
}
|
|
now := time.Now().UTC()
|
|
result := market.AuthorizationSnapshotApplyResult{}
|
|
effective := existing
|
|
if !hasRevision || snapshot.Revision > currentRevision {
|
|
incoming := make(map[string]market.AuthorizationProjection, len(snapshot.Connectors))
|
|
for _, projection := range snapshot.Connectors {
|
|
projection.AccountID = accountID
|
|
projection.ConnectorKey = strings.TrimSpace(projection.ConnectorKey)
|
|
if projection.ConnectorKey == "" {
|
|
return market.AuthorizationSnapshotApplyResult{}, errors.New("authorization snapshot contains an empty connector key")
|
|
}
|
|
projection.ServerRevision = snapshot.Revision
|
|
projection.ServerSynchronized = true
|
|
projection.UpdatedAt = now
|
|
incoming[projection.ConnectorKey] = projection
|
|
}
|
|
for connectorKey, previous := range existing {
|
|
if _, ok := incoming[connectorKey]; ok {
|
|
continue
|
|
}
|
|
if !previous.ServerSynchronized {
|
|
continue
|
|
}
|
|
incoming[connectorKey] = market.AuthorizationProjection{
|
|
AccountID: accountID, ConnectorKey: connectorKey, ServerRevision: snapshot.Revision,
|
|
ServerSynchronized: true, State: market.AuthorizationStateDisconnected, UpdatedAt: now,
|
|
}
|
|
}
|
|
result.ChangedConnectorKeys = make([]string, 0, len(incoming))
|
|
for connectorKey, projection := range incoming {
|
|
previous, exists := existing[connectorKey]
|
|
if !exists || previous.State != projection.State || previous.ConnectionID != projection.ConnectionID ||
|
|
previous.ConnectionVersion != projection.ConnectionVersion || previous.ConnectorVersion != projection.ConnectorVersion {
|
|
result.ChangedConnectorKeys = append(result.ChangedConnectorKeys, connectorKey)
|
|
}
|
|
payload, err := json.Marshal(projection)
|
|
if err != nil {
|
|
return market.AuthorizationSnapshotApplyResult{}, fmt.Errorf("encode connector authorization projection: %w", err)
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `INSERT INTO connector_market_authorization_projections (account_id, connector_key, projection_json)
|
|
VALUES (?, ?, ?) ON CONFLICT(account_id,connector_key) DO UPDATE SET projection_json = excluded.projection_json`, accountID, connectorKey, string(payload)); err != nil {
|
|
return market.AuthorizationSnapshotApplyResult{}, err
|
|
}
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `INSERT INTO connector_market_authorization_snapshot_revisions (account_id, revision) VALUES (?, ?)
|
|
ON CONFLICT(account_id) DO UPDATE SET revision = excluded.revision`, accountID, snapshot.Revision); err != nil {
|
|
return market.AuthorizationSnapshotApplyResult{}, err
|
|
}
|
|
sort.Strings(result.ChangedConnectorKeys)
|
|
for _, connectorKey := range result.ChangedConnectorKeys {
|
|
if err := bumpAuthorizationProjectionRevisionOn(ctx, tx, connectorKey); err != nil {
|
|
return market.AuthorizationSnapshotApplyResult{}, err
|
|
}
|
|
}
|
|
effective = incoming
|
|
}
|
|
pending, err := connectedAuthorizationReceiptConnectorKeys(ctx, tx, accountID, effective)
|
|
if err != nil {
|
|
return market.AuthorizationSnapshotApplyResult{}, err
|
|
}
|
|
result.PendingReceiptConnectorKeys = pending
|
|
sort.Strings(result.ChangedConnectorKeys)
|
|
sort.Strings(result.PendingReceiptConnectorKeys)
|
|
if err := tx.Commit(); err != nil {
|
|
return market.AuthorizationSnapshotApplyResult{}, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func samePublicAuthorizationProjection(left, right market.AuthorizationProjection) bool {
|
|
return left.State == right.State && left.ConnectionID == right.ConnectionID &&
|
|
left.FailureCode == right.FailureCode && left.ConnectionVersion == right.ConnectionVersion &&
|
|
left.ConnectorVersion == right.ConnectorVersion && left.ServerSynchronized == right.ServerSynchronized
|
|
}
|
|
|
|
func bumpAuthorizationProjectionRevisionOn(ctx context.Context, tx *sql.Tx, connectorKey string) error {
|
|
var connectorPayload string
|
|
if err := tx.QueryRowContext(ctx, `SELECT connector_json FROM connector_market_connectors WHERE connector_key = ?`, connectorKey).Scan(&connectorPayload); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
connector, err := decodeConnector(connectorPayload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var revision uint64
|
|
if err := tx.QueryRowContext(ctx, `UPDATE connector_market_metadata SET revision = revision + 1 WHERE id = ? RETURNING revision`, metadataID).Scan(&revision); err != nil {
|
|
return err
|
|
}
|
|
connector.Revision = revision
|
|
payload, err := json.Marshal(connector)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `UPDATE connector_market_connectors SET connector_json = ? WHERE connector_key = ?`, string(payload), connectorKey); err != nil {
|
|
return err
|
|
}
|
|
eventPayload, err := json.Marshal(market.ChangedEvent{ConnectorKey: connectorKey, Revision: revision})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = tx.ExecContext(ctx, `INSERT INTO connector_market_outbox (revision, event_json, published_at_unix_ms) VALUES (?, ?, NULL)`, revision, string(eventPayload))
|
|
return err
|
|
}
|
|
|
|
func connectedAuthorizationReceiptConnectorKeys(
|
|
ctx context.Context,
|
|
tx *sql.Tx,
|
|
accountID string,
|
|
projections map[string]market.AuthorizationProjection,
|
|
) ([]string, error) {
|
|
connected := make(map[string]struct{})
|
|
for connectorKey, projection := range projections {
|
|
if projection.ServerSynchronized && projection.State == market.AuthorizationStateConnected {
|
|
connected[connectorKey] = struct{}{}
|
|
}
|
|
}
|
|
if len(connected) == 0 {
|
|
return nil, nil
|
|
}
|
|
rows, err := tx.QueryContext(ctx, `SELECT operation_json FROM connector_market_operations
|
|
WHERE kind = 'start_authorization' AND state = 'completed' ORDER BY operation_id`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pending := make(map[string]struct{})
|
|
for rows.Next() {
|
|
var payload string
|
|
if err := rows.Scan(&payload); err != nil {
|
|
_ = rows.Close()
|
|
return nil, err
|
|
}
|
|
operation, err := decodeOperation(payload)
|
|
if err != nil {
|
|
_ = rows.Close()
|
|
return nil, err
|
|
}
|
|
if operation.Scope.AccountID != accountID || operation.Execution.AuthorizationSession == nil ||
|
|
operation.Execution.AuthorizationSession.IsResolved() {
|
|
continue
|
|
}
|
|
if _, ok := connected[operation.ConnectorKey]; !ok {
|
|
continue
|
|
}
|
|
pending[operation.ConnectorKey] = struct{}{}
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
keys := make([]string, 0, len(pending))
|
|
for connectorKey := range pending {
|
|
keys = append(keys, connectorKey)
|
|
}
|
|
return keys, nil
|
|
}
|