feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s

Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
+56 -4
View File
@@ -78,6 +78,10 @@ func (s *Service) CreateInventory(ctx context.Context, principal identity.Princi
if e != nil {
return nil, e
}
d, e = s.makeInventoryClaimable(ctx, tx, d, jobID, requestID)
if e != nil {
return nil, e
}
return inventoryResult(d, jobID), tx.Commit(ctx)
}
if !errors.Is(err, pgx.ErrNoRows) {
@@ -94,17 +98,22 @@ func (s *Service) CreateInventory(ctx context.Context, principal identity.Princi
if err != nil {
return nil, err
}
// A repair reuses the latest credential job. It never generates a key.
// Re-submitting an existing device also upgrades records created by the
// former delivery/release flow into the direct waiting-for-binding flow.
err = tx.QueryRow(ctx, `SELECT id::text FROM device_enrollment_job WHERE device_id=$1::uuid ORDER BY created_at DESC,id DESC LIMIT 1`, deviceID).Scan(&jobID)
if err != nil {
return nil, err
}
d, err = s.makeInventoryClaimable(ctx, tx, d, jobID, requestID)
if err != nil {
return nil, err
}
return inventoryResult(d, jobID), tx.Commit(ctx)
}
if !errors.Is(err, pgx.ErrNoRows) {
return nil, err
}
d = Device{ID: uuid.NewString(), PublicID: uuid.NewString(), TenantID: a.TenantID, FactoryID: "KP01-" + input.FactoryMAC, Model: input.Model, HardwareRevision: input.HardwareRevision, Mode: "physical_confirm"}
d = Device{ID: uuid.NewString(), PublicID: uuid.NewString(), TenantID: a.TenantID, FactoryID: "KP01-" + input.FactoryMAC, Model: input.Model, HardwareRevision: input.HardwareRevision, Mode: "physical_confirm", Lifecycle: string(deviceactivation.Factory)}
_, err = tx.Exec(ctx, `INSERT INTO device(id,public_id,tenant_id,serial_digest,model,hardware_revision,credential_ref,activation_state,activation_reason,enrollment_mode,factory_device_id,factory_mac) VALUES($1::uuid,$2::uuid,$3::uuid,$4,$5,$6,$7,'FACTORY','controlled-production','physical_confirm',$8,$9)`, d.ID, d.PublicID, d.TenantID, digest([]byte(d.FactoryID)), d.Model, d.HardwareRevision, "onboarding://"+d.FactoryID, d.FactoryID, input.FactoryMAC)
if err != nil {
return nil, err
@@ -113,11 +122,15 @@ func (s *Service) CreateInventory(ctx context.Context, principal identity.Princi
if err != nil {
return nil, err
}
_, err = tx.Exec(ctx, `INSERT INTO device_onboarding_key(device_id,tenant_id,key_version,ciphertext,status) VALUES($1::uuid,$2::uuid,1,$3,'PENDING')`, d.ID, d.TenantID, s.crypt.seal(randomBytes(32), keyAAD(d.ID, 1)))
_, err = tx.Exec(ctx, `INSERT INTO device_onboarding_key(device_id,tenant_id,key_version,ciphertext,status,activated_at) VALUES($1::uuid,$2::uuid,1,$3,'ACTIVE',now())`, d.ID, d.TenantID, s.crypt.seal(randomBytes(32), keyAAD(d.ID, 1)))
if err != nil {
return nil, err
}
err = tx.QueryRow(ctx, `INSERT INTO device_enrollment_job(device_id,tenant_id,actor_id,request_key,request_digest,batch_no) VALUES($1::uuid,$2::uuid,$3::uuid,$4,$5,$6) RETURNING id::text`, d.ID, d.TenantID, a.ID, idempotencyKey, digest(raw), input.BatchNo).Scan(&jobID)
err = tx.QueryRow(ctx, `INSERT INTO device_enrollment_job(device_id,tenant_id,actor_id,request_key,request_digest,batch_no,status,evidence_ref) VALUES($1::uuid,$2::uuid,$3::uuid,$4,$5,$6,'RELEASED','direct-create') RETURNING id::text`, d.ID, d.TenantID, a.ID, idempotencyKey, digest(raw), input.BatchNo).Scan(&jobID)
if err != nil {
return nil, err
}
d, err = s.makeInventoryClaimable(ctx, tx, d, jobID, requestID)
if err != nil {
return nil, err
}
@@ -126,6 +139,45 @@ func (s *Service) CreateInventory(ctx context.Context, principal identity.Princi
}
return inventoryResult(d, jobID), tx.Commit(ctx)
}
// makeInventoryClaimable finishes the internal credential setup in the same
// transaction as device creation. Operators no longer need to exchange an RSA
// envelope and perform a separate release before a device can wait for binding.
func (s *Service) makeInventoryClaimable(ctx context.Context, tx pgx.Tx, d Device, jobID, requestID string) (Device, error) {
if d.Released {
return d, nil
}
var version int64
var keyStatus string
if err := tx.QueryRow(ctx, `SELECT ej.key_version,k.status FROM device_enrollment_job ej JOIN device_onboarding_key k ON k.device_id=ej.device_id AND k.key_version=ej.key_version WHERE ej.id=$1::uuid AND ej.device_id=$2::uuid FOR UPDATE OF ej,k`, jobID, d.ID).Scan(&version, &keyStatus); err != nil {
return d, err
}
if keyStatus == "PENDING" {
if _, err := tx.Exec(ctx, `UPDATE device_onboarding_key SET status='REVOKED' WHERE device_id=$1::uuid AND status='ACTIVE' AND key_version<>$2`, d.ID, version); err != nil {
return d, err
}
if _, err := tx.Exec(ctx, `UPDATE device_onboarding_key SET status='ACTIVE',activated_at=COALESCE(activated_at,now()) WHERE device_id=$1::uuid AND key_version=$2 AND status='PENDING'`, d.ID, version); err != nil {
return d, err
}
} else if keyStatus != "ACTIVE" {
return d, fail(409, "INVENTORY_CREDENTIAL_CLOSED", "设备凭据已经关闭,不能直接创建")
}
if d.Lifecycle == string(deviceactivation.Factory) {
if err := requireLifecycle(ctx, tx, d, deviceactivation.Claimable, requestID); err != nil {
return d, err
}
}
if _, err := tx.Exec(ctx, `UPDATE device SET onboarding_released_at=COALESCE(onboarding_released_at,now()),onboarding_revision=onboarding_revision+1,version=version+1,updated_at=now() WHERE id=$1::uuid`, d.ID); err != nil {
return d, err
}
if _, err := tx.Exec(ctx, `UPDATE device_enrollment_job SET status='RELEASED',evidence_ref=COALESCE(evidence_ref,'direct-create'),delivery_ciphertext=NULL,delivery_expires_at=NULL WHERE id=$1::uuid`, jobID); err != nil {
return d, err
}
d.Released = true
d.Lifecycle = string(deviceactivation.Claimable)
d.KeyVersion = version
return d, nil
}
func inventoryResult(d Device, job string) map[string]any {
return map[string]any{"device_id": d.FactoryID, "platform_device_id": d.PublicID, "enrollment_job_id": job, "contract_version": ContractVersion, "released": d.Released}
}