//! SyncKit app, device, log, blob, and OTA models. use chrono::{DateTime, Utc}; use serde::Serialize; use sqlx::FromRow; use super::super::id_types::{ ItemId, OtaArtifactId, OtaReleaseId, ProjectId, SyncAppId, SyncBlobId, SyncDeviceId, SyncGroupId, UserId, }; /// A registered sync app with a hashed API key. #[derive(Debug, Clone, FromRow, Serialize)] pub struct DbSyncApp { /// Database primary key. pub id: SyncAppId, /// User who created and owns this app. pub creator_id: UserId, /// Human-readable app name (e.g. "GoingsOn", "AudioFiles"). pub name: String, /// SHA-256 hash of the API key (hex-encoded). The plaintext key is never stored. pub api_key_hash: String, /// First 8 hex chars of the original API key, for display only. pub api_key_prefix: String, /// SHA-256 hash of the keys-endpoint secret (hex-encoded), or `None` if /// the developer has not generated one. Distinct from `api_key_hash`: /// this credential authenticates `/api/sync/keys/*` and is never shipped /// to a client. The plaintext is never stored. pub keys_secret_hash: Option, /// First 8 hex chars of the keys-endpoint secret, for display only. pub keys_secret_prefix: Option, /// Whether this app is active and can accept sync requests. pub is_active: bool, /// When the app was created. pub created_at: DateTime, /// Optional link to an MNW project for dashboard grouping. pub project_id: Option, /// Optional link to an MNW item for dashboard grouping. pub item_id: Option, /// URL-friendly slug for OTA update endpoints. pub slug: Option, } /// A device registered for sync per user per app. #[derive(Debug, Clone, FromRow, Serialize)] pub struct DbSyncDevice { /// Database primary key. pub id: SyncDeviceId, /// Parent sync app this device belongs to. pub app_id: SyncAppId, /// User who owns this device registration. pub user_id: UserId, /// Human-readable device name (e.g. "Max's MacBook Pro"). pub device_name: String, /// Operating system / platform (macos, windows, linux, ios, android). pub platform: super::super::SyncPlatform, /// Updated on each push or pull to track device activity. pub last_seen_at: DateTime, /// When this device was first registered. pub created_at: DateTime, /// SyncKit SDK version last seen from this device, off its User-Agent. /// `None` for a client old enough not to send one. pub client_version: Option, } /// An entry in the append-only sync change log. #[derive(Debug, Clone, FromRow, Serialize)] pub struct DbSyncLogEntry { /// Server-assigned monotonic sequence number, used as a cursor for pull. pub seq: i64, /// Sync app this entry belongs to. pub app_id: SyncAppId, /// User who pushed this change. pub user_id: UserId, /// Device that originated this change. pub device_id: SyncDeviceId, /// Opaque table name from the client (e.g. "tasks", "contacts"). pub table_name: String, /// Type of change (insert, update, or delete). pub operation: super::super::SyncOperation, /// Client-side row identifier (opaque to the server). pub row_id: String, /// Timestamp assigned by the client when the change was made. pub client_timestamp: DateTime, /// Encrypted row data (JSON blob). Null for delete operations. pub data: Option, /// When the server received and recorded this entry. pub created_at: DateTime, /// Which encryption key was used. NULL means key_id 1 (pre-rotation entries). pub key_id: Option, } /// An in-progress key rotation for a user within a sync app. #[derive(Debug, Clone, FromRow)] pub struct DbSyncKeyRotation { pub id: uuid::Uuid, pub app_id: SyncAppId, pub user_id: UserId, pub device_id: SyncDeviceId, pub new_encrypted_key: String, pub old_key_version: i32, pub new_key_id: i32, pub re_encrypted_through_seq: i64, pub target_seq: i64, pub created_at: DateTime, pub updated_at: DateTime, } /// A SyncKit group: a shared, end-to-end-encrypted changelog owned by one admin, /// whose members each hold the group's GCK sealed to their identity key. #[derive(Debug, Clone, FromRow, Serialize)] pub struct DbSyncGroup { /// Database primary key. pub id: SyncGroupId, /// Sync app this group belongs to. pub app_id: SyncAppId, /// The admin who mints the GCK and manages membership; bears the storage bill. pub admin_user_id: UserId, /// Human-readable group name. pub name: String, /// Current GCK generation. Bumped on member removal (rotation). pub gck_version: i32, /// When the group was created. pub created_at: DateTime, } /// One member of a group, carrying that member's sealed GCK grant. #[derive(Debug, Clone, FromRow, Serialize)] pub struct DbSyncGroupMember { /// The group this membership belongs to. pub group_id: SyncGroupId, /// The member. pub user_id: UserId, /// The member's account email, joined from `users`. Surfaced so an admin panel /// can show who a member is rather than a bare user id. pub email: String, /// `admin` | `member`. Reserved for the later per-key permission system; MVP /// treats every member as a reader and writer. pub role: String, /// The GCK sealed to this member's X25519 public key (base64), opaque to the /// server. The member opens it with their identity private key. pub sealed_gck: String, /// The GCK generation `sealed_gck` was sealed under; a value below the /// group's current `gck_version` marks a stale grant. pub gck_version: i32, /// When this member was added. pub added_at: DateTime, } /// An entry in a group's append-only shared change log (`sync_group_log`). /// /// The group changelog is a separate table from the personal [`DbSyncLogEntry`] /// so personal-scope queries can never see group rows (which are sealed under the /// group's GCK, not the per-user key). There is deliberately no `key_id`: a group /// entry's key generation is the group's `gck_version`, not `sync_keys.key_id`. #[derive(Debug, Clone, FromRow, Serialize)] pub struct DbSyncGroupLogEntry { /// Server-assigned monotonic sequence number; the group pull cursor. pub seq: i64, /// Sync app this entry belongs to. pub app_id: SyncAppId, /// The group whose shared log this entry is in. pub group_id: SyncGroupId, /// The member who pushed this change (provenance). pub user_id: UserId, /// Device that originated this change. pub device_id: SyncDeviceId, /// Opaque table name from the client. pub table_name: String, /// Type of change (insert, update, or delete). pub operation: super::super::SyncOperation, /// Client-side row identifier (opaque to the server). pub row_id: String, /// Timestamp assigned by the client when the change was made. pub client_timestamp: DateTime, /// Encrypted row data (sealed under the group GCK). Null for deletes. pub data: Option, /// When the server received and recorded this entry. pub created_at: DateTime, } /// A blob uploaded to S3 via SyncKit, tracked for dedup and cleanup. #[derive(Debug, Clone, FromRow, Serialize)] pub struct DbSyncBlob { /// Database primary key. pub id: SyncBlobId, /// Sync app this blob belongs to. pub app_id: SyncAppId, /// User who uploaded this blob. pub user_id: UserId, /// Content-address hash provided by the client (used for deduplication). pub hash: String, /// S3 object key where the blob is stored (`{app_id}/{user_id}/{hash}`). pub s3_key: String, /// Size of the blob in bytes. pub size_bytes: i64, /// Developer-defined SDK key this blob was uploaded under. Used to /// attribute storage against the right per-key counter (per_key mode). pub key: String, /// When the blob upload was confirmed. pub uploaded_at: DateTime, } /// Sync app + billing columns + live usage counters (joined view). /// /// Mirrors columns added in migration 117 (`117_synckit_v2_billing.sql`). /// Built by joining `sync_apps` against `sync_app_usage_current` (LEFT JOIN). /// The usage row is created with the app (`create_sync_app`) and backfilled by /// migration 165; the LEFT JOIN is defense-in-depth against a missing row. #[derive(Debug, Clone, FromRow, Serialize)] pub struct DbSyncAppBilling { // sync_apps base pub id: SyncAppId, pub creator_id: UserId, pub name: String, /// First-party app; bypasses all billing logic. pub is_internal: bool, /// Stripe Customer ID for this app's developer (one customer per app). pub stripe_customer_id: Option, /// Stripe Subscription ID; set once billing activates. pub stripe_subscription_id: Option, /// Billing lifecycle (draft / active / suspended_unpaid / canceled). pub billing_status: super::super::SyncBillingStatus, /// Storage cap in GB. Set in bulk mode; NULL in per_key mode (capacity is /// derived from `key_cap × gb_per_key`) and in draft. pub storage_gb_cap: Option, /// `PerKey` | `Bulk`. Drives both pricing and degradation behavior. The DB /// column is `NOT NULL CHECK (enforcement_mode IN ('per_key','bulk'))`, so it /// always decodes to one of the two variants. pub enforcement_mode: super::super::SyncEnforcementMode, /// Max active keys. Set in per_key mode; NULL in bulk mode. pub key_cap: Option, /// GB allotment per active key (per_key mode only). Total storage /// capacity = `key_cap × gb_per_key`. pub gb_per_key: Option, pub current_period_start: Option>, pub current_period_end: Option>, // sync_app_usage_current (LEFT-joined, may be missing if row absent) pub bytes_stored: Option, pub bytes_egress_period: Option, pub keys_claimed: Option, pub last_warning_pct: Option, pub period_started_at: Option>, // projects (LEFT-joined). Some when the app is linked to a project, used // to route the Stripe billing portal back to the project dashboard. pub project_slug: Option, } /// A single active key claim (row in `sync_app_keys` with `released_at IS NULL`). /// /// See migration 117 for the full table definition. This is the projection /// used by the dashboard "Active keys" list; only the columns the UI needs. #[derive(Debug, Clone, FromRow, Serialize)] pub struct DbSyncAppKey { pub id: uuid::Uuid, pub key: String, pub claimed_at: DateTime, /// Bytes stored under this key (LEFT JOIN against /// `sync_key_usage_current`, `0` when no upload has landed yet). pub bytes_stored: i64, } // ── OTA models ── /// An OTA release for a sync app. #[derive(Debug, Clone, FromRow)] pub struct DbOtaRelease { pub id: OtaReleaseId, pub app_id: SyncAppId, pub version: String, pub notes: String, pub signature: String, pub pub_date: DateTime, pub created_at: DateTime, } /// An artifact (platform-specific binary) within an OTA release. #[derive(Debug, Clone, FromRow)] pub struct DbOtaArtifact { pub id: OtaArtifactId, pub release_id: OtaReleaseId, pub target: String, pub arch: String, pub s3_key: String, pub file_size: i64, /// The artifact's own minisign signature. Tauri signs each (target, arch) /// file independently, so the signature is per-artifact, not per-release. /// `updater_check` serves this one for the requested platform. pub signature: String, /// Malware-scan gate: only `clean` artifacts are advertised/downloadable. pub scan_status: super::super::FileScanStatus, pub created_at: DateTime, }