package projectinstance import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "time" "github.com/jackc/pgx/v5" ) func hashActivationToken(token string) string { sum := sha256.Sum256([]byte(token)) return hex.EncodeToString(sum[:]) } func (s *pgStore) SaveActivationToken(ctx context.Context, token string, projectID int64, planHash string, expectedVersion int, snapshot WorkflowPlan, createdBy int64, expiresAt time.Time, policyRevision *int64) error { raw, _ := json.Marshal(snapshot) tokenHash := hashActivationToken(token) _, err := s.pool.Exec(ctx, ` INSERT INTO workflow_activation_tokens (token, project_id, plan_hash, expected_project_version, config_snapshot, created_by, expires_at, author_only_policy_revision) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) `, tokenHash, projectID, planHash, expectedVersion, raw, createdBy, expiresAt, policyRevision) return err } func (s *pgStore) ConsumeActivationToken(ctx context.Context, token string, projectID int64, planHash string) (*WorkflowPlan, error) { tokenHash := hashActivationToken(token) var snapRaw []byte var storedHash string var usedAt *time.Time var expiresAt time.Time err := s.pool.QueryRow(ctx, ` SELECT plan_hash, config_snapshot, used_at, expires_at FROM workflow_activation_tokens WHERE token = $1 AND project_id = $2 `, tokenHash, projectID).Scan(&storedHash, &snapRaw, &usedAt, &expiresAt) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrTokenInvalid } if err != nil { return nil, err } if usedAt != nil || time.Now().After(expiresAt) || storedHash != planHash { return nil, ErrTokenInvalid } tag, err := s.pool.Exec(ctx, ` UPDATE workflow_activation_tokens SET used_at = now() WHERE token = $1 AND used_at IS NULL `, tokenHash) if err != nil { return nil, err } if tag.RowsAffected() == 0 { return nil, ErrTokenInvalid } var plan WorkflowPlan _ = json.Unmarshal(snapRaw, &plan) return &plan, nil }