package verification import ( "context" "crypto/rand" "encoding/base64" "errors" "strings" verificationmodel "git.sendico.io/tech/sxchnge/product/verification/model" mutil "git.sendico.io/tech/sxchnge/shared/database/query" "git.sendico.io/tech/sxchnge/shared/database/repository" "git.sendico.io/tech/sxchnge/shared/database/repository/builder" "git.sendico.io/tech/sxchnge/shared/errors" ) func normalizedIdempotencyKey(value *string) (string, bool) { if value == nil { return "", false } key := strings.TrimSpace(*value) if key == "" { return "", false } return key, true } func syntheticIdempotencyKey() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", merrors.InternalWrap(err, "generate verification key") } return "auto:" + base64.RawURLEncoding.EncodeToString(b), nil } func idempotencyContextFilter( request *Request, idempotencyKey string, ) builder.Query { return repository.Query().And( repository.Filter("accountRef", request.AccountRef), repository.Filter("purpose", request.Purpose), targetFilter(request.Target), repository.Filter("idempotencyKey", idempotencyKey), ) } func idempotencySeed(request *Request, idempotencyKey string) string { return strings.Join([]string{ request.AccountRef.Hex(), string(request.Purpose), request.Target, request.ContextRef, request.Kind, idempotencyKey, }, "|") } func (db *verificationDB) lookupIdempotencyReplay(ctx context.Context, request *Request, idempotencyKey string) (*verificationmodel.VerificationToken, bool, error) { tokens, err := mutil.GetObjects[verificationmodel.VerificationToken]( ctx, db.Logger, idempotencyContextFilter(request, idempotencyKey), nil, db.Repository, ) switch { case err == nil: case errors.Is(err, merrors.ErrNoData): return nil, false, nil default: return nil, false, err } var replayed *verificationmodel.VerificationToken for i := range tokens { existingContextRef := strings.TrimSpace(tokens[i].ContextRef) // The lookup intentionally ignores contextRef. A reused idempotency key // in another context is a client conflict, not a replay. if existingContextRef != request.ContextRef { return nil, false, ErrorIdempotencyConflict() } if tokens[i].UsedAt != nil { return nil, false, ErorrTokenAlreadyUsed() } if tokens[i].MaxRetries != nil && tokens[i].Attempts >= *tokens[i].MaxRetries { return nil, false, ErrorTokenAttemptsExceeded() } if replayed == nil { token := tokens[i] replayed = &token } } return replayed, replayed != nil, nil }