@feltdb/core 0.7.3 → 0.7.4

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 (95) hide show
  1. package/dist/application-development.d.ts +76 -0
  2. package/dist/application-development.d.ts.map +1 -0
  3. package/dist/application-development.js +137 -0
  4. package/dist/application-manifest.d.ts +5 -1
  5. package/dist/application-manifest.d.ts.map +1 -1
  6. package/dist/application-manifest.js +10 -3
  7. package/dist/canonical-application.d.ts +26 -0
  8. package/dist/canonical-application.d.ts.map +1 -0
  9. package/dist/canonical-application.js +45 -0
  10. package/dist/cli/ai-model-config.js +97 -0
  11. package/dist/cli/commands.js +466 -105
  12. package/dist/cli/index.js +1 -1
  13. package/dist/cli/proposal-client.js +54 -0
  14. package/dist/cli/publish-engine.js +162 -0
  15. package/dist/cli/source-sync.js +139 -0
  16. package/dist/create/cli.js +1 -1
  17. package/dist/create/create.js +30 -6
  18. package/dist/create/managed-account.js +14 -7
  19. package/dist/create/package-versions.js +1 -1
  20. package/dist/create/server-source/crates/feltdb/src/application.rs +31 -1
  21. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +84 -6
  22. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
  23. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
  24. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +46 -3
  25. package/dist/create/server-source/crates/feltdb-server/src/main.rs +315 -16
  26. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +20 -0
  27. package/dist/create/template/default-project/README.md +57 -0
  28. package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
  29. package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
  30. package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
  31. package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
  32. package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
  33. package/dist/create/template/default-project/feltdb.flow +169 -0
  34. package/dist/create/template/default-project/src/App.tsx +20 -0
  35. package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
  36. package/dist/create/template/default-project/src/feltdb.ts +87 -0
  37. package/dist/create/template/default-project/src/index.tsx +14 -0
  38. package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
  39. package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
  40. package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
  41. package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
  42. package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
  43. package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
  44. package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
  45. package/dist/create/template/default-project/src/styles-application.css +21 -0
  46. package/dist/create/template/default-project/src/styles.css +1 -0
  47. package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
  48. package/dist/create/template/default-project/workflows/invitation.ts +30 -0
  49. package/dist/create/template/default-project/workflows/project-created.ts +2 -0
  50. package/dist/db.d.ts +49 -1
  51. package/dist/db.d.ts.map +1 -1
  52. package/dist/db.js +77 -0
  53. package/dist/error-codes.d.ts +20 -1
  54. package/dist/error-codes.d.ts.map +1 -1
  55. package/dist/error-codes.js +25 -0
  56. package/dist/flowspec.d.ts.map +1 -1
  57. package/dist/flowspec.js +27 -6
  58. package/dist/http-db.d.ts +15 -2
  59. package/dist/http-db.d.ts.map +1 -1
  60. package/dist/http-db.js +113 -5
  61. package/dist/index-core.d.ts +3 -0
  62. package/dist/index-core.d.ts.map +1 -1
  63. package/dist/index-core.js +3 -0
  64. package/dist/proposal.d.ts +85 -0
  65. package/dist/proposal.d.ts.map +1 -0
  66. package/dist/proposal.js +105 -0
  67. package/dist/studio/app.d.ts +3 -1
  68. package/dist/studio/app.d.ts.map +1 -1
  69. package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
  70. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  71. package/dist/studio/components/AskFeltDB.d.ts +20 -0
  72. package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
  73. package/dist/studio/components/ProposalBrowser.d.ts +3 -0
  74. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
  75. package/dist/studio/components/index.d.ts +2 -0
  76. package/dist/studio/components/index.d.ts.map +1 -1
  77. package/dist/studio/components/index.js +4 -4
  78. package/dist/studio/{components-Dxuhrv8_.js → components-Bm4sjhcX.js} +335 -5
  79. package/dist/studio/index.js +168 -157
  80. package/dist/studio/studio.css +1 -1
  81. package/dist/studio-app/assets/dist-DA1MYSzp.js +1 -0
  82. package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-B-Ccgccb.js} +1 -1
  83. package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
  84. package/dist/studio-app/assets/{index-C1GWyazR.css → index-BRrmvaKw.css} +1 -1
  85. package/dist/studio-app/assets/index-CEIqowCh.js +42 -0
  86. package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
  87. package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
  88. package/dist/studio-app/index.html +2 -2
  89. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  90. package/dist/workspace/local-development-authority.d.ts +3 -0
  91. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  92. package/dist/workspace/local-development-authority.js +62 -1
  93. package/package.json +1 -1
  94. package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
  95. package/dist/studio-app/assets/index-B5tnmvSD.js +0 -28
@@ -7,7 +7,6 @@
7
7
  /// - DistributedExecutor: Orchestrates consistency semantics
8
8
  ///
9
9
  /// Invariant: All replicas converge to identical state_hash for same operations
10
-
11
10
  use crate::causal_dependency_barrier::CausalDependencyBarrier;
12
11
  use crate::convergence::VectorClock;
13
12
  use crate::durable_operation_log::OperationLog;
@@ -136,11 +135,7 @@ impl ReplicaState {
136
135
  }
137
136
 
138
137
  /// Update replica state after transaction
139
- pub fn apply_transaction(
140
- &mut self,
141
- envelope: &TransactionEnvelope,
142
- new_state_hash: StateHash,
143
- ) {
138
+ pub fn apply_transaction(&mut self, envelope: &TransactionEnvelope, new_state_hash: StateHash) {
144
139
  self.vector_clock.merge(&envelope.vector_clock);
145
140
  self.vector_clock.increment(&self.node_id);
146
141
  self.state_hash = new_state_hash;
@@ -276,7 +271,10 @@ impl CausalCapacity {
276
271
 
277
272
  /// A bound small enough to be reached by a test through the real path.
278
273
  pub fn new(max_pending_entries: usize, max_pending_bytes: usize) -> Self {
279
- Self { max_pending_entries, max_pending_bytes }
274
+ Self {
275
+ max_pending_entries,
276
+ max_pending_bytes,
277
+ }
280
278
  }
281
279
  }
282
280
 
@@ -393,8 +391,12 @@ impl DistributedTransactionExecutor {
393
391
  /// source `next_origin_sequence` is recovered from.
394
392
  pub fn origin_sequence_high_water(&self) -> std::collections::BTreeMap<String, u64> {
395
393
  let mut marks = std::collections::BTreeMap::new();
396
- let Some(ref log) = self.operation_log else { return marks };
397
- let Ok(envelopes) = log.load_all() else { return marks };
394
+ let Some(ref log) = self.operation_log else {
395
+ return marks;
396
+ };
397
+ let Ok(envelopes) = log.load_all() else {
398
+ return marks;
399
+ };
398
400
  for envelope in envelopes {
399
401
  let entry = marks
400
402
  .entry(envelope.envelope_id.originating_node.clone())
@@ -516,7 +518,9 @@ impl DistributedTransactionExecutor {
516
518
 
517
519
  /// The size local work would contribute, measured before it is executed.
518
520
  fn operations_bytes(operations: &[Operation]) -> usize {
519
- serde_json::to_string(operations).map(|json| json.len()).unwrap_or(0)
521
+ serde_json::to_string(operations)
522
+ .map(|json| json.len())
523
+ .unwrap_or(0)
520
524
  }
521
525
 
522
526
  /// Execute local work unconditionally.
@@ -582,8 +586,10 @@ impl DistributedTransactionExecutor {
582
586
  // Step 6: A locally executed operation is applied, so it advances the
583
587
  // frontier exactly as a replicated one does. Own writes are always
584
588
  // causally eligible: they are built on the frontier they advance.
585
- self.causal_barrier
586
- .record_applied(&Self::barrier_key(&envelope.envelope_id), &envelope.vector_clock);
589
+ self.causal_barrier.record_applied(
590
+ &Self::barrier_key(&envelope.envelope_id),
591
+ &envelope.vector_clock,
592
+ );
587
593
 
588
594
  Ok(envelope)
589
595
  }
@@ -712,7 +718,9 @@ impl DistributedTransactionExecutor {
712
718
  /// per-operation constant, so a large envelope consumes its actual byte
713
719
  /// count.
714
720
  fn envelope_bytes(envelope: &TransactionEnvelope) -> usize {
715
- serde_json::to_string(envelope).map(|json| json.len()).unwrap_or(0)
721
+ serde_json::to_string(envelope)
722
+ .map(|json| json.len())
723
+ .unwrap_or(0)
716
724
  }
717
725
 
718
726
  /// Apply one envelope and advance the applied frontier.
@@ -741,8 +749,10 @@ impl DistributedTransactionExecutor {
741
749
 
742
750
  // The frontier advances here and only here, so it describes what has
743
751
  // been successfully applied rather than what was claimed or received.
744
- self.causal_barrier
745
- .record_applied(&Self::barrier_key(&envelope.envelope_id), &envelope.vector_clock);
752
+ self.causal_barrier.record_applied(
753
+ &Self::barrier_key(&envelope.envelope_id),
754
+ &envelope.vector_clock,
755
+ );
746
756
 
747
757
  Ok(state_hash)
748
758
  }
@@ -816,16 +826,14 @@ impl DistributedTransactionExecutor {
816
826
  return Ok(0);
817
827
  }
818
828
  let by_key = self.log_by_key()?;
819
- let mut keys: Vec<String> = self
820
- .deferred
821
- .iter()
822
- .map(Self::barrier_key)
823
- .collect();
829
+ let mut keys: Vec<String> = self.deferred.iter().map(Self::barrier_key).collect();
824
830
  keys.sort();
825
831
 
826
832
  let mut admitted = 0;
827
833
  for key in keys {
828
- let Some(envelope) = by_key.get(&key).cloned() else { continue };
834
+ let Some(envelope) = by_key.get(&key).cloned() else {
835
+ continue;
836
+ };
829
837
  let dependencies = Self::dependencies_of(&envelope);
830
838
  let payload_bytes = Self::envelope_bytes(&envelope);
831
839
 
@@ -888,9 +896,7 @@ impl DistributedTransactionExecutor {
888
896
  .clocks
889
897
  .iter()
890
898
  .filter(|(node, required)| frontier.get(node) < **required)
891
- .map(|(node, required)| {
892
- format!("{}@{} (have {})", node, required, frontier.get(node))
893
- })
899
+ .map(|(node, required)| format!("{}@{} (have {})", node, required, frontier.get(node)))
894
900
  .collect();
895
901
  missing.sort();
896
902
  missing
@@ -1021,11 +1027,7 @@ impl DistributedTransactionExecutor {
1021
1027
  return true;
1022
1028
  }
1023
1029
 
1024
- let first_hash = self
1025
- .replicas
1026
- .values()
1027
- .next()
1028
- .map(|r| r.state_hash.clone());
1030
+ let first_hash = self.replicas.values().next().map(|r| r.state_hash.clone());
1029
1031
 
1030
1032
  if let Some(first) = first_hash {
1031
1033
  self.replicas.values().all(|r| r.state_hash == first)
@@ -1122,6 +1124,27 @@ impl DistributedTransactionExecutor {
1122
1124
  );
1123
1125
  }
1124
1126
 
1127
+ // Restore the observable local replica state from the same applied
1128
+ // subset. Recovery previously rebuilt the barrier and canonical data
1129
+ // but left this counter and clock at their constructor values, making
1130
+ // a fully recovered node report that it had applied zero operations.
1131
+ let applied_keys = self.causal_barrier.applied_keys();
1132
+ let operations_applied = envelopes
1133
+ .iter()
1134
+ .filter(|envelope| applied_keys.contains(&Self::barrier_key(&envelope.envelope_id)))
1135
+ .map(|envelope| envelope.operations.len() as u64)
1136
+ .sum();
1137
+ let recovered_clock = self.causal_barrier.get_frontier().clone();
1138
+ let recovered_hash = self
1139
+ .canonical_state()
1140
+ .map(|state| state.hash())
1141
+ .unwrap_or_else(|_| StateHash::from_hex("0".repeat(64)));
1142
+ if let Some(local) = self.replicas.get_mut(&self.local_node_id) {
1143
+ local.vector_clock = recovered_clock;
1144
+ local.operations_applied = operations_applied;
1145
+ local.state_hash = recovered_hash;
1146
+ }
1147
+
1125
1148
  // Identity is recovered from the same log, and this is the half that
1126
1149
  // was missing: the causal frontier was already rebuilt here, so a
1127
1150
  // restarted origin came back knowing its causal position and not its
@@ -1201,7 +1224,10 @@ mod tests {
1201
1224
 
1202
1225
  #[test]
1203
1226
  fn test_replica_state_creation() {
1204
- let state = ReplicaState::new("node1".to_string(), StateHash::from_hex("hash0".to_string()));
1227
+ let state = ReplicaState::new(
1228
+ "node1".to_string(),
1229
+ StateHash::from_hex("hash0".to_string()),
1230
+ );
1205
1231
 
1206
1232
  assert_eq!(state.node_id, "node1");
1207
1233
  assert_eq!(state.operations_applied, 0);
@@ -1224,7 +1250,10 @@ mod tests {
1224
1250
  StateHash::from_hex("hash0".to_string()),
1225
1251
  );
1226
1252
 
1227
- executor.register_replica("node2".to_string(), StateHash::from_hex("hash0".to_string()));
1253
+ executor.register_replica(
1254
+ "node2".to_string(),
1255
+ StateHash::from_hex("hash0".to_string()),
1256
+ );
1228
1257
 
1229
1258
  assert!(executor.replicas.contains_key("node2"));
1230
1259
  }
@@ -1247,8 +1276,14 @@ mod tests {
1247
1276
  StateHash::from_hex("hash0".to_string()),
1248
1277
  );
1249
1278
 
1250
- executor.register_replica("node2".to_string(), StateHash::from_hex("hash0".to_string()));
1251
- executor.register_replica("node3".to_string(), StateHash::from_hex("hash0".to_string()));
1279
+ executor.register_replica(
1280
+ "node2".to_string(),
1281
+ StateHash::from_hex("hash0".to_string()),
1282
+ );
1283
+ executor.register_replica(
1284
+ "node3".to_string(),
1285
+ StateHash::from_hex("hash0".to_string()),
1286
+ );
1252
1287
 
1253
1288
  assert!(executor.check_convergence());
1254
1289
  }
@@ -1260,7 +1295,10 @@ mod tests {
1260
1295
  StateHash::from_hex("hash0".to_string()),
1261
1296
  );
1262
1297
 
1263
- executor.register_replica("node2".to_string(), StateHash::from_hex("hash1".to_string()));
1298
+ executor.register_replica(
1299
+ "node2".to_string(),
1300
+ StateHash::from_hex("hash1".to_string()),
1301
+ );
1264
1302
 
1265
1303
  assert!(!executor.check_convergence());
1266
1304
  }
@@ -137,6 +137,36 @@ impl IdentityStore {
137
137
  .identities
138
138
  .clone())
139
139
  }
140
+ pub fn sign_up(&self, email: &str, password: &str, display_name: Option<String>) -> Result<(Identity, IdentitySession, String), String> {
141
+ let normalized = email.trim().to_lowercase();
142
+ if normalized.is_empty() || password.len() < 8 { return Err("email and a password of at least 8 characters are required".into()); }
143
+ if self.state.read().map_err(|_| "identity lock poisoned")?.identities.iter().any(|value| value.email.as_deref() == Some(&normalized)) {
144
+ return Err("identity already exists".into());
145
+ }
146
+ let mut identity = self.normalize("password", &normalized, Some(normalized.clone()), display_name, serde_json::json!({}))?;
147
+ identity.status = IdentityStatus::Active;
148
+ let mut state = self.state.write().map_err(|_| "identity lock poisoned")?;
149
+ if let Some(stored) = state.identities.iter_mut().find(|value| value.identity_id == identity.identity_id) { stored.status = IdentityStatus::Active; }
150
+ state.password_hashes.insert(identity.subject_id.clone(), Self::hash_password(password)?);
151
+ self.persist(&state)?;
152
+ drop(state);
153
+ let (session, token) = self.issue_session(&identity, "password", None, 15 * 60)?;
154
+ Ok((identity, session, token))
155
+ }
156
+ pub fn sign_in(&self, email: &str, password: &str) -> Result<(Identity, IdentitySession, String), String> {
157
+ let normalized = email.trim().to_lowercase();
158
+ let state = self.state.read().map_err(|_| "identity lock poisoned")?;
159
+ let identity = state.identities.iter().find(|value| value.email.as_deref() == Some(&normalized) && value.status == IdentityStatus::Active).cloned().ok_or("invalid credentials")?;
160
+ let valid = state.password_hashes.get(&identity.subject_id).is_some_and(|hash| Self::verify_password(hash, password));
161
+ drop(state);
162
+ if !valid { return Err("invalid credentials".into()); }
163
+ let (session, token) = self.issue_session(&identity, "password", None, 15 * 60)?;
164
+ Ok((identity, session, token))
165
+ }
166
+ pub fn authenticate_session(&self, token: &str) -> Option<IdentitySession> {
167
+ let current = now();
168
+ self.state.read().ok()?.sessions.iter().find(|value| value.refresh_hash.as_deref().is_some_and(|hash| Self::verify_password(hash, token)) && value.status == "active" && value.expires_at > current).cloned()
169
+ }
140
170
  pub fn sessions(&self, subject: &str) -> Result<Vec<IdentitySession>, String> {
141
171
  Ok(self
142
172
  .state
@@ -154,9 +184,10 @@ impl IdentityStore {
154
184
  provider: &str,
155
185
  device: Option<String>,
156
186
  ttl: u64,
157
- ) -> Result<IdentitySession, String> {
187
+ ) -> Result<(IdentitySession, String), String> {
158
188
  let mut s = self.state.write().map_err(|_| "identity lock poisoned")?;
159
189
  let t = now();
190
+ let token = id("session_token");
160
191
  let x = IdentitySession {
161
192
  session_id: id("session"),
162
193
  identity_id: identity.identity_id.clone(),
@@ -168,11 +199,11 @@ impl IdentityStore {
168
199
  provider: provider.into(),
169
200
  status: "active".into(),
170
201
  revocation_reason: None,
171
- refresh_hash: Some(id("refresh")),
202
+ refresh_hash: Some(Self::hash_password(&token)?),
172
203
  };
173
204
  s.sessions.push(x.clone());
174
205
  self.persist(&s)?;
175
- Ok(x)
206
+ Ok((x, token))
176
207
  }
177
208
  pub fn revoke_session(&self, subject: &str, session: &str, reason: &str) -> Result<(), String> {
178
209
  let mut s = self.state.write().map_err(|_| "identity lock poisoned")?;
@@ -247,4 +278,16 @@ mod tests {
247
278
  assert!(IdentityStore::verify_password(&h, "correct horse"));
248
279
  assert!(!IdentityStore::verify_password(&h, "wrong"));
249
280
  }
281
+ #[test]
282
+ fn actor_tokens_are_short_lived_hashed_and_revocable() {
283
+ let p = std::env::temp_dir().join(format!("identity-session-{}.json", id("test")));
284
+ let store = IdentityStore::load(&p).unwrap();
285
+ let (identity, session, token) = store.sign_up("actor@example.com", "correct horse", Some("Actor".into())).unwrap();
286
+ assert!(session.expires_at - session.issued_at <= 15 * 60);
287
+ assert_ne!(session.refresh_hash.as_deref(), Some(token.as_str()));
288
+ assert_eq!(store.authenticate_session(&token).unwrap().subject_id, identity.subject_id);
289
+ store.revoke_session(&identity.subject_id, &session.session_id, "test").unwrap();
290
+ assert!(store.authenticate_session(&token).is_none());
291
+ let _ = std::fs::remove_file(p);
292
+ }
250
293
  }