Skip to main content

max / makenotwork

11.7 KB · 294 lines History Blame Raw
1 //! SyncKit app, device, log, blob, and OTA models.
2
3 use chrono::{DateTime, Utc};
4 use serde::Serialize;
5 use sqlx::FromRow;
6
7 use super::super::id_types::{
8 ItemId, OtaArtifactId, OtaReleaseId, ProjectId, SyncAppId, SyncBlobId, SyncDeviceId,
9 SyncGroupId, UserId,
10 };
11
12 /// A registered sync app with a hashed API key.
13 #[derive(Debug, Clone, FromRow, Serialize)]
14 pub struct DbSyncApp {
15 /// Database primary key.
16 pub id: SyncAppId,
17 /// User who created and owns this app.
18 pub creator_id: UserId,
19 /// Human-readable app name (e.g. "GoingsOn", "AudioFiles").
20 pub name: String,
21 /// SHA-256 hash of the API key (hex-encoded). The plaintext key is never stored.
22 pub api_key_hash: String,
23 /// First 8 hex chars of the original API key, for display only.
24 pub api_key_prefix: String,
25 /// SHA-256 hash of the keys-endpoint secret (hex-encoded), or `None` if
26 /// the developer has not generated one. Distinct from `api_key_hash`:
27 /// this credential authenticates `/api/sync/keys/*` and is never shipped
28 /// to a client. The plaintext is never stored.
29 pub keys_secret_hash: Option<String>,
30 /// First 8 hex chars of the keys-endpoint secret, for display only.
31 pub keys_secret_prefix: Option<String>,
32 /// Whether this app is active and can accept sync requests.
33 pub is_active: bool,
34 /// When the app was created.
35 pub created_at: DateTime<Utc>,
36 /// Optional link to an MNW project for dashboard grouping.
37 pub project_id: Option<ProjectId>,
38 /// Optional link to an MNW item for dashboard grouping.
39 pub item_id: Option<ItemId>,
40 /// URL-friendly slug for OTA update endpoints.
41 pub slug: Option<String>,
42 }
43
44 /// A device registered for sync per user per app.
45 #[derive(Debug, Clone, FromRow, Serialize)]
46 pub struct DbSyncDevice {
47 /// Database primary key.
48 pub id: SyncDeviceId,
49 /// Parent sync app this device belongs to.
50 pub app_id: SyncAppId,
51 /// User who owns this device registration.
52 pub user_id: UserId,
53 /// Human-readable device name (e.g. "Max's MacBook Pro").
54 pub device_name: String,
55 /// Operating system / platform (macos, windows, linux, ios, android).
56 pub platform: super::super::SyncPlatform,
57 /// Updated on each push or pull to track device activity.
58 pub last_seen_at: DateTime<Utc>,
59 /// When this device was first registered.
60 pub created_at: DateTime<Utc>,
61 /// SyncKit SDK version last seen from this device, off its User-Agent.
62 /// `None` for a client old enough not to send one.
63 pub client_version: Option<String>,
64 }
65
66 /// An entry in the append-only sync change log.
67 #[derive(Debug, Clone, FromRow, Serialize)]
68 pub struct DbSyncLogEntry {
69 /// Server-assigned monotonic sequence number, used as a cursor for pull.
70 pub seq: i64,
71 /// Sync app this entry belongs to.
72 pub app_id: SyncAppId,
73 /// User who pushed this change.
74 pub user_id: UserId,
75 /// Device that originated this change.
76 pub device_id: SyncDeviceId,
77 /// Opaque table name from the client (e.g. "tasks", "contacts").
78 pub table_name: String,
79 /// Type of change (insert, update, or delete).
80 pub operation: super::super::SyncOperation,
81 /// Client-side row identifier (opaque to the server).
82 pub row_id: String,
83 /// Timestamp assigned by the client when the change was made.
84 pub client_timestamp: DateTime<Utc>,
85 /// Encrypted row data (JSON blob). Null for delete operations.
86 pub data: Option<serde_json::Value>,
87 /// When the server received and recorded this entry.
88 pub created_at: DateTime<Utc>,
89 /// Which encryption key was used. NULL means key_id 1 (pre-rotation entries).
90 pub key_id: Option<i32>,
91 }
92
93 /// An in-progress key rotation for a user within a sync app.
94 #[derive(Debug, Clone, FromRow)]
95 pub struct DbSyncKeyRotation {
96 pub id: uuid::Uuid,
97 pub app_id: SyncAppId,
98 pub user_id: UserId,
99 pub device_id: SyncDeviceId,
100 pub new_encrypted_key: String,
101 pub old_key_version: i32,
102 pub new_key_id: i32,
103 pub re_encrypted_through_seq: i64,
104 pub target_seq: i64,
105 pub created_at: DateTime<Utc>,
106 pub updated_at: DateTime<Utc>,
107 }
108
109 /// A SyncKit group: a shared, end-to-end-encrypted changelog owned by one admin,
110 /// whose members each hold the group's GCK sealed to their identity key.
111 #[derive(Debug, Clone, FromRow, Serialize)]
112 pub struct DbSyncGroup {
113 /// Database primary key.
114 pub id: SyncGroupId,
115 /// Sync app this group belongs to.
116 pub app_id: SyncAppId,
117 /// The admin who mints the GCK and manages membership; bears the storage bill.
118 pub admin_user_id: UserId,
119 /// Human-readable group name.
120 pub name: String,
121 /// Current GCK generation. Bumped on member removal (rotation).
122 pub gck_version: i32,
123 /// When the group was created.
124 pub created_at: DateTime<Utc>,
125 }
126
127 /// One member of a group, carrying that member's sealed GCK grant.
128 #[derive(Debug, Clone, FromRow, Serialize)]
129 pub struct DbSyncGroupMember {
130 /// The group this membership belongs to.
131 pub group_id: SyncGroupId,
132 /// The member.
133 pub user_id: UserId,
134 /// The member's account email, joined from `users`. Surfaced so an admin panel
135 /// can show who a member is rather than a bare user id.
136 pub email: String,
137 /// `admin` | `member`. Reserved for the later per-key permission system; MVP
138 /// treats every member as a reader and writer.
139 pub role: String,
140 /// The GCK sealed to this member's X25519 public key (base64), opaque to the
141 /// server. The member opens it with their identity private key.
142 pub sealed_gck: String,
143 /// The GCK generation `sealed_gck` was sealed under; a value below the
144 /// group's current `gck_version` marks a stale grant.
145 pub gck_version: i32,
146 /// When this member was added.
147 pub added_at: DateTime<Utc>,
148 }
149
150 /// An entry in a group's append-only shared change log (`sync_group_log`).
151 ///
152 /// The group changelog is a separate table from the personal [`DbSyncLogEntry`]
153 /// so personal-scope queries can never see group rows (which are sealed under the
154 /// group's GCK, not the per-user key). There is deliberately no `key_id`: a group
155 /// entry's key generation is the group's `gck_version`, not `sync_keys.key_id`.
156 #[derive(Debug, Clone, FromRow, Serialize)]
157 pub struct DbSyncGroupLogEntry {
158 /// Server-assigned monotonic sequence number; the group pull cursor.
159 pub seq: i64,
160 /// Sync app this entry belongs to.
161 pub app_id: SyncAppId,
162 /// The group whose shared log this entry is in.
163 pub group_id: SyncGroupId,
164 /// The member who pushed this change (provenance).
165 pub user_id: UserId,
166 /// Device that originated this change.
167 pub device_id: SyncDeviceId,
168 /// Opaque table name from the client.
169 pub table_name: String,
170 /// Type of change (insert, update, or delete).
171 pub operation: super::super::SyncOperation,
172 /// Client-side row identifier (opaque to the server).
173 pub row_id: String,
174 /// Timestamp assigned by the client when the change was made.
175 pub client_timestamp: DateTime<Utc>,
176 /// Encrypted row data (sealed under the group GCK). Null for deletes.
177 pub data: Option<serde_json::Value>,
178 /// When the server received and recorded this entry.
179 pub created_at: DateTime<Utc>,
180 }
181
182 /// A blob uploaded to S3 via SyncKit, tracked for dedup and cleanup.
183 #[derive(Debug, Clone, FromRow, Serialize)]
184 pub struct DbSyncBlob {
185 /// Database primary key.
186 pub id: SyncBlobId,
187 /// Sync app this blob belongs to.
188 pub app_id: SyncAppId,
189 /// User who uploaded this blob.
190 pub user_id: UserId,
191 /// Content-address hash provided by the client (used for deduplication).
192 pub hash: String,
193 /// S3 object key where the blob is stored (`{app_id}/{user_id}/{hash}`).
194 pub s3_key: String,
195 /// Size of the blob in bytes.
196 pub size_bytes: i64,
197 /// Developer-defined SDK key this blob was uploaded under. Used to
198 /// attribute storage against the right per-key counter (per_key mode).
199 pub key: String,
200 /// When the blob upload was confirmed.
201 pub uploaded_at: DateTime<Utc>,
202 }
203
204 /// Sync app + billing columns + live usage counters (joined view).
205 ///
206 /// Mirrors columns added in migration 117 (`117_synckit_v2_billing.sql`).
207 /// Built by joining `sync_apps` against `sync_app_usage_current` (LEFT JOIN).
208 /// The usage row is created with the app (`create_sync_app`) and backfilled by
209 /// migration 165; the LEFT JOIN is defense-in-depth against a missing row.
210 #[derive(Debug, Clone, FromRow, Serialize)]
211 pub struct DbSyncAppBilling {
212 // sync_apps base
213 pub id: SyncAppId,
214 pub creator_id: UserId,
215 pub name: String,
216 /// First-party app; bypasses all billing logic.
217 pub is_internal: bool,
218 /// Stripe Customer ID for this app's developer (one customer per app).
219 pub stripe_customer_id: Option<String>,
220 /// Stripe Subscription ID; set once billing activates.
221 pub stripe_subscription_id: Option<String>,
222 /// Billing lifecycle (draft / active / suspended_unpaid / canceled).
223 pub billing_status: super::super::SyncBillingStatus,
224 /// Storage cap in GB. Set in bulk mode; NULL in per_key mode (capacity is
225 /// derived from `key_cap × gb_per_key`) and in draft.
226 pub storage_gb_cap: Option<i32>,
227 /// `PerKey` | `Bulk`. Drives both pricing and degradation behavior. The DB
228 /// column is `NOT NULL CHECK (enforcement_mode IN ('per_key','bulk'))`, so it
229 /// always decodes to one of the two variants.
230 pub enforcement_mode: super::super::SyncEnforcementMode,
231 /// Max active keys. Set in per_key mode; NULL in bulk mode.
232 pub key_cap: Option<i32>,
233 /// GB allotment per active key (per_key mode only). Total storage
234 /// capacity = `key_cap × gb_per_key`.
235 pub gb_per_key: Option<i32>,
236 pub current_period_start: Option<DateTime<Utc>>,
237 pub current_period_end: Option<DateTime<Utc>>,
238 // sync_app_usage_current (LEFT-joined, may be missing if row absent)
239 pub bytes_stored: Option<i64>,
240 pub bytes_egress_period: Option<i64>,
241 pub keys_claimed: Option<i32>,
242 pub last_warning_pct: Option<i16>,
243 pub period_started_at: Option<DateTime<Utc>>,
244 // projects (LEFT-joined). Some when the app is linked to a project, used
245 // to route the Stripe billing portal back to the project dashboard.
246 pub project_slug: Option<String>,
247 }
248
249 /// A single active key claim (row in `sync_app_keys` with `released_at IS NULL`).
250 ///
251 /// See migration 117 for the full table definition. This is the projection
252 /// used by the dashboard "Active keys" list; only the columns the UI needs.
253 #[derive(Debug, Clone, FromRow, Serialize)]
254 pub struct DbSyncAppKey {
255 pub id: uuid::Uuid,
256 pub key: String,
257 pub claimed_at: DateTime<Utc>,
258 /// Bytes stored under this key (LEFT JOIN against
259 /// `sync_key_usage_current`, `0` when no upload has landed yet).
260 pub bytes_stored: i64,
261 }
262
263 // ── OTA models ──
264
265 /// An OTA release for a sync app.
266 #[derive(Debug, Clone, FromRow)]
267 pub struct DbOtaRelease {
268 pub id: OtaReleaseId,
269 pub app_id: SyncAppId,
270 pub version: String,
271 pub notes: String,
272 pub signature: String,
273 pub pub_date: DateTime<Utc>,
274 pub created_at: DateTime<Utc>,
275 }
276
277 /// An artifact (platform-specific binary) within an OTA release.
278 #[derive(Debug, Clone, FromRow)]
279 pub struct DbOtaArtifact {
280 pub id: OtaArtifactId,
281 pub release_id: OtaReleaseId,
282 pub target: String,
283 pub arch: String,
284 pub s3_key: String,
285 pub file_size: i64,
286 /// The artifact's own minisign signature. Tauri signs each (target, arch)
287 /// file independently, so the signature is per-artifact, not per-release.
288 /// `updater_check` serves this one for the requested platform.
289 pub signature: String,
290 /// Malware-scan gate: only `clean` artifacts are advertised/downloadable.
291 pub scan_status: super::super::FileScanStatus,
292 pub created_at: DateTime<Utc>,
293 }
294