@feltdb/core 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 (164) hide show
  1. package/dist/cli/commands.js +5 -2
  2. package/dist/cli/index.js +1 -1
  3. package/dist/create/cli.js +16 -1
  4. package/dist/create/create.js +30 -16
  5. package/dist/create/docker-compose-generator.js +48 -9
  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/index.js +1 -1
  160. package/dist/studio/{components-q43NSTTH.js → components-bHTARBin.js} +160 -89
  161. package/dist/studio/index.js +31 -28
  162. package/dist/studio-app/assets/{index-DZpBoVMp.js → index-BqKquLuU.js} +8 -8
  163. package/dist/studio-app/index.html +1 -1
  164. package/package.json +1 -1
@@ -0,0 +1,111 @@
1
+ use sha2::{Digest, Sha256};
2
+ use std::{fs, io, path::PathBuf, sync::Arc};
3
+
4
+ #[derive(Clone)]
5
+ pub struct ContentStore {
6
+ root: Arc<PathBuf>,
7
+ }
8
+
9
+ impl ContentStore {
10
+ pub fn new(root: PathBuf) -> io::Result<Self> {
11
+ fs::create_dir_all(&root)?;
12
+ Ok(Self {
13
+ root: Arc::new(root),
14
+ })
15
+ }
16
+ pub fn hash(bytes: &[u8]) -> String {
17
+ Sha256::digest(bytes)
18
+ .iter()
19
+ .map(|byte| format!("{byte:02x}"))
20
+ .collect()
21
+ }
22
+ pub fn valid_hash(hash: &str) -> bool {
23
+ hash.len() == 64
24
+ && hash
25
+ .bytes()
26
+ .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
27
+ }
28
+ pub fn store(&self, bytes: &[u8]) -> io::Result<String> {
29
+ let hash = Self::hash(bytes);
30
+ self.store_verified(&hash, bytes)?;
31
+ Ok(hash)
32
+ }
33
+ pub fn store_verified(&self, hash: &str, bytes: &[u8]) -> io::Result<()> {
34
+ if !Self::valid_hash(hash) || Self::hash(bytes) != hash {
35
+ return Err(io::Error::new(
36
+ io::ErrorKind::InvalidData,
37
+ "content hash mismatch",
38
+ ));
39
+ }
40
+ let path = self.root.join(hash);
41
+ if path.exists() {
42
+ return Ok(());
43
+ }
44
+ let temporary = self
45
+ .root
46
+ .join(format!(".{hash}.tmp-{}", std::process::id()));
47
+ fs::write(&temporary, bytes)?;
48
+ match fs::rename(&temporary, &path) {
49
+ Ok(()) => Ok(()),
50
+ Err(_error) if path.exists() => {
51
+ let _ = fs::remove_file(temporary);
52
+ Ok(())
53
+ }
54
+ Err(error) => Err(error),
55
+ }
56
+ }
57
+ pub fn get(&self, hash: &str) -> io::Result<Option<Vec<u8>>> {
58
+ if !Self::valid_hash(hash) {
59
+ return Ok(None);
60
+ }
61
+ let path = self.root.join(hash);
62
+ match fs::read(path) {
63
+ Ok(bytes) if Self::hash(&bytes) == hash => Ok(Some(bytes)),
64
+ Ok(_) => Err(io::Error::new(
65
+ io::ErrorKind::InvalidData,
66
+ "stored content failed verification",
67
+ )),
68
+ Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
69
+ Err(error) => Err(error),
70
+ }
71
+ }
72
+ pub fn exists(&self, hash: &str) -> io::Result<bool> {
73
+ Ok(self.get(hash)?.is_some())
74
+ }
75
+ pub fn delete(&self, hash: &str) -> io::Result<bool> {
76
+ if !Self::valid_hash(hash) {
77
+ return Ok(false);
78
+ }
79
+ match fs::remove_file(self.root.join(hash)) {
80
+ Ok(()) => Ok(true),
81
+ Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
82
+ Err(error) => Err(error),
83
+ }
84
+ }
85
+ pub fn verify(&self, hash: &str) -> io::Result<bool> {
86
+ self.get(hash).map(|value| value.is_some())
87
+ }
88
+ }
89
+
90
+ #[cfg(test)]
91
+ mod tests {
92
+ use super::*;
93
+ #[test]
94
+ fn content_is_verified_deduplicated_and_durable() {
95
+ let root = std::env::temp_dir().join(format!("feltdb-content-{}", std::process::id()));
96
+ let _ = fs::remove_dir_all(&root);
97
+ let store = ContentStore::new(root.clone()).unwrap();
98
+ let hash = store.store(b"customer content").unwrap();
99
+ assert_eq!(store.store(b"customer content").unwrap(), hash);
100
+ assert_eq!(
101
+ ContentStore::new(root.clone())
102
+ .unwrap()
103
+ .get(&hash)
104
+ .unwrap()
105
+ .unwrap(),
106
+ b"customer content"
107
+ );
108
+ assert!(store.store_verified(&hash, b"tampered").is_err());
109
+ let _ = fs::remove_dir_all(root);
110
+ }
111
+ }
@@ -0,0 +1,250 @@
1
+ use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
2
+ use password_hash::{rand_core::OsRng, SaltString};
3
+ use rand::{distributions::Alphanumeric, Rng};
4
+ use serde::{Deserialize, Serialize};
5
+ use std::{
6
+ fs,
7
+ path::PathBuf,
8
+ sync::{Arc, RwLock},
9
+ time::{SystemTime, UNIX_EPOCH},
10
+ };
11
+
12
+ fn now() -> u64 {
13
+ SystemTime::now()
14
+ .duration_since(UNIX_EPOCH)
15
+ .unwrap_or_default()
16
+ .as_secs()
17
+ }
18
+ fn id(prefix: &str) -> String {
19
+ format!(
20
+ "{}_{}",
21
+ prefix,
22
+ rand::thread_rng()
23
+ .sample_iter(&Alphanumeric)
24
+ .take(18)
25
+ .map(char::from)
26
+ .collect::<String>()
27
+ )
28
+ }
29
+
30
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31
+ #[serde(rename_all = "snake_case")]
32
+ pub enum IdentityStatus {
33
+ Unverified,
34
+ Active,
35
+ Suspended,
36
+ Locked,
37
+ Revoked,
38
+ }
39
+ #[derive(Debug, Clone, Serialize, Deserialize)]
40
+ pub struct Identity {
41
+ pub identity_id: String,
42
+ pub subject_id: String,
43
+ pub provider: String,
44
+ pub provider_subject: String,
45
+ pub email: Option<String>,
46
+ pub display_name: Option<String>,
47
+ pub status: IdentityStatus,
48
+ pub created_at: u64,
49
+ pub metadata: serde_json::Value,
50
+ }
51
+ #[derive(Debug, Clone, Serialize, Deserialize)]
52
+ pub struct IdentitySession {
53
+ pub session_id: String,
54
+ pub identity_id: String,
55
+ pub subject_id: String,
56
+ pub issued_at: u64,
57
+ pub expires_at: u64,
58
+ pub last_seen_at: u64,
59
+ pub device: Option<String>,
60
+ pub provider: String,
61
+ pub status: String,
62
+ pub revocation_reason: Option<String>,
63
+ pub refresh_hash: Option<String>,
64
+ }
65
+ #[derive(Debug, Default, Serialize, Deserialize)]
66
+ struct State {
67
+ identities: Vec<Identity>,
68
+ sessions: Vec<IdentitySession>,
69
+ password_hashes: std::collections::HashMap<String, String>,
70
+ }
71
+ #[derive(Clone)]
72
+ pub struct IdentityStore {
73
+ path: PathBuf,
74
+ state: Arc<RwLock<State>>,
75
+ }
76
+
77
+ impl IdentityStore {
78
+ pub fn load(path: impl Into<PathBuf>) -> Result<Self, String> {
79
+ let path = path.into();
80
+ let state = if path.exists() {
81
+ serde_json::from_slice(&fs::read(&path).map_err(|e| e.to_string())?)
82
+ .map_err(|e| e.to_string())?
83
+ } else {
84
+ State::default()
85
+ };
86
+ Ok(Self {
87
+ path,
88
+ state: Arc::new(RwLock::new(state)),
89
+ })
90
+ }
91
+ fn persist(&self, s: &State) -> Result<(), String> {
92
+ if let Some(p) = self.path.parent() {
93
+ fs::create_dir_all(p).map_err(|e| e.to_string())?
94
+ }
95
+ fs::write(
96
+ &self.path,
97
+ serde_json::to_vec_pretty(s).map_err(|e| e.to_string())?,
98
+ )
99
+ .map_err(|e| e.to_string())
100
+ }
101
+ pub fn normalize(
102
+ &self,
103
+ provider: &str,
104
+ provider_subject: &str,
105
+ email: Option<String>,
106
+ display_name: Option<String>,
107
+ metadata: serde_json::Value,
108
+ ) -> Result<Identity, String> {
109
+ let mut s = self.state.write().map_err(|_| "identity lock poisoned")?;
110
+ if let Some(i) = s
111
+ .identities
112
+ .iter()
113
+ .find(|i| i.provider == provider && i.provider_subject == provider_subject)
114
+ {
115
+ return Ok(i.clone());
116
+ }
117
+ let x = Identity {
118
+ identity_id: id("identity"),
119
+ subject_id: id("subject"),
120
+ provider: provider.into(),
121
+ provider_subject: provider_subject.into(),
122
+ email,
123
+ display_name,
124
+ status: IdentityStatus::Unverified,
125
+ created_at: now(),
126
+ metadata,
127
+ };
128
+ s.identities.push(x.clone());
129
+ self.persist(&s)?;
130
+ Ok(x)
131
+ }
132
+ pub fn list(&self) -> Result<Vec<Identity>, String> {
133
+ Ok(self
134
+ .state
135
+ .read()
136
+ .map_err(|_| "identity lock poisoned")?
137
+ .identities
138
+ .clone())
139
+ }
140
+ pub fn sessions(&self, subject: &str) -> Result<Vec<IdentitySession>, String> {
141
+ Ok(self
142
+ .state
143
+ .read()
144
+ .map_err(|_| "identity lock poisoned")?
145
+ .sessions
146
+ .iter()
147
+ .filter(|x| x.subject_id == subject)
148
+ .cloned()
149
+ .collect())
150
+ }
151
+ pub fn issue_session(
152
+ &self,
153
+ identity: &Identity,
154
+ provider: &str,
155
+ device: Option<String>,
156
+ ttl: u64,
157
+ ) -> Result<IdentitySession, String> {
158
+ let mut s = self.state.write().map_err(|_| "identity lock poisoned")?;
159
+ let t = now();
160
+ let x = IdentitySession {
161
+ session_id: id("session"),
162
+ identity_id: identity.identity_id.clone(),
163
+ subject_id: identity.subject_id.clone(),
164
+ issued_at: t,
165
+ expires_at: t + ttl,
166
+ last_seen_at: t,
167
+ device,
168
+ provider: provider.into(),
169
+ status: "active".into(),
170
+ revocation_reason: None,
171
+ refresh_hash: Some(id("refresh")),
172
+ };
173
+ s.sessions.push(x.clone());
174
+ self.persist(&s)?;
175
+ Ok(x)
176
+ }
177
+ pub fn revoke_session(&self, subject: &str, session: &str, reason: &str) -> Result<(), String> {
178
+ let mut s = self.state.write().map_err(|_| "identity lock poisoned")?;
179
+ let x = s
180
+ .sessions
181
+ .iter_mut()
182
+ .find(|x| x.subject_id == subject && x.session_id == session)
183
+ .ok_or("session not found")?;
184
+ x.status = "revoked".into();
185
+ x.revocation_reason = Some(reason.into());
186
+ self.persist(&s)
187
+ }
188
+ pub fn revoke_all(&self, subject: &str) -> Result<usize, String> {
189
+ let mut s = self.state.write().map_err(|_| "identity lock poisoned")?;
190
+ let mut n = 0;
191
+ for x in s
192
+ .sessions
193
+ .iter_mut()
194
+ .filter(|x| x.subject_id == subject && x.status == "active")
195
+ {
196
+ x.status = "revoked".into();
197
+ x.revocation_reason = Some("logout_all".into());
198
+ n += 1
199
+ }
200
+ self.persist(&s)?;
201
+ Ok(n)
202
+ }
203
+ pub fn hash_password(password: &str) -> Result<String, String> {
204
+ let salt = SaltString::generate(&mut OsRng);
205
+ Argon2::default()
206
+ .hash_password(password.as_bytes(), &salt)
207
+ .map(|x| x.to_string())
208
+ .map_err(|e| e.to_string())
209
+ }
210
+ pub fn verify_password(hash: &str, password: &str) -> bool {
211
+ PasswordHash::new(hash)
212
+ .map(|x| {
213
+ Argon2::default()
214
+ .verify_password(password.as_bytes(), &x)
215
+ .is_ok()
216
+ })
217
+ .unwrap_or(false)
218
+ }
219
+ }
220
+
221
+ #[cfg(test)]
222
+ mod tests {
223
+ use super::*;
224
+ #[test]
225
+ fn provider_identity_is_normalized_and_persisted() {
226
+ let p = std::env::temp_dir().join(format!("identity-{}.json", now()));
227
+ let s = IdentityStore::load(&p).unwrap();
228
+ let a = s
229
+ .normalize(
230
+ "github",
231
+ "42",
232
+ Some("a@example.com".into()),
233
+ None,
234
+ serde_json::json!({}),
235
+ )
236
+ .unwrap();
237
+ let b = s
238
+ .normalize("github", "42", None, None, serde_json::json!({}))
239
+ .unwrap();
240
+ assert_eq!(a.identity_id, b.identity_id);
241
+ assert_eq!(s.list().unwrap().len(), 1);
242
+ }
243
+ #[test]
244
+ fn password_hash_never_equals_password() {
245
+ let h = IdentityStore::hash_password("correct horse").unwrap();
246
+ assert_ne!(h, "correct horse");
247
+ assert!(IdentityStore::verify_password(&h, "correct horse"));
248
+ assert!(!IdentityStore::verify_password(&h, "wrong"));
249
+ }
250
+ }
@@ -0,0 +1,78 @@
1
+ use axum::{
2
+ extract::{Path, State},
3
+ http::StatusCode,
4
+ Json,
5
+ };
6
+ use serde::{Deserialize, Serialize};
7
+ use std::time::{SystemTime, UNIX_EPOCH};
8
+
9
+ use crate::{auth::ApiKeySummary, app_state::AppState};
10
+
11
+ #[derive(Debug, Serialize)]
12
+ pub struct KeyListResponse {
13
+ pub tokens: Vec<ApiKeySummary>,
14
+ }
15
+
16
+ #[derive(Debug, Deserialize)]
17
+ pub struct CreateKeyRequest {
18
+ pub id: Option<String>,
19
+ pub namespace: String,
20
+ pub scopes: Vec<String>,
21
+ }
22
+
23
+ #[derive(Debug, Serialize)]
24
+ pub struct CreateKeyResponse {
25
+ pub secret: String,
26
+ pub id: String,
27
+ }
28
+
29
+ pub async fn list_keys(
30
+ State(state): State<AppState>,
31
+ ) -> Result<Json<KeyListResponse>, (StatusCode, String)> {
32
+ let tokens = state
33
+ .keys
34
+ .list()
35
+ .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
36
+ Ok(Json(KeyListResponse { tokens }))
37
+ }
38
+
39
+ pub async fn create_key(
40
+ State(state): State<AppState>,
41
+ Json(req): Json<CreateKeyRequest>,
42
+ ) -> Result<(StatusCode, Json<CreateKeyResponse>), (StatusCode, String)> {
43
+ let timestamp = SystemTime::now()
44
+ .duration_since(UNIX_EPOCH)
45
+ .unwrap_or_default()
46
+ .as_secs();
47
+
48
+ let id = req.id.unwrap_or_else(|| format!("key-{}", timestamp));
49
+ let secret = state
50
+ .keys
51
+ .create(
52
+ id.clone(),
53
+ req.scopes,
54
+ vec![req.namespace],
55
+ )
56
+ .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
57
+
58
+ Ok((
59
+ StatusCode::CREATED,
60
+ Json(CreateKeyResponse { secret, id }),
61
+ ))
62
+ }
63
+
64
+ pub async fn revoke_key(
65
+ State(state): State<AppState>,
66
+ Path(id): Path<String>,
67
+ ) -> Result<StatusCode, (StatusCode, String)> {
68
+ let found = state
69
+ .keys
70
+ .revoke(&id)
71
+ .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
72
+
73
+ if found {
74
+ Ok(StatusCode::NO_CONTENT)
75
+ } else {
76
+ Err((StatusCode::NOT_FOUND, "Key not found".to_string()))
77
+ }
78
+ }
@@ -0,0 +1,247 @@
1
+ use base64::{engine::general_purpose::STANDARD, Engine};
2
+ use rand::RngCore;
3
+ use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM};
4
+ use serde::{Deserialize, Serialize};
5
+ use sha2::{Digest, Sha256};
6
+ use std::{
7
+ collections::BTreeMap,
8
+ fs,
9
+ path::PathBuf,
10
+ sync::{Arc, RwLock},
11
+ };
12
+
13
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
14
+ pub struct KeyScope {
15
+ pub tenant_id: String,
16
+ pub application_id: Option<String>,
17
+ pub purpose: String,
18
+ }
19
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20
+ pub struct KeyReference {
21
+ pub provider: String,
22
+ pub key_id: String,
23
+ pub version: u64,
24
+ }
25
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26
+ pub struct WrappedKey {
27
+ pub nonce: String,
28
+ pub ciphertext: String,
29
+ pub algorithm: String,
30
+ }
31
+ #[derive(Debug, Clone, Serialize)]
32
+ pub struct KeyProviderHealth {
33
+ pub provider: String,
34
+ pub available: bool,
35
+ pub active_key: String,
36
+ pub rotation_due: bool,
37
+ }
38
+ pub trait KeyProvider: Send + Sync {
39
+ fn active_key(&self, scope: &KeyScope) -> Result<KeyReference, String>;
40
+ fn encrypt_dek(
41
+ &self,
42
+ scope: &KeyScope,
43
+ dek: &[u8],
44
+ ) -> Result<(KeyReference, WrappedKey), String>;
45
+ fn decrypt_dek(
46
+ &self,
47
+ key: &KeyReference,
48
+ wrapped: &WrappedKey,
49
+ scope: &KeyScope,
50
+ ) -> Result<Vec<u8>, String>;
51
+ fn rotate(&self, scope: &KeyScope) -> Result<KeyReference, String>;
52
+ fn health(&self, scope: &KeyScope) -> KeyProviderHealth;
53
+ }
54
+ #[derive(Serialize, Deserialize, Default)]
55
+ struct LocalKeys {
56
+ active: BTreeMap<String, u64>,
57
+ keys: BTreeMap<String, String>,
58
+ }
59
+ #[derive(Clone)]
60
+ pub struct LocalKeyProvider {
61
+ path: PathBuf,
62
+ root: Arc<[u8; 32]>,
63
+ data: Arc<RwLock<LocalKeys>>,
64
+ }
65
+ fn scope_id(s: &KeyScope) -> String {
66
+ format!(
67
+ "{}|{}|{}",
68
+ s.tenant_id,
69
+ s.application_id.as_deref().unwrap_or("*"),
70
+ s.purpose
71
+ )
72
+ }
73
+ fn scope_hash(s: &str) -> String {
74
+ Sha256::digest(s.as_bytes())[..6]
75
+ .iter()
76
+ .map(|byte| format!("{byte:02x}"))
77
+ .collect()
78
+ }
79
+ fn aad(s: &KeyScope, k: &KeyReference) -> Vec<u8> {
80
+ format!(
81
+ "feltdb-kek-v1|{}|{}|{}|{}",
82
+ scope_id(s),
83
+ k.provider,
84
+ k.key_id,
85
+ k.version
86
+ )
87
+ .into_bytes()
88
+ }
89
+ impl LocalKeyProvider {
90
+ pub fn load(path: impl Into<PathBuf>, root: [u8; 32]) -> Result<Self, String> {
91
+ let path = path.into();
92
+ let data = if path.exists() {
93
+ serde_json::from_slice(&fs::read(&path).map_err(|_| "key provider unavailable")?)
94
+ .map_err(|_| "key provider metadata invalid")?
95
+ } else {
96
+ LocalKeys::default()
97
+ };
98
+ Ok(Self {
99
+ path,
100
+ root: Arc::new(root),
101
+ data: Arc::new(RwLock::new(data)),
102
+ })
103
+ }
104
+ fn persist(&self, d: &LocalKeys) -> Result<(), String> {
105
+ if let Some(p) = self.path.parent() {
106
+ fs::create_dir_all(p).map_err(|_| "key provider unavailable")?;
107
+ }
108
+ let t = self.path.with_extension("tmp");
109
+ fs::write(
110
+ &t,
111
+ serde_json::to_vec_pretty(d).map_err(|_| "key provider metadata invalid")?,
112
+ )
113
+ .map_err(|_| "key provider unavailable")?;
114
+ fs::rename(t, &self.path).map_err(|_| "key provider unavailable".to_string())
115
+ }
116
+ fn material(&self, k: &KeyReference) -> Result<[u8; 32], String> {
117
+ let d = self.data.read().map_err(|_| "key provider unavailable")?;
118
+ let seed = STANDARD
119
+ .decode(d.keys.get(&k.key_id).ok_or("key unavailable or revoked")?)
120
+ .map_err(|_| "key provider metadata invalid")?;
121
+ Ok(Sha256::digest([self.root.as_slice(), seed.as_slice()].concat()).into())
122
+ }
123
+ fn ensure(&self, s: &KeyScope) -> Result<KeyReference, String> {
124
+ self.active_key(s).or_else(|_| self.rotate(s))
125
+ }
126
+ }
127
+ impl KeyProvider for LocalKeyProvider {
128
+ fn active_key(&self, s: &KeyScope) -> Result<KeyReference, String> {
129
+ let d = self.data.read().map_err(|_| "key provider unavailable")?;
130
+ let v = *d.active.get(&scope_id(s)).ok_or("no active key")?;
131
+ Ok(KeyReference {
132
+ provider: "local".into(),
133
+ key_id: format!("key-{}-{v}", scope_hash(&scope_id(s))),
134
+ version: v,
135
+ })
136
+ }
137
+ fn encrypt_dek(&self, s: &KeyScope, dek: &[u8]) -> Result<(KeyReference, WrappedKey), String> {
138
+ let kr = self.ensure(s)?;
139
+ let material = self.material(&kr)?;
140
+ let key = LessSafeKey::new(
141
+ UnboundKey::new(&AES_256_GCM, &material).map_err(|_| "key provider unavailable")?,
142
+ );
143
+ let mut n = [0; 12];
144
+ rand::thread_rng().fill_bytes(&mut n);
145
+ let mut v = dek.to_vec();
146
+ key.seal_in_place_append_tag(
147
+ Nonce::assume_unique_for_key(n),
148
+ Aad::from(aad(s, &kr)),
149
+ &mut v,
150
+ )
151
+ .map_err(|_| "DEK wrap failed")?;
152
+ Ok((
153
+ kr,
154
+ WrappedKey {
155
+ nonce: STANDARD.encode(n),
156
+ ciphertext: STANDARD.encode(v),
157
+ algorithm: "AES-256-GCM".into(),
158
+ },
159
+ ))
160
+ }
161
+ fn decrypt_dek(
162
+ &self,
163
+ kr: &KeyReference,
164
+ w: &WrappedKey,
165
+ s: &KeyScope,
166
+ ) -> Result<Vec<u8>, String> {
167
+ if kr.provider != "local" {
168
+ return Err("incompatible key provider".into());
169
+ }
170
+ let material = self.material(kr)?;
171
+ let key = LessSafeKey::new(
172
+ UnboundKey::new(&AES_256_GCM, &material).map_err(|_| "key provider unavailable")?,
173
+ );
174
+ let n: [u8; 12] = STANDARD
175
+ .decode(&w.nonce)
176
+ .map_err(|_| "wrapped key invalid")?
177
+ .try_into()
178
+ .map_err(|_| "wrapped key invalid")?;
179
+ let mut v = STANDARD
180
+ .decode(&w.ciphertext)
181
+ .map_err(|_| "wrapped key invalid")?;
182
+ let len = key
183
+ .open_in_place(
184
+ Nonce::assume_unique_for_key(n),
185
+ Aad::from(aad(s, kr)),
186
+ &mut v,
187
+ )
188
+ .map_err(|_| "DEK unwrap failed")?
189
+ .len();
190
+ v.truncate(len);
191
+ Ok(v)
192
+ }
193
+ fn rotate(&self, s: &KeyScope) -> Result<KeyReference, String> {
194
+ let mut d = self.data.write().map_err(|_| "key provider unavailable")?;
195
+ let sid = scope_id(s);
196
+ let v = d.active.get(&sid).copied().unwrap_or(0) + 1;
197
+ let kid = format!("key-{}-{v}", scope_hash(&sid));
198
+ let mut seed = [0; 32];
199
+ rand::thread_rng().fill_bytes(&mut seed);
200
+ d.keys.insert(kid.clone(), STANDARD.encode(seed));
201
+ d.active.insert(sid, v);
202
+ self.persist(&d)?;
203
+ Ok(KeyReference {
204
+ provider: "local".into(),
205
+ key_id: kid,
206
+ version: v,
207
+ })
208
+ }
209
+ fn health(&self, s: &KeyScope) -> KeyProviderHealth {
210
+ match self.active_key(s) {
211
+ Ok(k) => KeyProviderHealth {
212
+ provider: "local".into(),
213
+ available: true,
214
+ active_key: k.key_id,
215
+ rotation_due: false,
216
+ },
217
+ Err(_) => KeyProviderHealth {
218
+ provider: "local".into(),
219
+ available: false,
220
+ active_key: "uninitialized".into(),
221
+ rotation_due: false,
222
+ },
223
+ }
224
+ }
225
+ }
226
+ pub fn provider_from_environment(
227
+ path: PathBuf,
228
+ production: bool,
229
+ ) -> Result<Arc<dyn KeyProvider>, String> {
230
+ let kind = std::env::var("FELTDB_KEY_PROVIDER").unwrap_or_else(|_| "local".into());
231
+ if kind != "local" && kind != "env" {
232
+ return Err(format!("key provider '{kind}' is not installed"));
233
+ }
234
+ let material = std::env::var("FELTDB_MASTER_KEY")
235
+ .or_else(|_| std::env::var("FELTDB_SECRET_KEY"))
236
+ .map_err(|_| {
237
+ if production {
238
+ "production requires an explicit key provider and root key"
239
+ } else {
240
+ "FELTDB_MASTER_KEY is required; keys are never generated implicitly"
241
+ }
242
+ })?;
243
+ Ok(Arc::new(LocalKeyProvider::load(
244
+ path,
245
+ Sha256::digest(material.as_bytes()).into(),
246
+ )?))
247
+ }