2026-08-04 22:33:16 +08:00
package taskclaim
import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"io"
"math"
"strings"
"sync"
"time"
"cmbuyer/admin/internal/deviceauth"
2026-08-05 01:25:23 +08:00
taskmodel "cmbuyer/admin/internal/tasks"
2026-08-04 22:33:16 +08:00
)
const writeTimeout = 2 * time . Second
type Store struct {
database * sql . DB
secret [] byte
leaseTTL time . Duration
now func () time . Time
random io . Reader
randomMu sync . Mutex
writeGate chan struct {}
// The unexported linearization hooks let package tests coordinate real SQLite
// transactions at the first write. Production construction always leaves them nil.
beforeLinearization func ()
afterLinearization func ()
}
func NewStore ( database * sql . DB , secret [] byte , leaseTTL time . Duration ) ( * Store , error ) {
if database == nil {
return nil , errors . New ( "task claim database is required" )
}
if len ( secret ) != sha256 . Size {
return nil , errors . New ( "task claim secret must be 32 bytes" )
}
if leaseTTL <= 0 {
return nil , errors . New ( "task claim lease TTL must be positive" )
}
if _ , err := database . Exec ( "SELECT attempt_id, claim_nonce, claim_token_sha256 FROM purchase_attempt_claims LIMIT 1" ); err != nil {
return nil , errors . New ( "task claim migration is not available" )
}
store := & Store {
database : database , secret : append ([] byte ( nil ), secret ... ), leaseTTL : leaseTTL ,
now : time . Now , random : rand . Reader , writeGate : make ( chan struct {}, 1 ),
}
if err := store . validateSecretIsolation (); err != nil {
return nil , err
}
if err := store . validateStoredClaims ( context . Background ()); err != nil {
return nil , err
}
return store , nil
}
// validateSecretIsolation ensures the HMAC key cannot also authenticate a device. The session
// secret comparison is performed while parsing configuration, before either secret is discarded.
func ( store * Store ) validateSecretIsolation () error {
digest := sha256 . Sum256 ( store . secret )
var count int
if err := store . database . QueryRow ( `SELECT COUNT(*) FROM device_credentials WHERE token_sha256 = ?` , digest [:]). Scan ( & count ); err != nil {
return errors . New ( "validate task claim secret isolation" )
}
if count != 0 {
return errors . New ( "task claim secret must be isolated from device credentials" )
}
return nil
}
// validateStoredClaims covers open and closed claims. Replacing the secret must fail startup;
// silently signing a new token would destroy idempotent recovery and the ownership audit chain.
func ( store * Store ) validateStoredClaims ( ctx context . Context ) error {
rows , err := store . database . QueryContext ( ctx , `SELECT claims.claimed_by_device_id, claims.task_id, claims.authorization_id,
claims.attempt_id, claims.claim_generation, claims.claim_nonce, typeof(claims.claim_nonce), length(claims.claim_nonce),
claims.claim_token_sha256, typeof(claims.claim_token_sha256), length(claims.claim_token_sha256),
2026-08-05 01:25:23 +08:00
claims.task_title, claims.authorization_task_version, claims.goods_id, claims.sku_color, claims.sku_size,
2026-08-04 22:33:16 +08:00
claims.quantity, claims.total_price_cap, claims.authorization_expires_at, claims.closed_at,
attempts.claim_generation, attempts.status, authorizations.status, tasks.status
FROM purchase_attempt_claims AS claims
LEFT JOIN purchase_attempts AS attempts ON attempts.id = claims.attempt_id
LEFT JOIN order_authorizations AS authorizations ON authorizations.id = claims.authorization_id
LEFT JOIN tasks ON tasks.id = claims.task_id
ORDER BY claims.attempt_id` )
if err != nil {
return errors . New ( "validate stored task claims" )
}
defer rows . Close ()
for rows . Next () {
var deviceID , taskID , authorizationID , attemptID string
var generation , authorizationTaskVersion , quantity int
var nonce , storedHash [] byte
2026-08-05 01:25:23 +08:00
var nonceType , hashType , title , goodsID , color , size , price , expires string
2026-08-04 22:33:16 +08:00
var nonceLength , hashLength int
var closed , attemptStatus , authorizationStatus , taskStatus sql . NullString
var attemptGeneration sql . NullInt64
if err := rows . Scan ( & deviceID , & taskID , & authorizationID , & attemptID , & generation ,
& nonce , & nonceType , & nonceLength , & storedHash , & hashType , & hashLength ,
2026-08-05 01:25:23 +08:00
& title , & authorizationTaskVersion , & goodsID , & color , & size , & quantity , & price , & expires , & closed ,
2026-08-04 22:33:16 +08:00
& attemptGeneration , & attemptStatus , & authorizationStatus , & taskStatus ); err != nil {
return errors . New ( "validate stored task claims" )
}
if ! deviceauth . ValidDeviceID ( deviceID ) || ! validUUID ( taskID ) || ! validUUID ( authorizationID ) || ! validUUID ( attemptID ) ||
generation <= 0 || nonceType != "blob" || nonceLength != sha256 . Size || len ( nonce ) != sha256 . Size ||
hashType != "blob" || hashLength != sha256 . Size || len ( storedHash ) != sha256 . Size ||
2026-08-05 01:25:23 +08:00
authorizationTaskVersion <= 0 || ! taskmodel . ValidTaskWireFields ( title , goodsID , color , size , price ) || quantity <= 0 ||
! validCanonicalTime ( expires ) || ( closed . Valid && ! validCanonicalTime ( closed . String )) ||
2026-08-04 22:33:16 +08:00
! attemptGeneration . Valid || attemptGeneration . Int64 != int64 ( generation ) ||
! validAttemptStatus ( attemptStatus ) || ! validAuthorizationStatus ( authorizationStatus ) || ! validTaskStatus ( taskStatus ) {
return errors . New ( "stored task claim metadata is invalid" )
}
token := deriveToken ( store . secret , deviceID , taskID , authorizationID , attemptID , generation , nonce )
if ! matchingHash ( tokenHash ( token ), storedHash ) {
return errors . New ( "task claim secret does not match stored claims" )
}
}
if err := rows . Err (); err != nil {
return errors . New ( "validate stored task claims" )
}
return nil
}
func ( store * Store ) ClaimNext ( ctx context . Context , deviceID string , command ClaimCommand ) ( ClaimResponse , bool , error ) {
if ! deviceauth . ValidDeviceID ( deviceID ) || ! validUUID ( command . SessionID ) || ! validUUID ( command . ClaimRequestID ) {
return ClaimResponse {}, false , ErrInvalid
}
writeCtx , cancel := context . WithTimeout ( ctx , writeTimeout )
defer cancel ()
select {
case store . writeGate <- struct {}{}:
defer func () { <- store . writeGate }()
case <- writeCtx . Done ():
return ClaimResponse {}, false , writeCtx . Err ()
}
transaction , err := store . database . BeginTx ( writeCtx , nil )
if err != nil {
return ClaimResponse {}, false , err
}
defer transaction . Rollback ()
// This must be the transaction's first database statement. The no-op conditional UPDATE takes
// SQLite's write position and linearizes a concurrent credential revocation before any replay,
// EMPTY response, conflict response, candidate read, or other business write is possible.
if store . beforeLinearization != nil {
store . beforeLinearization ()
}
active , err := transaction . ExecContext ( writeCtx , `UPDATE device_credentials SET status = status
WHERE device_id = ? AND status = 'ACTIVE' AND revoked_at IS NULL` , deviceID )
if err != nil {
return ClaimResponse {}, false , err
}
if ok , err := exactlyOne ( active ); err != nil {
return ClaimResponse {}, false , err
} else if ! ok {
return ClaimResponse {}, false , ErrDeviceInactive
}
if store . afterLinearization != nil {
store . afterLinearization ()
}
now , err := store . serverNow ()
if err != nil {
return ClaimResponse {}, false , err
}
request , found , err := findClaimRequest ( writeCtx , transaction , command . ClaimRequestID )
if err != nil {
return ClaimResponse {}, false , err
}
if found {
if request . DeviceID != deviceID || request . SessionID != command . SessionID {
return ClaimResponse {}, false , ErrIdempotencyConflict
}
switch request . Outcome {
case "EMPTY" :
if err := transaction . Commit (); err != nil {
return ClaimResponse {}, false , err
}
return ClaimResponse {}, false , nil
case "BLOCKED" :
if err := transaction . Commit (); err != nil {
return ClaimResponse {}, false , err
}
return ClaimResponse {}, false , ErrRequiresManual
case "CLAIMED" :
record , found , err := store . loadClaimByAttempt ( writeCtx , transaction , request . AttemptID )
if err != nil || ! found {
if err == nil {
err = errors . New ( "stored claim request has no claim" )
}
return ClaimResponse {}, false , err
}
response , err := store . responseFor ( record , request . ResponseLeaseExpiresAt )
if err != nil {
return ClaimResponse {}, false , err
}
if err := transaction . Commit (); err != nil {
return ClaimResponse {}, false , err
}
return response , true , nil
default :
return ClaimResponse {}, false , errors . New ( "stored claim request outcome is invalid" )
}
}
existing , found , err := store . loadOpenClaimByDevice ( writeCtx , transaction , deviceID )
if err != nil {
return ClaimResponse {}, false , err
}
if found {
current := existing . SessionID == command . SessionID && existing . ClosedAt == "" &&
existing . LeaseExpiresAt . After ( now ) && existing . AuthorizationExpiresAt . After ( now ) &&
existing . CurrentAuthorizationExpiresAt . After ( now ) && existing . AuthorizationStatus == "CLAIMED" &&
existing . authorizationConsistent () && existing . recoverableBusinessState ()
if ! current {
if err := insertClaimRequest ( writeCtx , transaction , command . ClaimRequestID , deviceID , command . SessionID , "BLOCKED" , "" , "" , "manual_recovery_required" , now ); err != nil {
return ClaimResponse {}, false , err
}
if err := transaction . Commit (); err != nil {
return ClaimResponse {}, false , err
}
return ClaimResponse {}, false , ErrRequiresManual
}
response , err := store . responseFor ( existing , existing . LeaseExpiresText )
if err != nil {
return ClaimResponse {}, false , err
}
if err := insertClaimRequest ( writeCtx , transaction , command . ClaimRequestID , deviceID , command . SessionID , "CLAIMED" , existing . AttemptID , existing . LeaseExpiresText , "" , now ); err != nil {
return ClaimResponse {}, false , err
}
if err := transaction . Commit (); err != nil {
return ClaimResponse {}, false , err
}
return response , true , nil
}
candidate , found , err := findCandidate ( writeCtx , transaction , now )
if err != nil {
return ClaimResponse {}, false , err
}
if ! found {
if err := insertClaimRequest ( writeCtx , transaction , command . ClaimRequestID , deviceID , command . SessionID , "EMPTY" , "" , "" , "" , now ); err != nil {
return ClaimResponse {}, false , err
}
if err := transaction . Commit (); err != nil {
return ClaimResponse {}, false , err
}
return ClaimResponse {}, false , nil
}
generation , err := nextGeneration ( writeCtx , transaction , candidate . TaskID )
if err != nil {
return ClaimResponse {}, false , err
}
attemptID , err := store . newUUID ()
if err != nil {
return ClaimResponse {}, false , err
}
nonce , err := store . randomBytes ( sha256 . Size )
if err != nil {
return ClaimResponse {}, false , err
}
token := deriveToken ( store . secret , deviceID , candidate . TaskID , candidate . AuthorizationID , attemptID , generation , nonce )
storedTokenHash := tokenHash ( token )
leaseExpires := now . Add ( store . leaseTTL )
if candidate . AuthorizationExpiresAt . Before ( leaseExpires ) {
leaseExpires = candidate . AuthorizationExpiresAt
}
leaseText := formatTime ( leaseExpires )
nowText := formatTime ( now )
authorizationUpdate , err := transaction . ExecContext ( writeCtx , `UPDATE order_authorizations SET status = 'CLAIMED'
WHERE id = ? AND task_id = ? AND status = 'ACTIVE' AND task_version = ?
AND goods_id = ? AND sku_color = ? AND sku_size = ? AND quantity = ?
AND total_price_cap = ? AND expires_at = ?` ,
candidate . AuthorizationID , candidate . TaskID , candidate . TaskVersion , candidate . GoodsID ,
candidate . SKUColor , candidate . SKUSize , candidate . Quantity , candidate . TotalPriceCap ,
candidate . AuthorizationExpiresText )
if err != nil {
return ClaimResponse {}, false , err
}
if ok , err := exactlyOne ( authorizationUpdate ); err != nil || ! ok {
if err == nil {
err = errors . New ( "authorization changed during claim" )
}
return ClaimResponse {}, false , err
}
taskUpdate , err := transaction . ExecContext ( writeCtx , `UPDATE tasks SET status = 'CLAIMED', version = version + 1, updated_at = ?
WHERE id = ? AND status = 'PENDING' AND version = ? AND title = ? AND goods_id = ?
AND sku_color = ? AND sku_size = ? AND quantity = ? AND max_total_price = ?` ,
nowText , candidate . TaskID , candidate . TaskVersion , candidate . Title , candidate . GoodsID ,
candidate . SKUColor , candidate . SKUSize , candidate . Quantity , candidate . TotalPriceCap )
if err != nil {
return ClaimResponse {}, false , err
}
if ok , err := exactlyOne ( taskUpdate ); err != nil || ! ok {
if err == nil {
err = errors . New ( "task changed during claim" )
}
return ClaimResponse {}, false , err
}
if _ , err := transaction . ExecContext ( writeCtx , `INSERT INTO purchase_attempts
(id, task_id, authorization_id, claim_generation, status, started_at)
VALUES (?, ?, ?, ?, 'CLAIMED', ?)` , attemptID , candidate . TaskID , candidate . AuthorizationID , generation , nowText ); err != nil {
return ClaimResponse {}, false , err
}
if _ , err := transaction . ExecContext ( writeCtx , `INSERT INTO purchase_attempt_claims
(attempt_id, task_id, authorization_id, claimed_by_device_id, session_id, claim_generation,
task_version, task_title, authorization_task_version, goods_id, sku_color, sku_size, quantity,
total_price_cap, authorization_expires_at, claim_nonce, claim_token_sha256,
lease_expires_at, claimed_at, closed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)` ,
attemptID , candidate . TaskID , candidate . AuthorizationID , deviceID , command . SessionID , generation ,
candidate . TaskVersion + 1 , candidate . Title , candidate . TaskVersion , candidate . GoodsID ,
candidate . SKUColor , candidate . SKUSize , candidate . Quantity , candidate . TotalPriceCap ,
candidate . AuthorizationExpiresText , nonce , storedTokenHash , leaseText , nowText ); err != nil {
return ClaimResponse {}, false , err
}
if err := insertClaimRequest ( writeCtx , transaction , command . ClaimRequestID , deviceID , command . SessionID , "CLAIMED" , attemptID , leaseText , "" , now ); err != nil {
return ClaimResponse {}, false , err
}
response := ClaimResponse {
Task : ClaimedTask { ID : candidate . TaskID , Version : candidate . TaskVersion + 1 , Title : candidate . Title ,
ProductURL : productURL ( candidate . GoodsID ), GoodsID : candidate . GoodsID , SKUColor : candidate . SKUColor ,
SKUSize : candidate . SKUSize , Quantity : candidate . Quantity , MaxTotalPrice : candidate . TotalPriceCap },
Authorization : ClaimedAuthorization { ID : candidate . AuthorizationID , TaskVersion : candidate . TaskVersion , ExpiresAt : candidate . AuthorizationExpiresText },
Attempt : ClaimedAttempt { ID : attemptID , ClaimToken : hex . EncodeToString ( token ), ClaimGeneration : generation , LeaseExpiresAt : leaseText },
}
if err := transaction . Commit (); err != nil {
return ClaimResponse {}, false , err
}
return response , true , nil
}
func ( store * Store ) Renew ( ctx context . Context , deviceID string , command RenewCommand ) ( RenewResponse , error ) {
providedToken , tokenOK := decodeToken ( command . ClaimToken )
if ! deviceauth . ValidDeviceID ( deviceID ) || ! validUUID ( command . TaskID ) || ! validUUID ( command . RenewRequestID ) ||
! validUUID ( command . SessionID ) || ! validUUID ( command . AttemptID ) || command . ClaimGeneration <= 0 ||
! tokenOK || ! validCanonicalTime ( command . ExpectedLeaseExpiresAt ) {
return RenewResponse {}, ErrInvalid
}
providedHash := tokenHash ( providedToken )
writeCtx , cancel := context . WithTimeout ( ctx , writeTimeout )
defer cancel ()
select {
case store . writeGate <- struct {}{}:
defer func () { <- store . writeGate }()
case <- writeCtx . Done ():
return RenewResponse {}, writeCtx . Err ()
}
transaction , err := store . database . BeginTx ( writeCtx , nil )
if err != nil {
return RenewResponse {}, err
}
defer transaction . Rollback ()
// As in ClaimNext, this is deliberately the first database statement in the transaction.
if store . beforeLinearization != nil {
store . beforeLinearization ()
}
active , err := transaction . ExecContext ( writeCtx , `UPDATE device_credentials SET status = status
WHERE device_id = ? AND status = 'ACTIVE' AND revoked_at IS NULL` , deviceID )
if err != nil {
return RenewResponse {}, err
}
if ok , err := exactlyOne ( active ); err != nil {
return RenewResponse {}, err
} else if ! ok {
return RenewResponse {}, ErrDeviceInactive
}
if store . afterLinearization != nil {
store . afterLinearization ()
}
renewal , found , err := findRenewal ( writeCtx , transaction , command . RenewRequestID )
if err != nil {
return RenewResponse {}, err
}
if found {
if renewal . TaskID != command . TaskID || renewal . AttemptID != command . AttemptID || renewal . DeviceID != deviceID ||
renewal . SessionID != command . SessionID || renewal . Generation != command . ClaimGeneration ||
renewal . ExpectedLeaseExpiresAt != command . ExpectedLeaseExpiresAt || ! matchingHash ( renewal . TokenHash , providedHash ) {
return RenewResponse {}, ErrIdempotencyConflict
}
response := RenewResponse { TaskID : renewal . TaskID , AttemptID : renewal . AttemptID , ClaimGeneration : renewal . Generation , LeaseExpiresAt : renewal . LeaseExpiresAt }
if err := transaction . Commit (); err != nil {
return RenewResponse {}, err
}
return response , nil
}
now , err := store . serverNow ()
if err != nil {
return RenewResponse {}, err
}
record , found , err := store . loadClaimByAttempt ( writeCtx , transaction , command . AttemptID )
if err != nil {
return RenewResponse {}, err
}
if ! found || record . TaskID != command . TaskID || record . DeviceID != deviceID || record . SessionID != command . SessionID ||
record . Generation != command . ClaimGeneration || ! matchingHash ( record . TokenHash , providedHash ) {
return RenewResponse {}, ErrNotCurrent
}
stateCurrent := record . ClosedAt == "" && record . LeaseExpiresAt . After ( now ) && record . AuthorizationExpiresAt . After ( now ) &&
record . CurrentAuthorizationExpiresAt . After ( now ) && record . AuthorizationStatus == "CLAIMED" &&
record . authorizationConsistent () && record . recoverableBusinessState ()
if ! stateCurrent || record . LeaseExpiresText != command . ExpectedLeaseExpiresAt {
return RenewResponse {}, ErrNotCurrent
}
leaseExpires := now . Add ( store . leaseTTL )
if record . AuthorizationExpiresAt . Before ( leaseExpires ) {
leaseExpires = record . AuthorizationExpiresAt
}
leaseText := formatTime ( leaseExpires )
updated , err := transaction . ExecContext ( writeCtx , `UPDATE purchase_attempt_claims SET lease_expires_at = ?
WHERE attempt_id = ? AND lease_expires_at = ? AND closed_at IS NULL` , leaseText , command . AttemptID , command . ExpectedLeaseExpiresAt )
if err != nil {
return RenewResponse {}, err
}
if ok , err := exactlyOne ( updated ); err != nil || ! ok {
if err == nil {
err = ErrNotCurrent
}
return RenewResponse {}, err
}
if _ , err := transaction . ExecContext ( writeCtx , `INSERT INTO purchase_attempt_lease_renewals
(renew_request_id, task_id, attempt_id, device_id, session_id, claim_generation,
claim_token_sha256, expected_lease_expires_at, lease_expires_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ,
command . RenewRequestID , command . TaskID , command . AttemptID , deviceID , command . SessionID ,
command . ClaimGeneration , record . TokenHash , command . ExpectedLeaseExpiresAt , leaseText , formatTime ( now )); err != nil {
return RenewResponse {}, err
}
response := RenewResponse { TaskID : command . TaskID , AttemptID : command . AttemptID , ClaimGeneration : command . ClaimGeneration , LeaseExpiresAt : leaseText }
if err := transaction . Commit (); err != nil {
return RenewResponse {}, err
}
return response , nil
}
type claimRequestRecord struct {
DeviceID , SessionID , Outcome , AttemptID , ResponseLeaseExpiresAt string
}
func findClaimRequest ( ctx context . Context , transaction * sql . Tx , requestID string ) ( claimRequestRecord , bool , error ) {
var record claimRequestRecord
var attemptID , responseLease sql . NullString
err := transaction . QueryRowContext ( ctx , `SELECT device_id, session_id, outcome, attempt_id, response_lease_expires_at
FROM task_claim_requests WHERE claim_request_id = ?` , requestID ).
Scan ( & record . DeviceID , & record . SessionID , & record . Outcome , & attemptID , & responseLease )
if errors . Is ( err , sql . ErrNoRows ) {
return claimRequestRecord {}, false , nil
}
if err != nil {
return claimRequestRecord {}, false , err
}
record . AttemptID , record . ResponseLeaseExpiresAt = attemptID . String , responseLease . String
return record , true , nil
}
func insertClaimRequest ( ctx context . Context , transaction * sql . Tx , requestID , deviceID , sessionID , outcome , attemptID , responseLease , errorCode string , now time . Time ) error {
var attempt , lease , code any
if attemptID != "" {
attempt = attemptID
}
if responseLease != "" {
lease = responseLease
}
if errorCode != "" {
code = errorCode
}
_ , err := transaction . ExecContext ( ctx , `INSERT INTO task_claim_requests
(claim_request_id, device_id, session_id, outcome, attempt_id, response_lease_expires_at, error_code, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)` , requestID , deviceID , sessionID , outcome , attempt , lease , code , formatTime ( now ))
return err
}
type renewalRecord struct {
TaskID , AttemptID , DeviceID , SessionID string
Generation int
TokenHash [] byte
ExpectedLeaseExpiresAt , LeaseExpiresAt string
}
func findRenewal ( ctx context . Context , transaction * sql . Tx , requestID string ) ( renewalRecord , bool , error ) {
var record renewalRecord
err := transaction . QueryRowContext ( ctx , `SELECT task_id, attempt_id, device_id, session_id,
claim_generation, claim_token_sha256, expected_lease_expires_at, lease_expires_at
FROM purchase_attempt_lease_renewals WHERE renew_request_id = ?` , requestID ).
Scan ( & record . TaskID , & record . AttemptID , & record . DeviceID , & record . SessionID , & record . Generation ,
& record . TokenHash , & record . ExpectedLeaseExpiresAt , & record . LeaseExpiresAt )
if errors . Is ( err , sql . ErrNoRows ) {
return renewalRecord {}, false , nil
}
return record , err == nil , err
}
type claimRecord struct {
AttemptID , TaskID , AuthorizationID , DeviceID , SessionID string
Generation , TaskVersion , CurrentTaskVersion int
TaskTitle string
Nonce , TokenHash [] byte
LeaseExpiresText , ClaimedAt , ClosedAt string
LeaseExpiresAt time . Time
AuthorizationTaskVersion int
GoodsID , SKUColor , SKUSize , TotalPriceCap string
Quantity int
AuthorizationExpiresText , AuthorizationStatus string
AuthorizationExpiresAt time . Time
CurrentAuthorizationTaskVersion int
CurrentGoodsID , CurrentSKUColor , CurrentSKUSize string
CurrentQuantity int
CurrentTotalPriceCap , CurrentAuthorizationExpiresText string
CurrentAuthorizationExpiresAt time . Time
AttemptStatus , TaskStatus string
CurrentTaskTitle , CurrentTaskGoodsID string
CurrentTaskSKUColor , CurrentTaskSKUSize string
CurrentTaskQuantity int
CurrentTaskMaxTotalPrice string
CurrentAttemptGeneration int
}
const claimSelect = `SELECT claims.attempt_id, claims.task_id, claims.authorization_id,
claims.claimed_by_device_id, claims.session_id, claims.claim_generation, claims.task_version,
claims.task_title, claims.authorization_task_version, claims.goods_id, claims.sku_color,
claims.sku_size, claims.quantity, claims.total_price_cap, claims.authorization_expires_at,
claims.claim_nonce, claims.claim_token_sha256, claims.lease_expires_at,
claims.claimed_at, claims.closed_at, authorizations.task_version, authorizations.goods_id,
authorizations.sku_color, authorizations.sku_size, authorizations.quantity,
authorizations.total_price_cap, authorizations.expires_at, authorizations.status,
attempts.claim_generation, attempts.status, tasks.status, tasks.version, tasks.title, tasks.goods_id,
tasks.sku_color, tasks.sku_size, tasks.quantity, tasks.max_total_price
FROM purchase_attempt_claims AS claims
JOIN order_authorizations AS authorizations
ON authorizations.task_id = claims.task_id AND authorizations.id = claims.authorization_id
JOIN purchase_attempts AS attempts ON attempts.id = claims.attempt_id
JOIN tasks ON tasks.id = claims.task_id `
func ( store * Store ) loadOpenClaimByDevice ( ctx context . Context , transaction * sql . Tx , deviceID string ) ( claimRecord , bool , error ) {
return store . scanClaim ( transaction . QueryRowContext ( ctx , claimSelect + `WHERE claims.claimed_by_device_id = ? AND claims.closed_at IS NULL` , deviceID ))
}
func ( store * Store ) loadClaimByAttempt ( ctx context . Context , transaction * sql . Tx , attemptID string ) ( claimRecord , bool , error ) {
return store . scanClaim ( transaction . QueryRowContext ( ctx , claimSelect + `WHERE claims.attempt_id = ?` , attemptID ))
}
type rowScanner interface { Scan ( ... any ) error }
func ( store * Store ) scanClaim ( row rowScanner ) ( claimRecord , bool , error ) {
var record claimRecord
var closed sql . NullString
err := row . Scan ( & record . AttemptID , & record . TaskID , & record . AuthorizationID , & record . DeviceID ,
& record . SessionID , & record . Generation , & record . TaskVersion , & record . TaskTitle ,
& record . AuthorizationTaskVersion , & record . GoodsID , & record . SKUColor , & record . SKUSize ,
& record . Quantity , & record . TotalPriceCap , & record . AuthorizationExpiresText ,
& record . Nonce , & record . TokenHash , & record . LeaseExpiresText , & record . ClaimedAt , & closed ,
& record . CurrentAuthorizationTaskVersion , & record . CurrentGoodsID , & record . CurrentSKUColor ,
& record . CurrentSKUSize , & record . CurrentQuantity , & record . CurrentTotalPriceCap ,
& record . CurrentAuthorizationExpiresText ,
& record . AuthorizationStatus , & record . CurrentAttemptGeneration , & record . AttemptStatus ,
& record . TaskStatus , & record . CurrentTaskVersion ,
& record . CurrentTaskTitle , & record . CurrentTaskGoodsID , & record . CurrentTaskSKUColor ,
& record . CurrentTaskSKUSize , & record . CurrentTaskQuantity , & record . CurrentTaskMaxTotalPrice )
if errors . Is ( err , sql . ErrNoRows ) {
return claimRecord {}, false , nil
}
if err != nil {
return claimRecord {}, false , err
}
record . ClosedAt = closed . String
if ! validUUID ( record . AttemptID ) || ! validUUID ( record . TaskID ) || ! validUUID ( record . AuthorizationID ) ||
! deviceauth . ValidDeviceID ( record . DeviceID ) || ! validUUID ( record . SessionID ) || record . Generation <= 0 ||
record . CurrentAttemptGeneration != record . Generation ||
2026-08-05 01:25:23 +08:00
record . TaskVersion <= 0 || record . AuthorizationTaskVersion <= 0 ||
! taskmodel . ValidTaskWireFields ( record . TaskTitle , record . GoodsID , record . SKUColor , record . SKUSize , record . TotalPriceCap ) ||
record . Quantity <= 0 || len ( record . Nonce ) != sha256 . Size || len ( record . TokenHash ) != sha256 . Size {
2026-08-04 22:33:16 +08:00
return claimRecord {}, false , errors . New ( "stored task claim metadata is invalid" )
}
record . LeaseExpiresAt , err = parseCanonicalTime ( record . LeaseExpiresText )
if err != nil {
return claimRecord {}, false , errors . New ( "stored task claim lease is invalid" )
}
record . AuthorizationExpiresAt , err = parseCanonicalTime ( record . AuthorizationExpiresText )
if err != nil {
return claimRecord {}, false , errors . New ( "stored authorization expiry is invalid" )
}
record . CurrentAuthorizationExpiresAt , err = parseCanonicalTime ( record . CurrentAuthorizationExpiresText )
if err != nil {
return claimRecord {}, false , errors . New ( "current authorization expiry is invalid" )
}
derived := deriveToken ( store . secret , record . DeviceID , record . TaskID , record . AuthorizationID , record . AttemptID , record . Generation , record . Nonce )
if ! matchingHash ( tokenHash ( derived ), record . TokenHash ) {
return claimRecord {}, false , errors . New ( "task claim secret does not match stored claim" )
}
return record , true , nil
}
func ( record claimRecord ) authorizationConsistent () bool {
2026-08-05 01:25:23 +08:00
return taskmodel . ValidAuthorizationFields ( record . CurrentGoodsID , record . CurrentSKUColor ,
record . CurrentSKUSize , record . CurrentTotalPriceCap ) &&
taskmodel . ValidTaskWireFields ( record . CurrentTaskTitle , record . CurrentTaskGoodsID ,
record . CurrentTaskSKUColor , record . CurrentTaskSKUSize , record . CurrentTaskMaxTotalPrice ) &&
record . AuthorizationTaskVersion == record . CurrentAuthorizationTaskVersion &&
2026-08-04 22:33:16 +08:00
record . GoodsID == record . CurrentGoodsID && record . SKUColor == record . CurrentSKUColor &&
record . SKUSize == record . CurrentSKUSize && record . Quantity == record . CurrentQuantity &&
record . TotalPriceCap == record . CurrentTotalPriceCap &&
record . AuthorizationExpiresText == record . CurrentAuthorizationExpiresText &&
record . TaskTitle == record . CurrentTaskTitle && record . GoodsID == record . CurrentTaskGoodsID &&
record . SKUColor == record . CurrentTaskSKUColor && record . SKUSize == record . CurrentTaskSKUSize &&
record . Quantity == record . CurrentTaskQuantity && record . TotalPriceCap == record . CurrentTaskMaxTotalPrice
}
func ( record claimRecord ) recoverableBusinessState () bool {
if record . TaskStatus == "CLAIMED" && record . AttemptStatus == "CLAIMED" {
return record . CurrentTaskVersion == record . TaskVersion
}
// A later server task may advance this same attempt to ORDERING. A valid lease and identical
// ownership recover that attempt; claim-next still cannot select another task.
return record . TaskStatus == "ORDERING" && record . AttemptStatus == "ORDERING" &&
record . TaskVersion < math . MaxInt && record . CurrentTaskVersion == record . TaskVersion + 1
}
func ( store * Store ) responseFor ( record claimRecord , responseLease string ) ( ClaimResponse , error ) {
2026-08-05 01:25:23 +08:00
// Exact idempotent replay is allowed to ignore later source-row drift, but the
// immutable response snapshot itself must still satisfy the current wire bounds.
if ! validCanonicalTime ( responseLease ) ||
! taskmodel . ValidTaskWireFields ( record . TaskTitle , record . GoodsID , record . SKUColor , record . SKUSize , record . TotalPriceCap ) ||
record . Quantity <= 0 {
return ClaimResponse {}, errors . New ( "stored claim response snapshot is invalid" )
2026-08-04 22:33:16 +08:00
}
token := deriveToken ( store . secret , record . DeviceID , record . TaskID , record . AuthorizationID , record . AttemptID , record . Generation , record . Nonce )
return ClaimResponse {
Task : ClaimedTask { ID : record . TaskID , Version : record . TaskVersion , Title : record . TaskTitle ,
ProductURL : productURL ( record . GoodsID ), GoodsID : record . GoodsID , SKUColor : record . SKUColor ,
SKUSize : record . SKUSize , Quantity : record . Quantity , MaxTotalPrice : record . TotalPriceCap },
Authorization : ClaimedAuthorization { ID : record . AuthorizationID , TaskVersion : record . AuthorizationTaskVersion , ExpiresAt : record . AuthorizationExpiresText },
Attempt : ClaimedAttempt { ID : record . AttemptID , ClaimToken : hex . EncodeToString ( token ), ClaimGeneration : record . Generation , LeaseExpiresAt : responseLease },
}, nil
}
type candidate struct {
AuthorizationID , TaskID , Title , GoodsID , SKUColor , SKUSize , TotalPriceCap string
TaskVersion , Quantity int
2026-08-05 01:39:35 +08:00
AuthorizationTaskVersion , AuthorizationQuantity int
AuthorizationGoodsID , AuthorizationSKUColor , AuthorizationSKUSize string
AuthorizationTotalPriceCap string
2026-08-04 22:33:16 +08:00
AuthorizationExpiresText string
AuthorizationExpiresAt time . Time
}
func findCandidate ( ctx context . Context , transaction * sql . Tx , now time . Time ) ( candidate , bool , error ) {
rows , err := transaction . QueryContext ( ctx , `SELECT authorizations.id, tasks.id, tasks.version,
tasks.title, tasks.goods_id, tasks.sku_color, tasks.sku_size, tasks.quantity,
2026-08-05 01:39:35 +08:00
tasks.max_total_price, authorizations.task_version, authorizations.goods_id,
authorizations.sku_color, authorizations.sku_size, authorizations.quantity,
authorizations.total_price_cap, authorizations.expires_at
2026-08-04 22:33:16 +08:00
FROM order_authorizations AS authorizations
JOIN tasks ON tasks.id = authorizations.task_id
WHERE authorizations.status = 'ACTIVE' AND tasks.status = 'PENDING'
ORDER BY authorizations.created_at, authorizations.rowid, authorizations.id` )
if err != nil {
return candidate {}, false , err
}
defer rows . Close ()
for rows . Next () {
var item candidate
if err := rows . Scan ( & item . AuthorizationID , & item . TaskID , & item . TaskVersion , & item . Title ,
& item . GoodsID , & item . SKUColor , & item . SKUSize , & item . Quantity , & item . TotalPriceCap ,
2026-08-05 01:39:35 +08:00
& item . AuthorizationTaskVersion , & item . AuthorizationGoodsID , & item . AuthorizationSKUColor ,
& item . AuthorizationSKUSize , & item . AuthorizationQuantity , & item . AuthorizationTotalPriceCap ,
2026-08-04 22:33:16 +08:00
& item . AuthorizationExpiresText ); err != nil {
return candidate {}, false , err
}
item . AuthorizationExpiresAt , err = parseCanonicalTime ( item . AuthorizationExpiresText )
if err != nil {
return candidate {}, false , errors . New ( "stored authorization expiry is invalid" )
}
if ! validCandidate ( item ) {
return candidate {}, false , errors . New ( "stored claim candidate is invalid" )
}
2026-08-05 01:39:35 +08:00
if ! candidateSnapshotMatches ( item ) {
continue
}
2026-08-04 22:33:16 +08:00
if item . AuthorizationExpiresAt . After ( now ) {
if err := rows . Close (); err != nil {
return candidate {}, false , err
}
return item , true , nil
}
}
if err := rows . Err (); err != nil {
return candidate {}, false , err
}
return candidate {}, false , nil
}
func validCandidate ( item candidate ) bool {
return validUUID ( item . AuthorizationID ) && validUUID ( item . TaskID ) && item . TaskVersion > 0 && item . TaskVersion < math . MaxInt &&
2026-08-05 01:25:23 +08:00
taskmodel . ValidTaskWireFields ( item . Title , item . GoodsID , item . SKUColor , item . SKUSize , item . TotalPriceCap ) &&
2026-08-05 01:39:35 +08:00
item . Quantity > 0 && item . AuthorizationTaskVersion > 0 && item . AuthorizationTaskVersion < math . MaxInt &&
taskmodel . ValidAuthorizationFields ( item . AuthorizationGoodsID , item . AuthorizationSKUColor ,
item . AuthorizationSKUSize , item . AuthorizationTotalPriceCap ) && item . AuthorizationQuantity > 0
}
func candidateSnapshotMatches ( item candidate ) bool {
return item . AuthorizationTaskVersion == item . TaskVersion && item . AuthorizationGoodsID == item . GoodsID &&
item . AuthorizationSKUColor == item . SKUColor && item . AuthorizationSKUSize == item . SKUSize &&
item . AuthorizationQuantity == item . Quantity && item . AuthorizationTotalPriceCap == item . TotalPriceCap
2026-08-04 22:33:16 +08:00
}
func validAttemptStatus ( value sql . NullString ) bool {
return value . Valid && oneOf ( value . String , "CLAIMED" , "ORDERING" , "FAILED" , "FENCED" , "ABANDONED" )
}
func validAuthorizationStatus ( value sql . NullString ) bool {
return value . Valid && oneOf ( value . String , "ACTIVE" , "CLAIMED" , "FENCED" , "CONSUMED" , "EXPIRED" , "ABANDONED" )
}
func validTaskStatus ( value sql . NullString ) bool {
return value . Valid && oneOf ( value . String , "DRAFT" , "PENDING" , "CLAIMED" , "ORDERING" , "NEEDS_MANUAL" ,
"WAITING_PAYMENT" , "RECONCILIATION_REQUIRED" , "SUCCEEDED" , "FAILED" , "CANCELED" )
}
func oneOf ( value string , allowed ... string ) bool {
for _ , item := range allowed {
if value == item {
return true
}
}
return false
}
func nextGeneration ( ctx context . Context , transaction * sql . Tx , taskID string ) ( int , error ) {
var maximum int64
if err := transaction . QueryRowContext ( ctx , `SELECT COALESCE(MAX(claim_generation), 0) FROM purchase_attempts WHERE task_id = ?` , taskID ). Scan ( & maximum ); err != nil {
return 0 , err
}
if maximum < 0 || maximum >= int64 ( math . MaxInt ) {
return 0 , errors . New ( "task claim generation is exhausted" )
}
return int ( maximum ) + 1 , nil
}
func ( store * Store ) serverNow () ( time . Time , error ) {
now := store . now (). UTC ()
if now . IsZero () {
return time . Time {}, errors . New ( "task claim clock is invalid" )
}
return now , nil
}
func ( store * Store ) randomBytes ( size int ) ([] byte , error ) {
value := make ([] byte , size )
store . randomMu . Lock ()
_ , err := io . ReadFull ( store . random , value )
store . randomMu . Unlock ()
if err != nil {
return nil , fmt . Errorf ( "generate task claim randomness: %w" , err )
}
return value , nil
}
func ( store * Store ) newUUID () ( string , error ) {
value , err := store . randomBytes ( 16 )
if err != nil {
return "" , err
}
value [ 6 ] = ( value [ 6 ] & 0x0f ) | 0x40
value [ 8 ] = ( value [ 8 ] & 0x3f ) | 0x80
encoded := hex . EncodeToString ( value )
return encoded [: 8 ] + "-" + encoded [ 8 : 12 ] + "-" + encoded [ 12 : 16 ] + "-" + encoded [ 16 : 20 ] + "-" + encoded [ 20 :], nil
}
func exactlyOne ( result sql . Result ) ( bool , error ) {
rows , err := result . RowsAffected ()
return rows == 1 , err
}
func formatTime ( value time . Time ) string { return value . UTC (). Format ( time . RFC3339Nano ) }
func parseCanonicalTime ( value string ) ( time . Time , error ) {
if ! strings . HasSuffix ( value , "Z" ) || strings . TrimSpace ( value ) != value {
return time . Time {}, ErrInvalid
}
parsed , err := time . Parse ( time . RFC3339Nano , value )
if err != nil || parsed . Location () != time . UTC || formatTime ( parsed ) != value {
return time . Time {}, ErrInvalid
}
return parsed , nil
}
func validCanonicalTime ( value string ) bool {
_ , err := parseCanonicalTime ( value )
return err == nil
}
func validUUID ( value string ) bool {
if len ( value ) != 36 {
return false
}
for index , character := range value {
if index == 8 || index == 13 || index == 18 || index == 23 {
if character != '-' {
return false
}
continue
}
if !( character >= '0' && character <= '9' || character >= 'a' && character <= 'f' ) {
return false
}
}
return value [ 14 ] == '4' && ( value [ 19 ] == '8' || value [ 19 ] == '9' || value [ 19 ] == 'a' || value [ 19 ] == 'b' )
}
func productURL ( goodsID string ) string {
return "https://mobile.yangkeduo.com/goods.html?goods_id=" + goodsID
}