create-feltdb 0.4.11 → 0.4.13

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.
Files changed (151) hide show
  1. package/dist/cli.js +16 -1
  2. package/dist/create.js +30 -16
  3. package/dist/docker-compose-generator.js +48 -9
  4. package/dist/package-versions.js +1 -1
  5. package/dist/server-source/Cargo.lock +2238 -0
  6. package/dist/server-source/Cargo.toml +7 -0
  7. package/dist/server-source/crates/feltdb/Cargo.lock +175 -0
  8. package/dist/server-source/crates/feltdb/Cargo.toml +79 -0
  9. package/dist/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
  10. package/dist/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
  11. package/dist/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
  12. package/dist/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
  13. package/dist/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
  14. package/dist/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
  15. package/dist/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
  16. package/dist/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
  17. package/dist/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
  18. package/dist/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
  19. package/dist/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
  20. package/dist/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
  21. package/dist/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
  22. package/dist/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
  23. package/dist/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
  24. package/dist/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
  25. package/dist/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
  26. package/dist/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
  27. package/dist/server-source/crates/feltdb/src/acquisition.rs +286 -0
  28. package/dist/server-source/crates/feltdb/src/admission.rs +192 -0
  29. package/dist/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
  30. package/dist/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
  31. package/dist/server-source/crates/feltdb/src/analytics.rs +475 -0
  32. package/dist/server-source/crates/feltdb/src/application.rs +2244 -0
  33. package/dist/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
  34. package/dist/server-source/crates/feltdb/src/authorization.rs +1030 -0
  35. package/dist/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
  36. package/dist/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
  37. package/dist/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
  38. package/dist/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
  39. package/dist/server-source/crates/feltdb/src/capability.rs +903 -0
  40. package/dist/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
  41. package/dist/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
  42. package/dist/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
  43. package/dist/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
  44. package/dist/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
  45. package/dist/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
  46. package/dist/server-source/crates/feltdb/src/content_distribution.rs +465 -0
  47. package/dist/server-source/crates/feltdb/src/convergence.rs +618 -0
  48. package/dist/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
  49. package/dist/server-source/crates/feltdb/src/crash_injection.rs +380 -0
  50. package/dist/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
  51. package/dist/server-source/crates/feltdb/src/cron.rs +294 -0
  52. package/dist/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
  53. package/dist/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
  54. package/dist/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
  55. package/dist/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
  56. package/dist/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
  57. package/dist/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
  58. package/dist/server-source/crates/feltdb/src/durable_sync.rs +316 -0
  59. package/dist/server-source/crates/feltdb/src/execution.rs +426 -0
  60. package/dist/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
  61. package/dist/server-source/crates/feltdb/src/indexing.rs +779 -0
  62. package/dist/server-source/crates/feltdb/src/lib.rs +2838 -0
  63. package/dist/server-source/crates/feltdb/src/materialization.rs +184 -0
  64. package/dist/server-source/crates/feltdb/src/metrics.rs +267 -0
  65. package/dist/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
  66. package/dist/server-source/crates/feltdb/src/observability.rs +366 -0
  67. package/dist/server-source/crates/feltdb/src/operation.rs +251 -0
  68. package/dist/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
  69. package/dist/server-source/crates/feltdb/src/operation_log.rs +344 -0
  70. package/dist/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
  71. package/dist/server-source/crates/feltdb/src/peer_registry.rs +166 -0
  72. package/dist/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
  73. package/dist/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
  74. package/dist/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
  75. package/dist/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
  76. package/dist/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
  77. package/dist/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
  78. package/dist/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
  79. package/dist/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
  80. package/dist/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
  81. package/dist/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
  82. package/dist/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
  83. package/dist/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
  84. package/dist/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
  85. package/dist/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
  86. package/dist/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
  87. package/dist/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
  88. package/dist/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
  89. package/dist/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
  90. package/dist/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
  91. package/dist/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
  92. package/dist/server-source/crates/feltdb/src/production_api.rs +400 -0
  93. package/dist/server-source/crates/feltdb/src/provenance.rs +207 -0
  94. package/dist/server-source/crates/feltdb/src/query_performance.rs +217 -0
  95. package/dist/server-source/crates/feltdb/src/references.rs +404 -0
  96. package/dist/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
  97. package/dist/server-source/crates/feltdb/src/replication_manager.rs +160 -0
  98. package/dist/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
  99. package/dist/server-source/crates/feltdb/src/routing.rs +409 -0
  100. package/dist/server-source/crates/feltdb/src/sharding.rs +523 -0
  101. package/dist/server-source/crates/feltdb/src/state_contract.rs +3118 -0
  102. package/dist/server-source/crates/feltdb/src/state_hash.rs +291 -0
  103. package/dist/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
  104. package/dist/server-source/crates/feltdb/src/storage.rs +267 -0
  105. package/dist/server-source/crates/feltdb/src/submission.rs +477 -0
  106. package/dist/server-source/crates/feltdb/src/sync.rs +483 -0
  107. package/dist/server-source/crates/feltdb/src/sync_contract.rs +822 -0
  108. package/dist/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
  109. package/dist/server-source/crates/feltdb/src/transaction_api.rs +473 -0
  110. package/dist/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
  111. package/dist/server-source/crates/feltdb/src/transactions.rs +646 -0
  112. package/dist/server-source/crates/feltdb/src/trigger.rs +390 -0
  113. package/dist/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
  114. package/dist/server-source/crates/feltdb/src/workflow.rs +713 -0
  115. package/dist/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
  116. package/dist/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
  117. package/dist/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
  118. package/dist/server-source/crates/feltdb/src/workload.rs +1310 -0
  119. package/dist/server-source/crates/feltdb-server/Cargo.toml +23 -0
  120. package/dist/server-source/crates/feltdb-server/README.md +79 -0
  121. package/dist/server-source/crates/feltdb-server/src/app_state.rs +73 -0
  122. package/dist/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
  123. package/dist/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
  124. package/dist/server-source/crates/feltdb-server/src/audit.rs +59 -0
  125. package/dist/server-source/crates/feltdb-server/src/auth.rs +308 -0
  126. package/dist/server-source/crates/feltdb-server/src/authorization.rs +228 -0
  127. package/dist/server-source/crates/feltdb-server/src/backup.rs +416 -0
  128. package/dist/server-source/crates/feltdb-server/src/causal.rs +218 -0
  129. package/dist/server-source/crates/feltdb-server/src/certification.rs +223 -0
  130. package/dist/server-source/crates/feltdb-server/src/clock.rs +132 -0
  131. package/dist/server-source/crates/feltdb-server/src/cluster.rs +165 -0
  132. package/dist/server-source/crates/feltdb-server/src/connections.rs +1044 -0
  133. package/dist/server-source/crates/feltdb-server/src/content.rs +111 -0
  134. package/dist/server-source/crates/feltdb-server/src/identity.rs +250 -0
  135. package/dist/server-source/crates/feltdb-server/src/key_management.rs +78 -0
  136. package/dist/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
  137. package/dist/server-source/crates/feltdb-server/src/leases.rs +123 -0
  138. package/dist/server-source/crates/feltdb-server/src/lib.rs +25 -0
  139. package/dist/server-source/crates/feltdb-server/src/main.rs +8715 -0
  140. package/dist/server-source/crates/feltdb-server/src/metrics.rs +97 -0
  141. package/dist/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
  142. package/dist/server-source/crates/feltdb-server/src/principals.rs +510 -0
  143. package/dist/server-source/crates/feltdb-server/src/providers.rs +269 -0
  144. package/dist/server-source/crates/feltdb-server/src/releases.rs +2563 -0
  145. package/dist/server-source/crates/feltdb-server/src/sessions.rs +156 -0
  146. package/dist/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
  147. package/dist/server-source/crates/feltdb-server/src/versions.rs +264 -0
  148. package/dist/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
  149. package/dist/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
  150. package/dist/server-source/crates/feltdb-wasm/test.db +0 -0
  151. package/package.json +3 -3
@@ -0,0 +1,449 @@
1
+ use crate::{content::ContentStore, tenancy::TenancyStore};
2
+ use rand::{distributions::Alphanumeric, Rng};
3
+ use serde::{Deserialize, Serialize};
4
+ use serde_json::Value;
5
+ use std::{
6
+ collections::{BTreeMap, BTreeSet},
7
+ fs,
8
+ path::PathBuf,
9
+ sync::{Arc, RwLock},
10
+ time::{SystemTime, UNIX_EPOCH},
11
+ };
12
+
13
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14
+ #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
15
+ pub enum ArtifactKind {
16
+ Source,
17
+ Document,
18
+ Image,
19
+ Dataset,
20
+ Form,
21
+ Build,
22
+ Package,
23
+ ModelOutput,
24
+ ExecutionOutput,
25
+ Log,
26
+ Report,
27
+ Backup,
28
+ Export,
29
+ Import,
30
+ Application,
31
+ Configuration,
32
+ Other(String),
33
+ }
34
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
35
+ #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
36
+ pub enum ArtifactLifecycle {
37
+ Created,
38
+ Active,
39
+ Superseded,
40
+ Archived,
41
+ Deleted,
42
+ }
43
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44
+ pub struct ProducerRef {
45
+ pub kind: String,
46
+ pub id: String,
47
+ }
48
+ #[derive(Debug, Clone, Serialize, Deserialize, Default)]
49
+ pub struct RetentionPolicy {
50
+ pub retain_until: Option<u64>,
51
+ pub retention_class: Option<String>,
52
+ pub legal_hold: bool,
53
+ pub delete_after: Option<u64>,
54
+ }
55
+ #[derive(Debug, Clone, Serialize, Deserialize, Default)]
56
+ pub struct ArtifactMetadata {
57
+ pub name: String,
58
+ pub environment: Option<String>,
59
+ #[serde(default)]
60
+ pub tags: BTreeSet<String>,
61
+ #[serde(default)]
62
+ pub labels: BTreeMap<String, String>,
63
+ pub sensitivity: Option<String>,
64
+ pub classification: Option<String>,
65
+ #[serde(default)]
66
+ pub retention: RetentionPolicy,
67
+ #[serde(default)]
68
+ pub application: Value,
69
+ #[serde(default)]
70
+ pub provider: Value,
71
+ }
72
+ #[derive(Debug, Clone, Serialize, Deserialize)]
73
+ pub struct Artifact {
74
+ pub id: String,
75
+ pub tenant_id: String,
76
+ pub application_id: String,
77
+ pub content_hash: String,
78
+ pub content_type: String,
79
+ pub size: u64,
80
+ pub kind: ArtifactKind,
81
+ pub lifecycle: ArtifactLifecycle,
82
+ pub created_by: String,
83
+ pub producer: ProducerRef,
84
+ pub source: Option<String>,
85
+ #[serde(default)]
86
+ pub parents: Vec<String>,
87
+ pub supersedes: Option<String>,
88
+ pub revision: Option<String>,
89
+ pub workload: Option<String>,
90
+ pub execution: Option<String>,
91
+ pub metadata: ArtifactMetadata,
92
+ pub created_at: u64,
93
+ }
94
+ impl Artifact {
95
+ pub fn reference(&self) -> String {
96
+ format!(
97
+ "artifact://{}/{}/{}",
98
+ self.tenant_id, self.application_id, self.id
99
+ )
100
+ }
101
+ }
102
+ #[derive(Debug, Default, Serialize, Deserialize)]
103
+ struct Records {
104
+ artifacts: Vec<Artifact>,
105
+ }
106
+ #[derive(Clone)]
107
+ pub struct ArtifactStore {
108
+ path: PathBuf,
109
+ content: ContentStore,
110
+ records: Arc<RwLock<Records>>,
111
+ }
112
+ fn now() -> u64 {
113
+ SystemTime::now()
114
+ .duration_since(UNIX_EPOCH)
115
+ .unwrap_or_default()
116
+ .as_secs()
117
+ }
118
+ fn id() -> String {
119
+ format!(
120
+ "artifact_{}",
121
+ rand::thread_rng()
122
+ .sample_iter(&Alphanumeric)
123
+ .take(20)
124
+ .map(char::from)
125
+ .collect::<String>()
126
+ )
127
+ }
128
+ impl ArtifactStore {
129
+ pub fn load(path: impl Into<PathBuf>, content: ContentStore) -> Result<Self, String> {
130
+ let path = path.into();
131
+ let records = if path.exists() {
132
+ serde_json::from_slice(&fs::read(&path).map_err(|e| e.to_string())?)
133
+ .map_err(|e| e.to_string())?
134
+ } else {
135
+ Records::default()
136
+ };
137
+ Ok(Self {
138
+ path,
139
+ content,
140
+ records: Arc::new(RwLock::new(records)),
141
+ })
142
+ }
143
+ fn persist(&self, r: &Records) -> Result<(), String> {
144
+ if let Some(p) = self.path.parent() {
145
+ fs::create_dir_all(p).map_err(|e| e.to_string())?
146
+ }
147
+ let tmp = self.path.with_extension("tmp");
148
+ fs::write(
149
+ &tmp,
150
+ serde_json::to_vec_pretty(r).map_err(|e| e.to_string())?,
151
+ )
152
+ .map_err(|e| e.to_string())?;
153
+ fs::rename(tmp, &self.path).map_err(|e| e.to_string())
154
+ }
155
+ fn allowed(actor: &str, tenancy: &TenancyStore, app: &str, capability: &str) -> bool {
156
+ tenancy
157
+ .application_tenant(app)
158
+ .map(|tenant| {
159
+ tenancy.authorize_application(actor, &tenant, app, capability)
160
+ || tenancy.authorize_application(actor, &tenant, app, "applications:write")
161
+ || tenancy.authorize_application(actor, &tenant, app, "applications:read")
162
+ })
163
+ .unwrap_or(false)
164
+ }
165
+ pub fn create(
166
+ &self,
167
+ actor: &str,
168
+ tenancy: &TenancyStore,
169
+ application_id: &str,
170
+ kind: ArtifactKind,
171
+ content_type: String,
172
+ bytes: &[u8],
173
+ producer: ProducerRef,
174
+ parents: Vec<String>,
175
+ supersedes: Option<String>,
176
+ metadata: ArtifactMetadata,
177
+ revision: Option<String>,
178
+ workload: Option<String>,
179
+ execution: Option<String>,
180
+ ) -> Result<Artifact, String> {
181
+ if !Self::allowed(actor, tenancy, application_id, "artifact:create") {
182
+ return Err("forbidden".into());
183
+ }
184
+ let tenant_id = tenancy
185
+ .application_tenant(application_id)
186
+ .ok_or("application not found")?;
187
+ let records = self
188
+ .records
189
+ .read()
190
+ .map_err(|_| "artifact store lock poisoned")?;
191
+ for reference in parents.iter().chain(supersedes.iter()) {
192
+ if !records.artifacts.iter().any(|a| {
193
+ &a.id == reference
194
+ && a.tenant_id == tenant_id
195
+ && a.application_id == application_id
196
+ && a.lifecycle != ArtifactLifecycle::Deleted
197
+ }) {
198
+ return Err("parent artifact unavailable or outside application boundary".into());
199
+ }
200
+ }
201
+ drop(records);
202
+ let hash = self.content.store(bytes).map_err(|e| e.to_string())?;
203
+ let artifact = Artifact {
204
+ id: id(),
205
+ tenant_id,
206
+ application_id: application_id.into(),
207
+ content_hash: hash,
208
+ content_type,
209
+ size: bytes.len() as u64,
210
+ kind,
211
+ lifecycle: ArtifactLifecycle::Active,
212
+ created_by: actor.into(),
213
+ producer,
214
+ source: None,
215
+ parents,
216
+ supersedes,
217
+ revision,
218
+ workload,
219
+ execution,
220
+ metadata,
221
+ created_at: now(),
222
+ };
223
+ let mut r = self
224
+ .records
225
+ .write()
226
+ .map_err(|_| "artifact store lock poisoned")?;
227
+ if let Some(old) = artifact
228
+ .supersedes
229
+ .as_ref()
230
+ .and_then(|id| r.artifacts.iter_mut().find(|a| &a.id == id))
231
+ {
232
+ old.lifecycle = ArtifactLifecycle::Superseded
233
+ }
234
+ r.artifacts.push(artifact.clone());
235
+ self.persist(&r)?;
236
+ Ok(artifact)
237
+ }
238
+ pub fn list(
239
+ &self,
240
+ actor: &str,
241
+ tenancy: &TenancyStore,
242
+ app: &str,
243
+ ) -> Result<Vec<Artifact>, String> {
244
+ if !Self::allowed(actor, tenancy, app, "artifact:read") {
245
+ return Err("forbidden".into());
246
+ }
247
+ Ok(self
248
+ .records
249
+ .read()
250
+ .map_err(|_| "artifact store lock poisoned")?
251
+ .artifacts
252
+ .iter()
253
+ .filter(|a| a.application_id == app && a.lifecycle != ArtifactLifecycle::Deleted)
254
+ .cloned()
255
+ .collect())
256
+ }
257
+ pub fn get(&self, actor: &str, tenancy: &TenancyStore, id: &str) -> Result<Artifact, String> {
258
+ let r = self
259
+ .records
260
+ .read()
261
+ .map_err(|_| "artifact store lock poisoned")?;
262
+ let a = r
263
+ .artifacts
264
+ .iter()
265
+ .find(|a| a.id == id && a.lifecycle != ArtifactLifecycle::Deleted)
266
+ .ok_or("artifact not found")?;
267
+ if !Self::allowed(actor, tenancy, &a.application_id, "artifact:read") {
268
+ return Err("forbidden".into());
269
+ }
270
+ Ok(a.clone())
271
+ }
272
+ pub fn content(
273
+ &self,
274
+ actor: &str,
275
+ tenancy: &TenancyStore,
276
+ id: &str,
277
+ ) -> Result<Vec<u8>, String> {
278
+ let a = self.get(actor, tenancy, id)?;
279
+ self.content
280
+ .get(&a.content_hash)
281
+ .map_err(|_| "artifact integrity failure".to_string())?
282
+ .ok_or("artifact content missing".into())
283
+ }
284
+ pub fn lifecycle(
285
+ &self,
286
+ actor: &str,
287
+ tenancy: &TenancyStore,
288
+ id: &str,
289
+ next: ArtifactLifecycle,
290
+ ) -> Result<Artifact, String> {
291
+ let mut r = self
292
+ .records
293
+ .write()
294
+ .map_err(|_| "artifact store lock poisoned")?;
295
+ let a = r
296
+ .artifacts
297
+ .iter_mut()
298
+ .find(|a| a.id == id)
299
+ .ok_or("artifact not found")?;
300
+ if !Self::allowed(actor, tenancy, &a.application_id, "artifact:write") {
301
+ return Err("forbidden".into());
302
+ }
303
+ let valid = matches!(
304
+ (a.lifecycle, next),
305
+ (ArtifactLifecycle::Active, ArtifactLifecycle::Archived)
306
+ | (ArtifactLifecycle::Archived, ArtifactLifecycle::Active)
307
+ | (_, ArtifactLifecycle::Deleted)
308
+ );
309
+ if !valid {
310
+ return Err("invalid artifact lifecycle transition".into());
311
+ }
312
+ if next == ArtifactLifecycle::Deleted
313
+ && (a.metadata.retention.legal_hold
314
+ || a.metadata
315
+ .retention
316
+ .retain_until
317
+ .is_some_and(|at| at > now()))
318
+ {
319
+ return Err("artifact retention policy prevents deletion".into());
320
+ }
321
+ a.lifecycle = next;
322
+ let out = a.clone();
323
+ self.persist(&r)?;
324
+ Ok(out)
325
+ }
326
+ pub fn parents(
327
+ &self,
328
+ actor: &str,
329
+ tenancy: &TenancyStore,
330
+ id: &str,
331
+ ) -> Result<Vec<Artifact>, String> {
332
+ let a = self.get(actor, tenancy, id)?;
333
+ let r = self
334
+ .records
335
+ .read()
336
+ .map_err(|_| "artifact store lock poisoned")?;
337
+ Ok(r.artifacts
338
+ .iter()
339
+ .filter(|candidate| {
340
+ a.parents.contains(&candidate.id) || a.supersedes.as_ref() == Some(&candidate.id)
341
+ })
342
+ .cloned()
343
+ .collect())
344
+ }
345
+ pub fn children(
346
+ &self,
347
+ actor: &str,
348
+ tenancy: &TenancyStore,
349
+ id: &str,
350
+ ) -> Result<Vec<Artifact>, String> {
351
+ let a = self.get(actor, tenancy, id)?;
352
+ let r = self
353
+ .records
354
+ .read()
355
+ .map_err(|_| "artifact store lock poisoned")?;
356
+ Ok(r.artifacts
357
+ .iter()
358
+ .filter(|candidate| {
359
+ candidate.application_id == a.application_id
360
+ && (candidate.parents.contains(&a.id)
361
+ || candidate.supersedes.as_ref() == Some(&a.id))
362
+ })
363
+ .cloned()
364
+ .collect())
365
+ }
366
+ pub fn verify(&self, actor: &str, tenancy: &TenancyStore, id: &str) -> Result<bool, String> {
367
+ self.content(actor, tenancy, id).map(|_| true)
368
+ }
369
+ pub fn content_references(&self, hash: &str) -> usize {
370
+ self.records
371
+ .read()
372
+ .map(|r| {
373
+ r.artifacts
374
+ .iter()
375
+ .filter(|a| a.content_hash == hash && a.lifecycle != ArtifactLifecycle::Deleted)
376
+ .count()
377
+ })
378
+ .unwrap_or(0)
379
+ }
380
+ }
381
+
382
+ #[cfg(test)]
383
+ mod tests {
384
+ use super::*;
385
+ #[test]
386
+ fn upload_dedup_immutability_lineage_and_gc_refs() {
387
+ let suffix = id();
388
+ let tenancy =
389
+ TenancyStore::load(std::env::temp_dir().join(format!("{suffix}-tenant"))).unwrap();
390
+ let tenant = tenancy.create_tenant("owner", "Artifacts").unwrap();
391
+ let app = tenancy
392
+ .create_application("owner", &tenant.id, "App")
393
+ .unwrap();
394
+ let root = std::env::temp_dir().join(format!("{suffix}-content"));
395
+ let content = ContentStore::new(root.clone()).unwrap();
396
+ let store = ArtifactStore::load(
397
+ std::env::temp_dir().join(format!("{suffix}-artifacts")),
398
+ content,
399
+ )
400
+ .unwrap();
401
+ let producer = ProducerRef {
402
+ kind: "human".into(),
403
+ id: "owner".into(),
404
+ };
405
+ let a = store
406
+ .create(
407
+ "owner",
408
+ &tenancy,
409
+ &app.id,
410
+ ArtifactKind::Document,
411
+ "text/plain".into(),
412
+ b"same",
413
+ producer.clone(),
414
+ vec![],
415
+ None,
416
+ ArtifactMetadata::default(),
417
+ None,
418
+ None,
419
+ None,
420
+ )
421
+ .unwrap();
422
+ let b = store
423
+ .create(
424
+ "owner",
425
+ &tenancy,
426
+ &app.id,
427
+ ArtifactKind::Report,
428
+ "text/plain".into(),
429
+ b"same",
430
+ producer,
431
+ vec![a.id.clone()],
432
+ Some(a.id.clone()),
433
+ ArtifactMetadata::default(),
434
+ None,
435
+ Some("workload-1".into()),
436
+ Some("execution-1".into()),
437
+ )
438
+ .unwrap();
439
+ assert_eq!(a.content_hash, b.content_hash);
440
+ assert_eq!(store.content_references(&a.content_hash), 2);
441
+ assert_eq!(store.parents("owner", &tenancy, &b.id).unwrap().len(), 1);
442
+ assert_eq!(store.content("owner", &tenancy, &b.id).unwrap(), b"same");
443
+ store
444
+ .lifecycle("owner", &tenancy, &b.id, ArtifactLifecycle::Deleted)
445
+ .unwrap();
446
+ assert_eq!(store.content_references(&a.content_hash), 1);
447
+ let _ = fs::remove_dir_all(root);
448
+ }
449
+ }
@@ -0,0 +1,59 @@
1
+ use std::{
2
+ fs::OpenOptions,
3
+ io::Write,
4
+ path::PathBuf,
5
+ sync::{Arc, Mutex},
6
+ time::{SystemTime, UNIX_EPOCH},
7
+ };
8
+
9
+ use serde::Serialize;
10
+
11
+ #[derive(Clone)]
12
+ pub struct AuditLog {
13
+ path: Arc<PathBuf>,
14
+ lock: Arc<Mutex<()>>,
15
+ }
16
+
17
+ #[derive(Serialize)]
18
+ pub struct AuditEvent<'a> {
19
+ pub timestamp_ms: u128,
20
+ pub namespace: &'a str,
21
+ pub principal: &'a str,
22
+ pub action: &'a str,
23
+ pub target: &'a str,
24
+ pub outcome: &'a str,
25
+ pub status: u16,
26
+ }
27
+
28
+ impl AuditLog {
29
+ pub fn new(path: PathBuf) -> Self {
30
+ Self {
31
+ path: Arc::new(path),
32
+ lock: Arc::new(Mutex::new(())),
33
+ }
34
+ }
35
+
36
+ pub fn record(&self, mut event: AuditEvent<'_>) -> Result<(), String> {
37
+ let _guard = self
38
+ .lock
39
+ .lock()
40
+ .map_err(|_| "audit log lock poisoned".to_string())?;
41
+ if let Some(parent) = self.path.parent() {
42
+ std::fs::create_dir_all(parent).map_err(|error| error.to_string())?;
43
+ }
44
+ event.timestamp_ms = SystemTime::now()
45
+ .duration_since(UNIX_EPOCH)
46
+ .map_err(|error| error.to_string())?
47
+ .as_millis();
48
+ let mut file = OpenOptions::new()
49
+ .create(true)
50
+ .append(true)
51
+ .open(&*self.path)
52
+ .map_err(|error| error.to_string())?;
53
+ serde_json::to_writer(&mut file, &event).map_err(|error| error.to_string())?;
54
+ file.write_all(b"\n").map_err(|error| error.to_string())?;
55
+ file.flush().map_err(|error| error.to_string())?;
56
+ file.sync_data().map_err(|error| error.to_string())?;
57
+ Ok(())
58
+ }
59
+ }