create-feltdb 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,6 +41,41 @@ pub struct Application {
41
41
  pub active_revision: Option<String>,
42
42
  #[serde(default)]
43
43
  pub environment_revisions: std::collections::BTreeMap<String, String>,
44
+ #[serde(default)]
45
+ pub qualification: Option<QualificationMarker>,
46
+ }
47
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
48
+ pub struct QualificationMarker {
49
+ pub purpose: String,
50
+ pub disposable: bool,
51
+ pub environment: String,
52
+ pub proof_run_id: String,
53
+ pub expires_at: u64,
54
+ }
55
+ impl QualificationMarker {
56
+ fn valid_for_creation(&self, timestamp: u64) -> bool {
57
+ self.purpose == "qualification"
58
+ && self.disposable
59
+ && self.environment == "qualification"
60
+ && !self.proof_run_id.trim().is_empty()
61
+ && self.expires_at > timestamp
62
+ }
63
+
64
+ fn authorize_destruction(&self, proof_run_id: &str, timestamp: u64) -> Result<(), String> {
65
+ if self.purpose != "qualification" || !self.disposable {
66
+ return Err("refusing destructive qualification: application is not marked disposable".into());
67
+ }
68
+ if self.environment != "qualification" {
69
+ return Err("refusing destructive qualification: environment is not qualification".into());
70
+ }
71
+ if self.proof_run_id.trim().is_empty() || proof_run_id.trim().is_empty() || self.proof_run_id != proof_run_id {
72
+ return Err("refusing destructive qualification: proof run does not match".into());
73
+ }
74
+ if self.expires_at <= timestamp {
75
+ return Err("refusing destructive qualification: disposable marker has expired".into());
76
+ }
77
+ Ok(())
78
+ }
44
79
  }
45
80
  #[derive(Debug, Clone, Serialize, Deserialize)]
46
81
  pub struct TenantMembership {
@@ -765,10 +800,25 @@ impl TenancyStore {
765
800
  actor: &str,
766
801
  tenant_id: &str,
767
802
  name: &str,
803
+ ) -> Result<Application, String> {
804
+ self.create_application_with_qualification(actor, tenant_id, name, None)
805
+ }
806
+
807
+ pub fn create_application_with_qualification(
808
+ &self,
809
+ actor: &str,
810
+ tenant_id: &str,
811
+ name: &str,
812
+ qualification: Option<QualificationMarker>,
768
813
  ) -> Result<Application, String> {
769
814
  if !self.authorize_tenant(actor, tenant_id, "applications:write") {
770
815
  return Err("forbidden".into());
771
816
  }
817
+ if let Some(marker) = qualification.as_ref() {
818
+ if !marker.valid_for_creation(now()) {
819
+ return Err("invalid disposable qualification marker".into());
820
+ }
821
+ }
772
822
  let mut records = self
773
823
  .records
774
824
  .write()
@@ -794,6 +844,7 @@ impl TenancyStore {
794
844
  status: application_status(),
795
845
  active_revision: None,
796
846
  environment_revisions: Default::default(),
847
+ qualification,
797
848
  };
798
849
  records.application_memberships.push(ApplicationMembership {
799
850
  application_id: application.id.clone(),
@@ -816,6 +867,22 @@ impl TenancyStore {
816
867
  Ok(application)
817
868
  }
818
869
 
870
+ pub fn delete_qualification_application(
871
+ &self,
872
+ actor: &str,
873
+ application_id: &str,
874
+ proof_run_id: &str,
875
+ ) -> Result<(), String> {
876
+ let application = self
877
+ .application_for(actor, application_id)
878
+ .ok_or_else(|| "application not found".to_string())?;
879
+ let marker = application.qualification.ok_or_else(|| {
880
+ "refusing destructive qualification: application is not marked disposable".to_string()
881
+ })?;
882
+ marker.authorize_destruction(proof_run_id, now())?;
883
+ self.delete_application(actor, application_id)
884
+ }
885
+
819
886
  pub fn delete_application(&self, actor: &str, application_id: &str) -> Result<(), String> {
820
887
  let tenant_id = self
821
888
  .application_tenant(application_id)
@@ -1238,6 +1305,73 @@ mod tests {
1238
1305
  assert!(!store.authorize_application("user_a", &tenant_a.id, &app_b.id, "state:read"));
1239
1306
  }
1240
1307
 
1308
+ #[test]
1309
+ fn destructive_qualification_requires_an_unexpired_matching_marker() {
1310
+ let valid_marker = QualificationMarker {
1311
+ purpose: "qualification".into(), disposable: true, environment: "qualification".into(), proof_run_id: "proof-1".into(), expires_at: now() + 300,
1312
+ };
1313
+ assert!(valid_marker.authorize_destruction("", now()).unwrap_err().contains("does not match"));
1314
+ assert!(valid_marker.authorize_destruction("proof-other", now()).unwrap_err().contains("does not match"));
1315
+ let mut wrong_environment = valid_marker.clone();
1316
+ wrong_environment.environment = "production".into();
1317
+ assert!(wrong_environment.authorize_destruction("proof-1", now()).unwrap_err().contains("environment"));
1318
+ let mut not_disposable = valid_marker.clone();
1319
+ not_disposable.disposable = false;
1320
+ assert!(not_disposable.authorize_destruction("proof-1", now()).unwrap_err().contains("not marked disposable"));
1321
+ let mut expired = valid_marker.clone();
1322
+ expired.expires_at = now().saturating_sub(1);
1323
+ assert!(expired.authorize_destruction("proof-1", now()).unwrap_err().contains("expired"));
1324
+ assert!(valid_marker.authorize_destruction("proof-1", now()).is_ok());
1325
+
1326
+ let store = store("qualification-guard");
1327
+ let tenant = store.create_tenant("owner", "Qualification").unwrap();
1328
+ let normal = store
1329
+ .create_application("owner", &tenant.id, "Normal")
1330
+ .unwrap();
1331
+ assert!(store
1332
+ .delete_qualification_application("owner", &normal.id, "proof-1")
1333
+ .unwrap_err()
1334
+ .contains("not marked disposable"));
1335
+ assert!(store
1336
+ .create_application_with_qualification(
1337
+ "owner",
1338
+ &tenant.id,
1339
+ "Expired",
1340
+ Some(QualificationMarker {
1341
+ purpose: "qualification".into(),
1342
+ disposable: true,
1343
+ environment: "qualification".into(),
1344
+ proof_run_id: "proof-expired".into(),
1345
+ expires_at: now().saturating_sub(1),
1346
+ }),
1347
+ )
1348
+ .unwrap_err()
1349
+ .contains("invalid disposable"));
1350
+
1351
+ let proof = store
1352
+ .create_application_with_qualification(
1353
+ "owner",
1354
+ &tenant.id,
1355
+ "Proof",
1356
+ Some(QualificationMarker {
1357
+ purpose: "qualification".into(),
1358
+ disposable: true,
1359
+ environment: "qualification".into(),
1360
+ proof_run_id: "proof-1".into(),
1361
+ expires_at: now() + 300,
1362
+ }),
1363
+ )
1364
+ .unwrap();
1365
+ assert!(store
1366
+ .delete_qualification_application("owner", &proof.id, "proof-other")
1367
+ .unwrap_err()
1368
+ .contains("does not match"));
1369
+ store
1370
+ .delete_qualification_application("owner", &proof.id, "proof-1")
1371
+ .unwrap();
1372
+ assert!(store.application_for("owner", &proof.id).is_none());
1373
+ }
1374
+
1241
1375
  #[test]
1242
1376
  fn invitation_acceptance_and_revocation_change_authorization() {
1243
1377
  let store = store("invitation");
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "create-feltdb",
3
3
  "private": false,
4
4
  "description": "Create a new FeltDB application with one command",
5
- "version": "0.8.1",
5
+ "version": "0.8.2",
6
6
  "license": "MIT",
7
7
  "bin": {
8
8
  "create-feltdb": "bin/create-feltdb.js"