package onboarding import ( "context" "crypto/hmac" "encoding/json" "errors" "fmt" "strings" "time" "data-terminal-cloud/internal/deviceactivation" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) type RegisterInput struct { ChallengeID string `json:"challenge_id"` BootID string `json:"boot_id"` Model string `json:"hardware_model"` HardwareRevision string `json:"hardware_revision"` FirmwareVersion string `json:"firmware_version"` SDKVersion string `json:"sdk_version"` } func (s *Service) Register(ctx context.Context, d Device, requestID string, raw []byte, in RegisterInput) (map[string]any, error) { if !uuidV4(in.ChallengeID) || !uuidV4(in.BootID) || in.Model != d.Model || in.HardwareRevision != d.HardwareRevision || len(in.FirmwareVersion) < 1 || len(in.FirmwareVersion) > 32 || len(in.SDKVersion) < 1 || len(in.SDKVersion) > 64 { return nil, fail(400, "VALIDATION_FAILED", "注册字段与生产记录不一致") } return s.mutate(ctx, d, "device:"+d.ID, "register", requestID, raw, 24*time.Hour, func(tx pgx.Tx, current Device) (map[string]any, error) { tag, err := tx.Exec(ctx, `UPDATE device_time_challenge SET consumed_by=$1::uuid WHERE id=$2::uuid AND device_id=$3::uuid AND key_version=$4 AND boot_id=$5::uuid AND expires_at>now() AND consumed_by IS NULL`, requestID, in.ChallengeID, d.ID, d.KeyVersion, in.BootID) if err != nil { return nil, err } if tag.RowsAffected() != 1 { return nil, fail(409, "CHALLENGE_INVALID", "请重新获取校时挑战") } if current.BootID != "" && current.BootID != in.BootID { if _, err = tx.Exec(ctx, `UPDATE device_setup_session SET closed_at=COALESCE(closed_at,now()),close_reason='DEVICE_REBOOT' WHERE device_id=$1::uuid`, d.ID); err != nil { return nil, err } if _, err = tx.Exec(ctx, `UPDATE device_binding_claim SET status='CANCELLED',completed_at=now() WHERE device_id=$1::uuid AND status='PENDING'`, d.ID); err != nil { return nil, err } // A new boot cannot reuse a still-live pre-reboot execution permit: // the previous SDK call may have reached the supplier before power loss. pending, e := tx.Exec(ctx, `UPDATE device_activation_attempt a SET status='UNKNOWN' FROM device_activation_job j WHERE a.activation_id=j.id AND j.device_id=$1::uuid AND a.executor='DEVICE' AND a.status='CONTACT_POSSIBLE'`, d.ID) if e != nil { return nil, e } if pending.RowsAffected() > 0 { if _, err = tx.Exec(ctx, `UPDATE device_activation_job SET status='RECONCILE_REQUIRED',last_error_code='DEVICE_REBOOT_DURING_ATTEMPT',version=version+1,updated_at=now() WHERE device_id=$1::uuid`, d.ID); err != nil { return nil, err } } } _, err = tx.Exec(ctx, `UPDATE device SET onboarding_boot_id=$2::uuid,onboarding_last_seen_at=now(),onboarding_revision=onboarding_revision+1 WHERE id=$1::uuid`, d.ID, in.BootID) if err != nil { return nil, err } _, err = tx.Exec(ctx, `UPDATE device_twin SET firmware_version=$2,board_revision=$3,rtc_sdk_version=$4,last_reported_at=now(),updated_at=now() WHERE device_id=$1::uuid`, d.ID, in.FirmwareVersion, in.HardwareRevision, in.SDKVersion) if err != nil { return nil, err } st, err := s.state(ctx, tx, d) return map[string]any{"state": st, "contract_version": ContractVersion}, err }) } type SetupInput struct { BootID string `json:"boot_id"` LocalSetupID string `json:"local_setup_id"` Transport string `json:"transport_profile"` } type setup struct { ID, DeviceID, BootID, LocalID, Mode string Generation int64 Expires time.Time Closed *time.Time } func readSetup(ctx context.Context, tx pgx.Tx, id string, d Device, open bool) (setup, error) { var v setup if !uuidV4(id) { return v, fail(400, "VALIDATION_FAILED", "设置会话编号不正确") } err := tx.QueryRow(ctx, `SELECT id::text,device_id::text,boot_id::text,local_setup_id::text,mode,binding_generation,expires_at,closed_at FROM device_setup_session WHERE id=$1::uuid AND device_id=$2::uuid AND tenant_id=$3::uuid FOR UPDATE`, id, d.ID, d.TenantID).Scan(&v.ID, &v.DeviceID, &v.BootID, &v.LocalID, &v.Mode, &v.Generation, &v.Expires, &v.Closed) if errors.Is(err, pgx.ErrNoRows) { return v, errAccess } if err != nil { return v, err } if v.BootID != d.BootID { return v, fail(409, "BOOT_CHANGED", "设备已重启,请恢复新的设置会话") } if v.Generation != d.Generation { return v, errState } if open && (v.Closed != nil || !v.Expires.After(time.Now())) { return v, fail(410, "SETUP_EXPIRED", "设置入口已关闭,请从设备恢复") } return v, nil } func (s *Service) Setup(ctx context.Context, d Device, requestID string, raw []byte, in SetupInput) (map[string]any, error) { if !uuidV4(in.BootID) || !uuidV4(in.LocalSetupID) || in.Transport != "LOCAL_WEB" { return nil, fail(400, "VALIDATION_FAILED", "设置会话参数不正确") } return s.mutate(ctx, d, "device:"+d.ID, "setup", requestID, raw, 15*time.Minute, func(tx pgx.Tx, current Device) (map[string]any, error) { if current.BootID != in.BootID { return nil, fail(409, "BOOT_CHANGED", "请先完成本次启动注册") } st, err := s.state(ctx, tx, current) if err != nil { return nil, err } mode := "ENROLL" if st.BindingState == "BOUND" { mode = "RECOVERY" } if mode == "ENROLL" && !s.Enabled() { return nil, fail(503, "ENROLLMENT_PAUSED", "首次绑定入口暂停") } var id string var expires time.Time err = tx.QueryRow(ctx, `SELECT id::text,expires_at FROM device_setup_session WHERE device_id=$1::uuid AND boot_id=$2::uuid AND local_setup_id=$3::uuid AND binding_generation=$4 AND mode=$5 AND closed_at IS NULL AND expires_at>now() ORDER BY created_at DESC LIMIT 1`, d.ID, in.BootID, in.LocalSetupID, current.Generation, mode).Scan(&id, &expires) if errors.Is(err, pgx.ErrNoRows) { expires = s.now().Add(15 * time.Minute) err = tx.QueryRow(ctx, `INSERT INTO device_setup_session(device_id,tenant_id,boot_id,local_setup_id,binding_generation,mode,expires_at) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,$5,$6,$7) RETURNING id::text`, d.ID, d.TenantID, in.BootID, in.LocalSetupID, current.Generation, mode, expires).Scan(&id) } return map[string]any{"setup_id": id, "expires_at_ms": expires.UnixMilli(), "poll_after_ms": 2000, "state": st}, err }) } type HandoffInput struct { SetupID string `json:"setup_id"` LocalID string `json:"local_setup_id"` Challenge string `json:"code_challenge"` } func (s *Service) Handoff(ctx context.Context, d Device, requestID string, raw []byte, in HandoffInput) (map[string]any, error) { if !uuidV4(in.LocalID) || !validB64(in.Challenge, 32) { return nil, fail(400, "VALIDATION_FAILED", "入口挑战不正确") } return s.mutate(ctx, d, "device:"+d.ID, "handoff", requestID, raw, 90*time.Second, func(tx pgx.Tx, current Device) (map[string]any, error) { setup, err := readSetup(ctx, tx, in.SetupID, current, true) if err != nil { return nil, err } if setup.Mode != "ENROLL" || setup.LocalID != in.LocalID { return nil, errAccess } var bound bool if err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_binding WHERE device_id=$1::uuid AND unbound_at IS NULL)`, d.ID).Scan(&bound); err != nil { return nil, err } if bound { return nil, errAccess } token := randomToken(32) expires := s.now().Add(90 * time.Second) if setup.Expires.Before(expires) { expires = setup.Expires } _, err = tx.Exec(ctx, `INSERT INTO device_handoff(token_hash,setup_id,code_challenge,expires_at) VALUES($1,$2::uuid,$3,$4)`, digest([]byte(token)), setup.ID, in.Challenge, expires) return map[string]any{"handoff_token": token, "expires_at_ms": expires.UnixMilli(), "entry_url": s.options.EntryURL}, err }) } type PollInput struct { SetupID string `json:"setup_id"` BootID string `json:"boot_id"` KnownRevision int64 `json:"known_revision"` } func (s *Service) Poll(ctx context.Context, d Device, in PollInput) (map[string]any, error) { if !uuidV4(in.BootID) || in.KnownRevision < 0 { return nil, fail(400, "VALIDATION_FAILED", "轮询参数不正确") } tx, err := s.pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) d, err = readDevice(ctx, tx, d.ID, true) if err != nil { return nil, err } if !deviceAllowed(d) { return nil, errAccess } setup, err := readSetup(ctx, tx, in.SetupID, d, false) if err != nil { return nil, err } if in.BootID != d.BootID { return nil, errState } if err = s.expireClaims(ctx, tx, d.ID); err != nil { return nil, err } st, err := s.state(ctx, tx, d) if err != nil { return nil, err } var command any if setup.Closed == nil && setup.Expires.After(s.now()) && setup.Mode == "ENROLL" { var id, hash, masked string var encrypted []byte var expires time.Time var gen int64 err = tx.QueryRow(ctx, `SELECT c.id::text,c.challenge_ciphertext,c.binding_digest,c.expected_generation,c.expires_at,COALESCE(ua.display_name,'') FROM device_binding_claim c JOIN user_account ua ON ua.id=c.user_id WHERE c.device_id=$1::uuid AND c.setup_id=$2::uuid AND c.status='PENDING' AND c.expires_at>now()`, d.ID, setup.ID).Scan(&id, &encrypted, &hash, &gen, &expires, &masked) if err == nil { plain, e := s.crypt.open(encrypted, "claim:"+id) if e != nil { return nil, e } runes := []rune(masked) if len(runes) > 4 { masked = "尾号" + string(runes[len(runes)-4:]) } else { masked = "当前登录账号" } command = map[string]any{"command_id": id, "type": "CONFIRM_CLAIM", "expires_at_ms": expires.UnixMilli(), "payload": map[string]any{"claim_id": id, "challenge": string(plain), "binding_digest": hash, "expected_binding_generation": gen, "masked_account": masked}} } else if !errors.Is(err, pgx.ErrNoRows) { return nil, err } } if command == nil && st.BindingState == "BOUND" { command, err = s.activationCommand(ctx, tx, d) if err != nil { return nil, err } } st, err = s.state(ctx, tx, d) if err != nil { return nil, err } interval := 2000 if st.BindingState == "BOUND" { interval = 10000 } if st.ActivationState == "READY" { interval = 60000 } return map[string]any{"state": st, "command": command, "poll_after_ms": interval}, tx.Commit(ctx) } func (s *Service) expireClaims(ctx context.Context, tx pgx.Tx, id string) error { tag, err := tx.Exec(ctx, `UPDATE device_binding_claim SET status='EXPIRED',completed_at=now() WHERE device_id=$1::uuid AND status='PENDING' AND expires_at<=now()`, id) if err == nil && tag.RowsAffected() > 0 { err = bump(ctx, tx, id) } return err } type ConfirmationInput struct { ClaimID string `json:"claim_id"` Challenge string `json:"challenge"` Digest string `json:"binding_digest"` ExpectedGeneration int64 `json:"expected_binding_generation"` PressEventID string `json:"press_event_id"` } func (s *Service) Confirm(ctx context.Context, d Device, requestID string, raw []byte, in ConfirmationInput) (map[string]any, error) { if !uuidV4(in.ClaimID) || !uuidV4(in.PressEventID) || !validB64(in.Challenge, 32) || !validDigest(in.Digest) || in.ExpectedGeneration < 0 { return nil, fail(400, "VALIDATION_FAILED", "设备确认字段不正确") } return s.mutate(ctx, d, "device:"+d.ID, "confirm", requestID, raw, 24*time.Hour, func(tx pgx.Tx, current Device) (map[string]any, error) { var claim claimRecord var err error claim, err = readClaim(ctx, tx, in.ClaimID, current.ID) if err != nil { return nil, err } if err = claimPermission(ctx, tx, claim); err != nil { return nil, err } if !hmac.Equal([]byte(claim.ChallengeHash), []byte(digest([]byte(in.Challenge)))) || !hmac.Equal([]byte(claim.Digest), []byte(in.Digest)) || claim.Generation != in.ExpectedGeneration { return nil, errState } if claim.Status == "CONFIRMED" { if claim.PressID != in.PressEventID || current.Generation != claim.Generation+1 { return nil, errState } var active bool err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_binding WHERE id=$1::uuid AND unbound_at IS NULL AND generation=$2)`, claim.BindingID, current.Generation).Scan(&active) if err != nil { return nil, err } if !active { return nil, errState } st, e := s.state(ctx, tx, current) return map[string]any{"claim_id": claim.ID, "state": st, "already_applied": true}, e } if current.Generation != in.ExpectedGeneration { return nil, errState } setup, err := readSetup(ctx, tx, claim.SetupID, current, true) if err != nil { return nil, err } if setup.Mode != "ENROLL" { return nil, errAccess } if claim.Status != "PENDING" || !claim.Expires.After(s.now()) { return nil, fail(410, "CLAIM_EXPIRED", "绑定申请已过期或取消") } var active bool err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_binding WHERE device_id=$1::uuid AND unbound_at IS NULL)`, d.ID).Scan(&active) if err != nil { return nil, err } if active { return nil, errState } if _, err = tx.Exec(ctx, `SELECT set_config('app.onboarding_claim',$1,true)`, claim.ID); err != nil { return nil, err } var bindingID string err = tx.QueryRow(ctx, `INSERT INTO device_binding(tenant_id,device_id,household_id,subject_id,bound_by,reason) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,$5::uuid,'physical_confirm') RETURNING id::text`, d.TenantID, d.ID, claim.HouseholdID, claim.SubjectID, claim.UserID).Scan(&bindingID) if err != nil { return nil, err } _, err = tx.Exec(ctx, `UPDATE device_binding_claim SET status='CONFIRMED',press_event_id=$2::uuid,binding_id=$3::uuid,completed_at=now() WHERE id=$1::uuid`, claim.ID, in.PressEventID, bindingID) if err != nil { return nil, err } _, err = tx.Exec(ctx, `UPDATE device_setup_session SET binding_generation=$2 WHERE id=$1::uuid`, setup.ID, current.Generation+1) if err != nil { return nil, err } if _, err = deviceactivation.Apply(ctx, tx, deviceactivation.Input{DeviceID: d.ID, TenantID: d.TenantID, Target: deviceactivation.Bound, Reason: "设备按键确认绑定", Source: "onboarding", ActorType: "device", ActorPublicID: &d.PublicID, RequestID: requestID}); err != nil { return nil, err } if err = s.enqueueActivation(ctx, tx, current, current.Generation+1, requestID); err != nil { return nil, err } // Match the platform's basic voice mode. Optional storage, extraction and // personalization stay off until their existing consent flows are used. _, err = tx.Exec(ctx, `INSERT INTO consent_snapshot(tenant_id,subject_id,snapshot_version,effective_consents,source_consent_ids) SELECT $1::uuid,$2::uuid,1,'{"voice_chat":true,"transcript_storage":false,"raw_audio_storage":false,"story_extraction":false,"personalized_memory":false,"family_sharing":false}'::jsonb,'[]'::jsonb WHERE NOT EXISTS(SELECT 1 FROM consent_snapshot WHERE tenant_id=$1::uuid AND subject_id=$2::uuid)`, current.TenantID, claim.SubjectID) if err != nil { return nil, err } if err = s.auditEvent(ctx, tx, d, "device", d.PublicID, "device.binding.confirm", requestID, map[string]any{"claim_id": claim.ID, "binding_generation": current.Generation + 1}); err != nil { return nil, err } st, err := s.state(ctx, tx, current) return map[string]any{"claim_id": claim.ID, "state": st, "already_applied": false}, err }) } type claimRecord struct { ID, SetupID, DeviceID, TenantID, UserID, HouseholdID, SubjectID, ChallengeHash, Digest, Status, BindingID, PressID string Generation int64 Expires time.Time } func readClaim(ctx context.Context, tx pgx.Tx, id, device string) (claimRecord, error) { var c claimRecord err := tx.QueryRow(ctx, `SELECT id::text,setup_id::text,device_id::text,tenant_id::text,user_id::text,household_id::text,subject_id::text,challenge_hash,binding_digest,status,COALESCE(binding_id::text,''),COALESCE(press_event_id::text,''),expected_generation,expires_at FROM device_binding_claim WHERE id=$1::uuid AND device_id=$2::uuid FOR UPDATE`, id, device).Scan(&c.ID, &c.SetupID, &c.DeviceID, &c.TenantID, &c.UserID, &c.HouseholdID, &c.SubjectID, &c.ChallengeHash, &c.Digest, &c.Status, &c.BindingID, &c.PressID, &c.Generation, &c.Expires) if errors.Is(err, pgx.ErrNoRows) { return c, errAccess } return c, err } func claimPermission(ctx context.Context, tx pgx.Tx, c claimRecord) error { var allowed bool err := tx.QueryRow(ctx, `SELECT true FROM household_member hm JOIN user_account ua ON ua.id=hm.user_id AND ua.status='active' JOIN household h ON h.id=hm.household_id AND h.status='active' JOIN subject_profile sp ON sp.household_id=h.id AND sp.tenant_id=h.tenant_id WHERE ua.id=$1::uuid AND hm.household_id=$2::uuid AND sp.id=$3::uuid AND hm.tenant_id=$4::uuid AND hm.status='active' AND hm.role IN ('owner','admin','editor') AND sp.status='active' FOR SHARE OF ua,hm,h,sp`, c.UserID, c.HouseholdID, c.SubjectID, c.TenantID).Scan(&allowed) if errors.Is(err, pgx.ErrNoRows) { return errAccess } if err != nil { return err } if !allowed { return errAccess } return nil } type StateAckInput struct { Revision int64 `json:"device_revision"` Generation int64 `json:"binding_generation"` LocalGeneration int64 `json:"local_generation"` Hash string `json:"state_hash"` } func (s *Service) StateAck(ctx context.Context, d Device, requestID string, raw []byte, in StateAckInput) (map[string]any, error) { if in.Revision < 1 || in.Generation < 0 || in.LocalGeneration < 0 || !validDigest(in.Hash) { return nil, fail(400, "VALIDATION_FAILED", "状态回执不正确") } return s.mutate(ctx, d, "device:"+d.ID, "state-ack", requestID, raw, 24*time.Hour, func(tx pgx.Tx, current Device) (map[string]any, error) { st, err := s.state(ctx, tx, current) if err != nil { return nil, err } if st.Revision != in.Revision || st.Generation != in.Generation || st.Hash != in.Hash { return nil, errState } var local int64 var boot string err = tx.QueryRow(ctx, `SELECT local_generation,boot_id::text FROM device_state_ack WHERE device_id=$1::uuid`, d.ID).Scan(&local, &boot) if err != nil && !errors.Is(err, pgx.ErrNoRows) { return nil, err } if err == nil && boot == current.BootID && local > in.LocalGeneration { return nil, errState } _, err = tx.Exec(ctx, `INSERT INTO device_state_ack(device_id,revision,binding_generation,state_hash,boot_id,local_generation) VALUES($1::uuid,$2,$3,$4,$5::uuid,$6) ON CONFLICT(device_id) DO UPDATE SET revision=EXCLUDED.revision,binding_generation=EXCLUDED.binding_generation,state_hash=EXCLUDED.state_hash,boot_id=EXCLUDED.boot_id,local_generation=EXCLUDED.local_generation,accepted_at=CASE WHEN device_state_ack.binding_generation=EXCLUDED.binding_generation AND device_state_ack.boot_id=EXCLUDED.boot_id THEN device_state_ack.accepted_at ELSE now() END`, d.ID, in.Revision, in.Generation, in.Hash, current.BootID, in.LocalGeneration) if err != nil { return nil, err } canClose, err := s.stateAckCanClose(ctx, tx, current) return map[string]any{"accepted_revision": in.Revision, "setup_can_close": canClose}, err }) } type CloseInput struct { SetupID string `json:"setup_id"` Reason string `json:"reason"` Revision int64 `json:"final_revision"` } func (s *Service) Close(ctx context.Context, d Device, requestID string, raw []byte, in CloseInput) (map[string]any, error) { if !uuidV4(in.SetupID) || in.Revision < 1 || !oneOf(in.Reason, "SUCCESS", "TIMEOUT", "CANCELLED", "NETWORK_SWITCH") { return nil, fail(400, "VALIDATION_FAILED", "收尾参数不正确") } return s.mutate(ctx, d, "device:"+d.ID, "close", requestID, raw, 24*time.Hour, func(tx pgx.Tx, current Device) (map[string]any, error) { setup, err := readSetup(ctx, tx, in.SetupID, current, false) if err != nil { return nil, err } if in.Revision > current.Revision { return nil, errState } if in.Reason == "SUCCESS" { _, allowed, e := s.closeDelay(ctx, tx, current, setup.ID) if e != nil { return nil, e } if !allowed { return nil, fail(409, "SETUP_CLOSE_WAIT", "请等待设备和页面回执的收尾时间") } var saved bool err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_state_ack a JOIN device_binding b ON b.device_id=a.device_id AND b.unbound_at IS NULL WHERE a.device_id=$1::uuid AND a.binding_generation=$2 AND a.boot_id=$3::uuid)`, d.ID, current.Generation, current.BootID).Scan(&saved) if err != nil { return nil, err } if !saved { return nil, fail(409, "STATE_ACK_REQUIRED", "设备保存后才能确认绑定收尾") } } if _, err = tx.Exec(ctx, `UPDATE device_setup_session SET closed_at=COALESCE(closed_at,now()),close_reason=COALESCE(close_reason,$2) WHERE id=$1::uuid`, setup.ID, in.Reason); err != nil { return nil, err } tag, err := tx.Exec(ctx, `UPDATE device_binding_claim SET status='CANCELLED',completed_at=now() WHERE setup_id=$1::uuid AND status='PENDING'`, setup.ID) if err != nil { return nil, err } if tag.RowsAffected() > 0 { if err = bump(ctx, tx, d.ID); err != nil { return nil, err } } st, err := s.state(ctx, tx, current) return map[string]any{"close_allowed": true, "state": st}, err }) } func oneOf(s string, values ...string) bool { for _, v := range values { if s == v { return true } } return false } func bindingDigest(d Device, user, household, subject, claim string) string { return digest([]byte(fmt.Sprintf("CLAIM1\n%s\n%s\n%s\n%s\n%s\n%d", d.FactoryID, user, household, subject, claim, d.Generation))) } func newClaim(ctx context.Context, s *Service, tx pgx.Tx, d Device, setupID, user, household, subject string) (map[string]any, error) { if err := s.expireClaims(ctx, tx, d.ID); err != nil { return nil, err } var pending bool err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_binding_claim WHERE device_id=$1::uuid AND status='PENDING')`, d.ID).Scan(&pending) if err != nil { return nil, err } if pending { return nil, fail(409, "CLAIM_BUSY", "设备正在等待另一笔确认,请稍后再试") } id := uuid.NewString() challenge := randomToken(32) hash := bindingDigest(d, user, household, subject, id) expires := s.now().Add(120 * time.Second) setup, err := readSetup(ctx, tx, setupID, d, true) if err != nil { return nil, err } if setup.Expires.Before(expires) { expires = setup.Expires } _, err = tx.Exec(ctx, `INSERT INTO device_binding_claim(id,device_id,tenant_id,setup_id,user_id,household_id,subject_id,expected_generation,challenge_hash,challenge_ciphertext,binding_digest,expires_at) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,$5::uuid,$6::uuid,$7::uuid,$8,$9,$10,$11,$12)`, id, d.ID, d.TenantID, setupID, user, household, subject, d.Generation, digest([]byte(challenge)), s.crypt.seal([]byte(challenge), "claim:"+id), hash, expires) if err != nil { return nil, err } if err = bump(ctx, tx, d.ID); err != nil { return nil, err } st, err := s.state(ctx, tx, d) return map[string]any{"claim_id": id, "expires_at_ms": expires.UnixMilli(), "state": st}, err } // Used by protocol tests without leaking transient challenges into logs. func requestJSON(v any) []byte { b, _ := json.Marshal(v); return b } func nonempty(v string, max int) bool { return len(strings.TrimSpace(v)) > 0 && len(v) <= max }