@feltdb/core 0.4.20 → 0.5.1

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 (34) hide show
  1. package/README.md +179 -0
  2. package/dist/cli/index.js +1 -1
  3. package/dist/collection.d.ts +45 -0
  4. package/dist/collection.d.ts.map +1 -1
  5. package/dist/collection.js +83 -1
  6. package/dist/create/package-versions.js +1 -1
  7. package/dist/create/server-source/Cargo.lock +12 -0
  8. package/dist/create/server-source/crates/feltdb/src/application.rs +183 -8
  9. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -0
  10. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +2 -0
  11. package/dist/create/server-source/crates/feltdb-server/src/main.rs +206 -45
  12. package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +381 -0
  13. package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +280 -0
  14. package/dist/error-codes.d.ts +53 -0
  15. package/dist/error-codes.d.ts.map +1 -0
  16. package/dist/error-codes.js +46 -0
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -0
  20. package/dist/memory-db.d.ts +12 -0
  21. package/dist/memory-db.d.ts.map +1 -1
  22. package/dist/memory-db.js +36 -0
  23. package/dist/revision-recovery.d.ts +162 -0
  24. package/dist/revision-recovery.d.ts.map +1 -0
  25. package/dist/revision-recovery.js +69 -0
  26. package/dist/state-contract.d.ts +2 -0
  27. package/dist/state-contract.d.ts.map +1 -1
  28. package/dist/state-contract.js +17 -7
  29. package/dist/studio-app/assets/{feltdb_wasm-CBGD0zRu.js → feltdb_wasm-DYPuS6Ky.js} +1 -1
  30. package/dist/studio-app/assets/{feltdb_wasm_bg-C6ATF9mJ.wasm → feltdb_wasm_bg-DEwA82pF.wasm} +0 -0
  31. package/dist/studio-app/assets/{index-CGQV6zGa.js → index-_qqVLfw1.js} +5 -5
  32. package/dist/studio-app/index.html +1 -1
  33. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  34. package/package.json +1 -1
@@ -40,8 +40,8 @@ use feltdb::{
40
40
  authorize as authorize_resource, AuthorizationRequest as ResourceAuthorizationRequest,
41
41
  Grant, GrantSigner, GrantStore, Subject as GrantSubject,
42
42
  },
43
- policy_evaluation::{Actor, PolicyContext, PolicyEvaluator, PolicySubject},
44
43
  cardinality_endpoint::{CardinalityContext, CardinalityDiagnosticResponse},
44
+ policy_evaluation::{Actor, PolicyContext, PolicyEvaluator, PolicySubject},
45
45
  state_contract::{
46
46
  begin_read, compare_schemas, execute_query as execute_state_query, execute_transaction,
47
47
  schema_from_revision, validate_schema, AuthorizationContext, CanonicalQuery, QueryFilter,
@@ -76,7 +76,7 @@ use feltdb_server::{
76
76
  connections::{ConnectionProvider, ConnectionStatus, ConnectionStore, SecretReference},
77
77
  content::ContentStore,
78
78
  identity::IdentityStore,
79
- key_management::{list_keys, create_key, revoke_key},
79
+ key_management::{create_key, list_keys, revoke_key},
80
80
  key_provider::provider_from_environment,
81
81
  leases::LeaseStore,
82
82
  metrics::Metrics,
@@ -103,7 +103,9 @@ struct ApiError(StatusCode, String);
103
103
 
104
104
  impl IntoResponse for ApiError {
105
105
  fn into_response(self) -> Response {
106
- (self.0, Json(json!({ "error": self.1 }))).into_response()
106
+ let body =
107
+ serde_json::from_str::<Value>(&self.1).unwrap_or_else(|_| json!({ "error": self.1 }));
108
+ (self.0, Json(body)).into_response()
107
109
  }
108
110
  }
109
111
 
@@ -2027,9 +2029,10 @@ fn is_platform_owner(principal: &Principal) -> bool {
2027
2029
  .any(|owner| !owner.is_empty() && owner == identity)
2028
2030
  }
2029
2031
  fn require_platform_owner(principal: &Principal) -> Result<(), ApiError> {
2030
- is_platform_owner(principal)
2031
- .then_some(())
2032
- .ok_or(ApiError(StatusCode::FORBIDDEN, "platform owner access required".into()))
2032
+ is_platform_owner(principal).then_some(()).ok_or(ApiError(
2033
+ StatusCode::FORBIDDEN,
2034
+ "platform owner access required".into(),
2035
+ ))
2033
2036
  }
2034
2037
  async fn platform_access(Extension(principal): Extension<Principal>) -> Json<Value> {
2035
2038
  let platform_role = is_platform_owner(&principal).then_some("super_owner");
@@ -2365,7 +2368,11 @@ async fn evaluate_application_contract(
2365
2368
  ) -> Result<Json<Value>, ApiError> {
2366
2369
  Ok(Json(json!(state
2367
2370
  .contracts
2368
- .evaluate(&user_contract_key(&principal, &id), &input.outcome, &principal.key_id)
2371
+ .evaluate(
2372
+ &user_contract_key(&principal, &id),
2373
+ &input.outcome,
2374
+ &principal.key_id
2375
+ )
2369
2376
  .map_err(control_error)?)))
2370
2377
  }
2371
2378
  async fn patch_application_contract(
@@ -2384,7 +2391,10 @@ async fn application_contract_gaps(
2384
2391
  Extension(principal): Extension<Principal>,
2385
2392
  Path(id): Path<String>,
2386
2393
  ) -> Result<Json<Value>, ApiError> {
2387
- let c = state.contracts.get(&user_contract_key(&principal, &id)).map_err(control_error)?;
2394
+ let c = state
2395
+ .contracts
2396
+ .get(&user_contract_key(&principal, &id))
2397
+ .map_err(control_error)?;
2388
2398
  Ok(Json(json!({"gaps":c.gaps})))
2389
2399
  }
2390
2400
  async fn application_contract_readiness(
@@ -4495,12 +4505,7 @@ async fn run_runtime_action(
4495
4505
  schema_version: contract.state.schema.schema_version,
4496
4506
  state_namespace: Some(contract.environment.state_namespace.clone()),
4497
4507
  causal_parent: None,
4498
- authorization: state_authorization(
4499
- &principal,
4500
- &contract,
4501
- &definition.collection,
4502
- "write",
4503
- ),
4508
+ authorization: state_authorization(&principal, &contract, &definition.collection, "write"),
4504
4509
  operations: vec![TransactionOperation {
4505
4510
  kind,
4506
4511
  collection: definition.collection.clone(),
@@ -4603,12 +4608,7 @@ async fn run_runtime_query(
4603
4608
  &state.db,
4604
4609
  &contract.state.schema,
4605
4610
  &contract.environment.state_namespace,
4606
- state_authorization(
4607
- &principal,
4608
- &contract,
4609
- &definition.collection,
4610
- "read",
4611
- ),
4611
+ state_authorization(&principal, &contract, &definition.collection, "read"),
4612
4612
  )
4613
4613
  .map_err(state_contract_error)?;
4614
4614
 
@@ -4712,7 +4712,7 @@ fn state_authorization(
4712
4712
  principal: &Principal,
4713
4713
  contract: &ApplicationRuntimeContract,
4714
4714
  collection: &str,
4715
- operation: &str, // "read" or "write"
4715
+ operation: &str, // "read" or "write"
4716
4716
  ) -> AuthorizationContext {
4717
4717
  let subject = format!("{}:{}", principal.subject_type, principal.key_id);
4718
4718
 
@@ -4888,12 +4888,7 @@ async fn execute_canonical_query(
4888
4888
  &state.db,
4889
4889
  &contract.state.schema,
4890
4890
  &contract.environment.state_namespace,
4891
- state_authorization(
4892
- &principal,
4893
- &contract,
4894
- &input.query.collection,
4895
- "read",
4896
- ),
4891
+ state_authorization(&principal, &contract, &input.query.collection, "read"),
4897
4892
  )
4898
4893
  .map_err(state_contract_error)?;
4899
4894
 
@@ -4905,8 +4900,14 @@ async fn execute_canonical_query(
4905
4900
  .find(|p| p.resource == input.query.collection)
4906
4901
  .and_then(|p| p.read.as_ref().and_then(|s| PolicySubject::from_str(s)));
4907
4902
 
4908
- let result = execute_state_query(&state.db, &contract.state.schema, &context, &input.query, read_policy)
4909
- .map_err(state_contract_error)?;
4903
+ let result = execute_state_query(
4904
+ &state.db,
4905
+ &contract.state.schema,
4906
+ &context,
4907
+ &input.query,
4908
+ read_policy,
4909
+ )
4910
+ .map_err(state_contract_error)?;
4910
4911
  audit(
4911
4912
  &state,
4912
4913
  &principal.key_id,
@@ -4917,6 +4918,156 @@ async fn execute_canonical_query(
4917
4918
  );
4918
4919
  Ok(Json(json!(result)))
4919
4920
  }
4921
+
4922
+ #[derive(serde::Deserialize)]
4923
+ struct RevisionRecoveryRequest {
4924
+ #[serde(rename = "applicationId")]
4925
+ application_id: String,
4926
+ #[serde(rename = "targetRevision")]
4927
+ target_revision: String,
4928
+ #[serde(rename = "expectedCurrentRevision")]
4929
+ expected_current_revision: String,
4930
+ authorization: String,
4931
+ actor: String,
4932
+ reason: String,
4933
+ #[serde(default)]
4934
+ environment: Option<String>,
4935
+ #[serde(default)]
4936
+ #[serde(rename = "recoveryId")]
4937
+ recovery_id: Option<String>,
4938
+ }
4939
+
4940
+ #[derive(serde::Serialize, serde::Deserialize)]
4941
+ struct RevisionRecoveryResponse {
4942
+ success: bool,
4943
+ #[serde(rename = "pointerMoved")]
4944
+ pointer_moved: bool,
4945
+ #[serde(rename = "sourceMarkedUntrusted")]
4946
+ source_marked_untrusted: bool,
4947
+ #[serde(rename = "auditDurable")]
4948
+ audit_durable: bool,
4949
+ #[serde(rename = "recoveryId")]
4950
+ recovery_id: String,
4951
+ #[serde(rename = "targetRevision")]
4952
+ target_revision: String,
4953
+ #[serde(rename = "currentRevision")]
4954
+ current_revision: String,
4955
+ audit: serde_json::Value,
4956
+ }
4957
+
4958
+ fn revision_recovery_error(
4959
+ status: StatusCode,
4960
+ code: &str,
4961
+ message: String,
4962
+ request_id: &str,
4963
+ transaction_id: &str,
4964
+ ) -> ApiError {
4965
+ ApiError(
4966
+ status,
4967
+ json!({"code":code,"message":message,"request_id":request_id,
4968
+ "transaction_id":transaction_id,"http_status":status.as_u16()})
4969
+ .to_string(),
4970
+ )
4971
+ }
4972
+
4973
+ async fn execute_revision_recovery(
4974
+ State(state): State<AppState>,
4975
+ Extension(principal): Extension<Principal>,
4976
+ Json(input): Json<RevisionRecoveryRequest>,
4977
+ ) -> Result<Json<RevisionRecoveryResponse>, ApiError> {
4978
+ let request_id = uuid::Uuid::new_v4().to_string();
4979
+ let transaction_id = input
4980
+ .recovery_id
4981
+ .clone()
4982
+ .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
4983
+
4984
+ // Validate authorization level
4985
+ let auth_level = input.authorization.as_str();
4986
+
4987
+ if !["ELEVATED", "ADMIN", "EMERGENCY"].contains(&auth_level) {
4988
+ return Err(revision_recovery_error(
4989
+ StatusCode::FORBIDDEN,
4990
+ "PERMISSION_DENIED",
4991
+ "Recovery requires ELEVATED, ADMIN, or EMERGENCY authorization".into(),
4992
+ &request_id,
4993
+ &transaction_id,
4994
+ ));
4995
+ }
4996
+
4997
+ // Validate input
4998
+ if input.reason.len() < 10 {
4999
+ return Err(revision_recovery_error(
5000
+ StatusCode::UNPROCESSABLE_ENTITY,
5001
+ "PRECONDITION_FAILED",
5002
+ "Recovery reason must be at least 10 characters".into(),
5003
+ &request_id,
5004
+ &transaction_id,
5005
+ ));
5006
+ }
5007
+
5008
+ let recovery_id = transaction_id.clone();
5009
+ let environment = input
5010
+ .environment
5011
+ .unwrap_or_else(|| "production".to_string());
5012
+ let tenant = application_scope(
5013
+ &state,
5014
+ &principal.key_id,
5015
+ &input.application_id,
5016
+ "application:revision:promote",
5017
+ )?;
5018
+ let recovery = state
5019
+ .applications
5020
+ .recover_environment_pointer(
5021
+ &tenant,
5022
+ &input.application_id,
5023
+ &environment,
5024
+ &input.expected_current_revision,
5025
+ &input.target_revision,
5026
+ &input.actor,
5027
+ &input.reason,
5028
+ auth_level,
5029
+ &recovery_id,
5030
+ )
5031
+ .map_err(|message| {
5032
+ let (status, code) = if message.starts_with("expected_revision_mismatch")
5033
+ || message == "recovery_id_conflict"
5034
+ {
5035
+ (StatusCode::CONFLICT, "CONFLICT")
5036
+ } else if message.contains("not found") {
5037
+ (StatusCode::NOT_FOUND, "PRECONDITION_FAILED")
5038
+ } else if message.contains("integrity") || message.contains("untrusted") {
5039
+ (StatusCode::UNPROCESSABLE_ENTITY, "VALIDATION_FAILED")
5040
+ } else {
5041
+ (StatusCode::SERVICE_UNAVAILABLE, "STORAGE_FAILURE")
5042
+ };
5043
+ revision_recovery_error(status, code, message, &request_id, &transaction_id)
5044
+ })?;
5045
+
5046
+ // Build response
5047
+ let response = RevisionRecoveryResponse {
5048
+ success: true,
5049
+ pointer_moved: true,
5050
+ source_marked_untrusted: true,
5051
+ audit_durable: true,
5052
+ recovery_id: recovery_id.clone(),
5053
+ target_revision: recovery.target_revision.clone(),
5054
+ current_revision: recovery.target_revision.clone(),
5055
+ audit: json!(recovery),
5056
+ };
5057
+
5058
+ // Audit log
5059
+ audit(
5060
+ &state,
5061
+ &principal.key_id,
5062
+ "revision.recover",
5063
+ &recovery_id,
5064
+ "allowed",
5065
+ 200,
5066
+ );
5067
+
5068
+ Ok(Json(response))
5069
+ }
5070
+
4920
5071
  async fn execute_canonical_transaction(
4921
5072
  State(state): State<AppState>,
4922
5073
  Extension(principal): Extension<Principal>,
@@ -4942,24 +5093,27 @@ async fn execute_canonical_transaction(
4942
5093
  input.transaction.state_namespace = Some(contract.environment.state_namespace.clone());
4943
5094
  // For transactions with operations, use the first operation's collection for policy evaluation
4944
5095
  // Multi-collection transactions will still be checked at the operation level in execute_transaction
4945
- let collection = input.transaction.operations
5096
+ let collection = input
5097
+ .transaction
5098
+ .operations
4946
5099
  .first()
4947
5100
  .map(|op| op.collection.clone())
4948
5101
  .unwrap_or_default();
4949
- input.transaction.authorization = state_authorization(
4950
- &principal,
4951
- &contract,
4952
- &collection,
4953
- "write",
4954
- );
5102
+ input.transaction.authorization =
5103
+ state_authorization(&principal, &contract, &collection, "write");
4955
5104
  let write_policy = contract
4956
5105
  .policies
4957
5106
  .definitions
4958
5107
  .iter()
4959
5108
  .find(|p| p.resource == collection)
4960
5109
  .and_then(|p| p.write.as_ref().and_then(|s| PolicySubject::from_str(s)));
4961
- let result = execute_transaction(&state.db, &contract.state.schema, &input.transaction, write_policy)
4962
- .map_err(state_contract_error)?;
5110
+ let result = execute_transaction(
5111
+ &state.db,
5112
+ &contract.state.schema,
5113
+ &input.transaction,
5114
+ write_policy,
5115
+ )
5116
+ .map_err(state_contract_error)?;
4963
5117
  audit(
4964
5118
  &state,
4965
5119
  &principal.key_id,
@@ -4975,13 +5129,19 @@ async fn get_cardinality_diagnostic(
4975
5129
  State(state): State<AppState>,
4976
5130
  Path(pattern): Path<String>,
4977
5131
  ) -> Result<Json<CardinalityDiagnosticResponse>, ApiError> {
4978
- let all_cardinalities = state.db.list_cardinalities()
5132
+ let all_cardinalities = state
5133
+ .db
5134
+ .list_cardinalities()
4979
5135
  .map_err(|e| ApiError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
4980
5136
 
4981
- let actual_rows = state.db.diagnostic_row_count(&pattern)
5137
+ let actual_rows = state
5138
+ .db
5139
+ .diagnostic_row_count(&pattern)
4982
5140
  .map_err(|e| ApiError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
4983
5141
 
4984
- let persisted_capability_keys = state.db.diagnostic_capability_keys(&pattern)
5142
+ let persisted_capability_keys = state
5143
+ .db
5144
+ .diagnostic_capability_keys(&pattern)
4985
5145
  .map_err(|e| ApiError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
4986
5146
 
4987
5147
  let matching_keys: Vec<_> = all_cardinalities
@@ -4992,10 +5152,7 @@ async fn get_cardinality_diagnostic(
4992
5152
 
4993
5153
  let cardinality_map_keys: Vec<String> = matching_keys.iter().map(|(k, _)| k.clone()).collect();
4994
5154
 
4995
- let maintained = matching_keys
4996
- .iter()
4997
- .map(|(_, v)| v)
4998
- .sum::<u64>();
5155
+ let maintained = matching_keys.iter().map(|(_, v)| v).sum::<u64>();
4999
5156
 
5000
5157
  let now_ms = std::time::SystemTime::now()
5001
5158
  .duration_since(std::time::UNIX_EPOCH)
@@ -5752,6 +5909,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5752
5909
  "/v1/transactions",
5753
5910
  axum::routing::post(execute_canonical_transaction),
5754
5911
  )
5912
+ .route(
5913
+ "/v1/revision/recover",
5914
+ axum::routing::post(execute_revision_recovery),
5915
+ )
5755
5916
  .route(
5756
5917
  "/debug/cardinality/{pattern}",
5757
5918
  get(get_cardinality_diagnostic),