package store import ( "context" "encoding/json" "errors" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) var ( ErrSecurityEventConnectionNotFound = errors.New("security event connection not found") ErrSecurityEventConnectionConflict = errors.New("security event connection conflicts with current state") ) type SecurityEventConnection struct { ConnectionID string `json:"connectionId"` TransmitterIssuer string `json:"transmitterIssuer"` EndpointURL string `json:"endpointUrl"` Audience *string `json:"audience,omitempty"` StreamID *string `json:"streamId,omitempty"` CredentialRef string `json:"-"` NextCredentialRef *string `json:"-"` ManagementClientID string `json:"managementClientId"` ManagementCredentialRef *string `json:"-"` LifecycleStatus string `json:"lifecycleStatus"` Version int64 `json:"version"` LastErrorCategory *string `json:"lastErrorCategory,omitempty"` IdempotencyKey string `json:"-"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` LastVerificationAt *time.Time `json:"lastVerificationAt,omitempty"` HealthMode string `json:"healthMode"` } type CreateSecurityEventConnectionInput struct { ConnectionID, TransmitterIssuer, EndpointURL, CredentialRef, ManagementClientID, ManagementCredentialRef, IdempotencyKey string } type SecurityEventConnectionIdempotency struct { RequestHash string Response json.RawMessage } func (s *Store) SecurityEventConnectionIdempotency(ctx context.Context, operation, key string) (SecurityEventConnectionIdempotency, error) { var value SecurityEventConnectionIdempotency err := s.pool.QueryRow(ctx, `SELECT request_hash,response FROM gateway_security_event_connection_idempotency WHERE operation=$1 AND idempotency_key=$2`, operation, key).Scan(&value.RequestHash, &value.Response) if errors.Is(err, pgx.ErrNoRows) { return SecurityEventConnectionIdempotency{}, ErrSecurityEventConnectionNotFound } return value, err } func (s *Store) RecordSecurityEventConnectionIdempotency(ctx context.Context, operation, key, requestHash string, response []byte) error { if !json.Valid(response) { return errors.New("security event idempotency response is invalid") } _, err := s.pool.Exec(ctx, `INSERT INTO gateway_security_event_connection_idempotency(operation,idempotency_key,request_hash,response) VALUES($1,$2,$3,$4::jsonb) ON CONFLICT(operation,idempotency_key) DO NOTHING`, operation, key, requestHash, response) return err } func (s *Store) CreateSecurityEventConnection(ctx context.Context, input CreateSecurityEventConnectionInput) (SecurityEventConnection, error) { var value SecurityEventConnection err := s.pool.QueryRow(ctx, ` INSERT INTO gateway_security_event_connections( connection_id,transmitter_issuer,endpoint_url,credential_ref,management_client_id,management_credential_ref,lifecycle_status,idempotency_key ) VALUES($1::uuid,$2,$3,$4,$5,$6,'connecting',$7) RETURNING connection_id::text,transmitter_issuer,endpoint_url,audience,stream_id::text,credential_ref, next_credential_ref,management_client_id,management_credential_ref,lifecycle_status,version,last_error_category,idempotency_key,created_at,updated_at`, input.ConnectionID, input.TransmitterIssuer, input.EndpointURL, input.CredentialRef, input.ManagementClientID, input.ManagementCredentialRef, input.IdempotencyKey, ).Scan(&value.ConnectionID, &value.TransmitterIssuer, &value.EndpointURL, &value.Audience, &value.StreamID, &value.CredentialRef, &value.NextCredentialRef, &value.ManagementClientID, &value.ManagementCredentialRef, &value.LifecycleStatus, &value.Version, &value.LastErrorCategory, &value.IdempotencyKey, &value.CreatedAt, &value.UpdatedAt) if err != nil { if isUniqueViolation(err) { existing, getErr := s.SecurityEventConnection(ctx) if getErr == nil && existing.IdempotencyKey == input.IdempotencyKey && existing.TransmitterIssuer == input.TransmitterIssuer { return existing, nil } return SecurityEventConnection{}, ErrSecurityEventConnectionConflict } return SecurityEventConnection{}, err } return value, nil } func (s *Store) SecurityEventConnection(ctx context.Context) (SecurityEventConnection, error) { var value SecurityEventConnection err := s.pool.QueryRow(ctx, ` SELECT connection.connection_id::text,connection.transmitter_issuer,connection.endpoint_url,connection.audience, connection.stream_id::text,connection.credential_ref,connection.next_credential_ref, COALESCE(connection.management_client_id,''),connection.management_credential_ref,connection.lifecycle_status, connection.version,connection.last_error_category,connection.idempotency_key,connection.created_at,connection.updated_at, state.last_verification_at,COALESCE(state.mode,'disabled') FROM gateway_security_event_connections connection LEFT JOIN gateway_security_event_stream_state state ON state.issuer=connection.transmitter_issuer AND state.audience=connection.audience WHERE connection.singleton=true`).Scan( &value.ConnectionID, &value.TransmitterIssuer, &value.EndpointURL, &value.Audience, &value.StreamID, &value.CredentialRef, &value.NextCredentialRef, &value.ManagementClientID, &value.ManagementCredentialRef, &value.LifecycleStatus, &value.Version, &value.LastErrorCategory, &value.IdempotencyKey, &value.CreatedAt, &value.UpdatedAt, &value.LastVerificationAt, &value.HealthMode, ) if errors.Is(err, pgx.ErrNoRows) { return SecurityEventConnection{}, ErrSecurityEventConnectionNotFound } return value, err } func (s *Store) BindSecurityEventConnection(ctx context.Context, connectionID, audience, streamID, lifecycle string, expectedVersion int64) (SecurityEventConnection, error) { tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections SET audience=$2,stream_id=$3::uuid,lifecycle_status=$4,last_error_category=NULL,version=version+1,updated_at=now() WHERE connection_id=$1::uuid AND version=$5`, connectionID, audience, streamID, lifecycle, expectedVersion) if err != nil { return SecurityEventConnection{}, err } if tag.RowsAffected() == 0 { return SecurityEventConnection{}, ErrSecurityEventConnectionConflict } return s.SecurityEventConnection(ctx) } func (s *Store) SetSecurityEventManagementCredential(ctx context.Context, connectionID, clientID, reference string) (SecurityEventConnection, error) { tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections SET management_client_id=$2,management_credential_ref=$3,last_error_category=NULL,updated_at=now() WHERE connection_id=$1::uuid`, connectionID, clientID, reference) if err != nil { return SecurityEventConnection{}, err } if tag.RowsAffected() == 0 { return SecurityEventConnection{}, ErrSecurityEventConnectionNotFound } return s.SecurityEventConnection(ctx) } func (s *Store) UpdateSecurityEventConnectionLifecycle(ctx context.Context, connectionID, lifecycle string, errorCategory *string) (SecurityEventConnection, error) { tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections SET lifecycle_status=$2,last_error_category=$3,version=version+1,updated_at=now() WHERE connection_id=$1::uuid`, connectionID, lifecycle, errorCategory) if err != nil { return SecurityEventConnection{}, err } if tag.RowsAffected() == 0 { return SecurityEventConnection{}, ErrSecurityEventConnectionNotFound } return s.SecurityEventConnection(ctx) } func (s *Store) SetSecurityEventNextCredential(ctx context.Context, connectionID, reference string) (SecurityEventConnection, error) { tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections SET next_credential_ref=$2,lifecycle_status='rotating',last_error_category=NULL,version=version+1,updated_at=now() WHERE connection_id=$1::uuid`, connectionID, reference) if err != nil { return SecurityEventConnection{}, err } if tag.RowsAffected() == 0 { return SecurityEventConnection{}, ErrSecurityEventConnectionNotFound } return s.SecurityEventConnection(ctx) } func (s *Store) PromoteSecurityEventCredential(ctx context.Context, connectionID, nextReference string) (SecurityEventConnection, error) { tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections SET credential_ref=$2,next_credential_ref=NULL,lifecycle_status='bootstrap',last_error_category=NULL, version=version+1,updated_at=now() WHERE connection_id=$1::uuid AND next_credential_ref=$2`, connectionID, nextReference) if err != nil { return SecurityEventConnection{}, err } if tag.RowsAffected() == 0 { return SecurityEventConnection{}, ErrSecurityEventConnectionConflict } return s.SecurityEventConnection(ctx) } func (s *Store) DeleteSecurityEventConnection(ctx context.Context, connectionID string) error { if _, err := uuid.Parse(connectionID); err != nil { return ErrSecurityEventConnectionNotFound } tag, err := s.pool.Exec(ctx, `DELETE FROM gateway_security_event_connections WHERE connection_id=$1::uuid`, connectionID) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrSecurityEventConnectionNotFound } return nil }