create-feltdb 0.4.11 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/cli.js +16 -1
  2. package/dist/create.js +30 -16
  3. package/dist/docker-compose-generator.js +48 -9
  4. package/dist/package-versions.js +1 -1
  5. package/dist/server-source/Cargo.lock +2238 -0
  6. package/dist/server-source/Cargo.toml +7 -0
  7. package/dist/server-source/crates/feltdb/Cargo.lock +175 -0
  8. package/dist/server-source/crates/feltdb/Cargo.toml +79 -0
  9. package/dist/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
  10. package/dist/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
  11. package/dist/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
  12. package/dist/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
  13. package/dist/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
  14. package/dist/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
  15. package/dist/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
  16. package/dist/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
  17. package/dist/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
  18. package/dist/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
  19. package/dist/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
  20. package/dist/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
  21. package/dist/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
  22. package/dist/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
  23. package/dist/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
  24. package/dist/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
  25. package/dist/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
  26. package/dist/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
  27. package/dist/server-source/crates/feltdb/src/acquisition.rs +286 -0
  28. package/dist/server-source/crates/feltdb/src/admission.rs +192 -0
  29. package/dist/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
  30. package/dist/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
  31. package/dist/server-source/crates/feltdb/src/analytics.rs +475 -0
  32. package/dist/server-source/crates/feltdb/src/application.rs +2244 -0
  33. package/dist/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
  34. package/dist/server-source/crates/feltdb/src/authorization.rs +1030 -0
  35. package/dist/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
  36. package/dist/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
  37. package/dist/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
  38. package/dist/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
  39. package/dist/server-source/crates/feltdb/src/capability.rs +903 -0
  40. package/dist/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
  41. package/dist/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
  42. package/dist/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
  43. package/dist/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
  44. package/dist/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
  45. package/dist/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
  46. package/dist/server-source/crates/feltdb/src/content_distribution.rs +465 -0
  47. package/dist/server-source/crates/feltdb/src/convergence.rs +618 -0
  48. package/dist/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
  49. package/dist/server-source/crates/feltdb/src/crash_injection.rs +380 -0
  50. package/dist/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
  51. package/dist/server-source/crates/feltdb/src/cron.rs +294 -0
  52. package/dist/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
  53. package/dist/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
  54. package/dist/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
  55. package/dist/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
  56. package/dist/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
  57. package/dist/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
  58. package/dist/server-source/crates/feltdb/src/durable_sync.rs +316 -0
  59. package/dist/server-source/crates/feltdb/src/execution.rs +426 -0
  60. package/dist/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
  61. package/dist/server-source/crates/feltdb/src/indexing.rs +779 -0
  62. package/dist/server-source/crates/feltdb/src/lib.rs +2838 -0
  63. package/dist/server-source/crates/feltdb/src/materialization.rs +184 -0
  64. package/dist/server-source/crates/feltdb/src/metrics.rs +267 -0
  65. package/dist/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
  66. package/dist/server-source/crates/feltdb/src/observability.rs +366 -0
  67. package/dist/server-source/crates/feltdb/src/operation.rs +251 -0
  68. package/dist/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
  69. package/dist/server-source/crates/feltdb/src/operation_log.rs +344 -0
  70. package/dist/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
  71. package/dist/server-source/crates/feltdb/src/peer_registry.rs +166 -0
  72. package/dist/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
  73. package/dist/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
  74. package/dist/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
  75. package/dist/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
  76. package/dist/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
  77. package/dist/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
  78. package/dist/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
  79. package/dist/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
  80. package/dist/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
  81. package/dist/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
  82. package/dist/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
  83. package/dist/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
  84. package/dist/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
  85. package/dist/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
  86. package/dist/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
  87. package/dist/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
  88. package/dist/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
  89. package/dist/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
  90. package/dist/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
  91. package/dist/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
  92. package/dist/server-source/crates/feltdb/src/production_api.rs +400 -0
  93. package/dist/server-source/crates/feltdb/src/provenance.rs +207 -0
  94. package/dist/server-source/crates/feltdb/src/query_performance.rs +217 -0
  95. package/dist/server-source/crates/feltdb/src/references.rs +404 -0
  96. package/dist/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
  97. package/dist/server-source/crates/feltdb/src/replication_manager.rs +160 -0
  98. package/dist/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
  99. package/dist/server-source/crates/feltdb/src/routing.rs +409 -0
  100. package/dist/server-source/crates/feltdb/src/sharding.rs +523 -0
  101. package/dist/server-source/crates/feltdb/src/state_contract.rs +3118 -0
  102. package/dist/server-source/crates/feltdb/src/state_hash.rs +291 -0
  103. package/dist/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
  104. package/dist/server-source/crates/feltdb/src/storage.rs +267 -0
  105. package/dist/server-source/crates/feltdb/src/submission.rs +477 -0
  106. package/dist/server-source/crates/feltdb/src/sync.rs +483 -0
  107. package/dist/server-source/crates/feltdb/src/sync_contract.rs +822 -0
  108. package/dist/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
  109. package/dist/server-source/crates/feltdb/src/transaction_api.rs +473 -0
  110. package/dist/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
  111. package/dist/server-source/crates/feltdb/src/transactions.rs +646 -0
  112. package/dist/server-source/crates/feltdb/src/trigger.rs +390 -0
  113. package/dist/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
  114. package/dist/server-source/crates/feltdb/src/workflow.rs +713 -0
  115. package/dist/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
  116. package/dist/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
  117. package/dist/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
  118. package/dist/server-source/crates/feltdb/src/workload.rs +1310 -0
  119. package/dist/server-source/crates/feltdb-server/Cargo.toml +23 -0
  120. package/dist/server-source/crates/feltdb-server/README.md +79 -0
  121. package/dist/server-source/crates/feltdb-server/src/app_state.rs +73 -0
  122. package/dist/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
  123. package/dist/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
  124. package/dist/server-source/crates/feltdb-server/src/audit.rs +59 -0
  125. package/dist/server-source/crates/feltdb-server/src/auth.rs +308 -0
  126. package/dist/server-source/crates/feltdb-server/src/authorization.rs +228 -0
  127. package/dist/server-source/crates/feltdb-server/src/backup.rs +416 -0
  128. package/dist/server-source/crates/feltdb-server/src/causal.rs +218 -0
  129. package/dist/server-source/crates/feltdb-server/src/certification.rs +223 -0
  130. package/dist/server-source/crates/feltdb-server/src/clock.rs +132 -0
  131. package/dist/server-source/crates/feltdb-server/src/cluster.rs +165 -0
  132. package/dist/server-source/crates/feltdb-server/src/connections.rs +1044 -0
  133. package/dist/server-source/crates/feltdb-server/src/content.rs +111 -0
  134. package/dist/server-source/crates/feltdb-server/src/identity.rs +250 -0
  135. package/dist/server-source/crates/feltdb-server/src/key_management.rs +78 -0
  136. package/dist/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
  137. package/dist/server-source/crates/feltdb-server/src/leases.rs +123 -0
  138. package/dist/server-source/crates/feltdb-server/src/lib.rs +25 -0
  139. package/dist/server-source/crates/feltdb-server/src/main.rs +8715 -0
  140. package/dist/server-source/crates/feltdb-server/src/metrics.rs +97 -0
  141. package/dist/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
  142. package/dist/server-source/crates/feltdb-server/src/principals.rs +510 -0
  143. package/dist/server-source/crates/feltdb-server/src/providers.rs +269 -0
  144. package/dist/server-source/crates/feltdb-server/src/releases.rs +2563 -0
  145. package/dist/server-source/crates/feltdb-server/src/sessions.rs +156 -0
  146. package/dist/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
  147. package/dist/server-source/crates/feltdb-server/src/versions.rs +264 -0
  148. package/dist/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
  149. package/dist/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
  150. package/dist/server-source/crates/feltdb-wasm/test.db +0 -0
  151. package/package.json +3 -3
@@ -0,0 +1,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
+ }