@feltdb/core 0.5.6 → 0.6.0

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 (48) hide show
  1. package/dist/cli/commands.js +68 -1
  2. package/dist/cli/workspace-integration.js +96 -0
  3. package/dist/create/cli.js +1 -1
  4. package/dist/create/create.js +21 -0
  5. package/dist/create/package-versions.js +1 -1
  6. package/dist/create/server-source/Cargo.lock +10 -0
  7. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -0
  8. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +5 -1
  9. package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +273 -0
  10. package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +528 -0
  11. package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +472 -0
  12. package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +992 -0
  13. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +9 -0
  14. package/dist/create/server-source/crates/feltdb-server/src/main.rs +157 -9
  15. package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +413 -0
  16. package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +488 -0
  17. package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +378 -0
  18. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +49 -0
  19. package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +525 -0
  20. package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +461 -0
  21. package/dist/create/template/dot-feltdb-README.md +58 -0
  22. package/dist/create/workspace-initialization.js +77 -0
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +6 -0
  26. package/dist/studio-app/assets/{feltdb_wasm-h9mxesnH.js → feltdb_wasm-B4wq4mqp.js} +1 -1
  27. package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
  28. package/dist/studio-app/assets/{index-B_EMnTaE.js → index-LQmvJSq6.js} +2 -2
  29. package/dist/studio-app/index.html +1 -1
  30. package/dist/telemetry.js +1 -1
  31. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  32. package/dist/workspace/development-node.d.ts +42 -0
  33. package/dist/workspace/development-node.d.ts.map +1 -0
  34. package/dist/workspace/development-node.js +208 -0
  35. package/dist/workspace/index.d.ts +20 -0
  36. package/dist/workspace/index.d.ts.map +1 -0
  37. package/dist/workspace/index.js +16 -0
  38. package/dist/workspace/workspace-connection.d.ts +174 -0
  39. package/dist/workspace/workspace-connection.d.ts.map +1 -0
  40. package/dist/workspace/workspace-connection.js +290 -0
  41. package/dist/workspace/workspace-identity.d.ts +13 -0
  42. package/dist/workspace/workspace-identity.d.ts.map +1 -0
  43. package/dist/workspace/workspace-identity.js +70 -0
  44. package/dist/workspace/workspace-types.d.ts +82 -0
  45. package/dist/workspace/workspace-types.d.ts.map +1 -0
  46. package/dist/workspace/workspace-types.js +7 -0
  47. package/package.json +5 -1
  48. package/dist/studio-app/assets/feltdb_wasm_bg-B8U4A1n1.wasm +0 -0
@@ -3,8 +3,12 @@ pub mod application_contract;
3
3
  pub mod artifacts;
4
4
  pub mod audit;
5
5
  pub mod auth;
6
+ pub mod authenticated_principal;
6
7
  pub mod authorization;
7
8
  pub mod backup;
9
+ pub mod certification_harness;
10
+ pub mod delegation_token;
11
+ pub mod durable_operations;
8
12
  pub mod key_management;
9
13
  pub mod causal;
10
14
  pub mod certification;
@@ -15,6 +19,8 @@ pub mod content;
15
19
  pub mod identity;
16
20
  pub mod key_provider;
17
21
  pub mod leases;
22
+ pub mod managed_diagnostics;
23
+ pub mod membership_policy;
18
24
  pub mod metrics;
19
25
  pub mod portable_bundle;
20
26
  pub mod principals;
@@ -22,6 +28,9 @@ pub mod providers;
22
28
  pub mod releases;
23
29
  pub mod request_telemetry;
24
30
  pub mod sessions;
31
+ pub mod snapshot_cursor;
32
+ pub mod tenant_policies;
25
33
  pub mod tenancy;
26
34
  pub mod transaction_idempotency;
35
+ pub mod transaction_recovery;
27
36
  pub mod versions;
@@ -9,7 +9,7 @@ use std::{
9
9
  atomic::{AtomicU64, Ordering},
10
10
  Arc,
11
11
  },
12
- time::Duration,
12
+ time::{Duration, Instant},
13
13
  };
14
14
 
15
15
  use axum::{
@@ -64,6 +64,7 @@ use feltdb_server::{
64
64
  artifacts::{ArtifactKind, ArtifactLifecycle, ArtifactMetadata, ArtifactStore, ProducerRef},
65
65
  audit::{AuditEvent, AuditLog},
66
66
  auth::{KeyStore, Principal},
67
+ authenticated_principal::AuthenticatedPrincipal,
67
68
  authorization::{authorize, AuthorizationRequest, Subject},
68
69
  backup::{
69
70
  create_bundle, create_bundle_with_virtual, restore_bundle, select_bundle_at,
@@ -103,12 +104,18 @@ struct ApiError(StatusCode, String);
103
104
 
104
105
  impl IntoResponse for ApiError {
105
106
  fn into_response(self) -> Response {
106
- let body =
107
- serde_json::from_str::<Value>(&self.1).unwrap_or_else(|_| json!({ "error": self.1 }));
107
+ let body = api_error_body(self.0, self.1);
108
108
  (self.0, Json(body)).into_response()
109
109
  }
110
110
  }
111
111
 
112
+ fn api_error_body(status: StatusCode, message: String) -> Value {
113
+ if status == StatusCode::FORBIDDEN {
114
+ return json!({ "error": "AUTHORIZATION_DENIED" });
115
+ }
116
+ serde_json::from_str::<Value>(&message).unwrap_or_else(|_| json!({ "error": message }))
117
+ }
118
+
112
119
  impl From<feltdb::FlowError> for ApiError {
113
120
  fn from(error: feltdb::FlowError) -> Self {
114
121
  Self(StatusCode::INTERNAL_SERVER_ERROR, error.to_string())
@@ -142,6 +149,19 @@ struct RuntimeResponse {
142
149
  features: Value,
143
150
  }
144
151
 
152
+ #[derive(Serialize)]
153
+ struct ManagedDiagnosticsResponse {
154
+ engine_version: String,
155
+ server_commit: String,
156
+ core_protocol_version: String,
157
+ authorization_contract: u32,
158
+ storage_format: u32,
159
+ promoted_revision: String,
160
+ environment: String,
161
+ recovery: String,
162
+ uptime: String,
163
+ }
164
+
145
165
  #[derive(Serialize)]
146
166
  struct RecordResponse {
147
167
  id: String,
@@ -2185,6 +2205,38 @@ async fn delete_application_control(
2185
2205
  .map_err(control_error)?;
2186
2206
  Ok(StatusCode::NO_CONTENT)
2187
2207
  }
2208
+
2209
+ async fn delete_certification_fixture(
2210
+ State(state): State<AppState>,
2211
+ Path(application_id): Path<String>,
2212
+ headers: HeaderMap,
2213
+ ) -> Result<Json<Value>, ApiError> {
2214
+ let expected = std::env::var("FELTDB_CERTIFICATION_RECOVERY_TOKEN").map_err(|_| {
2215
+ ApiError(
2216
+ StatusCode::SERVICE_UNAVAILABLE,
2217
+ "certification recovery is not configured".into(),
2218
+ )
2219
+ })?;
2220
+ let provided = headers
2221
+ .get("x-feltdb-certification-recovery")
2222
+ .and_then(|value| value.to_str().ok())
2223
+ .unwrap_or_default();
2224
+ if expected.len() < 32
2225
+ || ring::constant_time::verify_slices_are_equal(expected.as_bytes(), provided.as_bytes())
2226
+ .is_err()
2227
+ {
2228
+ return Err(ApiError(StatusCode::FORBIDDEN, "certification recovery denied".into()));
2229
+ }
2230
+ let tenant_id = state
2231
+ .tenancy
2232
+ .delete_certification_fixture(&application_id)
2233
+ .map_err(control_error)?;
2234
+ Ok(Json(json!({
2235
+ "deleted": true,
2236
+ "tenant_id": tenant_id,
2237
+ "application_id": application_id,
2238
+ })))
2239
+ }
2188
2240
  async fn update_application_control(
2189
2241
  State(state): State<AppState>,
2190
2242
  Extension(principal): Extension<Principal>,
@@ -3800,7 +3852,36 @@ async fn commit_application_draft(
3800
3852
  .applications
3801
3853
  .commit(&tenant, &application_id, &draft_id, &principal.key_id)
3802
3854
  .map_err(manifest_error)?;
3803
- Ok((StatusCode::CREATED, Json(json!(revision))))
3855
+ let encoded_revision = percent_encode_path_segment(&revision.revision_id);
3856
+ let mut response = serde_json::to_value(revision).map_err(|error| {
3857
+ ApiError(StatusCode::INTERNAL_SERVER_ERROR, error.to_string())
3858
+ })?;
3859
+ response
3860
+ .as_object_mut()
3861
+ .expect("application revisions serialize as objects")
3862
+ .insert(
3863
+ "promotion".into(),
3864
+ json!({
3865
+ "status": "ready",
3866
+ "url": format!(
3867
+ "/api/applications/{application_id}/revisions/{encoded_revision}/promote"
3868
+ ),
3869
+ }),
3870
+ );
3871
+ Ok((StatusCode::CREATED, Json(response)))
3872
+ }
3873
+
3874
+ fn percent_encode_path_segment(value: &str) -> String {
3875
+ value
3876
+ .bytes()
3877
+ .flat_map(|byte| {
3878
+ if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
3879
+ vec![byte as char]
3880
+ } else {
3881
+ format!("%{byte:02X}").chars().collect()
3882
+ }
3883
+ })
3884
+ .collect()
3804
3885
  }
3805
3886
  async fn list_application_revisions(
3806
3887
  State(state): State<AppState>,
@@ -5657,6 +5738,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5657
5738
  let provider_store = ProviderStore::load(config.data.with_extension("providers.json"))?;
5658
5739
  let readiness_probe = config.data.with_extension("readiness");
5659
5740
  let state = AppState {
5741
+ started_at: Instant::now(),
5660
5742
  ids: Arc::new(AtomicU64::new(db.sequence()?)),
5661
5743
  db,
5662
5744
  namespace: Arc::from(config.namespace.clone()),
@@ -5749,6 +5831,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5749
5831
  axum::routing::post(construct_application_contract),
5750
5832
  )
5751
5833
  .route("/v1/identity/me", get(identity_me))
5834
+ .route("/v1/diagnostics", get(managed_diagnostics))
5752
5835
  .route("/v1/identity/sessions", get(identity_sessions))
5753
5836
  .route(
5754
5837
  "/v1/identity/sessions/{id}/revoke",
@@ -5773,6 +5856,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5773
5856
  axum::routing::patch(platform_update_application_member),
5774
5857
  )
5775
5858
  .route("/api/tenants/{tenant_id}", get(get_tenant))
5859
+ .route(
5860
+ "/api/certification/fixtures/{application_id}",
5861
+ axum::routing::delete(delete_certification_fixture),
5862
+ )
5776
5863
  .route("/api/keys", get(list_keys).post(create_key))
5777
5864
  .route("/api/keys/{id}", axum::routing::delete(revoke_key))
5778
5865
  .route(
@@ -6298,6 +6385,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
6298
6385
  .route("/ready", get(readiness))
6299
6386
  .route("/runtime", get(runtime))
6300
6387
  .route("/metrics", get(network_metrics))
6388
+ .route(
6389
+ "/api/certification/fixtures/{application_id}",
6390
+ axum::routing::delete(delete_certification_fixture),
6391
+ )
6301
6392
  .merge(protected)
6302
6393
  .layer(middleware::from_fn(protocol_version))
6303
6394
  .layer(middleware::from_fn_with_state(state.clone(), count_request))
@@ -6493,7 +6584,12 @@ async fn authenticate(
6493
6584
  let human_contract = principal.subject_type == "human"
6494
6585
  && path.starts_with("/v1/applications/")
6495
6586
  && path.split('/').any(|segment| segment == "contract");
6496
- if !path.starts_with("/api/") && !human_contract && !decision.allowed {
6587
+ let authenticated_diagnostics = path == "/v1/diagnostics";
6588
+ if !path.starts_with("/api/")
6589
+ && !human_contract
6590
+ && !authenticated_diagnostics
6591
+ && !decision.allowed
6592
+ {
6497
6593
  state.metrics.authentication_failure();
6498
6594
  audit(&state, &principal.key_id, required, path, "denied", 403);
6499
6595
  return ApiError(StatusCode::FORBIDDEN, format!("missing scope: {required}"))
@@ -6506,8 +6602,18 @@ async fn authenticate(
6506
6602
  } else {
6507
6603
  state.metrics.state_mutation();
6508
6604
  }
6509
- request.extensions_mut().insert(principal);
6510
- let principal = request
6605
+
6606
+ let authenticated = AuthenticatedPrincipal::from_api_key(
6607
+ principal.key_id.clone(),
6608
+ principal.key_id.clone(),
6609
+ state.namespace.to_string(),
6610
+ principal.scopes.clone(),
6611
+ );
6612
+
6613
+ request.extensions_mut().insert(principal.clone());
6614
+ request.extensions_mut().insert(authenticated.clone());
6615
+
6616
+ let principal_id = request
6511
6617
  .extensions()
6512
6618
  .get::<Principal>()
6513
6619
  .expect("principal inserted")
@@ -6518,7 +6624,7 @@ async fn authenticate(
6518
6624
  let status = response.status().as_u16();
6519
6625
  audit(
6520
6626
  &state,
6521
- &principal,
6627
+ &principal_id,
6522
6628
  required,
6523
6629
  &target,
6524
6630
  if status < 400 { "allowed" } else { "failed" },
@@ -6594,6 +6700,32 @@ async fn health(State(state): State<AppState>) -> Json<HealthResponse<'static>>
6594
6700
  })
6595
6701
  }
6596
6702
 
6703
+ async fn managed_diagnostics(
6704
+ State(state): State<AppState>,
6705
+ ) -> Json<ManagedDiagnosticsResponse> {
6706
+ let recovery = match state.cluster.proposal().map(|value| value.phase) {
6707
+ Some(ProposalPhase::Preparing | ProposalPhase::Prepared) => "recovering",
6708
+ Some(ProposalPhase::Aborted) => "attention-required",
6709
+ _ if !state.lease_clock.is_healthy() => "degraded",
6710
+ _ => "healthy",
6711
+ };
6712
+ let uptime_seconds = state.started_at.elapsed().as_secs();
6713
+
6714
+ Json(ManagedDiagnosticsResponse {
6715
+ engine_version: env!("CARGO_PKG_VERSION").into(),
6716
+ server_commit: option_env!("FELTDB_GIT_COMMIT").unwrap_or("unknown").into(),
6717
+ core_protocol_version: PROTOCOL_VERSION.into(),
6718
+ authorization_contract: 2,
6719
+ storage_format: 1,
6720
+ promoted_revision: std::env::var("FELTDB_PROMOTED_REVISION")
6721
+ .unwrap_or_else(|_| "unknown".into()),
6722
+ environment: std::env::var("FELTDB_ENVIRONMENT")
6723
+ .unwrap_or_else(|_| "development".into()),
6724
+ recovery: recovery.into(),
6725
+ uptime: format!("{uptime_seconds}s"),
6726
+ })
6727
+ }
6728
+
6597
6729
  async fn readiness(State(state): State<AppState>) -> Result<Json<Value>, ApiError> {
6598
6730
  if let Some(parent) = state.readiness_probe.parent() {
6599
6731
  std::fs::create_dir_all(parent)
@@ -9114,7 +9246,8 @@ async fn shutdown_signal() {
9114
9246
 
9115
9247
  #[cfg(test)]
9116
9248
  mod authority_gate_tests {
9117
- use super::{authorize_transaction_collections, state_authorization};
9249
+ use super::{api_error_body, authorize_transaction_collections, state_authorization};
9250
+ use axum::http::StatusCode;
9118
9251
  use feltdb::{
9119
9252
  application::{manifest_hash, ApplicationManifest, ApplicationRevision, PolicyDefinition, RevisionStatus},
9120
9253
  application_runtime::{resolve_runtime, ApplicationRuntimeContract, RuntimeInventory},
@@ -9199,4 +9332,19 @@ mod authority_gate_tests {
9199
9332
  );
9200
9333
  assert!(!allowed);
9201
9334
  }
9335
+
9336
+ #[test]
9337
+ fn every_forbidden_error_uses_the_canonical_wire_contract() {
9338
+ for internal_message in [
9339
+ "forbidden",
9340
+ "tenant access denied",
9341
+ "missing scope: state:read",
9342
+ r#"{"code":"AUTHORIZATION_DENIED","message":"policy denied"}"#,
9343
+ ] {
9344
+ assert_eq!(
9345
+ api_error_body(StatusCode::FORBIDDEN, internal_message.into()),
9346
+ serde_json::json!({ "error": "AUTHORIZATION_DENIED" })
9347
+ );
9348
+ }
9349
+ }
9202
9350
  }