main
tag: launch-2026-06-01
tag: magicmirror-v0.1.1
tag: magicmirror-v0.3.0
tag: mnw-cli-v0.1.2
tag: mnw-cli-v0.1.3
tag: mnw-cli-v0.1.4
tag: pom-v0.4.1
tag: pom-v0.4.2
tag: pom-v0.4.3
tag: pom-v0.4.4
tag: pom-v0.4.5
tag: wam-v0.3.0
tag: wam-v0.3.1
Files
Commits
Tags
Notes
Issues
server: report authoritative license activation_count (ultra-fuzz Run #1 Payments MINOR)
validate_key returned a pre-lock read on the already-activated fast path
and a manual `+ 1` guess on the new-activation path, both of which drift
under concurrent activations. try_create_activation already recomputes the
denormalized count under the row lock; add get_activation_count and report
that from both response branches.
Co-Authored-By
Claude Opus 4.8 <noreply@anthropic.com>
2 files changed,
+26 insertions,
-2 deletions
163
163
Ok(())
164
164
}
165
165
166
+
/// Read the denormalized active-activation count for a key.
167
+
///
168
+
/// `try_create_activation` keeps `license_keys.activation_count` authoritative
169
+
/// (it recomputes it under the row lock), so this is the value to report back to
170
+
/// the client rather than a pre-lock read or a manual `+ 1` guess that drifts
171
+
/// under concurrent activations (ultra-fuzz Run #1 Payments MINOR).
172
+
#[tracing::instrument(skip_all)]
173
+
pub async fn get_activation_count(pool: &PgPool, license_key_id: LicenseKeyId) -> Result<i32> {
174
+
let count: i32 = sqlx::query_scalar(
175
+
"SELECT activation_count FROM license_keys WHERE id = $1",
176
+
)
177
+
.bind(license_key_id)
178
+
.fetch_one(pool)
179
+
.await?;
180
+
Ok(count)
181
+
}
182
+
166
183
/// Activate a license key on a machine, atomically enforcing max_activations.
167
184
///
168
185
/// Uses a transaction with `FOR UPDATE` to serialize concurrent activations
147
147
&& activation.is_active
148
148
{
149
149
db::license_keys::touch_activation(&state.db, activation.id).await?;
150
+
// Report the authoritative denormalized count, not the pre-lookup read,
151
+
// so a concurrent activation on another machine isn't reflected stale.
152
+
let activation_count = db::license_keys::get_activation_count(&state.db, key.id).await?;
150
153
return Ok(Json(ValidateKeyResponse {
151
154
valid: true,
152
155
activated: None,
154
157
license: Some(ValidateKeyLicense {
155
158
item_id: key.item_id,
156
159
max_activations: key.max_activations,
157
-
activation_count: key.activation_count,
160
+
activation_count,
158
161
created_at: key.created_at,
159
162
}),
160
163
}));
178
181
}));
179
182
}
180
183
184
+
// try_create_activation recomputed the denormalized count under the row
185
+
// lock; read it back rather than guessing `+ 1` (which is wrong if another
186
+
// machine activated concurrently).
187
+
let activation_count = db::license_keys::get_activation_count(&state.db, key.id).await?;
181
188
Ok(Json(ValidateKeyResponse {
182
189
valid: true,
183
190
activated: Some(true),
185
192
license: Some(ValidateKeyLicense {
186
193
item_id: key.item_id,
187
194
max_activations: key.max_activations,
188
-
activation_count: key.activation_count + 1,
195
+
activation_count,
189
196
created_at: key.created_at,
190
197
}),
191
198
}))