@feltdb/core 0.8.0 → 0.8.2

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 (85) hide show
  1. package/dist/application-development.d.ts +7 -2
  2. package/dist/application-development.d.ts.map +1 -1
  3. package/dist/application-development.js +7 -13
  4. package/dist/application-manifest.d.ts +1 -0
  5. package/dist/application-manifest.d.ts.map +1 -1
  6. package/dist/application-manifest.js +1 -1
  7. package/dist/application-understanding.d.ts +76 -0
  8. package/dist/application-understanding.d.ts.map +1 -0
  9. package/dist/application-understanding.js +282 -0
  10. package/dist/cli/ai-model-config.js +3 -3
  11. package/dist/cli/commands.js +131 -38
  12. package/dist/cli/proposal-client.js +5 -1
  13. package/dist/cli/publish-engine.js +1 -1
  14. package/dist/cli/repository-evidence.js +55 -0
  15. package/dist/cli/source-sync.js +190 -75
  16. package/dist/convergence.d.ts +55 -0
  17. package/dist/convergence.d.ts.map +1 -0
  18. package/dist/convergence.js +63 -0
  19. package/dist/create/managed-account.js +1 -0
  20. package/dist/create/package-versions.js +1 -1
  21. package/dist/create/server-source/crates/feltdb/src/application.rs +23 -0
  22. package/dist/create/server-source/crates/feltdb/src/authority.rs +349 -0
  23. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +32 -0
  24. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +19 -0
  25. package/dist/create/server-source/crates/feltdb/src/lib.rs +444 -189
  26. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +244 -51
  27. package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +297 -0
  28. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +41 -11
  29. package/dist/create/server-source/crates/feltdb-server/src/connections.rs +14 -0
  30. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +8 -3
  31. package/dist/create/server-source/crates/feltdb-server/src/main.rs +1262 -173
  32. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +142 -1
  33. package/dist/create/template/default-project/src/context/AuthContext.tsx +9 -1
  34. package/dist/create/template/default-project/src/pages/SignUp.tsx +1 -1
  35. package/dist/flowspec.d.ts.map +1 -1
  36. package/dist/flowspec.js +6 -1
  37. package/dist/http-client.d.ts +32 -1
  38. package/dist/http-client.d.ts.map +1 -1
  39. package/dist/http-client.js +14 -2
  40. package/dist/http-db.d.ts +6 -0
  41. package/dist/http-db.d.ts.map +1 -1
  42. package/dist/http-db.js +30 -4
  43. package/dist/index-core.d.ts +3 -0
  44. package/dist/index-core.d.ts.map +1 -1
  45. package/dist/index-core.js +3 -0
  46. package/dist/intent.d.ts +98 -0
  47. package/dist/intent.d.ts.map +1 -0
  48. package/dist/intent.js +343 -0
  49. package/dist/module-intent.d.ts +19 -5
  50. package/dist/module-intent.d.ts.map +1 -1
  51. package/dist/module-intent.js +211 -62
  52. package/dist/module.d.ts +41 -12
  53. package/dist/module.d.ts.map +1 -1
  54. package/dist/module.js +543 -38
  55. package/dist/proposal.d.ts +13 -1
  56. package/dist/proposal.d.ts.map +1 -1
  57. package/dist/proposal.js +21 -4
  58. package/dist/state-contract.d.ts +4 -1
  59. package/dist/state-contract.d.ts.map +1 -1
  60. package/dist/state-contract.js +1 -1
  61. package/dist/studio/components/AskFeltDB.d.ts.map +1 -1
  62. package/dist/studio/components/ProposalBrowser.d.ts +1 -1
  63. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -1
  64. package/dist/studio/components/index.js +1 -1
  65. package/dist/studio/{components-CNSNrmA9.js → components-Duq2xQWt.js} +251 -193
  66. package/dist/studio/index.js +1 -1
  67. package/dist/studio/studio.css +1 -1
  68. package/dist/studio/utils/proposal-api.d.ts +4 -1
  69. package/dist/studio/utils/proposal-api.d.ts.map +1 -1
  70. package/dist/studio/utils/proposal-api.js +3 -0
  71. package/dist/studio-app/assets/{feltdb_wasm-CSB6KVgw.js → feltdb_wasm-DB8cX151.js} +1 -1
  72. package/dist/studio-app/assets/feltdb_wasm_bg-ClhDHp0S.wasm +0 -0
  73. package/dist/studio-app/assets/index-B0k4UAlI.js +29 -0
  74. package/dist/studio-app/assets/{index-yazaWMUN.css → index-Cd_NPw14.css} +1 -1
  75. package/dist/studio-app/index.html +2 -2
  76. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  77. package/dist/workspace/local-development-authority.d.ts +2 -0
  78. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  79. package/dist/workspace/local-development-authority.js +65 -2
  80. package/package.json +1 -1
  81. package/dist/studio-app/assets/dist-CCOk39Uc.js +0 -1
  82. package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
  83. package/dist/studio-app/assets/index-Bncji3aN.js +0 -43
  84. package/dist/studio-app/assets/lib-B2_7fcn7.js +0 -69
  85. package/dist/studio-app/assets/worker-CARZ5g7K.js +0 -69
@@ -0,0 +1,297 @@
1
+ #![cfg(unix)]
2
+
3
+ use feltdb::{
4
+ authority::{
5
+ AuthorityClient, AuthorityTransaction, EmbeddedAuthority, FeltDbAuthority, ReadKind,
6
+ },
7
+ AtomicMutation,
8
+ };
9
+ use serde_json::json;
10
+ use std::{
11
+ process::{Child, Command},
12
+ time::Duration,
13
+ };
14
+ use tempfile::TempDir;
15
+
16
+ static PROCESS_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
17
+
18
+ struct RunningAuthority {
19
+ child: Child,
20
+ socket: std::path::PathBuf,
21
+ storage: std::path::PathBuf,
22
+ }
23
+
24
+ impl Drop for RunningAuthority {
25
+ fn drop(&mut self) {
26
+ let _ = self.child.kill();
27
+ let _ = self.child.wait();
28
+ }
29
+ }
30
+
31
+ fn start(directory: &TempDir) -> RunningAuthority {
32
+ let socket = directory.path().join("authority.sock");
33
+ let storage = directory.path().join("authority.db");
34
+ let child = Command::new(env!("CARGO_BIN_EXE_feltdb-authority"))
35
+ .arg(&socket)
36
+ .arg(&storage)
37
+ .spawn()
38
+ .expect("authority starts");
39
+ for _ in 0..200 {
40
+ if socket.exists() {
41
+ return RunningAuthority {
42
+ child,
43
+ socket,
44
+ storage,
45
+ };
46
+ }
47
+ std::thread::sleep(Duration::from_millis(5));
48
+ }
49
+ panic!("authority socket was not ready");
50
+ }
51
+
52
+ fn writer(socket: &std::path::Path, id: usize) -> Child {
53
+ Command::new(env!("CARGO_BIN_EXE_feltdb-authority-client"))
54
+ .arg(socket)
55
+ .arg(id.to_string())
56
+ .spawn()
57
+ .expect("writer starts")
58
+ }
59
+
60
+ #[tokio::test(flavor = "current_thread")]
61
+ async fn seed_34002_ten_process_writers_preserve_ten_records() {
62
+ let _process_test = PROCESS_TESTS
63
+ .lock()
64
+ .unwrap_or_else(|error| error.into_inner());
65
+ let directory = TempDir::new().unwrap();
66
+ let authority = start(&directory);
67
+ let mut writers: Vec<_> = (0..10).map(|id| writer(&authority.socket, id)).collect();
68
+ for writer in &mut writers {
69
+ assert!(writer.wait().unwrap().success());
70
+ }
71
+ let read = AuthorityClient::connect(&authority.socket)
72
+ .read_authoritative()
73
+ .await
74
+ .unwrap();
75
+ assert_eq!(read.authoritative_revision, 10);
76
+ assert_eq!(
77
+ read.rows
78
+ .iter()
79
+ .filter(|row| row.capability == "registrations")
80
+ .count(),
81
+ 10
82
+ );
83
+ assert!(!directory.path().join("authority.tmp").exists());
84
+ }
85
+
86
+ #[tokio::test(flavor = "current_thread")]
87
+ async fn same_revision_is_coherent_across_disposable_clients() {
88
+ let _process_test = PROCESS_TESTS
89
+ .lock()
90
+ .unwrap_or_else(|error| error.into_inner());
91
+ let directory = TempDir::new().unwrap();
92
+ let authority = start(&directory);
93
+ let mut writers: Vec<_> = (0..10).map(|id| writer(&authority.socket, id)).collect();
94
+ for writer in &mut writers {
95
+ assert!(writer.wait().unwrap().success());
96
+ }
97
+ let first = AuthorityClient::connect(&authority.socket)
98
+ .read_authoritative()
99
+ .await
100
+ .unwrap();
101
+ let second = AuthorityClient::connect(&authority.socket)
102
+ .read_authoritative()
103
+ .await
104
+ .unwrap();
105
+ assert_eq!(first.authoritative_revision, second.authoritative_revision);
106
+ assert_eq!(first.content_hash, second.content_hash);
107
+ assert_eq!(first.rows.len(), second.rows.len());
108
+ }
109
+
110
+ #[tokio::test(flavor = "current_thread")]
111
+ async fn authoritative_reads_during_concurrent_writes_are_committed_states() {
112
+ let _process_test = PROCESS_TESTS
113
+ .lock()
114
+ .unwrap_or_else(|error| error.into_inner());
115
+ let directory = TempDir::new().unwrap();
116
+ let authority = start(&directory);
117
+ let mut writers: Vec<_> = (0..10).map(|id| writer(&authority.socket, id)).collect();
118
+ let client = AuthorityClient::connect(&authority.socket);
119
+ let mut last_revision = 0;
120
+ while writers
121
+ .iter_mut()
122
+ .any(|writer| writer.try_wait().unwrap().is_none())
123
+ {
124
+ let read = client.read_authoritative().await.unwrap();
125
+ assert!(read.authoritative_revision >= last_revision);
126
+ assert_eq!(read.observed_revision, read.authoritative_revision);
127
+ assert_eq!(read.rows.len() as u64, read.authoritative_revision);
128
+ last_revision = read.authoritative_revision;
129
+ }
130
+ for writer in &mut writers {
131
+ if writer.try_wait().unwrap().is_none() {
132
+ assert!(writer.wait().unwrap().success());
133
+ }
134
+ }
135
+ assert_eq!(client.read_authoritative().await.unwrap().rows.len(), 10);
136
+ }
137
+
138
+ #[tokio::test(flavor = "current_thread")]
139
+ async fn recorded_deterministic_stress_seeds_preserve_every_commit() {
140
+ let _process_test = PROCESS_TESTS
141
+ .lock()
142
+ .unwrap_or_else(|error| error.into_inner());
143
+ for seed in [34002_u64, 1, 7, 42, 34003] {
144
+ let directory = TempDir::new().unwrap();
145
+ let authority = start(&directory);
146
+ let mut order: Vec<usize> = (0..10).collect();
147
+ order.rotate_left((seed as usize) % 10);
148
+ if seed & 1 == 1 {
149
+ order.reverse();
150
+ }
151
+ let mut writers: Vec<_> = order
152
+ .into_iter()
153
+ .map(|id| writer(&authority.socket, id))
154
+ .collect();
155
+ for writer in &mut writers {
156
+ assert!(writer.wait().unwrap().success(), "seed {seed}");
157
+ }
158
+ let read = AuthorityClient::connect(&authority.socket)
159
+ .read_authoritative()
160
+ .await
161
+ .unwrap();
162
+ assert_eq!(read.rows.len(), 10, "seed {seed}");
163
+ assert_eq!(read.authoritative_revision, 10, "seed {seed}");
164
+ }
165
+ }
166
+
167
+ #[tokio::test(flavor = "current_thread")]
168
+ async fn stale_snapshot_is_explicit_and_stale_commit_is_rejected() {
169
+ let _process_test = PROCESS_TESTS
170
+ .lock()
171
+ .unwrap_or_else(|error| error.into_inner());
172
+ let directory = TempDir::new().unwrap();
173
+ let embedded = EmbeddedAuthority::open(directory.path().join("embedded.db")).unwrap();
174
+ let snapshot = embedded.snapshot().unwrap();
175
+ assert_eq!(snapshot.kind, ReadKind::Snapshot);
176
+ let commit = |id: &str, base_revision| AuthorityTransaction {
177
+ transaction_id: id.into(),
178
+ base_revision,
179
+ preconditions: vec![],
180
+ record_preconditions: vec![],
181
+ mutations: vec![AtomicMutation {
182
+ capability: "cells".into(),
183
+ key: format!("cells:{id}"),
184
+ value: Some(json!({"id": id})),
185
+ }],
186
+ audit: None,
187
+ };
188
+ let first = embedded
189
+ .commit(commit("first", Some(snapshot.observed_revision)))
190
+ .await
191
+ .unwrap();
192
+ assert_eq!(first.commit_revision, snapshot.observed_revision + 1);
193
+ let duplicate = embedded
194
+ .commit(commit("first", Some(snapshot.observed_revision)))
195
+ .await
196
+ .unwrap();
197
+ assert!(duplicate.duplicate);
198
+ assert_eq!(duplicate.base_revision, first.base_revision);
199
+ assert_eq!(duplicate.commit_revision, first.commit_revision);
200
+ let error = embedded
201
+ .commit(commit("stale", Some(snapshot.observed_revision)))
202
+ .await
203
+ .unwrap_err();
204
+ assert_eq!(error.code, "REVISION_CONFLICT");
205
+ assert_eq!(error.authoritative_revision, Some(first.commit_revision));
206
+ }
207
+
208
+ #[tokio::test(flavor = "current_thread")]
209
+ async fn authority_restart_recovers_revision_and_continues_monotonically() {
210
+ let _process_test = PROCESS_TESTS
211
+ .lock()
212
+ .unwrap_or_else(|error| error.into_inner());
213
+ let directory = TempDir::new().unwrap();
214
+ let mut authority = start(&directory);
215
+ let mut writers: Vec<_> = (0..3).map(|id| writer(&authority.socket, id)).collect();
216
+ for writer in &mut writers {
217
+ assert!(writer.wait().unwrap().success());
218
+ }
219
+ authority.child.kill().unwrap();
220
+ authority.child.wait().unwrap();
221
+ let _ = std::fs::remove_file(&authority.socket);
222
+ let restarted = start(&directory);
223
+ let client = AuthorityClient::connect(&restarted.socket);
224
+ assert_eq!(client.current_revision().await.unwrap(), 3);
225
+ let mut next = writer(&restarted.socket, 4);
226
+ assert!(next.wait().unwrap().success());
227
+ assert_eq!(client.current_revision().await.unwrap(), 4);
228
+ assert_eq!(client.read_authoritative().await.unwrap().rows.len(), 4);
229
+ }
230
+
231
+ #[tokio::test(flavor = "current_thread")]
232
+ async fn torn_commit_tail_recovers_only_the_previous_committed_revision() {
233
+ use std::io::Write;
234
+ let _process_test = PROCESS_TESTS
235
+ .lock()
236
+ .unwrap_or_else(|error| error.into_inner());
237
+ let directory = TempDir::new().unwrap();
238
+ let mut authority = start(&directory);
239
+ let mut first = writer(&authority.socket, 1);
240
+ assert!(first.wait().unwrap().success());
241
+ authority.child.kill().unwrap();
242
+ authority.child.wait().unwrap();
243
+ let mut log = std::fs::OpenOptions::new()
244
+ .append(true)
245
+ .open(&authority.storage)
246
+ .unwrap();
247
+ log.write_all(br#"{"record_type":"feltdb.transaction.v1","transaction_id":"torn""#)
248
+ .unwrap();
249
+ log.sync_all().unwrap();
250
+ let _ = std::fs::remove_file(&authority.socket);
251
+ let recovered = start(&directory);
252
+ let client = AuthorityClient::connect(&recovered.socket);
253
+ let read = client.read_authoritative().await.unwrap();
254
+ assert_eq!(read.authoritative_revision, 1);
255
+ assert_eq!(read.rows.len(), 1);
256
+ let mut second = writer(&recovered.socket, 2);
257
+ assert!(second.wait().unwrap().success());
258
+ assert_eq!(client.current_revision().await.unwrap(), 2);
259
+ }
260
+
261
+ #[tokio::test(flavor = "current_thread")]
262
+ async fn f2_authority_schedule_exposes_the_removal_at_a_new_revision() {
263
+ let _process_test = PROCESS_TESTS
264
+ .lock()
265
+ .unwrap_or_else(|error| error.into_inner());
266
+ let directory = TempDir::new().unwrap();
267
+ let authority = start(&directory);
268
+ let client = AuthorityClient::connect(&authority.socket);
269
+ let mut initial = writer(&authority.socket, 1);
270
+ assert!(initial.wait().unwrap().success());
271
+ let consumer_snapshot = client.read_authoritative().await.unwrap();
272
+ client
273
+ .commit(AuthorityTransaction {
274
+ transaction_id: "provider-removes-registration".into(),
275
+ base_revision: Some(consumer_snapshot.authoritative_revision),
276
+ preconditions: vec![],
277
+ record_preconditions: vec![],
278
+ mutations: vec![AtomicMutation {
279
+ capability: "registrations".into(),
280
+ key: "registrations:1".into(),
281
+ value: None,
282
+ }],
283
+ audit: None,
284
+ })
285
+ .await
286
+ .unwrap();
287
+ let resumed = AuthorityClient::connect(&authority.socket)
288
+ .read_authoritative()
289
+ .await
290
+ .unwrap();
291
+ assert!(resumed.authoritative_revision > consumer_snapshot.observed_revision);
292
+ assert!(resumed.rows.iter().all(|row| row.key != "registrations:1"));
293
+ assert!(consumer_snapshot
294
+ .rows
295
+ .iter()
296
+ .any(|row| row.key == "registrations:1"));
297
+ }
@@ -10,6 +10,7 @@ use argon2::{
10
10
  };
11
11
  use rand::{distributions::Alphanumeric, Rng};
12
12
  use serde::{Deserialize, Serialize};
13
+ use sha2::{Digest, Sha256};
13
14
 
14
15
  #[derive(Debug, Deserialize)]
15
16
  struct LegacyApiKeyStore {
@@ -128,18 +129,18 @@ impl KeyStore {
128
129
  let records = self.records.read().ok()?;
129
130
  records
130
131
  .iter()
132
+ // Newly issued high-entropy keys use the fast digest format and
133
+ // are appended to the store. Check them before legacy Argon2
134
+ // records so current requests never pay historical password-hash
135
+ // verification costs.
136
+ .rev()
131
137
  .filter(|record| !record.revoked)
132
138
  .find_map(|record| {
133
139
  let namespace_allowed = record
134
140
  .namespaces
135
141
  .iter()
136
142
  .any(|allowed| allowed == "*" || allowed == namespace);
137
- let parsed = PasswordHash::new(&record.hash).ok()?;
138
- if namespace_allowed
139
- && Argon2::default()
140
- .verify_password(token.as_bytes(), &parsed)
141
- .is_ok()
142
- {
143
+ if namespace_allowed && verify_api_key(token, &record.hash) {
143
144
  Some(Principal {
144
145
  key_id: record.id.clone(),
145
146
  scopes: record.scopes.clone(),
@@ -165,11 +166,10 @@ impl KeyStore {
165
166
  .map(char::from)
166
167
  .collect();
167
168
  let token = format!("fdb_live_{secret}");
168
- let salt = SaltString::generate(&mut OsRng);
169
- let hash = Argon2::default()
170
- .hash_password(token.as_bytes(), &salt)
171
- .map_err(|error| error.to_string())?
172
- .to_string();
169
+ // Service keys are high-entropy random secrets, not user-selected
170
+ // passwords. A deterministic digest keeps them out of the credential
171
+ // store without imposing Argon2's CPU cost on every API request.
172
+ let hash = hash_api_key(&token);
173
173
  let id: String = rand::thread_rng()
174
174
  .sample_iter(&Alphanumeric)
175
175
  .take(12)
@@ -246,6 +246,35 @@ impl KeyStore {
246
246
  }
247
247
  }
248
248
 
249
+ fn hash_api_key(token: &str) -> String {
250
+ let digest = Sha256::digest(token.as_bytes());
251
+ let encoded = digest
252
+ .iter()
253
+ .map(|byte| format!("{byte:02x}"))
254
+ .collect::<String>();
255
+ format!("sha256:{encoded}")
256
+ }
257
+
258
+ fn verify_api_key(token: &str, stored_hash: &str) -> bool {
259
+ if stored_hash.starts_with("sha256:") {
260
+ let candidate = hash_api_key(token);
261
+ return candidate.len() == stored_hash.len()
262
+ && ring::constant_time::verify_slices_are_equal(
263
+ candidate.as_bytes(),
264
+ stored_hash.as_bytes(),
265
+ )
266
+ .is_ok();
267
+ }
268
+
269
+ PasswordHash::new(stored_hash)
270
+ .ok()
271
+ .is_some_and(|parsed| {
272
+ Argon2::default()
273
+ .verify_password(token.as_bytes(), &parsed)
274
+ .is_ok()
275
+ })
276
+ }
277
+
249
278
  #[cfg(test)]
250
279
  mod tests {
251
280
  use super::*;
@@ -272,6 +301,7 @@ mod tests {
272
301
 
273
302
  let file = fs::read_to_string(&path).unwrap();
274
303
  assert!(!file.contains(&token));
304
+ assert!(file.contains("sha256:"));
275
305
  assert!(store
276
306
  .authenticate(&token, "alpha")
277
307
  .unwrap()
@@ -590,6 +590,20 @@ impl ConnectionStore {
590
590
  .cloned()
591
591
  .collect())
592
592
  }
593
+
594
+ /// Internal inventory lookup used after the caller has already passed an
595
+ /// application-scoped runtime or promotion authorization check.
596
+ pub fn application_connections(&self, application_id: &str) -> Result<Vec<Connection>, String> {
597
+ Ok(self
598
+ .records
599
+ .read()
600
+ .map_err(|_| "connection store lock poisoned")?
601
+ .connections
602
+ .iter()
603
+ .filter(|connection| connection.application_id == application_id)
604
+ .cloned()
605
+ .collect())
606
+ }
593
607
  pub fn disable(
594
608
  &self,
595
609
  actor: &str,
@@ -59,9 +59,14 @@ pub async fn create_key(
59
59
  {
60
60
  authentication_namespaces.push(state.namespace.to_string());
61
61
  }
62
- let key = state
63
- .keys
64
- .create(name, scopes.clone(), authentication_namespaces)
62
+ // Argon2 is intentionally CPU/memory expensive. Running it on the async
63
+ // request worker makes readiness and unrelated control-plane requests
64
+ // unavailable while qualification creates several scoped keys.
65
+ let keys = state.keys.clone();
66
+ let key_scopes = scopes.clone();
67
+ let key = tokio::task::spawn_blocking(move || keys.create(name, key_scopes, authentication_namespaces))
68
+ .await
69
+ .map_err(|error| (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()))?
65
70
  .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
66
71
 
67
72
  if let Some(application_id) = req.application_id {