Skip to main content

max / makenotwork

v0.8.0: SyncKit true per-key storage JWT carries a developer-defined `key`. Per-key storage tracked in sync_key_usage_current (migration 119). would_exceed_storage branches per mode: per_key checks the offending key's allotment, returning 402 with dimension=storage_per_key and the key name. Bulk mode unchanged. Dashboard shows top-N keys with mini-gauges in per_key mode. Warning emails fan out per key (e.g. "storage for key 'alpha'"). SDK is synckit-client 0.4.0 — authenticate() signature widened to take key (breaking). 5 new integration tests cover per-key cap isolation, bulk shape, drift reconciliation, /api/sync/keys/list bytes_stored, and per-key warning fan-out + stamping.
Co-Authored-By
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-22 00:22 UTC
Commit: 6586d772972c17e4d5b4654271820dbfa6ae61b3
Parent: fbff1a4
37 files changed, +1146 insertions, -114 deletions
@@ -3551,7 +3551,7 @@
3551 3551
3552 3552 [[package]]
3553 3553 name = "makenotwork"
3554 - version = "0.7.3"
3554 + version = "0.8.0"
3555 3555 dependencies = [
3556 3556 "anyhow",
3557 3557 "argon2",
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makenotwork"
3 - version = "0.7.3"
3 + version = "0.8.0"
4 4 edition = "2024"
5 5 license-file = "LICENSE"
6 6
@@ -21,6 +21,10 @@
21 21 pub sub: UserId,
22 22 /// App ID
23 23 pub app: SyncAppId,
24 + /// Developer-defined SDK key this session belongs to. Required for
25 + /// per-key storage attribution. The dev's backend picks the key when
26 + /// minting the session — typically one key per workspace/org/end-user.
27 + pub key: String,
24 28 /// Issuer
25 29 pub iss: String,
26 30 /// Expiration (Unix timestamp)
@@ -30,11 +34,17 @@
30 34 }
31 35
32 36 /// Create a signed JWT for a sync user.
33 - pub fn create_sync_token(secret: &str, user_id: UserId, app_id: SyncAppId) -> Result<String, AppError> {
37 + pub fn create_sync_token(
38 + secret: &str,
39 + user_id: UserId,
40 + app_id: SyncAppId,
41 + key: &str,
42 + ) -> Result<String, AppError> {
34 43 let now = chrono::Utc::now().timestamp();
35 44 let claims = SyncClaims {
36 45 sub: user_id,
37 46 app: app_id,
47 + key: key.to_string(),
38 48 iss: SYNCKIT_JWT_ISSUER.to_string(),
39 49 exp: now + SYNCKIT_JWT_EXPIRY_SECS,
40 50 iat: now,
@@ -71,6 +81,8 @@
71 81 pub struct SyncUser {
72 82 pub user_id: UserId,
73 83 pub app_id: SyncAppId,
84 + /// SDK key this session was minted under. All writes attributed here.
85 + pub key: String,
74 86 }
75 87
76 88 impl FromRequestParts<AppState> for SyncUser {
@@ -123,9 +135,14 @@
123 135 return Err(AppError::Unauthorized);
124 136 }
125 137
138 + if claims.key.is_empty() {
139 + return Err(AppError::Unauthorized);
140 + }
141 +
126 142 Ok(SyncUser {
127 143 user_id: claims.sub,
128 144 app_id: claims.app,
145 + key: claims.key,
129 146 })
130 147 }
131 148 }
@@ -135,17 +152,19 @@
135 152 use super::*;
136 153
137 154 const TEST_SECRET: &str = "test-secret-key-for-synckit-jwt";
155 + const TEST_KEY: &str = "test-key";
138 156
139 157 #[test]
140 158 fn jwt_round_trip() {
141 159 let user_id = UserId::new();
142 160 let app_id = SyncAppId::new();
143 161
144 - let token = create_sync_token(TEST_SECRET, user_id, app_id).unwrap();
162 + let token = create_sync_token(TEST_SECRET, user_id, app_id, TEST_KEY).unwrap();
145 163 let claims = decode_sync_token(TEST_SECRET, &token).unwrap();
146 164
147 165 assert_eq!(claims.sub, user_id);
148 166 assert_eq!(claims.app, app_id);
167 + assert_eq!(claims.key, TEST_KEY);
149 168 }
150 169
151 170 #[test]
@@ -157,6 +176,7 @@
157 176 let claims = SyncClaims {
158 177 sub: user_id,
159 178 app: app_id,
179 + key: TEST_KEY.to_string(),
160 180 iss: SYNCKIT_JWT_ISSUER.to_string(),
161 181 exp: now - 3600, // expired 1 hour ago
162 182 iat: now - 7200,
@@ -182,7 +202,7 @@
182 202 let user_id = UserId::new();
183 203 let app_id = SyncAppId::new();
184 204
185 - let token = create_sync_token(TEST_SECRET, user_id, app_id).unwrap();
205 + let token = create_sync_token(TEST_SECRET, user_id, app_id, TEST_KEY).unwrap();
186 206 assert!(decode_sync_token("wrong-secret", &token).is_err());
187 207 }
188 208
@@ -212,6 +232,7 @@
212 232 let claims = SyncClaims {
213 233 sub: user_id,
214 234 app: app_id,
235 + key: TEST_KEY.to_string(),
215 236 iss: "wrong-issuer".to_string(),
216 237 exp: now + SYNCKIT_JWT_EXPIRY_SECS,
217 238 iat: now,
@@ -261,7 +282,7 @@
261 282 let user_id = UserId::new();
262 283 let app_id = SyncAppId::new();
263 284
264 - let token = create_sync_token(TEST_SECRET, user_id, app_id).unwrap();
285 + let token = create_sync_token(TEST_SECRET, user_id, app_id, TEST_KEY).unwrap();
265 286 let parts: Vec<&str> = token.split('.').collect();
266 287 assert_eq!(parts.len(), 3);
267 288
@@ -291,6 +312,7 @@
291 312 let claims = SyncClaims {
292 313 sub: user_id,
293 314 app: app_id,
315 + key: TEST_KEY.to_string(),
294 316 iss: SYNCKIT_JWT_ISSUER.to_string(),
295 317 exp: now + SYNCKIT_JWT_EXPIRY_SECS,
296 318 iat: now + 86400 * 365, // 1 year in the future
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "synckit-client"
3 - version = "0.3.1"
3 + version = "0.4.0"
4 4 edition = "2024"
5 5 description = "SyncKit client SDK with end-to-end encryption"
6 6 license-file = "LICENSE"
@@ -24,8 +24,9 @@
24 24 api_key: "your-api-key".into(),
25 25 });
26 26
27 - // Authenticate
28 - let (user_id, app_id) = client.authenticate("user@example.com", "password").await?;
27 + // Authenticate. The third arg is the developer-defined SDK key for billing
28 + // attribution — typically one key per workspace/org/end-user.
29 + let (user_id, app_id) = client.authenticate("user@example.com", "password", "workspace-42").await?;
29 30
30 31 // Set up encryption (first device)
31 32 client.setup_encryption_new("password").await?;