@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,308 @@
1
+ use std::{
2
+ fs,
3
+ path::{Path, PathBuf},
4
+ sync::{Arc, RwLock},
5
+ };
6
+
7
+ use argon2::{
8
+ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
9
+ Argon2,
10
+ };
11
+ use rand::{distributions::Alphanumeric, Rng};
12
+ use serde::{Deserialize, Serialize};
13
+
14
+ #[derive(Debug, Deserialize)]
15
+ struct LegacyApiKeyStore {
16
+ tokens: Vec<LegacyApiKeyRecord>,
17
+ }
18
+
19
+ #[derive(Debug, Deserialize)]
20
+ struct LegacyApiKeyRecord {
21
+ id: String,
22
+ #[serde(default = "default_legacy_key_name")]
23
+ name: String,
24
+ secret: String,
25
+ namespace: String,
26
+ scopes: Vec<String>,
27
+ }
28
+
29
+ fn default_legacy_key_name() -> String {
30
+ "managed-instance".to_string()
31
+ }
32
+
33
+ #[derive(Debug, Clone, Serialize, Deserialize)]
34
+ pub struct ApiKeyRecord {
35
+ pub id: String,
36
+ pub name: String,
37
+ pub hash: String,
38
+ pub scopes: Vec<String>,
39
+ pub namespaces: Vec<String>,
40
+ #[serde(default)]
41
+ pub revoked: bool,
42
+ }
43
+
44
+ #[derive(Debug, Clone, Serialize)]
45
+ pub struct ApiKeySummary {
46
+ pub id: String,
47
+ pub name: String,
48
+ pub scopes: Vec<String>,
49
+ pub namespaces: Vec<String>,
50
+ pub revoked: bool,
51
+ }
52
+
53
+ #[derive(Debug, Clone)]
54
+ pub struct Principal {
55
+ pub key_id: String,
56
+ pub scopes: Vec<String>,
57
+ pub subject_type: String,
58
+ pub identity_id: Option<String>,
59
+ pub session_id: Option<String>,
60
+ }
61
+
62
+ impl Principal {
63
+ pub fn permits(&self, required: &str) -> bool {
64
+ self.scopes
65
+ .iter()
66
+ .any(|scope| scope == "*" || scope == required)
67
+ }
68
+ }
69
+
70
+ #[derive(Clone)]
71
+ pub struct KeyStore {
72
+ path: PathBuf,
73
+ records: Arc<RwLock<Vec<ApiKeyRecord>>>,
74
+ }
75
+
76
+ impl KeyStore {
77
+ pub fn load(path: impl Into<PathBuf>) -> Result<Self, String> {
78
+ let path = path.into();
79
+ let (records, migrated) = if path.exists() {
80
+ let bytes = fs::read(&path).map_err(|error| error.to_string())?;
81
+ match serde_json::from_slice::<Vec<ApiKeyRecord>>(&bytes) {
82
+ Ok(records) => (records, false),
83
+ Err(current_error) => {
84
+ let legacy: LegacyApiKeyStore = serde_json::from_slice(&bytes)
85
+ .map_err(|_| format!("invalid API key store: {current_error}"))?;
86
+ let mut records = Vec::with_capacity(legacy.tokens.len());
87
+ for token in legacy.tokens {
88
+ let salt = SaltString::generate(&mut OsRng);
89
+ let hash = Argon2::default()
90
+ .hash_password(token.secret.as_bytes(), &salt)
91
+ .map_err(|error| error.to_string())?
92
+ .to_string();
93
+ records.push(ApiKeyRecord {
94
+ id: token.id,
95
+ name: token.name,
96
+ hash,
97
+ scopes: token.scopes,
98
+ namespaces: vec![token.namespace],
99
+ revoked: false,
100
+ });
101
+ }
102
+ (records, true)
103
+ }
104
+ }
105
+ } else {
106
+ (Vec::new(), false)
107
+ };
108
+ let store = Self {
109
+ path,
110
+ records: Arc::new(RwLock::new(records)),
111
+ };
112
+ if migrated {
113
+ store.persist()?;
114
+ }
115
+ Ok(store)
116
+ }
117
+
118
+ pub fn authenticate(&self, token: &str, namespace: &str) -> Option<Principal> {
119
+ if !token.starts_with("fdb_live_") || token.len() > 256 {
120
+ return None;
121
+ }
122
+ let records = self.records.read().ok()?;
123
+ records
124
+ .iter()
125
+ .filter(|record| !record.revoked)
126
+ .find_map(|record| {
127
+ let namespace_allowed = record
128
+ .namespaces
129
+ .iter()
130
+ .any(|allowed| allowed == "*" || allowed == namespace);
131
+ let parsed = PasswordHash::new(&record.hash).ok()?;
132
+ if namespace_allowed
133
+ && Argon2::default()
134
+ .verify_password(token.as_bytes(), &parsed)
135
+ .is_ok()
136
+ {
137
+ Some(Principal {
138
+ key_id: record.id.clone(),
139
+ scopes: record.scopes.clone(),
140
+ subject_type: "service".into(),
141
+ identity_id: None,
142
+ session_id: None,
143
+ })
144
+ } else {
145
+ None
146
+ }
147
+ })
148
+ }
149
+
150
+ pub fn create(
151
+ &self,
152
+ name: String,
153
+ scopes: Vec<String>,
154
+ namespaces: Vec<String>,
155
+ ) -> Result<String, String> {
156
+ let secret: String = rand::thread_rng()
157
+ .sample_iter(&Alphanumeric)
158
+ .take(48)
159
+ .map(char::from)
160
+ .collect();
161
+ let token = format!("fdb_live_{secret}");
162
+ let salt = SaltString::generate(&mut OsRng);
163
+ let hash = Argon2::default()
164
+ .hash_password(token.as_bytes(), &salt)
165
+ .map_err(|error| error.to_string())?
166
+ .to_string();
167
+ let id: String = rand::thread_rng()
168
+ .sample_iter(&Alphanumeric)
169
+ .take(12)
170
+ .map(char::from)
171
+ .collect();
172
+ self.records
173
+ .write()
174
+ .map_err(|_| "API key store lock poisoned".to_string())?
175
+ .push(ApiKeyRecord {
176
+ id,
177
+ name,
178
+ hash,
179
+ scopes,
180
+ namespaces,
181
+ revoked: false,
182
+ });
183
+ self.persist()?;
184
+ Ok(token)
185
+ }
186
+
187
+ pub fn list(&self) -> Result<Vec<ApiKeySummary>, String> {
188
+ Ok(self
189
+ .records
190
+ .read()
191
+ .map_err(|_| "API key store lock poisoned".to_string())?
192
+ .iter()
193
+ .map(|record| ApiKeySummary {
194
+ id: record.id.clone(),
195
+ name: record.name.clone(),
196
+ scopes: record.scopes.clone(),
197
+ namespaces: record.namespaces.clone(),
198
+ revoked: record.revoked,
199
+ })
200
+ .collect())
201
+ }
202
+
203
+ pub fn revoke(&self, id: &str) -> Result<bool, String> {
204
+ let found = {
205
+ let mut records = self
206
+ .records
207
+ .write()
208
+ .map_err(|_| "API key store lock poisoned".to_string())?;
209
+ match records.iter_mut().find(|record| record.id == id) {
210
+ Some(record) => {
211
+ record.revoked = true;
212
+ true
213
+ }
214
+ None => false,
215
+ }
216
+ };
217
+ if found {
218
+ self.persist()?;
219
+ }
220
+ Ok(found)
221
+ }
222
+
223
+ fn persist(&self) -> Result<(), String> {
224
+ if let Some(parent) = self.path.parent() {
225
+ fs::create_dir_all(parent).map_err(|error| error.to_string())?;
226
+ }
227
+ let temporary = self.path.with_extension("tmp");
228
+ let records = self
229
+ .records
230
+ .read()
231
+ .map_err(|_| "API key store lock poisoned".to_string())?;
232
+ let bytes = serde_json::to_vec_pretty(&*records).map_err(|error| error.to_string())?;
233
+ fs::write(&temporary, bytes).map_err(|error| error.to_string())?;
234
+ fs::rename(&temporary, &self.path).map_err(|error| error.to_string())?;
235
+ Ok(())
236
+ }
237
+
238
+ pub fn path(&self) -> &Path {
239
+ &self.path
240
+ }
241
+ }
242
+
243
+ #[cfg(test)]
244
+ mod tests {
245
+ use super::*;
246
+ use std::time::{SystemTime, UNIX_EPOCH};
247
+
248
+ #[test]
249
+ fn keys_are_hashed_scoped_and_namespace_bound() {
250
+ let path = std::env::temp_dir().join(format!(
251
+ "feltdb-keys-{}.json",
252
+ SystemTime::now()
253
+ .duration_since(UNIX_EPOCH)
254
+ .unwrap()
255
+ .as_nanos()
256
+ ));
257
+ let store = KeyStore::load(&path).unwrap();
258
+ let token = store
259
+ .create(
260
+ "test".to_string(),
261
+ vec!["state:read".to_string()],
262
+ vec!["alpha".to_string()],
263
+ )
264
+ .unwrap();
265
+
266
+ let file = fs::read_to_string(&path).unwrap();
267
+ assert!(!file.contains(&token));
268
+ assert!(store
269
+ .authenticate(&token, "alpha")
270
+ .unwrap()
271
+ .permits("state:read"));
272
+ assert!(store.authenticate(&token, "beta").is_none());
273
+ assert!(store.authenticate("fdb_live_wrong", "alpha").is_none());
274
+ let id = store.list().unwrap().pop().unwrap().id;
275
+ assert!(store.revoke(&id).unwrap());
276
+ assert!(store.authenticate(&token, "alpha").is_none());
277
+ assert!(!store.revoke("missing").unwrap());
278
+ let _ = fs::remove_file(path);
279
+ }
280
+
281
+ #[test]
282
+ fn legacy_managed_key_store_is_migrated() {
283
+ let path = std::env::temp_dir().join(format!(
284
+ "feltdb-legacy-keys-{}.json",
285
+ SystemTime::now()
286
+ .duration_since(UNIX_EPOCH)
287
+ .unwrap()
288
+ .as_nanos()
289
+ ));
290
+ let token = "fdb_live_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV";
291
+ fs::write(
292
+ &path,
293
+ format!(
294
+ r#"{{"tokens":[{{"id":"default-token","secret":"{token}","namespace":"alpha","scopes":["state:read"],"created_at":"2026-01-01T00:00:00Z","expires_at":"2026-12-31T00:00:00Z"}}]}}"#
295
+ ),
296
+ )
297
+ .unwrap();
298
+
299
+ let store = KeyStore::load(&path).unwrap();
300
+ assert!(store.authenticate(token, "alpha").is_some());
301
+ assert!(store.authenticate(token, "beta").is_none());
302
+ let migrated = fs::read_to_string(&path).unwrap();
303
+ assert!(migrated.trim_start().starts_with('['));
304
+ assert!(!migrated.contains(token));
305
+ assert!(migrated.contains("managed-instance"));
306
+ let _ = fs::remove_file(path);
307
+ }
308
+ }
@@ -0,0 +1,228 @@
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ /// Every FeltDB surface resolves to one of these subjects before authorization.
4
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5
+ #[serde(tag = "kind", content = "id", rename_all = "snake_case")]
6
+ pub enum Subject {
7
+ User(String),
8
+ Service(String),
9
+ Workload(String),
10
+ Agent(String),
11
+ Connection(String),
12
+ Worker(String),
13
+ }
14
+
15
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16
+ pub struct Membership {
17
+ pub subject_id: String,
18
+ pub tenant_id: String,
19
+ pub application_id: Option<String>,
20
+ pub roles: Vec<String>,
21
+ #[serde(default)]
22
+ pub revoked: bool,
23
+ }
24
+
25
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26
+ pub struct RoleGrant {
27
+ pub role: String,
28
+ pub capabilities: Vec<String>,
29
+ }
30
+
31
+ #[derive(Debug, Clone, PartialEq, Eq)]
32
+ pub struct AuthorizationRequest<'a> {
33
+ pub subject: &'a Subject,
34
+ pub tenant_id: &'a str,
35
+ pub application_id: Option<&'a str>,
36
+ pub capability: &'a str,
37
+ /// Direct grants are used by machine credentials and narrowly delegated identities.
38
+ pub direct_grants: &'a [String],
39
+ pub memberships: &'a [Membership],
40
+ pub roles: &'a [RoleGrant],
41
+ }
42
+
43
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
44
+ pub struct AuthorizationDecision {
45
+ pub allowed: bool,
46
+ pub capability: String,
47
+ pub reason: &'static str,
48
+ }
49
+
50
+ impl Subject {
51
+ pub fn id(&self) -> &str {
52
+ match self {
53
+ Self::User(id)
54
+ | Self::Service(id)
55
+ | Self::Workload(id)
56
+ | Self::Agent(id)
57
+ | Self::Connection(id)
58
+ | Self::Worker(id) => id,
59
+ }
60
+ }
61
+
62
+ fn can_use_direct_grants(&self) -> bool {
63
+ !matches!(self, Self::User(_))
64
+ }
65
+ }
66
+
67
+ fn grants(capabilities: &[String], required: &str) -> bool {
68
+ capabilities
69
+ .iter()
70
+ .any(|grant| grant == "*" || grant == required)
71
+ }
72
+
73
+ /// Canonical FeltDB authorization decision.
74
+ ///
75
+ /// Human users must pass tenant and application membership before role grants are
76
+ /// evaluated. Non-human subjects use credentials or delegated capabilities that
77
+ /// are already bound to a tenant/application by their authenticator.
78
+ pub fn authorize(request: &AuthorizationRequest<'_>) -> AuthorizationDecision {
79
+ if request.subject.can_use_direct_grants() && grants(request.direct_grants, request.capability)
80
+ {
81
+ return AuthorizationDecision {
82
+ allowed: true,
83
+ capability: request.capability.into(),
84
+ reason: "direct capability grant",
85
+ };
86
+ }
87
+
88
+ let memberships: Vec<&Membership> = request
89
+ .memberships
90
+ .iter()
91
+ .filter(|membership| {
92
+ !membership.revoked
93
+ && membership.subject_id == request.subject.id()
94
+ && membership.tenant_id == request.tenant_id
95
+ && match (request.application_id, membership.application_id.as_deref()) {
96
+ (Some(required), Some(allowed)) => required == allowed,
97
+ (Some(_), None) => false,
98
+ (None, None) => true,
99
+ (None, Some(_)) => true,
100
+ }
101
+ })
102
+ .collect();
103
+
104
+ if memberships.is_empty() {
105
+ return AuthorizationDecision {
106
+ allowed: false,
107
+ capability: request.capability.into(),
108
+ reason: "no active membership for resource scope",
109
+ };
110
+ }
111
+
112
+ let allowed = memberships
113
+ .iter()
114
+ .flat_map(|membership| membership.roles.iter())
115
+ .any(|role| {
116
+ request
117
+ .roles
118
+ .iter()
119
+ .any(|grant| grant.role == *role && grants(&grant.capabilities, request.capability))
120
+ });
121
+ AuthorizationDecision {
122
+ allowed,
123
+ capability: request.capability.into(),
124
+ reason: if allowed {
125
+ "role grants capability"
126
+ } else {
127
+ "capability not granted"
128
+ },
129
+ }
130
+ }
131
+
132
+ #[cfg(test)]
133
+ mod tests {
134
+ use super::*;
135
+
136
+ fn roles() -> Vec<RoleGrant> {
137
+ vec![RoleGrant {
138
+ role: "operator".into(),
139
+ capabilities: vec!["state:read".into(), "workflows:run".into()],
140
+ }]
141
+ }
142
+
143
+ #[test]
144
+ fn user_requires_matching_application_membership_and_role() {
145
+ let subject = Subject::User("user_1".into());
146
+ let memberships = vec![Membership {
147
+ subject_id: "user_1".into(),
148
+ tenant_id: "tenant_a".into(),
149
+ application_id: Some("app_a".into()),
150
+ roles: vec!["operator".into()],
151
+ revoked: false,
152
+ }];
153
+ let role_grants = roles();
154
+ let allowed = authorize(&AuthorizationRequest {
155
+ subject: &subject,
156
+ tenant_id: "tenant_a",
157
+ application_id: Some("app_a"),
158
+ capability: "state:read",
159
+ direct_grants: &[],
160
+ memberships: &memberships,
161
+ roles: &role_grants,
162
+ });
163
+ assert!(allowed.allowed);
164
+ let cross_app = authorize(&AuthorizationRequest {
165
+ subject: &subject,
166
+ tenant_id: "tenant_a",
167
+ application_id: Some("app_b"),
168
+ capability: "state:read",
169
+ direct_grants: &[],
170
+ memberships: &memberships,
171
+ roles: &role_grants,
172
+ });
173
+ assert!(!cross_app.allowed);
174
+ }
175
+
176
+ #[test]
177
+ fn role_does_not_grant_unlisted_capability() {
178
+ let subject = Subject::User("user_1".into());
179
+ let memberships = vec![Membership {
180
+ subject_id: "user_1".into(),
181
+ tenant_id: "tenant_a".into(),
182
+ application_id: Some("app_a".into()),
183
+ roles: vec!["operator".into()],
184
+ revoked: false,
185
+ }];
186
+ let role_grants = roles();
187
+ let decision = authorize(&AuthorizationRequest {
188
+ subject: &subject,
189
+ tenant_id: "tenant_a",
190
+ application_id: Some("app_a"),
191
+ capability: "members:write",
192
+ direct_grants: &[],
193
+ memberships: &memberships,
194
+ roles: &role_grants,
195
+ });
196
+ assert!(!decision.allowed);
197
+ }
198
+
199
+ #[test]
200
+ fn machine_identity_uses_scoped_direct_grants() {
201
+ let subject = Subject::Workload("workload_7".into());
202
+ let grants = vec!["state:read".into()];
203
+ assert!(
204
+ authorize(&AuthorizationRequest {
205
+ subject: &subject,
206
+ tenant_id: "tenant_a",
207
+ application_id: Some("app_a"),
208
+ capability: "state:read",
209
+ direct_grants: &grants,
210
+ memberships: &[],
211
+ roles: &[]
212
+ })
213
+ .allowed
214
+ );
215
+ assert!(
216
+ !authorize(&AuthorizationRequest {
217
+ subject: &subject,
218
+ tenant_id: "tenant_a",
219
+ application_id: Some("app_a"),
220
+ capability: "state:write",
221
+ direct_grants: &grants,
222
+ memberships: &[],
223
+ roles: &[]
224
+ })
225
+ .allowed
226
+ );
227
+ }
228
+ }