@feltdb/core 0.7.3 → 0.8.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/README.md +25 -0
- package/dist/application-development.d.ts +78 -0
- package/dist/application-development.d.ts.map +1 -0
- package/dist/application-development.js +153 -0
- package/dist/application-manifest.d.ts +6 -1
- package/dist/application-manifest.d.ts.map +1 -1
- package/dist/application-manifest.js +10 -3
- package/dist/canonical-application.d.ts +27 -0
- package/dist/canonical-application.d.ts.map +1 -0
- package/dist/canonical-application.js +46 -0
- package/dist/cli/ai-model-config.js +97 -0
- package/dist/cli/cli.js +1 -1
- package/dist/cli/commands.js +664 -120
- package/dist/cli/index.js +1 -1
- package/dist/cli/managed-environment.js +6 -0
- package/dist/cli/managed-project.js +6 -0
- package/dist/cli/proposal-client.js +63 -0
- package/dist/cli/publish-engine.js +165 -0
- package/dist/cli/source-sync.js +183 -0
- package/dist/create/cli.js +11 -2
- package/dist/create/create.js +38 -16
- package/dist/create/managed-account.js +82 -11
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +41 -1
- package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +84 -6
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
- package/dist/create/server-source/crates/feltdb/src/lib.rs +20 -2
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +12 -24
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
- package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
- package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
- package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
- package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +111 -3
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +28 -7
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +5 -5
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +1960 -143
- package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
- package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
- package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
- package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +195 -13
- package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
- package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
- package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
- package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
- package/dist/create/template/default-project/README.md +57 -0
- package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
- package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
- package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
- package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
- package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
- package/dist/create/template/default-project/feltdb.flow +174 -0
- package/dist/create/template/default-project/src/App.tsx +20 -0
- package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
- package/dist/create/template/default-project/src/feltdb.ts +87 -0
- package/dist/create/template/default-project/src/index.tsx +14 -0
- package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
- package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
- package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
- package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
- package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
- package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
- package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
- package/dist/create/template/default-project/src/styles-application.css +21 -0
- package/dist/create/template/default-project/src/styles.css +1 -0
- package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
- package/dist/create/template/default-project/workflows/invitation.ts +30 -0
- package/dist/create/template/default-project/workflows/project-created.ts +2 -0
- package/dist/db.d.ts +49 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +77 -0
- package/dist/error-codes.d.ts +20 -1
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +25 -0
- package/dist/flowspec.d.ts +1 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +66 -11
- package/dist/http-client.d.ts +135 -14
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +79 -31
- package/dist/http-db.d.ts +15 -2
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +113 -5
- package/dist/index-core.d.ts +5 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +5 -0
- package/dist/module-intent.d.ts +37 -0
- package/dist/module-intent.d.ts.map +1 -0
- package/dist/module-intent.js +81 -0
- package/dist/module.d.ts +76 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +61 -0
- package/dist/proposal.d.ts +161 -0
- package/dist/proposal.d.ts.map +1 -0
- package/dist/proposal.js +232 -0
- package/dist/studio/app.d.ts +5 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/AskFeltDB.d.ts +21 -0
- package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
- package/dist/studio/components/ContractInspector.d.ts +7 -0
- package/dist/studio/components/ContractInspector.d.ts.map +1 -0
- package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -1
- package/dist/studio/components/ManagedInstancePanel.js +1 -5
- package/dist/studio/components/ModuleExplorer.d.ts +6 -0
- package/dist/studio/components/ModuleExplorer.d.ts.map +1 -0
- package/dist/studio/components/ProposalBrowser.d.ts +12 -0
- package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
- package/dist/studio/components/StateExplorer.d.ts +6 -7
- package/dist/studio/components/StateExplorer.d.ts.map +1 -1
- package/dist/studio/components/index.d.ts +4 -0
- package/dist/studio/components/index.d.ts.map +1 -1
- package/dist/studio/components/index.js +4 -4
- package/dist/studio/components-CNSNrmA9.js +2787 -0
- package/dist/studio/index.js +214 -157
- package/dist/studio/studio.css +1 -1
- package/dist/studio/utils/index.d.ts +2 -0
- package/dist/studio/utils/index.d.ts.map +1 -1
- package/dist/studio/utils/index.js +14 -12
- package/dist/studio/utils/managed-instance.d.ts +0 -4
- package/dist/studio/utils/managed-instance.d.ts.map +1 -1
- package/dist/studio/utils/managed-instance.js +0 -4
- package/dist/studio/utils/proposal-api.d.ts +53 -0
- package/dist/studio/utils/proposal-api.d.ts.map +1 -0
- package/dist/studio/utils/proposal-api.js +91 -0
- package/dist/studio/utils/service-api.d.ts +81 -0
- package/dist/studio/utils/service-api.d.ts.map +1 -0
- package/dist/studio/utils/service-api.js +51 -0
- package/dist/studio-app/assets/dist-CCOk39Uc.js +1 -0
- package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-CSB6KVgw.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
- package/dist/studio-app/assets/index-Bncji3aN.js +43 -0
- package/dist/studio-app/assets/{index-C1GWyazR.css → index-yazaWMUN.css} +1 -1
- package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
- package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
- package/dist/studio-app/index.html +2 -2
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/local-development-authority.d.ts +3 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -1
- package/dist/workspace/local-development-authority.js +124 -2
- package/package.json +1 -1
- package/dist/studio/components-Dxuhrv8_.js +0 -1832
- package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
- package/dist/studio-app/assets/index-B5tnmvSD.js +0 -28
|
@@ -122,10 +122,7 @@ pub struct DiagnosticsCollector {
|
|
|
122
122
|
|
|
123
123
|
impl DiagnosticsCollector {
|
|
124
124
|
/// Creates a new diagnostics collector.
|
|
125
|
-
pub fn new(
|
|
126
|
-
server_commit: String,
|
|
127
|
-
environment: Environment,
|
|
128
|
-
) -> Self {
|
|
125
|
+
pub fn new(server_commit: String, environment: Environment) -> Self {
|
|
129
126
|
Self {
|
|
130
127
|
engine_version: ENGINE_VERSION.to_string(),
|
|
131
128
|
server_commit,
|
|
@@ -137,10 +134,7 @@ impl DiagnosticsCollector {
|
|
|
137
134
|
}
|
|
138
135
|
|
|
139
136
|
/// Generates a diagnostics report.
|
|
140
|
-
pub fn collect_report(
|
|
141
|
-
&self,
|
|
142
|
-
recovery_status: RecoveryStatus,
|
|
143
|
-
) -> DiagnosticsReport {
|
|
137
|
+
pub fn collect_report(&self, recovery_status: RecoveryStatus) -> DiagnosticsReport {
|
|
144
138
|
let uptime = now() - self.start_time;
|
|
145
139
|
let overall_status = if recovery_status.status.is_healthy() {
|
|
146
140
|
HealthStatus::Healthy
|
|
@@ -289,10 +283,7 @@ mod tests {
|
|
|
289
283
|
|
|
290
284
|
#[test]
|
|
291
285
|
fn create_diagnostics_collector() {
|
|
292
|
-
let collector = DiagnosticsCollector::new(
|
|
293
|
-
"abc123def456".to_string(),
|
|
294
|
-
Environment::Prod,
|
|
295
|
-
);
|
|
286
|
+
let collector = DiagnosticsCollector::new("abc123def456".to_string(), Environment::Prod);
|
|
296
287
|
|
|
297
288
|
assert_eq!(collector.engine_version, ENGINE_VERSION);
|
|
298
289
|
assert_eq!(collector.protocol_version, PROTOCOL_VERSION);
|
|
@@ -302,10 +293,7 @@ mod tests {
|
|
|
302
293
|
|
|
303
294
|
#[test]
|
|
304
295
|
fn collect_report_includes_uptime() {
|
|
305
|
-
let collector = DiagnosticsCollector::new(
|
|
306
|
-
"abc123def456".to_string(),
|
|
307
|
-
Environment::Staging,
|
|
308
|
-
);
|
|
296
|
+
let collector = DiagnosticsCollector::new("abc123def456".to_string(), Environment::Staging);
|
|
309
297
|
|
|
310
298
|
let recovery_status = RecoveryStatus::default();
|
|
311
299
|
let report = collector.collect_report(recovery_status);
|
|
@@ -327,7 +315,9 @@ mod tests {
|
|
|
327
315
|
assert_eq!(recovery.durable_operations, HealthStatus::Degraded);
|
|
328
316
|
assert_eq!(recovery.status, HealthStatus::Degraded);
|
|
329
317
|
assert_eq!(recovery.pending_recovery_count, 5);
|
|
330
|
-
assert!(recovery
|
|
318
|
+
assert!(recovery
|
|
319
|
+
.issues
|
|
320
|
+
.contains(&"durable operations slow".to_string()));
|
|
331
321
|
}
|
|
332
322
|
|
|
333
323
|
#[test]
|
|
@@ -341,11 +331,7 @@ mod tests {
|
|
|
341
331
|
|
|
342
332
|
#[test]
|
|
343
333
|
fn environment_serialization() {
|
|
344
|
-
let envs = vec![
|
|
345
|
-
Environment::Dev,
|
|
346
|
-
Environment::Staging,
|
|
347
|
-
Environment::Prod,
|
|
348
|
-
];
|
|
334
|
+
let envs = vec![Environment::Dev, Environment::Staging, Environment::Prod];
|
|
349
335
|
|
|
350
336
|
for env in envs {
|
|
351
337
|
let json = serde_json::to_string(&env).expect("serialize failed");
|
|
@@ -357,10 +343,7 @@ mod tests {
|
|
|
357
343
|
|
|
358
344
|
#[test]
|
|
359
345
|
fn diagnostics_report_serialization() {
|
|
360
|
-
let collector = DiagnosticsCollector::new(
|
|
361
|
-
"abc123def456".to_string(),
|
|
362
|
-
Environment::Prod,
|
|
363
|
-
);
|
|
346
|
+
let collector = DiagnosticsCollector::new("abc123def456".to_string(), Environment::Prod);
|
|
364
347
|
|
|
365
348
|
let recovery_status = RecoveryStatusBuilder::new()
|
|
366
349
|
.with_pending_recovery_count(3)
|
|
@@ -378,10 +361,7 @@ mod tests {
|
|
|
378
361
|
|
|
379
362
|
#[test]
|
|
380
363
|
fn add_subsystem_metrics() {
|
|
381
|
-
let collector = DiagnosticsCollector::new(
|
|
382
|
-
"abc123def456".to_string(),
|
|
383
|
-
Environment::Dev,
|
|
384
|
-
);
|
|
364
|
+
let collector = DiagnosticsCollector::new("abc123def456".to_string(), Environment::Dev);
|
|
385
365
|
|
|
386
366
|
let mut report = collector.collect_report(RecoveryStatus::default());
|
|
387
367
|
let mut details = HashMap::new();
|
|
@@ -153,7 +153,9 @@ impl MembershipVerifier {
|
|
|
153
153
|
) -> MembershipDecision {
|
|
154
154
|
let policy = match self.store.get(policy_name) {
|
|
155
155
|
Some(p) => p,
|
|
156
|
-
None =>
|
|
156
|
+
None => {
|
|
157
|
+
return MembershipDecision::Denied(format!("policy not found: {}", policy_name))
|
|
158
|
+
}
|
|
157
159
|
};
|
|
158
160
|
|
|
159
161
|
// Verify actor field
|
|
@@ -203,7 +205,10 @@ impl MembershipVerifier {
|
|
|
203
205
|
));
|
|
204
206
|
}
|
|
205
207
|
} else {
|
|
206
|
-
return MembershipDecision::Denied(format!(
|
|
208
|
+
return MembershipDecision::Denied(format!(
|
|
209
|
+
"required role not found: {}",
|
|
210
|
+
required
|
|
211
|
+
));
|
|
207
212
|
}
|
|
208
213
|
}
|
|
209
214
|
|
|
@@ -221,7 +226,9 @@ impl MembershipVerifier {
|
|
|
221
226
|
// Find matching membership record
|
|
222
227
|
let policy = match self.store.get(policy_name) {
|
|
223
228
|
Some(p) => p,
|
|
224
|
-
None =>
|
|
229
|
+
None => {
|
|
230
|
+
return MembershipDecision::Denied(format!("policy not found: {}", policy_name))
|
|
231
|
+
}
|
|
225
232
|
};
|
|
226
233
|
|
|
227
234
|
for record in records {
|
|
@@ -260,7 +267,8 @@ impl MembershipVerifier {
|
|
|
260
267
|
let record_actor = self.extract_string(&policy.actor_field, record);
|
|
261
268
|
let record_tenant = self.extract_string(&policy.tenant_field, record);
|
|
262
269
|
|
|
263
|
-
if record_actor.as_deref() == Some(actor_id)
|
|
270
|
+
if record_actor.as_deref() == Some(actor_id)
|
|
271
|
+
&& record_tenant.as_deref() == Some(tenant_id)
|
|
264
272
|
{
|
|
265
273
|
// Check status
|
|
266
274
|
if let Some(status_field) = &policy.status_field {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
1
2
|
use std::sync::{Arc, Mutex};
|
|
2
3
|
use std::time::{Duration, Instant, SystemTime};
|
|
3
|
-
use serde::{Deserialize, Serialize};
|
|
4
4
|
use uuid::Uuid;
|
|
5
5
|
|
|
6
6
|
/// Request lifecycle telemetry: captures all timing and context for every /v1/ request.
|
|
@@ -101,10 +101,7 @@ impl RequestTelemetryStore {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
pub fn all(&self) -> Vec<RequestTelemetry> {
|
|
104
|
-
self.records
|
|
105
|
-
.lock()
|
|
106
|
-
.map(|r| r.clone())
|
|
107
|
-
.unwrap_or_default()
|
|
104
|
+
self.records.lock().map(|r| r.clone()).unwrap_or_default()
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
pub fn recent(&self, count: usize) -> Vec<RequestTelemetry> {
|
|
@@ -120,7 +117,12 @@ impl RequestTelemetryStore {
|
|
|
120
117
|
pub fn by_transaction_id(&self, tx_id: &str) -> Vec<RequestTelemetry> {
|
|
121
118
|
self.records
|
|
122
119
|
.lock()
|
|
123
|
-
.map(|r|
|
|
120
|
+
.map(|r| {
|
|
121
|
+
r.iter()
|
|
122
|
+
.filter(|t| t.transaction_id.as_deref() == Some(tx_id))
|
|
123
|
+
.cloned()
|
|
124
|
+
.collect()
|
|
125
|
+
})
|
|
124
126
|
.unwrap_or_default()
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -62,8 +62,7 @@ impl CursorCodec {
|
|
|
62
62
|
let bytes = base64::engine::general_purpose::STANDARD
|
|
63
63
|
.decode(encoded)
|
|
64
64
|
.map_err(|e| format!("invalid cursor encoding: {e}"))?;
|
|
65
|
-
let json = String::from_utf8(bytes)
|
|
66
|
-
.map_err(|e| format!("cursor not valid UTF-8: {e}"))?;
|
|
65
|
+
let json = String::from_utf8(bytes).map_err(|e| format!("cursor not valid UTF-8: {e}"))?;
|
|
67
66
|
serde_json::from_str(&json).map_err(|e| format!("invalid cursor JSON: {e}"))
|
|
68
67
|
}
|
|
69
68
|
}
|
|
@@ -100,10 +99,7 @@ impl SnapshotCursorManager {
|
|
|
100
99
|
};
|
|
101
100
|
|
|
102
101
|
let checksum = self.compute_checksum(&cursor)?;
|
|
103
|
-
let cursor_with_checksum = SnapshotCursor {
|
|
104
|
-
checksum,
|
|
105
|
-
..cursor
|
|
106
|
-
};
|
|
102
|
+
let cursor_with_checksum = SnapshotCursor { checksum, ..cursor };
|
|
107
103
|
|
|
108
104
|
CursorCodec::encode(&cursor_with_checksum)
|
|
109
105
|
}
|
|
@@ -144,9 +140,7 @@ impl SnapshotCursorManager {
|
|
|
144
140
|
|
|
145
141
|
// Verify authorization context hasn't changed
|
|
146
142
|
if cursor.auth_context_hash != current_auth_context_hash {
|
|
147
|
-
return Err(
|
|
148
|
-
"authorization context changed since cursor creation".into(),
|
|
149
|
-
);
|
|
143
|
+
return Err("authorization context changed since cursor creation".into());
|
|
150
144
|
}
|
|
151
145
|
|
|
152
146
|
// Verify checksum
|
|
@@ -166,7 +160,11 @@ impl SnapshotCursorManager {
|
|
|
166
160
|
}
|
|
167
161
|
|
|
168
162
|
/// Computes hash of authorization context.
|
|
169
|
-
pub fn hash_auth_context(
|
|
163
|
+
pub fn hash_auth_context(
|
|
164
|
+
actor_id: &str,
|
|
165
|
+
tenant_id: &str,
|
|
166
|
+
required_role: Option<&str>,
|
|
167
|
+
) -> String {
|
|
170
168
|
let mut hasher = Sha256::new();
|
|
171
169
|
hasher.update(actor_id.as_bytes());
|
|
172
170
|
hasher.update(tenant_id.as_bytes());
|
|
@@ -289,10 +287,8 @@ mod tests {
|
|
|
289
287
|
#[test]
|
|
290
288
|
fn reject_cursor_when_auth_context_changes() {
|
|
291
289
|
let manager = SnapshotCursorManager::new("release_jobs".into());
|
|
292
|
-
let auth_hash1 =
|
|
293
|
-
|
|
294
|
-
let auth_hash2 =
|
|
295
|
-
SnapshotCursorManager::hash_auth_context("user_123", "tenant_other", None);
|
|
290
|
+
let auth_hash1 = SnapshotCursorManager::hash_auth_context("user_123", "tenant_prod", None);
|
|
291
|
+
let auth_hash2 = SnapshotCursorManager::hash_auth_context("user_123", "tenant_other", None);
|
|
296
292
|
|
|
297
293
|
let encoded = manager
|
|
298
294
|
.create_cursor("snap_123".into(), 5, 0, auth_hash1)
|
|
@@ -67,6 +67,16 @@ pub struct PlatformUser {
|
|
|
67
67
|
pub status: MembershipStatus,
|
|
68
68
|
pub created_at: u64,
|
|
69
69
|
}
|
|
70
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
71
|
+
pub struct ApplicationServicePrincipal {
|
|
72
|
+
pub service_id: String,
|
|
73
|
+
pub tenant_id: String,
|
|
74
|
+
pub application_id: String,
|
|
75
|
+
pub capabilities: Vec<String>,
|
|
76
|
+
pub status: MembershipStatus,
|
|
77
|
+
pub created_by: String,
|
|
78
|
+
pub created_at: u64,
|
|
79
|
+
}
|
|
70
80
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
71
81
|
#[serde(rename_all = "snake_case")]
|
|
72
82
|
pub enum MembershipStatus {
|
|
@@ -104,6 +114,8 @@ struct PlatformRecords {
|
|
|
104
114
|
applications: Vec<Application>,
|
|
105
115
|
tenant_memberships: Vec<TenantMembership>,
|
|
106
116
|
application_memberships: Vec<ApplicationMembership>,
|
|
117
|
+
#[serde(default)]
|
|
118
|
+
application_service_principals: Vec<ApplicationServicePrincipal>,
|
|
107
119
|
invitations: Vec<Invitation>,
|
|
108
120
|
audit: Vec<PlatformAuditEvent>,
|
|
109
121
|
}
|
|
@@ -151,7 +163,16 @@ fn platform_owner_user(user_id: &str) -> bool {
|
|
|
151
163
|
.split(',')
|
|
152
164
|
.map(|owner| owner.trim().to_lowercase())
|
|
153
165
|
.filter(|owner| !owner.is_empty())
|
|
154
|
-
.map(|owner|
|
|
166
|
+
.map(|owner| {
|
|
167
|
+
format!(
|
|
168
|
+
"user_{}",
|
|
169
|
+
URL_SAFE_NO_PAD
|
|
170
|
+
.encode(owner)
|
|
171
|
+
.chars()
|
|
172
|
+
.take(18)
|
|
173
|
+
.collect::<String>()
|
|
174
|
+
)
|
|
175
|
+
})
|
|
155
176
|
.any(|owner_id| owner_id == user_id)
|
|
156
177
|
}
|
|
157
178
|
|
|
@@ -241,6 +262,19 @@ pub fn application_role_grants() -> Vec<RoleGrant> {
|
|
|
241
262
|
"deployment:execute".into(),
|
|
242
263
|
],
|
|
243
264
|
},
|
|
265
|
+
RoleGrant {
|
|
266
|
+
role: "actor".into(),
|
|
267
|
+
capabilities: vec![
|
|
268
|
+
"application:read".into(),
|
|
269
|
+
"application:revision:read".into(),
|
|
270
|
+
"state:read".into(),
|
|
271
|
+
"state:write".into(),
|
|
272
|
+
"events:read".into(),
|
|
273
|
+
"workflows:run".into(),
|
|
274
|
+
"agents:run".into(),
|
|
275
|
+
"capabilities:run".into(),
|
|
276
|
+
],
|
|
277
|
+
},
|
|
244
278
|
RoleGrant {
|
|
245
279
|
role: "viewer".into(),
|
|
246
280
|
capabilities: vec![
|
|
@@ -271,6 +305,90 @@ impl TenancyStore {
|
|
|
271
305
|
records: Arc::new(RwLock::new(records)),
|
|
272
306
|
})
|
|
273
307
|
}
|
|
308
|
+
pub fn register_application_actor(
|
|
309
|
+
&self,
|
|
310
|
+
user_id: &str,
|
|
311
|
+
application_id: &str,
|
|
312
|
+
) -> Result<(), String> {
|
|
313
|
+
let mut records = self
|
|
314
|
+
.records
|
|
315
|
+
.write()
|
|
316
|
+
.map_err(|_| "tenancy store lock poisoned")?;
|
|
317
|
+
if !records
|
|
318
|
+
.applications
|
|
319
|
+
.iter()
|
|
320
|
+
.any(|application| application.id == application_id && application.status == "active")
|
|
321
|
+
{
|
|
322
|
+
return Err("application not found".into());
|
|
323
|
+
}
|
|
324
|
+
if !records.application_memberships.iter().any(|membership| {
|
|
325
|
+
membership.application_id == application_id
|
|
326
|
+
&& membership.user_id == user_id
|
|
327
|
+
&& membership.status == MembershipStatus::Active
|
|
328
|
+
}) {
|
|
329
|
+
let timestamp = now();
|
|
330
|
+
records.application_memberships.push(ApplicationMembership {
|
|
331
|
+
application_id: application_id.into(),
|
|
332
|
+
user_id: user_id.into(),
|
|
333
|
+
role: "actor".into(),
|
|
334
|
+
status: MembershipStatus::Active,
|
|
335
|
+
created_at: timestamp,
|
|
336
|
+
updated_at: timestamp,
|
|
337
|
+
});
|
|
338
|
+
self.persist(&records)?;
|
|
339
|
+
}
|
|
340
|
+
Ok(())
|
|
341
|
+
}
|
|
342
|
+
pub fn register_application_service(
|
|
343
|
+
&self,
|
|
344
|
+
actor: &str,
|
|
345
|
+
service_id: &str,
|
|
346
|
+
application_id: &str,
|
|
347
|
+
capabilities: Vec<String>,
|
|
348
|
+
) -> Result<(), String> {
|
|
349
|
+
let tenant_id = self
|
|
350
|
+
.application_tenant(application_id)
|
|
351
|
+
.ok_or_else(|| "application not found".to_string())?;
|
|
352
|
+
if !self.authorize_application(actor, &tenant_id, application_id, "members:write") {
|
|
353
|
+
return Err("forbidden".into());
|
|
354
|
+
}
|
|
355
|
+
if capabilities.is_empty() {
|
|
356
|
+
return Err("service capabilities are required".into());
|
|
357
|
+
}
|
|
358
|
+
let mut records = self
|
|
359
|
+
.records
|
|
360
|
+
.write()
|
|
361
|
+
.map_err(|_| "tenancy store lock poisoned")?;
|
|
362
|
+
if records
|
|
363
|
+
.application_service_principals
|
|
364
|
+
.iter()
|
|
365
|
+
.any(|principal| {
|
|
366
|
+
principal.service_id == service_id && principal.status == MembershipStatus::Active
|
|
367
|
+
})
|
|
368
|
+
{
|
|
369
|
+
return Err("service principal already exists".into());
|
|
370
|
+
}
|
|
371
|
+
records
|
|
372
|
+
.application_service_principals
|
|
373
|
+
.push(ApplicationServicePrincipal {
|
|
374
|
+
service_id: service_id.into(),
|
|
375
|
+
tenant_id: tenant_id.clone(),
|
|
376
|
+
application_id: application_id.into(),
|
|
377
|
+
capabilities,
|
|
378
|
+
status: MembershipStatus::Active,
|
|
379
|
+
created_by: actor.into(),
|
|
380
|
+
created_at: now(),
|
|
381
|
+
});
|
|
382
|
+
Self::audit(
|
|
383
|
+
&mut records,
|
|
384
|
+
"application.service.created",
|
|
385
|
+
actor,
|
|
386
|
+
&tenant_id,
|
|
387
|
+
Some(application_id),
|
|
388
|
+
service_id,
|
|
389
|
+
);
|
|
390
|
+
self.persist(&records)
|
|
391
|
+
}
|
|
274
392
|
fn persist(&self, records: &PlatformRecords) -> Result<(), String> {
|
|
275
393
|
if let Some(parent) = self.path.parent() {
|
|
276
394
|
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
|
|
@@ -325,13 +443,16 @@ impl TenancyStore {
|
|
|
325
443
|
}
|
|
326
444
|
pub fn platform_inventory(
|
|
327
445
|
&self,
|
|
328
|
-
) -> Result<
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
446
|
+
) -> Result<
|
|
447
|
+
(
|
|
448
|
+
Vec<PlatformUser>,
|
|
449
|
+
Vec<Tenant>,
|
|
450
|
+
Vec<Application>,
|
|
451
|
+
Vec<TenantMembership>,
|
|
452
|
+
Vec<ApplicationMembership>,
|
|
453
|
+
),
|
|
454
|
+
String,
|
|
455
|
+
> {
|
|
335
456
|
let records = self
|
|
336
457
|
.records
|
|
337
458
|
.read()
|
|
@@ -382,7 +503,14 @@ impl TenancyStore {
|
|
|
382
503
|
.ok_or("platform user not found")?;
|
|
383
504
|
user.status = status;
|
|
384
505
|
let updated = user.clone();
|
|
385
|
-
Self::audit(
|
|
506
|
+
Self::audit(
|
|
507
|
+
&mut records,
|
|
508
|
+
"platform.user.updated",
|
|
509
|
+
actor,
|
|
510
|
+
"platform",
|
|
511
|
+
None,
|
|
512
|
+
user_id,
|
|
513
|
+
);
|
|
386
514
|
self.persist(&records)?;
|
|
387
515
|
Ok(updated)
|
|
388
516
|
}
|
|
@@ -616,13 +744,18 @@ impl TenancyStore {
|
|
|
616
744
|
records
|
|
617
745
|
.application_memberships
|
|
618
746
|
.retain(|membership| !application_ids.contains(&membership.application_id));
|
|
747
|
+
records
|
|
748
|
+
.application_service_principals
|
|
749
|
+
.retain(|principal| !application_ids.contains(&principal.application_id));
|
|
619
750
|
records
|
|
620
751
|
.tenant_memberships
|
|
621
752
|
.retain(|membership| membership.tenant_id != tenant_id);
|
|
622
753
|
records
|
|
623
754
|
.invitations
|
|
624
755
|
.retain(|invitation| invitation.tenant_id != tenant_id);
|
|
625
|
-
records
|
|
756
|
+
records
|
|
757
|
+
.tenants
|
|
758
|
+
.retain(|candidate| candidate.id != tenant_id);
|
|
626
759
|
self.persist(&records)?;
|
|
627
760
|
Ok(tenant_id)
|
|
628
761
|
}
|
|
@@ -708,6 +841,9 @@ impl TenancyStore {
|
|
|
708
841
|
records
|
|
709
842
|
.application_memberships
|
|
710
843
|
.retain(|membership| membership.application_id != application_id);
|
|
844
|
+
records
|
|
845
|
+
.application_service_principals
|
|
846
|
+
.retain(|principal| principal.application_id != application_id);
|
|
711
847
|
self.persist(&records)
|
|
712
848
|
}
|
|
713
849
|
|
|
@@ -852,9 +988,6 @@ impl TenancyStore {
|
|
|
852
988
|
application_id: &str,
|
|
853
989
|
capability: &str,
|
|
854
990
|
) -> bool {
|
|
855
|
-
if !self.platform_user_active(user_id) {
|
|
856
|
-
return false;
|
|
857
|
-
}
|
|
858
991
|
let records = match self.records.read() {
|
|
859
992
|
Ok(records) => records,
|
|
860
993
|
Err(_) => return false,
|
|
@@ -866,6 +999,30 @@ impl TenancyStore {
|
|
|
866
999
|
{
|
|
867
1000
|
return false;
|
|
868
1001
|
}
|
|
1002
|
+
if records
|
|
1003
|
+
.application_service_principals
|
|
1004
|
+
.iter()
|
|
1005
|
+
.any(|principal| {
|
|
1006
|
+
principal.service_id == user_id
|
|
1007
|
+
&& principal.tenant_id == tenant_id
|
|
1008
|
+
&& principal.application_id == application_id
|
|
1009
|
+
&& principal.status == MembershipStatus::Active
|
|
1010
|
+
&& principal
|
|
1011
|
+
.capabilities
|
|
1012
|
+
.iter()
|
|
1013
|
+
.any(|allowed| allowed == "*" || allowed == capability)
|
|
1014
|
+
})
|
|
1015
|
+
{
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
if records
|
|
1019
|
+
.users
|
|
1020
|
+
.iter()
|
|
1021
|
+
.find(|user| user.user_id == user_id)
|
|
1022
|
+
.is_some_and(|user| user.status != MembershipStatus::Active)
|
|
1023
|
+
{
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
869
1026
|
if platform_owner_user(user_id) {
|
|
870
1027
|
return true;
|
|
871
1028
|
}
|
|
@@ -1143,4 +1300,29 @@ mod tests {
|
|
|
1143
1300
|
"application:revision:promote"
|
|
1144
1301
|
));
|
|
1145
1302
|
}
|
|
1303
|
+
|
|
1304
|
+
#[test]
|
|
1305
|
+
fn application_service_is_bound_to_one_application_and_explicit_capabilities() {
|
|
1306
|
+
let store = store("application-service");
|
|
1307
|
+
let tenant = store.create_tenant("owner", "Acme").unwrap();
|
|
1308
|
+
let app = store
|
|
1309
|
+
.create_application("owner", &tenant.id, "Runtime")
|
|
1310
|
+
.unwrap();
|
|
1311
|
+
let other = store
|
|
1312
|
+
.create_application("owner", &tenant.id, "Other")
|
|
1313
|
+
.unwrap();
|
|
1314
|
+
|
|
1315
|
+
store
|
|
1316
|
+
.register_application_service(
|
|
1317
|
+
"owner",
|
|
1318
|
+
"key_runtime",
|
|
1319
|
+
&app.id,
|
|
1320
|
+
vec!["application:read".into(), "state:read".into()],
|
|
1321
|
+
)
|
|
1322
|
+
.unwrap();
|
|
1323
|
+
|
|
1324
|
+
assert!(store.authorize_application("key_runtime", &tenant.id, &app.id, "state:read"));
|
|
1325
|
+
assert!(!store.authorize_application("key_runtime", &tenant.id, &app.id, "state:write"));
|
|
1326
|
+
assert!(!store.authorize_application("key_runtime", &tenant.id, &other.id, "state:read"));
|
|
1327
|
+
}
|
|
1146
1328
|
}
|
|
@@ -159,10 +159,7 @@ impl PolicyEvaluator {
|
|
|
159
159
|
};
|
|
160
160
|
|
|
161
161
|
if !policy.operations.read {
|
|
162
|
-
return PolicyDecision::Denied(format!(
|
|
163
|
-
"read operation not allowed on {}",
|
|
164
|
-
resource
|
|
165
|
-
));
|
|
162
|
+
return PolicyDecision::Denied(format!("read operation not allowed on {}", resource));
|
|
166
163
|
}
|
|
167
164
|
|
|
168
165
|
self.check_tenant_access(context, &policy, record)
|
|
@@ -181,10 +178,7 @@ impl PolicyEvaluator {
|
|
|
181
178
|
};
|
|
182
179
|
|
|
183
180
|
if !policy.operations.write {
|
|
184
|
-
return PolicyDecision::Denied(format!(
|
|
185
|
-
"write operation not allowed on {}",
|
|
186
|
-
resource
|
|
187
|
-
));
|
|
181
|
+
return PolicyDecision::Denied(format!("write operation not allowed on {}", resource));
|
|
188
182
|
}
|
|
189
183
|
|
|
190
184
|
// Check that new record has correct tenant_id
|
|
@@ -206,20 +200,19 @@ impl PolicyEvaluator {
|
|
|
206
200
|
};
|
|
207
201
|
|
|
208
202
|
if !policy.operations.write {
|
|
209
|
-
return PolicyDecision::Denied(format!(
|
|
210
|
-
"write operation not allowed on {}",
|
|
211
|
-
resource
|
|
212
|
-
));
|
|
203
|
+
return PolicyDecision::Denied(format!("write operation not allowed on {}", resource));
|
|
213
204
|
}
|
|
214
205
|
|
|
215
206
|
// Must authorize existing record
|
|
216
|
-
if let PolicyDecision::Denied(msg) =
|
|
207
|
+
if let PolicyDecision::Denied(msg) =
|
|
208
|
+
self.check_tenant_access(context, &policy, existing_record)
|
|
217
209
|
{
|
|
218
210
|
return PolicyDecision::Denied(format!("cannot update: {}", msg));
|
|
219
211
|
}
|
|
220
212
|
|
|
221
213
|
// Must authorize new record state
|
|
222
|
-
if let PolicyDecision::Denied(msg) = self.check_tenant_access(context, &policy, new_record)
|
|
214
|
+
if let PolicyDecision::Denied(msg) = self.check_tenant_access(context, &policy, new_record)
|
|
215
|
+
{
|
|
223
216
|
return PolicyDecision::Denied(format!("update would result in: {}", msg));
|
|
224
217
|
}
|
|
225
218
|
|
|
@@ -257,10 +250,7 @@ impl PolicyEvaluator {
|
|
|
257
250
|
};
|
|
258
251
|
|
|
259
252
|
if !policy.operations.delete {
|
|
260
|
-
return PolicyDecision::Denied(format!(
|
|
261
|
-
"delete operation not allowed on {}",
|
|
262
|
-
resource
|
|
263
|
-
));
|
|
253
|
+
return PolicyDecision::Denied(format!("delete operation not allowed on {}", resource));
|
|
264
254
|
}
|
|
265
255
|
|
|
266
256
|
self.check_tenant_access(context, &policy, record)
|
|
@@ -297,9 +287,7 @@ impl PolicyEvaluator {
|
|
|
297
287
|
let record_tenant = self.extract_tenant_id(policy, record);
|
|
298
288
|
|
|
299
289
|
if record_tenant != Some(context.tenant_id.clone()) {
|
|
300
|
-
return PolicyDecision::Denied(format!(
|
|
301
|
-
"record belongs to different tenant"
|
|
302
|
-
));
|
|
290
|
+
return PolicyDecision::Denied(format!("record belongs to different tenant"));
|
|
303
291
|
}
|
|
304
292
|
|
|
305
293
|
// Check owner field if policy specifies one
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
use std::collections::HashMap;
|
|
2
|
-
use std::sync::{Arc, Mutex};
|
|
3
1
|
use serde::{Deserialize, Serialize};
|
|
4
2
|
use sha2::{Digest, Sha256};
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
use std::sync::{Arc, Mutex};
|
|
5
5
|
|
|
6
6
|
/// Durable transaction idempotency: prevents re-execution of already-committed transactions.
|
|
7
7
|
/// Every committed transaction is stored with its result. If the client retries with the
|
|
@@ -148,7 +148,9 @@ impl TransactionIdempotencyStore {
|
|
|
148
148
|
let total_entries = store.values().map(|v| v.len()).sum();
|
|
149
149
|
let replay_protected = store
|
|
150
150
|
.values()
|
|
151
|
-
.filter(|entries|
|
|
151
|
+
.filter(|entries| {
|
|
152
|
+
!entries.is_empty() && entries[0].status == TransactionStatus::Committed
|
|
153
|
+
})
|
|
152
154
|
.count();
|
|
153
155
|
|
|
154
156
|
IdempotencyStatistics {
|
|
@@ -227,7 +227,8 @@ impl TransactionRecoveryStore {
|
|
|
227
227
|
Some(r) => {
|
|
228
228
|
// Same payload = safe to retry
|
|
229
229
|
if r.payload_hash == payload_hash {
|
|
230
|
-
Ok(r.status == TransactionStatus::Committed
|
|
230
|
+
Ok(r.status == TransactionStatus::Committed
|
|
231
|
+
|| r.status == TransactionStatus::Unknown)
|
|
231
232
|
} else {
|
|
232
233
|
// Different payload = conflict
|
|
233
234
|
Ok(false)
|
|
@@ -323,7 +324,9 @@ mod tests {
|
|
|
323
324
|
.record_committed("txn_123".into(), hash1, 42)
|
|
324
325
|
.expect("record failed");
|
|
325
326
|
|
|
326
|
-
let is_safe = store
|
|
327
|
+
let is_safe = store
|
|
328
|
+
.is_retry_safe("txn_123", &hash2)
|
|
329
|
+
.expect("check failed");
|
|
327
330
|
assert!(!is_safe);
|
|
328
331
|
}
|
|
329
332
|
|
|
@@ -332,7 +335,9 @@ mod tests {
|
|
|
332
335
|
let store = TransactionRecoveryStore::new();
|
|
333
336
|
let hash = test_payload_hash();
|
|
334
337
|
|
|
335
|
-
let is_safe = store
|
|
338
|
+
let is_safe = store
|
|
339
|
+
.is_retry_safe("txn_unknown", &hash)
|
|
340
|
+
.expect("check failed");
|
|
336
341
|
assert!(is_safe);
|
|
337
342
|
}
|
|
338
343
|
|
|
@@ -381,11 +386,7 @@ mod tests {
|
|
|
381
386
|
let hash = test_payload_hash();
|
|
382
387
|
|
|
383
388
|
store
|
|
384
|
-
.record_rejected(
|
|
385
|
-
"txn_123".into(),
|
|
386
|
-
hash,
|
|
387
|
-
"required field missing".into(),
|
|
388
|
-
)
|
|
389
|
+
.record_rejected("txn_123".into(), hash, "required field missing".into())
|
|
389
390
|
.expect("record failed");
|
|
390
391
|
|
|
391
392
|
let status = store.get_status("txn_123").expect("get failed");
|
package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs
CHANGED
|
@@ -124,10 +124,7 @@ mod revision_recovery_tests {
|
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
// Both requests should have the same recovery_id
|
|
127
|
-
assert_eq!(
|
|
128
|
-
request1.get("recoveryId"),
|
|
129
|
-
request2.get("recoveryId")
|
|
130
|
-
);
|
|
127
|
+
assert_eq!(request1.get("recoveryId"), request2.get("recoveryId"));
|
|
131
128
|
}
|
|
132
129
|
|
|
133
130
|
/// Test environment parameter handling
|