@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.
Files changed (147) hide show
  1. package/README.md +25 -0
  2. package/dist/application-development.d.ts +78 -0
  3. package/dist/application-development.d.ts.map +1 -0
  4. package/dist/application-development.js +153 -0
  5. package/dist/application-manifest.d.ts +6 -1
  6. package/dist/application-manifest.d.ts.map +1 -1
  7. package/dist/application-manifest.js +10 -3
  8. package/dist/canonical-application.d.ts +27 -0
  9. package/dist/canonical-application.d.ts.map +1 -0
  10. package/dist/canonical-application.js +46 -0
  11. package/dist/cli/ai-model-config.js +97 -0
  12. package/dist/cli/cli.js +1 -1
  13. package/dist/cli/commands.js +664 -120
  14. package/dist/cli/index.js +1 -1
  15. package/dist/cli/managed-environment.js +6 -0
  16. package/dist/cli/managed-project.js +6 -0
  17. package/dist/cli/proposal-client.js +63 -0
  18. package/dist/cli/publish-engine.js +165 -0
  19. package/dist/cli/source-sync.js +183 -0
  20. package/dist/create/cli.js +11 -2
  21. package/dist/create/create.js +38 -16
  22. package/dist/create/managed-account.js +82 -11
  23. package/dist/create/package-versions.js +1 -1
  24. package/dist/create/server-source/crates/feltdb/src/application.rs +41 -1
  25. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +84 -6
  26. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
  27. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
  28. package/dist/create/server-source/crates/feltdb/src/lib.rs +20 -2
  29. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +12 -24
  30. package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
  31. package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
  32. package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
  33. package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
  34. package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
  35. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +111 -3
  36. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +28 -7
  37. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +5 -5
  38. package/dist/create/server-source/crates/feltdb-server/src/main.rs +1960 -143
  39. package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
  40. package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
  41. package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
  42. package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
  43. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +195 -13
  44. package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
  45. package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
  46. package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
  47. package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
  48. package/dist/create/template/default-project/README.md +57 -0
  49. package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
  50. package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
  51. package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
  52. package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
  53. package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
  54. package/dist/create/template/default-project/feltdb.flow +174 -0
  55. package/dist/create/template/default-project/src/App.tsx +20 -0
  56. package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
  57. package/dist/create/template/default-project/src/feltdb.ts +87 -0
  58. package/dist/create/template/default-project/src/index.tsx +14 -0
  59. package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
  60. package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
  61. package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
  62. package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
  63. package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
  64. package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
  65. package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
  66. package/dist/create/template/default-project/src/styles-application.css +21 -0
  67. package/dist/create/template/default-project/src/styles.css +1 -0
  68. package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
  69. package/dist/create/template/default-project/workflows/invitation.ts +30 -0
  70. package/dist/create/template/default-project/workflows/project-created.ts +2 -0
  71. package/dist/db.d.ts +49 -1
  72. package/dist/db.d.ts.map +1 -1
  73. package/dist/db.js +77 -0
  74. package/dist/error-codes.d.ts +20 -1
  75. package/dist/error-codes.d.ts.map +1 -1
  76. package/dist/error-codes.js +25 -0
  77. package/dist/flowspec.d.ts +1 -0
  78. package/dist/flowspec.d.ts.map +1 -1
  79. package/dist/flowspec.js +66 -11
  80. package/dist/http-client.d.ts +135 -14
  81. package/dist/http-client.d.ts.map +1 -1
  82. package/dist/http-client.js +79 -31
  83. package/dist/http-db.d.ts +15 -2
  84. package/dist/http-db.d.ts.map +1 -1
  85. package/dist/http-db.js +113 -5
  86. package/dist/index-core.d.ts +5 -0
  87. package/dist/index-core.d.ts.map +1 -1
  88. package/dist/index-core.js +5 -0
  89. package/dist/module-intent.d.ts +37 -0
  90. package/dist/module-intent.d.ts.map +1 -0
  91. package/dist/module-intent.js +81 -0
  92. package/dist/module.d.ts +76 -0
  93. package/dist/module.d.ts.map +1 -0
  94. package/dist/module.js +61 -0
  95. package/dist/proposal.d.ts +161 -0
  96. package/dist/proposal.d.ts.map +1 -0
  97. package/dist/proposal.js +232 -0
  98. package/dist/studio/app.d.ts +5 -1
  99. package/dist/studio/app.d.ts.map +1 -1
  100. package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
  101. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  102. package/dist/studio/components/AskFeltDB.d.ts +21 -0
  103. package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
  104. package/dist/studio/components/ContractInspector.d.ts +7 -0
  105. package/dist/studio/components/ContractInspector.d.ts.map +1 -0
  106. package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -1
  107. package/dist/studio/components/ManagedInstancePanel.js +1 -5
  108. package/dist/studio/components/ModuleExplorer.d.ts +6 -0
  109. package/dist/studio/components/ModuleExplorer.d.ts.map +1 -0
  110. package/dist/studio/components/ProposalBrowser.d.ts +12 -0
  111. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
  112. package/dist/studio/components/StateExplorer.d.ts +6 -7
  113. package/dist/studio/components/StateExplorer.d.ts.map +1 -1
  114. package/dist/studio/components/index.d.ts +4 -0
  115. package/dist/studio/components/index.d.ts.map +1 -1
  116. package/dist/studio/components/index.js +4 -4
  117. package/dist/studio/components-CNSNrmA9.js +2787 -0
  118. package/dist/studio/index.js +214 -157
  119. package/dist/studio/studio.css +1 -1
  120. package/dist/studio/utils/index.d.ts +2 -0
  121. package/dist/studio/utils/index.d.ts.map +1 -1
  122. package/dist/studio/utils/index.js +14 -12
  123. package/dist/studio/utils/managed-instance.d.ts +0 -4
  124. package/dist/studio/utils/managed-instance.d.ts.map +1 -1
  125. package/dist/studio/utils/managed-instance.js +0 -4
  126. package/dist/studio/utils/proposal-api.d.ts +53 -0
  127. package/dist/studio/utils/proposal-api.d.ts.map +1 -0
  128. package/dist/studio/utils/proposal-api.js +91 -0
  129. package/dist/studio/utils/service-api.d.ts +81 -0
  130. package/dist/studio/utils/service-api.d.ts.map +1 -0
  131. package/dist/studio/utils/service-api.js +51 -0
  132. package/dist/studio-app/assets/dist-CCOk39Uc.js +1 -0
  133. package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-CSB6KVgw.js} +1 -1
  134. package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
  135. package/dist/studio-app/assets/index-Bncji3aN.js +43 -0
  136. package/dist/studio-app/assets/{index-C1GWyazR.css → index-yazaWMUN.css} +1 -1
  137. package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
  138. package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
  139. package/dist/studio-app/index.html +2 -2
  140. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  141. package/dist/workspace/local-development-authority.d.ts +3 -0
  142. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  143. package/dist/workspace/local-development-authority.js +124 -2
  144. package/package.json +1 -1
  145. package/dist/studio/components-Dxuhrv8_.js +0 -1832
  146. package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
  147. 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" => "Who will use this application, and what roles do they have?".into(),
287
- "entities" => "What information or records should the application keep track of?".into(),
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" => "Who can view, create, edit, delete, or administer this information?".into(),
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![],
@@ -154,7 +154,6 @@ fn now() -> u64 {
154
154
  .as_secs()
155
155
  }
156
156
 
157
-
158
157
  #[cfg(test)]
159
158
  mod tests {
160
159
  use super::*;
@@ -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
- TestSuiteDefinition::standard(),
444
- TestEnvironment::Local,
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!(TestEnvironment::ManagedProduction.as_str(), "managed_production");
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 = TestResult::new(
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(format!("test_{}", i), TestCategory::Authorization, format!("test_{}", i))
516
- .failed("test failed".into(), 50)
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(format!("test_{}", i), TestCategory::Authorization, format!("test_{}", i))
519
- .passed(50)
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(id("refresh")),
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::{auth::ApiKeySummary, app_state::AppState};
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;