create-feltdb 0.11.8 → 0.11.9
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/package-versions.js +1 -1
- package/dist/server-source/crates/feltdb/src/lib.rs +363 -2
- package/dist/server-source/crates/feltdb/src/read_basis.rs +257 -0
- package/dist/server-source/crates/feltdb/src/state_contract.rs +442 -73
- package/dist/server-source/crates/feltdb/src/workload_diagnostics.rs +18 -4
- package/dist/server-source/crates/feltdb-server/src/main.rs +198 -10
- package/package.json +1 -1
- package/dist/server-source/crates/feltdb-wasm/test.db +0 -0
- package/dist/server-source/crates/feltdb-wasm/test_wasm.db.lock +0 -0
|
@@ -135,10 +135,18 @@ pub enum Phase {
|
|
|
135
135
|
/// AuditWrite` is therefore routing plus decode plus encode, measured as a
|
|
136
136
|
/// difference of two spans rather than asserted.
|
|
137
137
|
HandlerBody,
|
|
138
|
+
/// Resolving a managed request's application contract: the revision, the
|
|
139
|
+
/// runtime inventory it is validated against, and the resolved runtime.
|
|
140
|
+
/// Every `/v1` request pays it, so it must not depend on unrelated state.
|
|
141
|
+
ContractResolution,
|
|
142
|
+
/// Cloning one state namespace's rows for a state-contract read or
|
|
143
|
+
/// transaction. Proportional to that application's stored rows, not to
|
|
144
|
+
/// other applications'.
|
|
145
|
+
NamespaceSnapshot,
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
impl Phase {
|
|
141
|
-
pub const ALL: [Phase;
|
|
149
|
+
pub const ALL: [Phase; 27] = [
|
|
142
150
|
Phase::StateLockWait,
|
|
143
151
|
Phase::StateLockHold,
|
|
144
152
|
Phase::IndexedQuery,
|
|
@@ -164,6 +172,8 @@ impl Phase {
|
|
|
164
172
|
Phase::AuditGroupSync,
|
|
165
173
|
Phase::AuditClose,
|
|
166
174
|
Phase::HandlerBody,
|
|
175
|
+
Phase::ContractResolution,
|
|
176
|
+
Phase::NamespaceSnapshot,
|
|
167
177
|
];
|
|
168
178
|
|
|
169
179
|
/// The stable name this phase is reported under.
|
|
@@ -194,6 +204,8 @@ impl Phase {
|
|
|
194
204
|
Phase::AuditGroupSync => "audit_group_sync",
|
|
195
205
|
Phase::AuditClose => "audit_close",
|
|
196
206
|
Phase::HandlerBody => "handler_body",
|
|
207
|
+
Phase::ContractResolution => "contract_resolution",
|
|
208
|
+
Phase::NamespaceSnapshot => "namespace_snapshot",
|
|
197
209
|
}
|
|
198
210
|
}
|
|
199
211
|
|
|
@@ -210,9 +222,11 @@ impl Phase {
|
|
|
210
222
|
// The predicate runs on both executions, so its parent is the lock
|
|
211
223
|
// rather than either query phase.
|
|
212
224
|
Phase::PredicateEvaluation => Some(Phase::StateLockHold),
|
|
213
|
-
Phase::Authorization
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
Phase::Authorization
|
|
226
|
+
| Phase::AuditWrite
|
|
227
|
+
| Phase::HandlerBody
|
|
228
|
+
| Phase::ContractResolution
|
|
229
|
+
| Phase::NamespaceSnapshot => Some(Phase::HttpHandler),
|
|
216
230
|
Phase::Ordering | Phase::ResultMaterialization => Some(Phase::HandlerBody),
|
|
217
231
|
Phase::RecordMutation | Phase::IndexMaintenance | Phase::Persistence => {
|
|
218
232
|
Some(Phase::StateLockHold)
|
|
@@ -601,6 +601,14 @@ struct CanonicalTransactionRequest {
|
|
|
601
601
|
environment: String,
|
|
602
602
|
transaction: TransactionRequest,
|
|
603
603
|
}
|
|
604
|
+
#[derive(Deserialize)]
|
|
605
|
+
struct CanonicalReadBasisRequest {
|
|
606
|
+
application_id: String,
|
|
607
|
+
revision_id: String,
|
|
608
|
+
#[serde(default = "production_environment")]
|
|
609
|
+
environment: String,
|
|
610
|
+
reads: feltdb::ReadSet,
|
|
611
|
+
}
|
|
604
612
|
fn production_environment() -> String {
|
|
605
613
|
"production".into()
|
|
606
614
|
}
|
|
@@ -1207,6 +1215,7 @@ async fn push_sync(
|
|
|
1207
1215
|
),
|
|
1208
1216
|
operations: vec![operation.operation.clone()],
|
|
1209
1217
|
preconditions: vec![],
|
|
1218
|
+
fences: vec![],
|
|
1210
1219
|
};
|
|
1211
1220
|
let _transaction_permit = state
|
|
1212
1221
|
.state_transaction_admission
|
|
@@ -4620,12 +4629,15 @@ fn runtime_inventory(
|
|
|
4620
4629
|
.filter(|value| value.status == ConnectionStatus::Active)
|
|
4621
4630
|
.map(|value| value.id)
|
|
4622
4631
|
.collect();
|
|
4623
|
-
|
|
4632
|
+
// Every managed request resolves its contract through here, so this reads
|
|
4633
|
+
// only the capability registry's own rows. A whole-database snapshot would
|
|
4634
|
+
// clone, sort, and hash every tenant's state per request.
|
|
4624
4635
|
let mut capabilities = BTreeSet::new();
|
|
4625
|
-
for row in
|
|
4626
|
-
.
|
|
4636
|
+
for row in state
|
|
4637
|
+
.db
|
|
4638
|
+
.list_collection("_flow_capabilities")?
|
|
4627
4639
|
.iter()
|
|
4628
|
-
.filter(|row|
|
|
4640
|
+
.filter(|row| !row.deleted)
|
|
4629
4641
|
{
|
|
4630
4642
|
if let Some(name) = row.value.get("name").and_then(Value::as_str) {
|
|
4631
4643
|
capabilities.insert(name.to_string());
|
|
@@ -4653,6 +4665,8 @@ fn revision_contract(
|
|
|
4653
4665
|
revision_id: &str,
|
|
4654
4666
|
environment: &str,
|
|
4655
4667
|
) -> Result<ApplicationRuntimeContract, ApiError> {
|
|
4668
|
+
let _span =
|
|
4669
|
+
feltdb::workload_diagnostics::span(feltdb::workload_diagnostics::Phase::ContractResolution);
|
|
4656
4670
|
let tenant = application_scope(state, actor, application_id, "application:revision:read")?;
|
|
4657
4671
|
let revision = state
|
|
4658
4672
|
.applications
|
|
@@ -7209,6 +7223,7 @@ async fn run_runtime_action(
|
|
|
7209
7223
|
if_version,
|
|
7210
7224
|
}],
|
|
7211
7225
|
preconditions: vec![],
|
|
7226
|
+
fences: vec![],
|
|
7212
7227
|
};
|
|
7213
7228
|
let write_policy = contract
|
|
7214
7229
|
.policies
|
|
@@ -7658,11 +7673,34 @@ async fn execute_canonical_query(
|
|
|
7658
7673
|
&input.revision_id,
|
|
7659
7674
|
&input.environment,
|
|
7660
7675
|
)?;
|
|
7676
|
+
let authorization = if feltdb::state_contract::is_substrate_collection(&input.query.collection)
|
|
7677
|
+
{
|
|
7678
|
+
let mut authorization = state_authorization_legacy(
|
|
7679
|
+
&principal,
|
|
7680
|
+
&tenant,
|
|
7681
|
+
&input.application_id,
|
|
7682
|
+
&contract.revision_id,
|
|
7683
|
+
"state:denied",
|
|
7684
|
+
);
|
|
7685
|
+
if application_scope(
|
|
7686
|
+
&state,
|
|
7687
|
+
&principal.key_id,
|
|
7688
|
+
&input.application_id,
|
|
7689
|
+
"composition:read",
|
|
7690
|
+
)
|
|
7691
|
+
.is_ok()
|
|
7692
|
+
{
|
|
7693
|
+
authorization.capabilities.insert("composition:read".into());
|
|
7694
|
+
}
|
|
7695
|
+
authorization
|
|
7696
|
+
} else {
|
|
7697
|
+
state_authorization(&principal, &contract, &input.query.collection, "read")
|
|
7698
|
+
};
|
|
7661
7699
|
let context = feltdb::state_contract::begin_query_read(
|
|
7662
7700
|
&state.db,
|
|
7663
7701
|
&contract.state.schema,
|
|
7664
7702
|
&contract.environment.state_namespace,
|
|
7665
|
-
|
|
7703
|
+
authorization,
|
|
7666
7704
|
)
|
|
7667
7705
|
.map_err(state_contract_error)?;
|
|
7668
7706
|
|
|
@@ -7885,6 +7923,76 @@ async fn execute_revision_recovery(
|
|
|
7885
7923
|
Ok(Json(response))
|
|
7886
7924
|
}
|
|
7887
7925
|
|
|
7926
|
+
/// Whether `principal` may read every record of `collection` — the condition
|
|
7927
|
+
/// for a read basis, captured or committed, to cover it.
|
|
7928
|
+
///
|
|
7929
|
+
/// A token digests every row it covers, and a refused fence reveals that one
|
|
7930
|
+
/// of them changed. Both are reads, so both require the right to read the whole
|
|
7931
|
+
/// collection: `state:read` under a read policy that does not depend on the
|
|
7932
|
+
/// record (none, or `authenticated`), or `composition:read` for a substrate
|
|
7933
|
+
/// collection. Owner, member, and self policies filter rows per actor; a read
|
|
7934
|
+
/// basis over such a collection is refused rather than digested.
|
|
7935
|
+
fn read_basis_readable(
|
|
7936
|
+
state: &AppState,
|
|
7937
|
+
principal: &Principal,
|
|
7938
|
+
contract: &ApplicationRuntimeContract,
|
|
7939
|
+
collection: &str,
|
|
7940
|
+
) -> bool {
|
|
7941
|
+
if feltdb::state_contract::is_substrate_collection(collection) {
|
|
7942
|
+
return application_scope(
|
|
7943
|
+
state,
|
|
7944
|
+
&principal.key_id,
|
|
7945
|
+
&contract.application_id,
|
|
7946
|
+
"composition:read",
|
|
7947
|
+
)
|
|
7948
|
+
.is_ok();
|
|
7949
|
+
}
|
|
7950
|
+
if !state_authorization(principal, contract, collection, "read")
|
|
7951
|
+
.capabilities
|
|
7952
|
+
.contains("state:read")
|
|
7953
|
+
{
|
|
7954
|
+
return false;
|
|
7955
|
+
}
|
|
7956
|
+
matches!(
|
|
7957
|
+
matching_state_policy(contract, collection)
|
|
7958
|
+
.and_then(|policy| policy.read.as_deref())
|
|
7959
|
+
.and_then(PolicySubject::from_str),
|
|
7960
|
+
None | Some(PolicySubject::Authenticated)
|
|
7961
|
+
)
|
|
7962
|
+
}
|
|
7963
|
+
|
|
7964
|
+
/// Capture a read basis over the application's state namespace.
|
|
7965
|
+
async fn capture_canonical_read_basis(
|
|
7966
|
+
State(state): State<AppState>,
|
|
7967
|
+
Extension(principal): Extension<Principal>,
|
|
7968
|
+
headers: HeaderMap,
|
|
7969
|
+
Json(input): Json<CanonicalReadBasisRequest>,
|
|
7970
|
+
) -> Result<Json<Value>, ApiError> {
|
|
7971
|
+
let tenant = application_scope(
|
|
7972
|
+
&state,
|
|
7973
|
+
&principal.key_id,
|
|
7974
|
+
&input.application_id,
|
|
7975
|
+
"state:read",
|
|
7976
|
+
)?;
|
|
7977
|
+
require_bound_tenant(&headers, &tenant)?;
|
|
7978
|
+
let contract = revision_contract(
|
|
7979
|
+
&state,
|
|
7980
|
+
&principal.key_id,
|
|
7981
|
+
&input.application_id,
|
|
7982
|
+
&input.revision_id,
|
|
7983
|
+
&input.environment,
|
|
7984
|
+
)?;
|
|
7985
|
+
let basis = feltdb::state_contract::capture_read_basis(
|
|
7986
|
+
&state.db,
|
|
7987
|
+
&contract.state.schema,
|
|
7988
|
+
&contract.environment.state_namespace,
|
|
7989
|
+
&input.reads,
|
|
7990
|
+
&|collection| read_basis_readable(&state, &principal, &contract, collection),
|
|
7991
|
+
)
|
|
7992
|
+
.map_err(state_contract_error)?;
|
|
7993
|
+
Ok(Json(json!(basis)))
|
|
7994
|
+
}
|
|
7995
|
+
|
|
7888
7996
|
async fn execute_canonical_transaction(
|
|
7889
7997
|
State(state): State<AppState>,
|
|
7890
7998
|
Extension(principal): Extension<Principal>,
|
|
@@ -7921,14 +8029,49 @@ async fn execute_canonical_transaction(
|
|
|
7921
8029
|
input.transaction.revision_id = contract.revision_id.clone();
|
|
7922
8030
|
input.transaction.schema_version = contract.state.schema.schema_version;
|
|
7923
8031
|
input.transaction.state_namespace = Some(contract.environment.state_namespace.clone());
|
|
7924
|
-
let collections = input
|
|
8032
|
+
let (substrate, collections): (Vec<String>, Vec<String>) = input
|
|
7925
8033
|
.transaction
|
|
7926
8034
|
.operations
|
|
7927
8035
|
.iter()
|
|
7928
8036
|
.map(|operation| operation.collection.clone())
|
|
7929
|
-
.
|
|
7930
|
-
let (authorized, write_policies) =
|
|
7931
|
-
|
|
8037
|
+
.partition(|collection| feltdb::state_contract::is_substrate_collection(collection));
|
|
8038
|
+
let (authorized, write_policies) = if collections.is_empty() && !substrate.is_empty() {
|
|
8039
|
+
(tenant == contract.tenant_id, BTreeMap::new())
|
|
8040
|
+
} else {
|
|
8041
|
+
authorize_transaction_collections(&principal, &contract, &tenant, &collections)
|
|
8042
|
+
};
|
|
8043
|
+
// Substrate records are written only under the dedicated capability.
|
|
8044
|
+
let substrate_authorized = substrate.is_empty()
|
|
8045
|
+
|| application_scope(
|
|
8046
|
+
&state,
|
|
8047
|
+
&principal.key_id,
|
|
8048
|
+
&input.application_id,
|
|
8049
|
+
"composition:write",
|
|
8050
|
+
)
|
|
8051
|
+
.is_ok();
|
|
8052
|
+
// A fence is a read of everything it covers: the committer must be able to
|
|
8053
|
+
// read all of it, exactly as capturing it required.
|
|
8054
|
+
for fence in &input.transaction.fences {
|
|
8055
|
+
for collection in fence
|
|
8056
|
+
.reads
|
|
8057
|
+
.records
|
|
8058
|
+
.iter()
|
|
8059
|
+
.map(|record| record.collection.as_str())
|
|
8060
|
+
.chain(
|
|
8061
|
+
fence
|
|
8062
|
+
.reads
|
|
8063
|
+
.predicates
|
|
8064
|
+
.iter()
|
|
8065
|
+
.map(|predicate| predicate.collection.as_str()),
|
|
8066
|
+
)
|
|
8067
|
+
{
|
|
8068
|
+
if !read_basis_readable(&state, &principal, &contract, collection) {
|
|
8069
|
+
return Err(state_contract_error(
|
|
8070
|
+
feltdb::state_contract::read_basis_denied(collection),
|
|
8071
|
+
));
|
|
8072
|
+
}
|
|
8073
|
+
}
|
|
8074
|
+
}
|
|
7932
8075
|
input.transaction.authorization = state_authorization_legacy(
|
|
7933
8076
|
&principal,
|
|
7934
8077
|
&tenant,
|
|
@@ -7940,6 +8083,13 @@ async fn execute_canonical_transaction(
|
|
|
7940
8083
|
"state:denied"
|
|
7941
8084
|
},
|
|
7942
8085
|
);
|
|
8086
|
+
if authorized && substrate_authorized && !substrate.is_empty() {
|
|
8087
|
+
input
|
|
8088
|
+
.transaction
|
|
8089
|
+
.authorization
|
|
8090
|
+
.capabilities
|
|
8091
|
+
.insert("composition:write".into());
|
|
8092
|
+
}
|
|
7943
8093
|
let _transaction_permit = state
|
|
7944
8094
|
.state_transaction_admission
|
|
7945
8095
|
.acquire()
|
|
@@ -9036,6 +9186,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
9036
9186
|
"/v1/transactions",
|
|
9037
9187
|
axum::routing::post(execute_canonical_transaction),
|
|
9038
9188
|
)
|
|
9189
|
+
.route(
|
|
9190
|
+
"/v1/transactions/read-basis",
|
|
9191
|
+
axum::routing::post(capture_canonical_read_basis),
|
|
9192
|
+
)
|
|
9039
9193
|
.route(
|
|
9040
9194
|
"/v1/revision/recover",
|
|
9041
9195
|
axum::routing::post(execute_revision_recovery),
|
|
@@ -9312,6 +9466,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
9312
9466
|
axum::routing::delete(delete_connection_control),
|
|
9313
9467
|
)
|
|
9314
9468
|
.route("/transactions", axum::routing::post(commit_transaction))
|
|
9469
|
+
.route(
|
|
9470
|
+
"/transactions/read-basis",
|
|
9471
|
+
axum::routing::post(capture_read_basis),
|
|
9472
|
+
)
|
|
9315
9473
|
.route("/query", axum::routing::post(execute_bounded_query))
|
|
9316
9474
|
.route("/revision", get(get_revision))
|
|
9317
9475
|
.route(
|
|
@@ -9631,6 +9789,8 @@ async fn authenticate(
|
|
|
9631
9789
|
|| path == "/v1/state/conflicts"
|
|
9632
9790
|
|| path == "/v1/state/reconciliation/plan"
|
|
9633
9791
|
|| path == "/v1/experimental/semantic-decisions"
|
|
9792
|
+
|| path == "/transactions/read-basis"
|
|
9793
|
+
|| path == "/v1/transactions/read-basis"
|
|
9634
9794
|
{
|
|
9635
9795
|
// Describing state is reading it. These are POSTs because they take
|
|
9636
9796
|
// three revision identities and a plan, not because they change
|
|
@@ -13873,6 +14033,28 @@ struct AtomicTxRequest {
|
|
|
13873
14033
|
/// exactly as before.
|
|
13874
14034
|
#[serde(default)]
|
|
13875
14035
|
preconditions: Vec<AtomicTxPreconditionRequest>,
|
|
14036
|
+
/// Read bases this authority issued (`POST /transactions/read-basis`),
|
|
14037
|
+
/// re-evaluated inside the commit lock. Absent means no fence.
|
|
14038
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
14039
|
+
fences: Vec<feltdb::ReadBasis>,
|
|
14040
|
+
}
|
|
14041
|
+
|
|
14042
|
+
/// Capture a read basis: what the authority holds, now, for a set of records
|
|
14043
|
+
/// and equality predicates. Reading, so it requires `state:read`.
|
|
14044
|
+
async fn capture_read_basis(
|
|
14045
|
+
State(state): State<AppState>,
|
|
14046
|
+
Json(reads): Json<feltdb::ReadSet>,
|
|
14047
|
+
) -> Result<Json<feltdb::ReadBasis>, ApiError> {
|
|
14048
|
+
state
|
|
14049
|
+
.db
|
|
14050
|
+
.read_basis(&reads)
|
|
14051
|
+
.map(Json)
|
|
14052
|
+
.map_err(|error| match error {
|
|
14053
|
+
FlowError::CapabilityError(message) if message.starts_with("INVALID_READ_SET:") => {
|
|
14054
|
+
ApiError(StatusCode::BAD_REQUEST, message)
|
|
14055
|
+
}
|
|
14056
|
+
other => ApiError(StatusCode::INTERNAL_SERVER_ERROR, other.to_string()),
|
|
14057
|
+
})
|
|
13876
14058
|
}
|
|
13877
14059
|
|
|
13878
14060
|
#[derive(Serialize)]
|
|
@@ -13934,6 +14116,8 @@ async fn commit_transaction(
|
|
|
13934
14116
|
let mut record_preconditions: Vec<RecordPrecondition> = Vec::new();
|
|
13935
14117
|
let mut seen = std::collections::HashSet::new();
|
|
13936
14118
|
|
|
14119
|
+
let fences = request.fences.clone();
|
|
14120
|
+
|
|
13937
14121
|
// Transaction-level preconditions first, on records the transaction need
|
|
13938
14122
|
// not write at all.
|
|
13939
14123
|
for condition in &request.preconditions {
|
|
@@ -14011,12 +14195,13 @@ async fn commit_transaction(
|
|
|
14011
14195
|
let operations = mutations.len();
|
|
14012
14196
|
let commit = state
|
|
14013
14197
|
.db
|
|
14014
|
-
.
|
|
14198
|
+
.apply_atomic_transaction_fenced(
|
|
14015
14199
|
&request.transaction_id,
|
|
14016
14200
|
Some(&payload_hash),
|
|
14017
14201
|
None,
|
|
14018
14202
|
&[],
|
|
14019
14203
|
&record_preconditions,
|
|
14204
|
+
&fences,
|
|
14020
14205
|
&mutations,
|
|
14021
14206
|
None,
|
|
14022
14207
|
)
|
|
@@ -14036,6 +14221,9 @@ async fn commit_transaction(
|
|
|
14036
14221
|
"failure": *failure,
|
|
14037
14222
|
}),
|
|
14038
14223
|
),
|
|
14224
|
+
FlowError::CapabilityError(message) if message.starts_with("INVALID_READ_SET:") => {
|
|
14225
|
+
ApiError(StatusCode::BAD_REQUEST, message)
|
|
14226
|
+
}
|
|
14039
14227
|
FlowError::CapabilityError(message)
|
|
14040
14228
|
if message.starts_with("TRANSACTION_ID_PAYLOAD_MISMATCH:") =>
|
|
14041
14229
|
{
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|