@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.
- package/dist/cli/commands.js +68 -1
- package/dist/cli/workspace-integration.js +96 -0
- package/dist/create/cli.js +1 -1
- package/dist/create/create.js +21 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +10 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +5 -1
- package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +273 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +528 -0
- package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +472 -0
- package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +992 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +9 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +157 -9
- package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +413 -0
- package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +488 -0
- package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +378 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +49 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +525 -0
- package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +461 -0
- package/dist/create/template/dot-feltdb-README.md +58 -0
- package/dist/create/workspace-initialization.js +77 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/studio-app/assets/{feltdb_wasm-h9mxesnH.js → feltdb_wasm-B4wq4mqp.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
- package/dist/studio-app/assets/{index-B_EMnTaE.js → index-LQmvJSq6.js} +2 -2
- package/dist/studio-app/index.html +1 -1
- package/dist/telemetry.js +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/development-node.d.ts +42 -0
- package/dist/workspace/development-node.d.ts.map +1 -0
- package/dist/workspace/development-node.js +208 -0
- package/dist/workspace/index.d.ts +20 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +16 -0
- package/dist/workspace/workspace-connection.d.ts +174 -0
- package/dist/workspace/workspace-connection.d.ts.map +1 -0
- package/dist/workspace/workspace-connection.js +290 -0
- package/dist/workspace/workspace-identity.d.ts +13 -0
- package/dist/workspace/workspace-identity.d.ts.map +1 -0
- package/dist/workspace/workspace-identity.js +70 -0
- package/dist/workspace/workspace-types.d.ts +82 -0
- package/dist/workspace/workspace-types.d.ts.map +1 -0
- package/dist/workspace/workspace-types.js +7 -0
- package/package.json +5 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6510
|
-
let
|
|
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
|
-
&
|
|
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
|
}
|