Skip to main content

max / makenotwork

13.6 KB · 334 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, SyncGroupInvitationId, 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. Grants live in `sync_group_grants`, one row per
128 /// generation, so a member keeps the keys for every generation they were a member
129 /// during; membership here is role and provenance only.
130 #[derive(Debug, Clone, FromRow, Serialize)]
131 pub struct DbSyncGroupMember {
132 /// The group this membership belongs to.
133 pub group_id: SyncGroupId,
134 /// The member.
135 pub user_id: UserId,
136 /// The member's account email, joined from `users`. Surfaced so an admin panel
137 /// can show who a member is rather than a bare user id.
138 pub email: String,
139 /// `admin` | `member`. Reserved for the later per-key permission system; MVP
140 /// treats every member as a reader and writer.
141 pub role: String,
142 /// When this member was added.
143 pub added_at: DateTime<Utc>,
144 }
145
146 /// An outstanding or settled invitation to join a group.
147 ///
148 /// The invitation exists so onboarding is one link rather than a two-channel
149 /// exchange of an email address and a pasted public key. It never carries the
150 /// Group Content Key: the admin still seals the grant client-side, after
151 /// confirming the invitee's key fingerprint, so a link is an invitation and not
152 /// membership.
153 ///
154 /// The token is absent by construction. Only its SHA-256 is stored, and the
155 /// plaintext lives in the link the admin sent.
156 #[derive(Debug, Clone, FromRow, Serialize)]
157 pub struct DbSyncGroupInvitation {
158 /// Database primary key.
159 pub id: SyncGroupInvitationId,
160 /// The group this invitation joins.
161 pub group_id: SyncGroupId,
162 /// The admin who issued it.
163 pub inviter_user_id: UserId,
164 /// The account that accepted, or `None` while the invitation is outstanding.
165 pub invitee_user_id: Option<UserId>,
166 /// The accepting account's email, joined from `users`. What the admin reads
167 /// in the pending list; `None` until acceptance.
168 pub invitee_email: Option<String>,
169 /// The invitee's identity public key (base64), posted on acceptance. This is
170 /// what the admin seals the GCK to, and the value whose fingerprint the admin
171 /// confirms out of band before doing so.
172 pub invitee_pubkey: Option<String>,
173 /// When the invitee accepted.
174 pub accepted_at: Option<DateTime<Utc>>,
175 /// When the admin confirmed and sealed the grant. Terminal.
176 pub redeemed_at: Option<DateTime<Utc>>,
177 /// When the admin cancelled it.
178 pub revoked_at: Option<DateTime<Utc>>,
179 /// When an unredeemed invitation stops being redeemable.
180 pub expires_at: DateTime<Utc>,
181 /// When the invitation was issued.
182 pub created_at: DateTime<Utc>,
183 }
184
185 /// An entry in a group's append-only shared change log (`sync_group_log`).
186 ///
187 /// The group changelog is a separate table from the personal [`DbSyncLogEntry`]
188 /// so personal-scope queries can never see group rows (which are sealed under the
189 /// group's GCK, not the per-user key). There is deliberately no `key_id`: a group
190 /// entry's key generation is its own `gck_version`, not `sync_keys.key_id`.
191 #[derive(Debug, Clone, FromRow, Serialize)]
192 pub struct DbSyncGroupLogEntry {
193 /// Server-assigned monotonic sequence number; the group pull cursor.
194 pub seq: i64,
195 /// Sync app this entry belongs to.
196 pub app_id: SyncAppId,
197 /// The group whose shared log this entry is in.
198 pub group_id: SyncGroupId,
199 /// The member who pushed this change (provenance).
200 pub user_id: UserId,
201 /// Device that originated this change.
202 pub device_id: SyncDeviceId,
203 /// Opaque table name from the client.
204 pub table_name: String,
205 /// Type of change (insert, update, or delete).
206 pub operation: super::super::SyncOperation,
207 /// Client-side row identifier (opaque to the server).
208 pub row_id: String,
209 /// Timestamp assigned by the client when the change was made.
210 pub client_timestamp: DateTime<Utc>,
211 /// Encrypted row data (sealed under the group GCK). Null for deletes.
212 pub data: Option<serde_json::Value>,
213 /// The GCK generation `data` is sealed under, stamped at push. A rotation
214 /// bumps the group's generation without touching existing rows, so this is
215 /// what lets a member decrypt entries that predate a rotation they lived
216 /// through.
217 pub gck_version: i32,
218 /// When the server received and recorded this entry.
219 pub created_at: DateTime<Utc>,
220 }
221
222 /// A blob uploaded to S3 via SyncKit, tracked for dedup and cleanup.
223 #[derive(Debug, Clone, FromRow, Serialize)]
224 pub struct DbSyncBlob {
225 /// Database primary key.
226 pub id: SyncBlobId,
227 /// Sync app this blob belongs to.
228 pub app_id: SyncAppId,
229 /// User who uploaded this blob.
230 pub user_id: UserId,
231 /// Content-address hash provided by the client (used for deduplication).
232 pub hash: String,
233 /// S3 object key where the blob is stored (`{app_id}/{user_id}/{hash}`).
234 pub s3_key: String,
235 /// Size of the blob in bytes.
236 pub size_bytes: i64,
237 /// Developer-defined SDK key this blob was uploaded under. Used to
238 /// attribute storage against the right per-key counter (per_key mode).
239 pub key: String,
240 /// When the blob upload was confirmed.
241 pub uploaded_at: DateTime<Utc>,
242 }
243
244 /// Sync app + billing columns + live usage counters (joined view).
245 ///
246 /// Mirrors columns added in migration 117 (`117_synckit_v2_billing.sql`).
247 /// Built by joining `sync_apps` against `sync_app_usage_current` (LEFT JOIN).
248 /// The usage row is created with the app (`create_sync_app`) and backfilled by
249 /// migration 165; the LEFT JOIN is defense-in-depth against a missing row.
250 #[derive(Debug, Clone, FromRow, Serialize)]
251 pub struct DbSyncAppBilling {
252 // sync_apps base
253 pub id: SyncAppId,
254 pub creator_id: UserId,
255 pub name: String,
256 /// First-party app; bypasses all billing logic.
257 pub is_internal: bool,
258 /// Stripe Customer ID for this app's developer (one customer per app).
259 pub stripe_customer_id: Option<String>,
260 /// Stripe Subscription ID; set once billing activates.
261 pub stripe_subscription_id: Option<String>,
262 /// Billing lifecycle (draft / active / suspended_unpaid / canceled).
263 pub billing_status: super::super::SyncBillingStatus,
264 /// Storage cap in GB. Set in bulk mode; NULL in per_key mode (capacity is
265 /// derived from `key_cap × gb_per_key`) and in draft.
266 pub storage_gb_cap: Option<i32>,
267 /// `PerKey` | `Bulk`. Drives both pricing and degradation behavior. The DB
268 /// column is `NOT NULL CHECK (enforcement_mode IN ('per_key','bulk'))`, so it
269 /// always decodes to one of the two variants.
270 pub enforcement_mode: super::super::SyncEnforcementMode,
271 /// Max active keys. Set in per_key mode; NULL in bulk mode.
272 pub key_cap: Option<i32>,
273 /// GB allotment per active key (per_key mode only). Total storage
274 /// capacity = `key_cap × gb_per_key`.
275 pub gb_per_key: Option<i32>,
276 pub current_period_start: Option<DateTime<Utc>>,
277 pub current_period_end: Option<DateTime<Utc>>,
278 // sync_app_usage_current (LEFT-joined, may be missing if row absent)
279 pub bytes_stored: Option<i64>,
280 pub bytes_egress_period: Option<i64>,
281 pub keys_claimed: Option<i32>,
282 pub last_warning_pct: Option<i16>,
283 pub period_started_at: Option<DateTime<Utc>>,
284 // projects (LEFT-joined). Some when the app is linked to a project, used
285 // to route the Stripe billing portal back to the project dashboard.
286 pub project_slug: Option<String>,
287 }
288
289 /// A single active key claim (row in `sync_app_keys` with `released_at IS NULL`).
290 ///
291 /// See migration 117 for the full table definition. This is the projection
292 /// used by the dashboard "Active keys" list; only the columns the UI needs.
293 #[derive(Debug, Clone, FromRow, Serialize)]
294 pub struct DbSyncAppKey {
295 pub id: uuid::Uuid,
296 pub key: String,
297 pub claimed_at: DateTime<Utc>,
298 /// Bytes stored under this key (LEFT JOIN against
299 /// `sync_key_usage_current`, `0` when no upload has landed yet).
300 pub bytes_stored: i64,
301 }
302
303 // ── OTA models ──
304
305 /// An OTA release for a sync app.
306 #[derive(Debug, Clone, FromRow)]
307 pub struct DbOtaRelease {
308 pub id: OtaReleaseId,
309 pub app_id: SyncAppId,
310 pub version: String,
311 pub notes: String,
312 pub signature: String,
313 pub pub_date: DateTime<Utc>,
314 pub created_at: DateTime<Utc>,
315 }
316
317 /// An artifact (platform-specific binary) within an OTA release.
318 #[derive(Debug, Clone, FromRow)]
319 pub struct DbOtaArtifact {
320 pub id: OtaArtifactId,
321 pub release_id: OtaReleaseId,
322 pub target: String,
323 pub arch: String,
324 pub s3_key: String,
325 pub file_size: i64,
326 /// The artifact's own minisign signature. Tauri signs each (target, arch)
327 /// file independently, so the signature is per-artifact, not per-release.
328 /// `updater_check` serves this one for the requested platform.
329 pub signature: String,
330 /// Malware-scan gate: only `clean` artifacts are advertised/downloadable.
331 pub scan_status: super::super::FileScanStatus,
332 pub created_at: DateTime<Utc>,
333 }
334