@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
|
@@ -283,10 +283,18 @@ impl ApplicationContractStore {
|
|
|
283
283
|
confidence: 0.0,
|
|
284
284
|
evidence_required: vec!["contract evidence".into()],
|
|
285
285
|
suggested_questions: vec![match p {
|
|
286
|
-
"actors" =>
|
|
287
|
-
|
|
286
|
+
"actors" => {
|
|
287
|
+
"Who will use this application, and what roles do they have?".into()
|
|
288
|
+
}
|
|
289
|
+
"entities" => {
|
|
290
|
+
"What information or records should the application keep track of?"
|
|
291
|
+
.into()
|
|
292
|
+
}
|
|
288
293
|
"actions" => "What should people be able to do with those records?".into(),
|
|
289
|
-
"permissions" =>
|
|
294
|
+
"permissions" => {
|
|
295
|
+
"Who can view, create, edit, delete, or administer this information?"
|
|
296
|
+
.into()
|
|
297
|
+
}
|
|
290
298
|
_ => format!("What should FeltDB understand about {p}?"),
|
|
291
299
|
}],
|
|
292
300
|
candidate_resolutions: vec![],
|
|
@@ -224,10 +224,7 @@ impl CertificationHarness {
|
|
|
224
224
|
|
|
225
225
|
/// Gets the count of tests in a category.
|
|
226
226
|
pub fn test_count(&self, category: TestCategory) -> usize {
|
|
227
|
-
self.results
|
|
228
|
-
.get(&category)
|
|
229
|
-
.map(|v| v.len())
|
|
230
|
-
.unwrap_or(0)
|
|
227
|
+
self.results.get(&category).map(|v| v.len()).unwrap_or(0)
|
|
231
228
|
}
|
|
232
229
|
|
|
233
230
|
/// Gets all failed tests.
|
|
@@ -439,13 +436,10 @@ mod tests {
|
|
|
439
436
|
|
|
440
437
|
#[test]
|
|
441
438
|
fn test_execution_plan_builder() {
|
|
442
|
-
let plan = TestExecutionPlan::new(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
.with_parallel()
|
|
447
|
-
.with_timeout(600)
|
|
448
|
-
.with_retry(3);
|
|
439
|
+
let plan = TestExecutionPlan::new(TestSuiteDefinition::standard(), TestEnvironment::Local)
|
|
440
|
+
.with_parallel()
|
|
441
|
+
.with_timeout(600)
|
|
442
|
+
.with_retry(3);
|
|
449
443
|
|
|
450
444
|
assert!(plan.parallel_execution);
|
|
451
445
|
assert_eq!(plan.timeout_secs, 600);
|
|
@@ -480,19 +474,18 @@ mod tests {
|
|
|
480
474
|
fn test_environment_strings() {
|
|
481
475
|
assert_eq!(TestEnvironment::Local.as_str(), "local");
|
|
482
476
|
assert_eq!(TestEnvironment::ManagedStaging.as_str(), "managed_staging");
|
|
483
|
-
assert_eq!(
|
|
477
|
+
assert_eq!(
|
|
478
|
+
TestEnvironment::ManagedProduction.as_str(),
|
|
479
|
+
"managed_production"
|
|
480
|
+
);
|
|
484
481
|
}
|
|
485
482
|
|
|
486
483
|
#[test]
|
|
487
484
|
fn certification_report_serialization() {
|
|
488
485
|
let mut harness = CertificationHarness::new(TestEnvironment::Local);
|
|
489
486
|
|
|
490
|
-
let result =
|
|
491
|
-
"test_1".into(),
|
|
492
|
-
TestCategory::Operations,
|
|
493
|
-
"test_1".into(),
|
|
494
|
-
)
|
|
495
|
-
.passed(50);
|
|
487
|
+
let result =
|
|
488
|
+
TestResult::new("test_1".into(), TestCategory::Operations, "test_1".into()).passed(50);
|
|
496
489
|
|
|
497
490
|
harness.record_result(result);
|
|
498
491
|
|
|
@@ -512,11 +505,19 @@ mod tests {
|
|
|
512
505
|
|
|
513
506
|
for i in 0..3 {
|
|
514
507
|
let result = if i == 1 {
|
|
515
|
-
TestResult::new(
|
|
516
|
-
|
|
508
|
+
TestResult::new(
|
|
509
|
+
format!("test_{}", i),
|
|
510
|
+
TestCategory::Authorization,
|
|
511
|
+
format!("test_{}", i),
|
|
512
|
+
)
|
|
513
|
+
.failed("test failed".into(), 50)
|
|
517
514
|
} else {
|
|
518
|
-
TestResult::new(
|
|
519
|
-
|
|
515
|
+
TestResult::new(
|
|
516
|
+
format!("test_{}", i),
|
|
517
|
+
TestCategory::Authorization,
|
|
518
|
+
format!("test_{}", i),
|
|
519
|
+
)
|
|
520
|
+
.passed(50)
|
|
520
521
|
};
|
|
521
522
|
harness.record_result(result);
|
|
522
523
|
}
|
|
@@ -132,10 +132,7 @@ impl DelegationTokenIssuer {
|
|
|
132
132
|
let payload = self.canonicalize_for_signing(&token)?;
|
|
133
133
|
let signature = self.sign(&payload)?;
|
|
134
134
|
|
|
135
|
-
Ok(DelegationToken {
|
|
136
|
-
signature,
|
|
137
|
-
..token
|
|
138
|
-
})
|
|
135
|
+
Ok(DelegationToken { signature, ..token })
|
|
139
136
|
}
|
|
140
137
|
|
|
141
138
|
/// Canonicalizes token for consistent signing/verification.
|
|
@@ -180,11 +177,7 @@ impl DelegationTokenIssuer {
|
|
|
180
177
|
|
|
181
178
|
impl DelegationTokenValidator {
|
|
182
179
|
/// Creates a new token validator.
|
|
183
|
-
pub fn new(
|
|
184
|
-
expected_issuer: String,
|
|
185
|
-
expected_audience: String,
|
|
186
|
-
shared_secret: Vec<u8>,
|
|
187
|
-
) -> Self {
|
|
180
|
+
pub fn new(expected_issuer: String, expected_audience: String, shared_secret: Vec<u8>) -> Self {
|
|
188
181
|
Self {
|
|
189
182
|
expected_issuer,
|
|
190
183
|
expected_audience,
|
|
@@ -316,8 +309,7 @@ mod tests {
|
|
|
316
309
|
fn issue_and_validate_delegation_token() {
|
|
317
310
|
let secret = test_shared_secret();
|
|
318
311
|
let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
|
|
319
|
-
let validator =
|
|
320
|
-
DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
|
|
312
|
+
let validator = DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
|
|
321
313
|
|
|
322
314
|
let token = issuer
|
|
323
315
|
.issue(
|
|
@@ -337,8 +329,7 @@ mod tests {
|
|
|
337
329
|
fn reject_expired_token() {
|
|
338
330
|
let secret = test_shared_secret();
|
|
339
331
|
let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
|
|
340
|
-
let validator =
|
|
341
|
-
DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
|
|
332
|
+
let validator = DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
|
|
342
333
|
|
|
343
334
|
let mut token = issuer
|
|
344
335
|
.issue(
|
|
@@ -450,8 +441,7 @@ mod tests {
|
|
|
450
441
|
fn tampering_invalidates_signature() {
|
|
451
442
|
let secret = test_shared_secret();
|
|
452
443
|
let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
|
|
453
|
-
let validator =
|
|
454
|
-
DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
|
|
444
|
+
let validator = DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
|
|
455
445
|
|
|
456
446
|
let mut token = issuer
|
|
457
447
|
.issue(
|
|
@@ -114,10 +114,7 @@ impl DurableOperationStore {
|
|
|
114
114
|
.map_err(|_| "store lock poisoned".to_string())?;
|
|
115
115
|
|
|
116
116
|
if ops.contains_key(&operation_id) {
|
|
117
|
-
return Err(format!(
|
|
118
|
-
"operation already exists: {}",
|
|
119
|
-
operation_id
|
|
120
|
-
));
|
|
117
|
+
return Err(format!("operation already exists: {}", operation_id));
|
|
121
118
|
}
|
|
122
119
|
|
|
123
120
|
let now = now();
|
|
@@ -158,7 +155,8 @@ impl DurableOperationStore {
|
|
|
158
155
|
.ok_or(format!("operation not found: {}", operation_id))?;
|
|
159
156
|
|
|
160
157
|
// Can only claim Pending or Unknown operations
|
|
161
|
-
if operation.state != OperationState::Pending && operation.state != OperationState::Unknown
|
|
158
|
+
if operation.state != OperationState::Pending && operation.state != OperationState::Unknown
|
|
159
|
+
{
|
|
162
160
|
return Err(format!(
|
|
163
161
|
"cannot claim operation in state {:?}",
|
|
164
162
|
operation.state
|
|
@@ -339,7 +337,8 @@ impl DurableOperationStore {
|
|
|
339
337
|
}
|
|
340
338
|
|
|
341
339
|
// Must be in Claimed or Running state
|
|
342
|
-
if operation.state != OperationState::Claimed && operation.state != OperationState::Running
|
|
340
|
+
if operation.state != OperationState::Claimed && operation.state != OperationState::Running
|
|
341
|
+
{
|
|
343
342
|
return Err(format!(
|
|
344
343
|
"cannot fail operation in state {:?}",
|
|
345
344
|
operation.state
|
|
@@ -356,10 +355,7 @@ impl DurableOperationStore {
|
|
|
356
355
|
}
|
|
357
356
|
|
|
358
357
|
/// Marks an operation as Unknown (when lease expires).
|
|
359
|
-
pub fn mark_unknown(
|
|
360
|
-
&self,
|
|
361
|
-
operation_id: &str,
|
|
362
|
-
) -> Result<DurableOperation, String> {
|
|
358
|
+
pub fn mark_unknown(&self, operation_id: &str) -> Result<DurableOperation, String> {
|
|
363
359
|
let mut ops = self
|
|
364
360
|
.operations
|
|
365
361
|
.write()
|
|
@@ -372,7 +368,8 @@ impl DurableOperationStore {
|
|
|
372
368
|
let now = now();
|
|
373
369
|
|
|
374
370
|
// Can only mark Unknown if in Claimed or Running state
|
|
375
|
-
if operation.state != OperationState::Claimed && operation.state != OperationState::Running
|
|
371
|
+
if operation.state != OperationState::Claimed && operation.state != OperationState::Running
|
|
372
|
+
{
|
|
376
373
|
return Err(format!(
|
|
377
374
|
"cannot mark Unknown operation in state {:?}",
|
|
378
375
|
operation.state
|
|
@@ -484,9 +481,7 @@ impl DurableOperationStore {
|
|
|
484
481
|
.map_err(|_| "store lock poisoned".to_string())?;
|
|
485
482
|
|
|
486
483
|
let before_len = ops.len();
|
|
487
|
-
ops.retain(|_, op|
|
|
488
|
-
!(op.state == OperationState::Completed && op.updated_at < cutoff_time)
|
|
489
|
-
});
|
|
484
|
+
ops.retain(|_, op| !(op.state == OperationState::Completed && op.updated_at < cutoff_time));
|
|
490
485
|
let after_len = ops.len();
|
|
491
486
|
|
|
492
487
|
Ok(before_len - after_len)
|
|
@@ -755,9 +750,7 @@ mod tests {
|
|
|
755
750
|
.claim_operation("op_123", "worker_1".into(), 60)
|
|
756
751
|
.expect("claim failed");
|
|
757
752
|
|
|
758
|
-
let unknown = store
|
|
759
|
-
.mark_unknown("op_123")
|
|
760
|
-
.expect("mark unknown failed");
|
|
753
|
+
let unknown = store.mark_unknown("op_123").expect("mark unknown failed");
|
|
761
754
|
assert_eq!(unknown.state, OperationState::Unknown);
|
|
762
755
|
|
|
763
756
|
let reconciled = store
|
|
@@ -826,9 +819,7 @@ mod tests {
|
|
|
826
819
|
)
|
|
827
820
|
.expect("create failed");
|
|
828
821
|
|
|
829
|
-
let ops = store
|
|
830
|
-
.list_operations("tenant_prod")
|
|
831
|
-
.expect("list failed");
|
|
822
|
+
let ops = store.list_operations("tenant_prod").expect("list failed");
|
|
832
823
|
assert_eq!(ops.len(), 2);
|
|
833
824
|
}
|
|
834
825
|
|
|
@@ -915,9 +906,7 @@ mod tests {
|
|
|
915
906
|
.expect("cleanup failed");
|
|
916
907
|
assert_eq!(deleted, 1);
|
|
917
908
|
|
|
918
|
-
let ops = store
|
|
919
|
-
.list_operations("tenant_prod")
|
|
920
|
-
.expect("list failed");
|
|
909
|
+
let ops = store.list_operations("tenant_prod").expect("list failed");
|
|
921
910
|
assert_eq!(ops.len(), 1);
|
|
922
911
|
assert_eq!(ops[0].operation_id, "op_pending");
|
|
923
912
|
}
|
|
@@ -978,11 +967,7 @@ mod tests {
|
|
|
978
967
|
.expect("start failed");
|
|
979
968
|
|
|
980
969
|
store
|
|
981
|
-
.complete_operation(
|
|
982
|
-
"op_123",
|
|
983
|
-
"worker_1",
|
|
984
|
-
serde_json::json!({"result": "done"}),
|
|
985
|
-
)
|
|
970
|
+
.complete_operation("op_123", "worker_1", serde_json::json!({"result": "done"}))
|
|
986
971
|
.expect("complete failed");
|
|
987
972
|
|
|
988
973
|
let final_op = store.get_operation("op_123").expect("get failed");
|
|
@@ -137,6 +137,94 @@ impl IdentityStore {
|
|
|
137
137
|
.identities
|
|
138
138
|
.clone())
|
|
139
139
|
}
|
|
140
|
+
pub fn sign_up(
|
|
141
|
+
&self,
|
|
142
|
+
email: &str,
|
|
143
|
+
password: &str,
|
|
144
|
+
display_name: Option<String>,
|
|
145
|
+
) -> Result<(Identity, IdentitySession, String), String> {
|
|
146
|
+
let normalized = email.trim().to_lowercase();
|
|
147
|
+
if normalized.is_empty() || password.len() < 8 {
|
|
148
|
+
return Err("email and a password of at least 8 characters are required".into());
|
|
149
|
+
}
|
|
150
|
+
if self
|
|
151
|
+
.state
|
|
152
|
+
.read()
|
|
153
|
+
.map_err(|_| "identity lock poisoned")?
|
|
154
|
+
.identities
|
|
155
|
+
.iter()
|
|
156
|
+
.any(|value| value.email.as_deref() == Some(&normalized))
|
|
157
|
+
{
|
|
158
|
+
return Err("identity already exists".into());
|
|
159
|
+
}
|
|
160
|
+
let mut identity = self.normalize(
|
|
161
|
+
"password",
|
|
162
|
+
&normalized,
|
|
163
|
+
Some(normalized.clone()),
|
|
164
|
+
display_name,
|
|
165
|
+
serde_json::json!({}),
|
|
166
|
+
)?;
|
|
167
|
+
identity.status = IdentityStatus::Active;
|
|
168
|
+
let mut state = self.state.write().map_err(|_| "identity lock poisoned")?;
|
|
169
|
+
if let Some(stored) = state
|
|
170
|
+
.identities
|
|
171
|
+
.iter_mut()
|
|
172
|
+
.find(|value| value.identity_id == identity.identity_id)
|
|
173
|
+
{
|
|
174
|
+
stored.status = IdentityStatus::Active;
|
|
175
|
+
}
|
|
176
|
+
state
|
|
177
|
+
.password_hashes
|
|
178
|
+
.insert(identity.subject_id.clone(), Self::hash_password(password)?);
|
|
179
|
+
self.persist(&state)?;
|
|
180
|
+
drop(state);
|
|
181
|
+
let (session, token) = self.issue_session(&identity, "password", None, 15 * 60)?;
|
|
182
|
+
Ok((identity, session, token))
|
|
183
|
+
}
|
|
184
|
+
pub fn sign_in(
|
|
185
|
+
&self,
|
|
186
|
+
email: &str,
|
|
187
|
+
password: &str,
|
|
188
|
+
) -> Result<(Identity, IdentitySession, String), String> {
|
|
189
|
+
let normalized = email.trim().to_lowercase();
|
|
190
|
+
let state = self.state.read().map_err(|_| "identity lock poisoned")?;
|
|
191
|
+
let identity = state
|
|
192
|
+
.identities
|
|
193
|
+
.iter()
|
|
194
|
+
.find(|value| {
|
|
195
|
+
value.email.as_deref() == Some(&normalized)
|
|
196
|
+
&& value.status == IdentityStatus::Active
|
|
197
|
+
})
|
|
198
|
+
.cloned()
|
|
199
|
+
.ok_or("invalid credentials")?;
|
|
200
|
+
let valid = state
|
|
201
|
+
.password_hashes
|
|
202
|
+
.get(&identity.subject_id)
|
|
203
|
+
.is_some_and(|hash| Self::verify_password(hash, password));
|
|
204
|
+
drop(state);
|
|
205
|
+
if !valid {
|
|
206
|
+
return Err("invalid credentials".into());
|
|
207
|
+
}
|
|
208
|
+
let (session, token) = self.issue_session(&identity, "password", None, 15 * 60)?;
|
|
209
|
+
Ok((identity, session, token))
|
|
210
|
+
}
|
|
211
|
+
pub fn authenticate_session(&self, token: &str) -> Option<IdentitySession> {
|
|
212
|
+
let current = now();
|
|
213
|
+
self.state
|
|
214
|
+
.read()
|
|
215
|
+
.ok()?
|
|
216
|
+
.sessions
|
|
217
|
+
.iter()
|
|
218
|
+
.find(|value| {
|
|
219
|
+
value
|
|
220
|
+
.refresh_hash
|
|
221
|
+
.as_deref()
|
|
222
|
+
.is_some_and(|hash| Self::verify_password(hash, token))
|
|
223
|
+
&& value.status == "active"
|
|
224
|
+
&& value.expires_at > current
|
|
225
|
+
})
|
|
226
|
+
.cloned()
|
|
227
|
+
}
|
|
140
228
|
pub fn sessions(&self, subject: &str) -> Result<Vec<IdentitySession>, String> {
|
|
141
229
|
Ok(self
|
|
142
230
|
.state
|
|
@@ -154,9 +242,10 @@ impl IdentityStore {
|
|
|
154
242
|
provider: &str,
|
|
155
243
|
device: Option<String>,
|
|
156
244
|
ttl: u64,
|
|
157
|
-
) -> Result<IdentitySession, String> {
|
|
245
|
+
) -> Result<(IdentitySession, String), String> {
|
|
158
246
|
let mut s = self.state.write().map_err(|_| "identity lock poisoned")?;
|
|
159
247
|
let t = now();
|
|
248
|
+
let token = id("session_token");
|
|
160
249
|
let x = IdentitySession {
|
|
161
250
|
session_id: id("session"),
|
|
162
251
|
identity_id: identity.identity_id.clone(),
|
|
@@ -168,11 +257,11 @@ impl IdentityStore {
|
|
|
168
257
|
provider: provider.into(),
|
|
169
258
|
status: "active".into(),
|
|
170
259
|
revocation_reason: None,
|
|
171
|
-
refresh_hash: Some(
|
|
260
|
+
refresh_hash: Some(Self::hash_password(&token)?),
|
|
172
261
|
};
|
|
173
262
|
s.sessions.push(x.clone());
|
|
174
263
|
self.persist(&s)?;
|
|
175
|
-
Ok(x)
|
|
264
|
+
Ok((x, token))
|
|
176
265
|
}
|
|
177
266
|
pub fn revoke_session(&self, subject: &str, session: &str, reason: &str) -> Result<(), String> {
|
|
178
267
|
let mut s = self.state.write().map_err(|_| "identity lock poisoned")?;
|
|
@@ -247,4 +336,23 @@ mod tests {
|
|
|
247
336
|
assert!(IdentityStore::verify_password(&h, "correct horse"));
|
|
248
337
|
assert!(!IdentityStore::verify_password(&h, "wrong"));
|
|
249
338
|
}
|
|
339
|
+
#[test]
|
|
340
|
+
fn actor_tokens_are_short_lived_hashed_and_revocable() {
|
|
341
|
+
let p = std::env::temp_dir().join(format!("identity-session-{}.json", id("test")));
|
|
342
|
+
let store = IdentityStore::load(&p).unwrap();
|
|
343
|
+
let (identity, session, token) = store
|
|
344
|
+
.sign_up("actor@example.com", "correct horse", Some("Actor".into()))
|
|
345
|
+
.unwrap();
|
|
346
|
+
assert!(session.expires_at - session.issued_at <= 15 * 60);
|
|
347
|
+
assert_ne!(session.refresh_hash.as_deref(), Some(token.as_str()));
|
|
348
|
+
assert_eq!(
|
|
349
|
+
store.authenticate_session(&token).unwrap().subject_id,
|
|
350
|
+
identity.subject_id
|
|
351
|
+
);
|
|
352
|
+
store
|
|
353
|
+
.revoke_session(&identity.subject_id, &session.session_id, "test")
|
|
354
|
+
.unwrap();
|
|
355
|
+
assert!(store.authenticate_session(&token).is_none());
|
|
356
|
+
let _ = std::fs::remove_file(p);
|
|
357
|
+
}
|
|
250
358
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
use axum::{
|
|
2
|
-
extract::{Path, State},
|
|
2
|
+
extract::{Extension, Path, State},
|
|
3
3
|
http::StatusCode,
|
|
4
4
|
Json,
|
|
5
5
|
};
|
|
6
6
|
use serde::{Deserialize, Serialize};
|
|
7
7
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
8
8
|
|
|
9
|
-
use crate::{
|
|
9
|
+
use crate::{
|
|
10
|
+
app_state::AppState,
|
|
11
|
+
auth::{ApiKeySummary, Principal},
|
|
12
|
+
};
|
|
10
13
|
|
|
11
14
|
#[derive(Debug, Serialize)]
|
|
12
15
|
pub struct KeyListResponse {
|
|
@@ -18,6 +21,7 @@ pub struct CreateKeyRequest {
|
|
|
18
21
|
pub id: Option<String>,
|
|
19
22
|
pub namespace: String,
|
|
20
23
|
pub scopes: Vec<String>,
|
|
24
|
+
pub application_id: Option<String>,
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
#[derive(Debug, Serialize)]
|
|
@@ -38,6 +42,7 @@ pub async fn list_keys(
|
|
|
38
42
|
|
|
39
43
|
pub async fn create_key(
|
|
40
44
|
State(state): State<AppState>,
|
|
45
|
+
Extension(principal): Extension<Principal>,
|
|
41
46
|
Json(req): Json<CreateKeyRequest>,
|
|
42
47
|
) -> Result<(StatusCode, Json<CreateKeyResponse>), (StatusCode, String)> {
|
|
43
48
|
let timestamp = SystemTime::now()
|
|
@@ -46,15 +51,31 @@ pub async fn create_key(
|
|
|
46
51
|
.as_secs();
|
|
47
52
|
|
|
48
53
|
let name = req.id.unwrap_or_else(|| format!("key-{}", timestamp));
|
|
54
|
+
let scopes = req.scopes;
|
|
55
|
+
let mut authentication_namespaces = vec![req.namespace];
|
|
56
|
+
if !authentication_namespaces
|
|
57
|
+
.iter()
|
|
58
|
+
.any(|value| value == state.namespace.as_ref())
|
|
59
|
+
{
|
|
60
|
+
authentication_namespaces.push(state.namespace.to_string());
|
|
61
|
+
}
|
|
49
62
|
let key = state
|
|
50
63
|
.keys
|
|
51
|
-
.create(
|
|
52
|
-
name,
|
|
53
|
-
req.scopes,
|
|
54
|
-
vec![req.namespace],
|
|
55
|
-
)
|
|
64
|
+
.create(name, scopes.clone(), authentication_namespaces)
|
|
56
65
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
|
57
66
|
|
|
67
|
+
if let Some(application_id) = req.application_id {
|
|
68
|
+
if let Err(error) = state.tenancy.register_application_service(
|
|
69
|
+
&principal.key_id,
|
|
70
|
+
&key.id,
|
|
71
|
+
&application_id,
|
|
72
|
+
scopes,
|
|
73
|
+
) {
|
|
74
|
+
let _ = state.keys.revoke(&key.id);
|
|
75
|
+
return Err((StatusCode::FORBIDDEN, error));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
58
79
|
Ok((
|
|
59
80
|
StatusCode::CREATED,
|
|
60
81
|
Json(CreateKeyResponse {
|
|
@@ -6,17 +6,17 @@ pub mod auth;
|
|
|
6
6
|
pub mod authenticated_principal;
|
|
7
7
|
pub mod authorization;
|
|
8
8
|
pub mod backup;
|
|
9
|
-
pub mod certification_harness;
|
|
10
|
-
pub mod delegation_token;
|
|
11
|
-
pub mod durable_operations;
|
|
12
|
-
pub mod key_management;
|
|
13
9
|
pub mod causal;
|
|
14
10
|
pub mod certification;
|
|
11
|
+
pub mod certification_harness;
|
|
15
12
|
pub mod clock;
|
|
16
13
|
pub mod cluster;
|
|
17
14
|
pub mod connections;
|
|
18
15
|
pub mod content;
|
|
16
|
+
pub mod delegation_token;
|
|
17
|
+
pub mod durable_operations;
|
|
19
18
|
pub mod identity;
|
|
19
|
+
pub mod key_management;
|
|
20
20
|
pub mod key_provider;
|
|
21
21
|
pub mod leases;
|
|
22
22
|
pub mod managed_diagnostics;
|
|
@@ -29,8 +29,8 @@ pub mod releases;
|
|
|
29
29
|
pub mod request_telemetry;
|
|
30
30
|
pub mod sessions;
|
|
31
31
|
pub mod snapshot_cursor;
|
|
32
|
-
pub mod tenant_policies;
|
|
33
32
|
pub mod tenancy;
|
|
33
|
+
pub mod tenant_policies;
|
|
34
34
|
pub mod transaction_idempotency;
|
|
35
35
|
pub mod transaction_recovery;
|
|
36
36
|
pub mod versions;
|