@feltdb/core 0.4.12 → 0.4.14

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 (165) hide show
  1. package/dist/cli/commands.js +42 -25
  2. package/dist/cli/index.js +1 -1
  3. package/dist/create/cli.js +39 -4
  4. package/dist/create/create.js +30 -18
  5. package/dist/create/docker-compose-generator.js +68 -11
  6. package/dist/create/package-versions.js +1 -1
  7. package/dist/create/server-source/Cargo.lock +2238 -0
  8. package/dist/create/server-source/Cargo.toml +7 -0
  9. package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
  10. package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
  11. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
  12. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
  13. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
  14. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
  15. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
  19. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
  20. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
  21. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
  22. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
  23. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
  24. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
  25. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
  26. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
  27. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
  28. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
  29. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
  30. package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
  31. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
  32. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
  33. package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
  34. package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
  35. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
  36. package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
  37. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
  38. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
  39. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
  40. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
  41. package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
  42. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
  43. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
  44. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
  45. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
  46. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
  47. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
  48. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
  49. package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
  50. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
  51. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
  52. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
  53. package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
  54. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
  55. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
  56. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
  57. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
  58. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
  59. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
  60. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
  61. package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
  62. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
  63. package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
  64. package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
  65. package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
  66. package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
  67. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
  68. package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
  69. package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
  70. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
  71. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
  72. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
  73. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
  74. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
  75. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
  76. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
  77. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
  78. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
  79. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
  80. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
  81. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
  82. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
  83. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
  84. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
  85. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
  86. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
  87. package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
  88. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
  89. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
  90. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
  91. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
  92. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
  93. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
  94. package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
  95. package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
  96. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
  97. package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
  98. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
  99. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
  100. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
  101. package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
  102. package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
  103. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
  104. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
  105. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
  106. package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
  107. package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
  108. package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
  109. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
  110. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
  111. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
  112. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
  113. package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
  114. package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
  115. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
  116. package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
  117. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
  118. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
  119. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
  120. package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
  121. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
  122. package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
  123. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
  124. package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
  125. package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
  126. package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
  127. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
  128. package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
  129. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
  130. package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
  131. package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
  132. package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
  133. package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
  134. package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
  135. package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
  136. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
  137. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
  138. package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
  139. package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
  140. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
  141. package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
  142. package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
  143. package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
  144. package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
  145. package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
  146. package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
  147. package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
  148. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
  149. package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
  150. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
  151. package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
  152. package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
  153. package/dist/flowspec.d.ts +2 -0
  154. package/dist/flowspec.d.ts.map +1 -1
  155. package/dist/flowspec.js +28 -4
  156. package/dist/state-contract.d.ts +7 -1
  157. package/dist/state-contract.d.ts.map +1 -1
  158. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  159. package/dist/studio/components/ConflictExplorer.d.ts.map +1 -1
  160. package/dist/studio/components/index.js +1 -1
  161. package/dist/studio/{components-Q0Nx6gLE.js → components-9kDSWiGL.js} +163 -47
  162. package/dist/studio/index.js +31 -28
  163. package/dist/studio-app/assets/{index-BHOmLZF-.js → index-DVdvmf69.js} +9 -9
  164. package/dist/studio-app/index.html +1 -1
  165. package/package.json +1 -1
@@ -0,0 +1,416 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use sha2::{Digest, Sha256};
3
+ use std::{
4
+ fs::{self, File},
5
+ io::{self, Read, Write},
6
+ path::{Path, PathBuf},
7
+ time::{SystemTime, UNIX_EPOCH},
8
+ };
9
+
10
+ pub const BACKUP_FORMAT: u32 = 1;
11
+
12
+ #[derive(Clone, Debug, Serialize, Deserialize)]
13
+ pub struct BackupEntry {
14
+ pub path: String,
15
+ pub bytes: u64,
16
+ pub sha256: String,
17
+ }
18
+
19
+ #[derive(Clone, Debug, Serialize, Deserialize)]
20
+ pub struct BackupManifest {
21
+ pub format: u32,
22
+ pub created_ms: u64,
23
+ pub entries: Vec<BackupEntry>,
24
+ }
25
+
26
+ pub fn server_artifacts(data: &Path, keys: &Path, audit: &Path) -> Vec<(String, PathBuf)> {
27
+ vec![
28
+ ("state.log".into(), data.to_path_buf()),
29
+ ("state.checkpoint".into(), data.with_extension("checkpoint")),
30
+ ("state.sync.json".into(), data.with_extension("sync.json")),
31
+ (
32
+ "state.cluster.json".into(),
33
+ data.with_extension("cluster.json"),
34
+ ),
35
+ (
36
+ "state.membership.json".into(),
37
+ data.with_extension("membership.json"),
38
+ ),
39
+ (
40
+ "state.leases.json".into(),
41
+ data.with_extension("leases.json"),
42
+ ),
43
+ ("state.content".into(), data.with_extension("content")),
44
+ ("platform.json".into(), data.with_extension("platform.json")),
45
+ (
46
+ "applications.json".into(),
47
+ data.with_extension("applications.json"),
48
+ ),
49
+ (
50
+ "application-runtimes.json".into(),
51
+ data.with_extension("application-runtimes.json"),
52
+ ),
53
+ (
54
+ "principals.json".into(),
55
+ data.with_extension("principals.json"),
56
+ ),
57
+ (
58
+ "connections.json".into(),
59
+ data.with_extension("connections.json"),
60
+ ),
61
+ (
62
+ "revoked-sessions.json".into(),
63
+ data.with_extension("revoked-sessions.json"),
64
+ ),
65
+ ("api-keys.json".into(), keys.to_path_buf()),
66
+ ("audit.log".into(), audit.to_path_buf()),
67
+ ]
68
+ }
69
+
70
+ pub fn create_bundle(output: &Path, artifacts: &[(String, PathBuf)]) -> io::Result<BackupManifest> {
71
+ create_bundle_with_virtual(output, artifacts, &[])
72
+ }
73
+
74
+ pub fn create_bundle_with_virtual(
75
+ output: &Path,
76
+ artifacts: &[(String, PathBuf)],
77
+ virtual_files: &[(String, Vec<u8>)],
78
+ ) -> io::Result<BackupManifest> {
79
+ if output.exists() {
80
+ return Err(io::Error::new(
81
+ io::ErrorKind::AlreadyExists,
82
+ "backup output already exists",
83
+ ));
84
+ }
85
+ let staging = output.with_extension("partial");
86
+ if staging.exists() {
87
+ return Err(io::Error::new(
88
+ io::ErrorKind::AlreadyExists,
89
+ "an interrupted backup staging directory exists",
90
+ ));
91
+ }
92
+ fs::create_dir_all(staging.join("files"))?;
93
+ let result = (|| {
94
+ let mut entries = Vec::new();
95
+ for (label, source) in artifacts {
96
+ validate_relative(label)?;
97
+ if !source.exists() {
98
+ continue;
99
+ }
100
+ copy_source(
101
+ source,
102
+ &staging.join("files").join(label),
103
+ &format!("files/{label}"),
104
+ &mut entries,
105
+ )?;
106
+ }
107
+ for (label, bytes) in virtual_files {
108
+ validate_relative(label)?;
109
+ let destination = staging.join("files").join(label);
110
+ if let Some(parent) = destination.parent() {
111
+ fs::create_dir_all(parent)?;
112
+ }
113
+ write_synced(&destination, bytes)?;
114
+ entries.push(BackupEntry {
115
+ path: format!("files/{label}"),
116
+ bytes: bytes.len() as u64,
117
+ sha256: hash_file(&destination)?,
118
+ });
119
+ }
120
+ entries.sort_by(|left, right| left.path.cmp(&right.path));
121
+ let manifest = BackupManifest {
122
+ format: BACKUP_FORMAT,
123
+ created_ms: SystemTime::now()
124
+ .duration_since(UNIX_EPOCH)
125
+ .unwrap_or_default()
126
+ .as_millis() as u64,
127
+ entries,
128
+ };
129
+ write_synced(
130
+ &staging.join("manifest.json"),
131
+ &serde_json::to_vec_pretty(&manifest).map_err(io::Error::other)?,
132
+ )?;
133
+ sync_directory(&staging)?;
134
+ fs::rename(&staging, output)?;
135
+ if let Some(parent) = output.parent() {
136
+ sync_directory(parent)?;
137
+ }
138
+ Ok(manifest)
139
+ })();
140
+ if result.is_err() {
141
+ let _ = fs::remove_dir_all(&staging);
142
+ }
143
+ result
144
+ }
145
+
146
+ pub fn verify_bundle(archive: &Path) -> io::Result<BackupManifest> {
147
+ if archive.with_extension("partial").exists() {
148
+ return Err(io::Error::new(
149
+ io::ErrorKind::InvalidData,
150
+ "interrupted backup staging directory detected",
151
+ ));
152
+ }
153
+ let manifest: BackupManifest =
154
+ serde_json::from_slice(&fs::read(archive.join("manifest.json"))?)
155
+ .map_err(io::Error::other)?;
156
+ if manifest.format != BACKUP_FORMAT {
157
+ return Err(io::Error::new(
158
+ io::ErrorKind::InvalidData,
159
+ format!("unsupported backup format {}", manifest.format),
160
+ ));
161
+ }
162
+ for entry in &manifest.entries {
163
+ validate_relative(&entry.path)?;
164
+ let path = archive.join(&entry.path);
165
+ let metadata = fs::symlink_metadata(&path)?;
166
+ if !metadata.is_file() || metadata.len() != entry.bytes || hash_file(&path)? != entry.sha256
167
+ {
168
+ return Err(io::Error::new(
169
+ io::ErrorKind::InvalidData,
170
+ format!("backup entry failed verification: {}", entry.path),
171
+ ));
172
+ }
173
+ }
174
+ Ok(manifest)
175
+ }
176
+
177
+ /// Select the newest verified recovery point whose completed manifest timestamp
178
+ /// does not exceed `at_ms`. Every manifest-bearing child is verified first so a
179
+ /// corrupt recent backup cannot cause a silent fallback to older data.
180
+ pub fn select_bundle_at(catalog: &Path, at_ms: u64) -> io::Result<PathBuf> {
181
+ let mut candidates = Vec::new();
182
+ for entry in fs::read_dir(catalog)? {
183
+ let path = entry?.path();
184
+ if !path.is_dir() || !path.join("manifest.json").is_file() {
185
+ continue;
186
+ }
187
+ let manifest = verify_bundle(&path)?;
188
+ if manifest.created_ms <= at_ms {
189
+ candidates.push((manifest.created_ms, path));
190
+ }
191
+ }
192
+ candidates
193
+ .into_iter()
194
+ .max_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1)))
195
+ .map(|(_, path)| path)
196
+ .ok_or_else(|| {
197
+ io::Error::new(
198
+ io::ErrorKind::NotFound,
199
+ format!("no verified recovery point exists at or before {at_ms}"),
200
+ )
201
+ })
202
+ }
203
+
204
+ pub fn restore_bundle(archive: &Path, output: &Path) -> io::Result<BackupManifest> {
205
+ let manifest = verify_bundle(archive)?;
206
+ if output.exists() {
207
+ return Err(io::Error::new(
208
+ io::ErrorKind::AlreadyExists,
209
+ "restore output already exists",
210
+ ));
211
+ }
212
+ let staging = output.with_extension("restore.partial");
213
+ if staging.exists() {
214
+ return Err(io::Error::new(
215
+ io::ErrorKind::AlreadyExists,
216
+ "an interrupted restore staging directory exists",
217
+ ));
218
+ }
219
+ fs::create_dir_all(&staging)?;
220
+ let result = (|| {
221
+ copy_tree(&archive.join("files"), &staging.join("files"))?;
222
+ write_synced(
223
+ &staging.join("manifest.json"),
224
+ &fs::read(archive.join("manifest.json"))?,
225
+ )?;
226
+ sync_directory(&staging)?;
227
+ fs::rename(&staging, output)?;
228
+ if let Some(parent) = output.parent() {
229
+ sync_directory(parent)?;
230
+ }
231
+ Ok(manifest.clone())
232
+ })();
233
+ if result.is_err() {
234
+ let _ = fs::remove_dir_all(&staging);
235
+ }
236
+ result
237
+ }
238
+
239
+ fn copy_source(
240
+ source: &Path,
241
+ destination: &Path,
242
+ relative: &str,
243
+ entries: &mut Vec<BackupEntry>,
244
+ ) -> io::Result<()> {
245
+ let metadata = fs::symlink_metadata(source)?;
246
+ if metadata.file_type().is_symlink() {
247
+ return Err(io::Error::new(
248
+ io::ErrorKind::InvalidData,
249
+ "backup sources may not contain symlinks",
250
+ ));
251
+ }
252
+ if metadata.is_dir() {
253
+ fs::create_dir_all(destination)?;
254
+ let mut children = fs::read_dir(source)?.collect::<Result<Vec<_>, _>>()?;
255
+ children.sort_by_key(|value| value.file_name());
256
+ for child in children {
257
+ copy_source(
258
+ &child.path(),
259
+ &destination.join(child.file_name()),
260
+ &format!("{relative}/{}", child.file_name().to_string_lossy()),
261
+ entries,
262
+ )?;
263
+ }
264
+ } else if metadata.is_file() {
265
+ if let Some(parent) = destination.parent() {
266
+ fs::create_dir_all(parent)?;
267
+ }
268
+ fs::copy(source, destination)?;
269
+ sync_file(destination)?;
270
+ entries.push(BackupEntry {
271
+ path: relative.into(),
272
+ bytes: metadata.len(),
273
+ sha256: hash_file(destination)?,
274
+ });
275
+ }
276
+ Ok(())
277
+ }
278
+ fn copy_tree(source: &Path, destination: &Path) -> io::Result<()> {
279
+ if !source.exists() {
280
+ return Ok(());
281
+ }
282
+ let mut ignored = Vec::new();
283
+ copy_source(source, destination, "files", &mut ignored)
284
+ }
285
+ fn validate_relative(value: &str) -> io::Result<()> {
286
+ let path = Path::new(value);
287
+ if value.is_empty()
288
+ || path.is_absolute()
289
+ || path.components().any(|part| {
290
+ matches!(
291
+ part,
292
+ std::path::Component::ParentDir
293
+ | std::path::Component::RootDir
294
+ | std::path::Component::Prefix(_)
295
+ )
296
+ })
297
+ {
298
+ return Err(io::Error::new(
299
+ io::ErrorKind::InvalidInput,
300
+ "backup path must be relative and traversal-free",
301
+ ));
302
+ }
303
+ Ok(())
304
+ }
305
+ fn hash_file(path: &Path) -> io::Result<String> {
306
+ let mut file = File::open(path)?;
307
+ let mut hash = Sha256::new();
308
+ let mut buffer = [0; 64 * 1024];
309
+ loop {
310
+ let count = file.read(&mut buffer)?;
311
+ if count == 0 {
312
+ break;
313
+ }
314
+ hash.update(&buffer[..count]);
315
+ }
316
+ Ok(format!("{:x}", hash.finalize()))
317
+ }
318
+ fn write_synced(path: &Path, value: &[u8]) -> io::Result<()> {
319
+ let mut file = File::create(path)?;
320
+ file.write_all(value)?;
321
+ file.sync_all()
322
+ }
323
+ fn sync_file(path: &Path) -> io::Result<()> {
324
+ File::open(path)?.sync_all()
325
+ }
326
+ fn sync_directory(path: &Path) -> io::Result<()> {
327
+ File::open(path)?.sync_all()
328
+ }
329
+
330
+ #[cfg(test)]
331
+ mod tests {
332
+ use super::*;
333
+ #[test]
334
+ fn backup_verifies_restores_and_rejects_corruption() {
335
+ let root = std::env::temp_dir().join(format!(
336
+ "feltdb-backup-{}-{}",
337
+ std::process::id(),
338
+ rand::random::<u64>()
339
+ ));
340
+ fs::create_dir_all(root.join("source/content")).unwrap();
341
+ fs::write(root.join("source/state.log"), b"canonical operations").unwrap();
342
+ fs::write(root.join("source/content/object"), b"content bytes").unwrap();
343
+ let archive = root.join("archive");
344
+ let artifacts = vec![
345
+ ("state.log".into(), root.join("source/state.log")),
346
+ ("content".into(), root.join("source/content")),
347
+ ];
348
+ let created = create_bundle(&archive, &artifacts).unwrap();
349
+ assert_eq!(created.entries.len(), 2);
350
+ assert_eq!(verify_bundle(&archive).unwrap().entries.len(), 2);
351
+ let restored = root.join("restored");
352
+ restore_bundle(&archive, &restored).unwrap();
353
+ assert_eq!(
354
+ fs::read(restored.join("files/state.log")).unwrap(),
355
+ b"canonical operations"
356
+ );
357
+ fs::write(archive.join("files/state.log"), b"corrupt").unwrap();
358
+ assert_eq!(
359
+ verify_bundle(&archive).unwrap_err().kind(),
360
+ io::ErrorKind::InvalidData
361
+ );
362
+ let _ = fs::remove_dir_all(root);
363
+ }
364
+ #[test]
365
+ fn partial_backups_are_detected_and_never_restored() {
366
+ let root = std::env::temp_dir().join(format!(
367
+ "feltdb-partial-{}-{}",
368
+ std::process::id(),
369
+ rand::random::<u64>()
370
+ ));
371
+ let archive = root.join("archive");
372
+ fs::create_dir_all(archive.with_extension("partial")).unwrap();
373
+ assert_eq!(
374
+ verify_bundle(&archive).unwrap_err().kind(),
375
+ io::ErrorKind::InvalidData
376
+ );
377
+ assert!(restore_bundle(&archive, &root.join("restore")).is_err());
378
+ assert!(!root.join("restore").exists());
379
+ let _ = fs::remove_dir_all(root);
380
+ }
381
+
382
+ #[test]
383
+ fn point_in_time_selection_uses_latest_verified_preceding_bundle() {
384
+ let root = std::env::temp_dir().join(format!(
385
+ "feltdb-point-in-time-{}-{}",
386
+ std::process::id(),
387
+ rand::random::<u64>()
388
+ ));
389
+ fs::create_dir_all(&root).unwrap();
390
+ let source = root.join("source.log");
391
+ fs::write(&source, b"first").unwrap();
392
+ let first = create_bundle(
393
+ &root.join("catalog/first"),
394
+ &[("state.log".into(), source.clone())],
395
+ )
396
+ .unwrap();
397
+ std::thread::sleep(std::time::Duration::from_millis(2));
398
+ fs::write(&source, b"second").unwrap();
399
+ let second = create_bundle(
400
+ &root.join("catalog/second"),
401
+ &[("state.log".into(), source)],
402
+ )
403
+ .unwrap();
404
+
405
+ assert_eq!(
406
+ select_bundle_at(&root.join("catalog"), first.created_ms).unwrap(),
407
+ root.join("catalog/first")
408
+ );
409
+ assert_eq!(
410
+ select_bundle_at(&root.join("catalog"), second.created_ms).unwrap(),
411
+ root.join("catalog/second")
412
+ );
413
+ assert!(select_bundle_at(&root.join("catalog"), first.created_ms - 1).is_err());
414
+ fs::remove_dir_all(root).unwrap();
415
+ }
416
+ }
@@ -0,0 +1,218 @@
1
+ use rand::{distributions::Alphanumeric, Rng};
2
+ use serde::{Deserialize, Serialize};
3
+ use serde_json::Value;
4
+ use std::{
5
+ collections::BTreeMap,
6
+ fs,
7
+ path::PathBuf,
8
+ sync::{Arc, RwLock},
9
+ time::{SystemTime, UNIX_EPOCH},
10
+ };
11
+ #[derive(Debug, Clone, Serialize, Deserialize)]
12
+ pub struct CausalEvent {
13
+ pub event_id: String,
14
+ pub causal_id: String,
15
+ pub parent_event_id: Option<String>,
16
+ pub timestamp: u64,
17
+ pub tenant_id: String,
18
+ pub application_id: String,
19
+ pub revision_id: Option<String>,
20
+ pub subject_id: String,
21
+ pub subject_type: String,
22
+ pub authorization_decision_id: Option<String>,
23
+ pub capability: Option<String>,
24
+ pub resource: Option<String>,
25
+ pub operation: String,
26
+ pub workload_id: Option<String>,
27
+ pub attempt_id: Option<String>,
28
+ pub worker_id: Option<String>,
29
+ pub connection_id: Option<String>,
30
+ pub execution_id: Option<String>,
31
+ pub state_version: Option<String>,
32
+ pub operation_ids: Vec<String>,
33
+ pub artifact_ids: Vec<String>,
34
+ pub result_id: Option<String>,
35
+ pub status: String,
36
+ pub error_code: Option<String>,
37
+ pub metadata: BTreeMap<String, Value>,
38
+ pub schema_version: u32,
39
+ }
40
+ #[derive(Debug, Clone, Serialize, Deserialize)]
41
+ pub struct CausalContext {
42
+ pub causal_id: String,
43
+ pub parent_event_id: Option<String>,
44
+ }
45
+ #[derive(Debug, Default, Serialize, Deserialize)]
46
+ struct Records {
47
+ events: Vec<CausalEvent>,
48
+ }
49
+ #[derive(Clone)]
50
+ pub struct CausalStore {
51
+ path: PathBuf,
52
+ records: Arc<RwLock<Records>>,
53
+ }
54
+ fn now() -> u64 {
55
+ SystemTime::now()
56
+ .duration_since(UNIX_EPOCH)
57
+ .unwrap_or_default()
58
+ .as_secs()
59
+ }
60
+ fn id(prefix: &str) -> String {
61
+ format!(
62
+ "{prefix}_{}",
63
+ rand::thread_rng()
64
+ .sample_iter(&Alphanumeric)
65
+ .take(18)
66
+ .map(char::from)
67
+ .collect::<String>()
68
+ )
69
+ }
70
+ impl CausalStore {
71
+ pub fn load(path: impl Into<PathBuf>) -> Result<Self, String> {
72
+ let path = path.into();
73
+ let records = if path.exists() {
74
+ serde_json::from_slice(&fs::read(&path).map_err(|e| e.to_string())?)
75
+ .map_err(|e| e.to_string())?
76
+ } else {
77
+ Records::default()
78
+ };
79
+ Ok(Self {
80
+ path,
81
+ records: Arc::new(RwLock::new(records)),
82
+ })
83
+ }
84
+ fn persist(&self, r: &Records) -> Result<(), String> {
85
+ if let Some(p) = self.path.parent() {
86
+ fs::create_dir_all(p).map_err(|e| e.to_string())?
87
+ }
88
+ let tmp = self.path.with_extension("tmp");
89
+ fs::write(
90
+ &tmp,
91
+ serde_json::to_vec_pretty(r).map_err(|e| e.to_string())?,
92
+ )
93
+ .map_err(|e| e.to_string())?;
94
+ fs::rename(tmp, &self.path).map_err(|e| e.to_string())
95
+ }
96
+ pub fn append(&self, mut event: CausalEvent) -> Result<CausalEvent, String> {
97
+ if event.causal_id.is_empty() {
98
+ event.causal_id = id("causal")
99
+ }
100
+ event.event_id = if event.event_id.is_empty() {
101
+ id("event")
102
+ } else {
103
+ event.event_id
104
+ };
105
+ event.timestamp = if event.timestamp == 0 {
106
+ now()
107
+ } else {
108
+ event.timestamp
109
+ };
110
+ event.schema_version = 1;
111
+ let mut r = self
112
+ .records
113
+ .write()
114
+ .map_err(|_| "causal store lock poisoned")?;
115
+ if r.events.iter().any(|e| e.event_id == event.event_id) {
116
+ return Ok(event);
117
+ }
118
+ r.events.push(event.clone());
119
+ self.persist(&r)?;
120
+ Ok(event)
121
+ }
122
+ pub fn list(&self, causal_id: &str) -> Vec<CausalEvent> {
123
+ self.records
124
+ .read()
125
+ .map(|r| {
126
+ r.events
127
+ .iter()
128
+ .filter(|e| e.causal_id == causal_id)
129
+ .cloned()
130
+ .collect()
131
+ })
132
+ .unwrap_or_default()
133
+ }
134
+ pub fn workload(&self, id: &str) -> Vec<CausalEvent> {
135
+ self.records
136
+ .read()
137
+ .map(|r| {
138
+ r.events
139
+ .iter()
140
+ .filter(|e| e.workload_id.as_deref() == Some(id))
141
+ .cloned()
142
+ .collect()
143
+ })
144
+ .unwrap_or_default()
145
+ }
146
+ pub fn execution(&self, id: &str) -> Vec<CausalEvent> {
147
+ self.records
148
+ .read()
149
+ .map(|r| {
150
+ r.events
151
+ .iter()
152
+ .filter(|e| e.execution_id.as_deref() == Some(id))
153
+ .cloned()
154
+ .collect()
155
+ })
156
+ .unwrap_or_default()
157
+ }
158
+ pub fn revision(&self, id: &str) -> Vec<CausalEvent> {
159
+ self.records
160
+ .read()
161
+ .map(|r| {
162
+ r.events
163
+ .iter()
164
+ .filter(|e| e.revision_id.as_deref() == Some(id))
165
+ .cloned()
166
+ .collect()
167
+ })
168
+ .unwrap_or_default()
169
+ }
170
+ pub fn all(&self) -> Vec<CausalEvent> {
171
+ self.records
172
+ .read()
173
+ .map(|r| r.events.clone())
174
+ .unwrap_or_default()
175
+ }
176
+ }
177
+ #[cfg(test)]
178
+ mod tests {
179
+ use super::*;
180
+ #[test]
181
+ fn causal_chain_is_durable_idempotent_and_queryable() {
182
+ let p = std::env::temp_dir().join(format!("causal-{}", now()));
183
+ let s = CausalStore::load(&p).unwrap();
184
+ let e = CausalEvent {
185
+ event_id: "e1".into(),
186
+ causal_id: "c1".into(),
187
+ parent_event_id: None,
188
+ timestamp: 0,
189
+ tenant_id: "t".into(),
190
+ application_id: "a".into(),
191
+ revision_id: Some("r".into()),
192
+ subject_id: "u".into(),
193
+ subject_type: "human".into(),
194
+ authorization_decision_id: None,
195
+ capability: None,
196
+ resource: None,
197
+ operation: "test".into(),
198
+ workload_id: Some("w".into()),
199
+ attempt_id: None,
200
+ worker_id: None,
201
+ connection_id: None,
202
+ execution_id: None,
203
+ state_version: None,
204
+ operation_ids: vec![],
205
+ artifact_ids: vec![],
206
+ result_id: None,
207
+ status: "succeeded".into(),
208
+ error_code: None,
209
+ metadata: BTreeMap::new(),
210
+ schema_version: 0,
211
+ };
212
+ s.append(e.clone()).unwrap();
213
+ s.append(e).unwrap();
214
+ assert_eq!(s.list("c1").len(), 1);
215
+ assert_eq!(s.workload("w").len(), 1);
216
+ let _ = fs::remove_file(p);
217
+ }
218
+ }