@feltdb/core 0.4.11 → 0.4.13

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 (164) hide show
  1. package/dist/cli/commands.js +5 -2
  2. package/dist/cli/index.js +1 -1
  3. package/dist/create/cli.js +16 -1
  4. package/dist/create/create.js +30 -16
  5. package/dist/create/docker-compose-generator.js +48 -9
  6. package/dist/create/package-versions.js +1 -1
  7. package/dist/create/server-source/Cargo.lock +2238 -0
  8. package/dist/create/server-source/Cargo.toml +7 -0
  9. package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
  10. package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
  11. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
  12. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
  13. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
  14. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
  15. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
  19. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
  20. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
  21. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
  22. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
  23. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
  24. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
  25. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
  26. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
  27. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
  28. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
  29. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
  30. package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
  31. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
  32. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
  33. package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
  34. package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
  35. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
  36. package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
  37. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
  38. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
  39. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
  40. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
  41. package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
  42. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
  43. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
  44. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
  45. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
  46. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
  47. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
  48. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
  49. package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
  50. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
  51. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
  52. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
  53. package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
  54. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
  55. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
  56. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
  57. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
  58. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
  59. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
  60. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
  61. package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
  62. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
  63. package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
  64. package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
  65. package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
  66. package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
  67. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
  68. package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
  69. package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
  70. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
  71. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
  72. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
  73. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
  74. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
  75. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
  76. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
  77. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
  78. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
  79. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
  80. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
  81. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
  82. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
  83. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
  84. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
  85. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
  86. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
  87. package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
  88. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
  89. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
  90. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
  91. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
  92. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
  93. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
  94. package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
  95. package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
  96. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
  97. package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
  98. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
  99. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
  100. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
  101. package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
  102. package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
  103. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
  104. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
  105. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
  106. package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
  107. package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
  108. package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
  109. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
  110. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
  111. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
  112. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
  113. package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
  114. package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
  115. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
  116. package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
  117. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
  118. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
  119. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
  120. package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
  121. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
  122. package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
  123. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
  124. package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
  125. package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
  126. package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
  127. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
  128. package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
  129. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
  130. package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
  131. package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
  132. package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
  133. package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
  134. package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
  135. package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
  136. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
  137. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
  138. package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
  139. package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
  140. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
  141. package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
  142. package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
  143. package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
  144. package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
  145. package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
  146. package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
  147. package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
  148. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
  149. package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
  150. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
  151. package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
  152. package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
  153. package/dist/flowspec.d.ts +2 -0
  154. package/dist/flowspec.d.ts.map +1 -1
  155. package/dist/flowspec.js +28 -4
  156. package/dist/state-contract.d.ts +7 -1
  157. package/dist/state-contract.d.ts.map +1 -1
  158. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  159. package/dist/studio/components/index.js +1 -1
  160. package/dist/studio/{components-q43NSTTH.js → components-bHTARBin.js} +160 -89
  161. package/dist/studio/index.js +31 -28
  162. package/dist/studio-app/assets/{index-DZpBoVMp.js → index-BqKquLuU.js} +8 -8
  163. package/dist/studio-app/index.html +1 -1
  164. package/package.json +1 -1
@@ -0,0 +1,1044 @@
1
+ use crate::{
2
+ key_provider::{
3
+ KeyProvider, KeyProviderHealth, KeyReference, KeyScope, LocalKeyProvider, WrappedKey,
4
+ },
5
+ principals::PrincipalStore,
6
+ tenancy::TenancyStore,
7
+ };
8
+ use base64::{engine::general_purpose::STANDARD, Engine};
9
+ use rand::{distributions::Alphanumeric, Rng, RngCore};
10
+ use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM};
11
+ use serde::{Deserialize, Serialize};
12
+ use serde_json::Value;
13
+ use std::{
14
+ collections::HashMap,
15
+ fs,
16
+ path::PathBuf,
17
+ sync::{Arc, RwLock},
18
+ time::{SystemTime, UNIX_EPOCH},
19
+ };
20
+
21
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22
+ #[serde(rename_all = "snake_case")]
23
+ pub enum ConnectionStatus {
24
+ Active,
25
+ Disabled,
26
+ }
27
+ #[derive(Debug, Clone, Serialize, Deserialize)]
28
+ pub struct Connection {
29
+ pub id: String,
30
+ pub application_id: String,
31
+ pub provider: String,
32
+ pub name: String,
33
+ pub status: ConnectionStatus,
34
+ pub configuration: Value,
35
+ pub configuration_schema: Value,
36
+ pub secret_refs: Vec<SecretReference>,
37
+ pub created_by: String,
38
+ pub created_at: u64,
39
+ pub updated_at: u64,
40
+ }
41
+ #[derive(Debug, Clone, Serialize, Deserialize)]
42
+ pub struct SecretReference {
43
+ pub binding: String,
44
+ pub secret_id: String,
45
+ }
46
+ #[derive(Debug, Clone, Serialize)]
47
+ pub struct SecretMetadata {
48
+ pub id: String,
49
+ pub application_id: String,
50
+ pub name: String,
51
+ pub scope: String,
52
+ pub version: u64,
53
+ pub status: SecretStatus,
54
+ pub key_reference: KeyReference,
55
+ pub created_by: String,
56
+ pub created_at: u64,
57
+ pub updated_at: u64,
58
+ }
59
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
60
+ #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
61
+ pub enum SecretStatus {
62
+ Active,
63
+ Revoked,
64
+ Retired,
65
+ }
66
+ #[derive(Debug, Clone, Serialize, Deserialize)]
67
+ struct EncryptedSecret {
68
+ id: String,
69
+ tenant_id: String,
70
+ application_id: String,
71
+ name: String,
72
+ scope: String,
73
+ nonce: String,
74
+ ciphertext: String,
75
+ key_reference: KeyReference,
76
+ wrapped_dek: WrappedKey,
77
+ algorithm: String,
78
+ purpose: String,
79
+ status: SecretStatus,
80
+ version: u64,
81
+ created_by: String,
82
+ created_at: u64,
83
+ updated_at: u64,
84
+ }
85
+ #[derive(Debug, Clone, Serialize, Deserialize)]
86
+ pub struct SecretAuditEvent {
87
+ pub event: String,
88
+ pub principal_id: String,
89
+ pub workload_id: String,
90
+ pub worker_id: String,
91
+ pub connection_id: String,
92
+ pub secret_id: String,
93
+ pub decision: bool,
94
+ pub at: u64,
95
+ }
96
+ #[derive(Debug, Default, Serialize, Deserialize)]
97
+ struct ConnectionRecords {
98
+ connections: Vec<Connection>,
99
+ secrets: Vec<EncryptedSecret>,
100
+ audit: Vec<SecretAuditEvent>,
101
+ }
102
+ #[derive(Clone)]
103
+ pub struct ConnectionStore {
104
+ path: PathBuf,
105
+ key_provider: Arc<dyn KeyProvider>,
106
+ records: Arc<RwLock<ConnectionRecords>>,
107
+ }
108
+ #[derive(Debug, Clone, Serialize)]
109
+ pub struct ExecutionContext {
110
+ pub id: String,
111
+ pub tenant_id: String,
112
+ pub workload_id: String,
113
+ pub principal_id: String,
114
+ pub worker_id: String,
115
+ pub application_id: String,
116
+ pub capabilities: Vec<String>,
117
+ pub connections: Vec<String>,
118
+ pub secret_bindings: Vec<String>,
119
+ pub expires_at: u64,
120
+ pub issued_at: u64,
121
+ pub used: bool,
122
+ }
123
+ pub trait ConnectionProvider: Send + Sync {
124
+ fn identify(&self) -> &str;
125
+ fn configuration_schema(&self) -> Value;
126
+ fn validate(&self, configuration: &Value) -> Result<(), String>;
127
+ fn health(&self, configuration: &Value) -> Result<(), String>;
128
+ }
129
+
130
+ fn now() -> u64 {
131
+ SystemTime::now()
132
+ .duration_since(UNIX_EPOCH)
133
+ .unwrap_or_default()
134
+ .as_secs()
135
+ }
136
+ fn id(prefix: &str) -> String {
137
+ let value: String = rand::thread_rng()
138
+ .sample_iter(&Alphanumeric)
139
+ .take(18)
140
+ .map(char::from)
141
+ .collect();
142
+ format!("{prefix}_{value}")
143
+ }
144
+ impl From<&EncryptedSecret> for SecretMetadata {
145
+ fn from(secret: &EncryptedSecret) -> Self {
146
+ Self {
147
+ id: secret.id.clone(),
148
+ application_id: secret.application_id.clone(),
149
+ name: secret.name.clone(),
150
+ scope: secret.scope.clone(),
151
+ version: secret.version,
152
+ status: secret.status,
153
+ key_reference: secret.key_reference.clone(),
154
+ created_by: secret.created_by.clone(),
155
+ created_at: secret.created_at,
156
+ updated_at: secret.updated_at,
157
+ }
158
+ }
159
+ }
160
+
161
+ impl ConnectionStore {
162
+ pub fn key_provider_health(
163
+ &self,
164
+ tenant_id: &str,
165
+ application_id: Option<&str>,
166
+ ) -> KeyProviderHealth {
167
+ self.key_provider.health(&KeyScope {
168
+ tenant_id: tenant_id.into(),
169
+ application_id: application_id.map(Into::into),
170
+ purpose: "connection-secret".into(),
171
+ })
172
+ }
173
+ pub fn rotate_encryption_key(
174
+ &self,
175
+ tenant_id: &str,
176
+ application_id: Option<&str>,
177
+ ) -> Result<KeyReference, String> {
178
+ self.key_provider.rotate(&KeyScope {
179
+ tenant_id: tenant_id.into(),
180
+ application_id: application_id.map(Into::into),
181
+ purpose: "connection-secret".into(),
182
+ })
183
+ }
184
+ pub fn rewrap_application(
185
+ &self,
186
+ actor: &str,
187
+ tenancy: &TenancyStore,
188
+ application_id: &str,
189
+ ) -> Result<usize, String> {
190
+ if !Self::allowed(actor, tenancy, application_id, "applications:write") {
191
+ return Err("forbidden".into());
192
+ }
193
+ let tenant_id = tenancy
194
+ .application_tenant(application_id)
195
+ .ok_or("application not found")?;
196
+ let scope = KeyScope {
197
+ tenant_id,
198
+ application_id: Some(application_id.into()),
199
+ purpose: "connection-secret".into(),
200
+ };
201
+ self.key_provider.rotate(&scope)?;
202
+ let mut records = self
203
+ .records
204
+ .write()
205
+ .map_err(|_| "connection store lock poisoned")?;
206
+ let mut count = 0;
207
+ for secret in records.secrets.iter_mut().filter(|secret| {
208
+ secret.application_id == application_id && secret.status == SecretStatus::Active
209
+ }) {
210
+ let mut dek = self.key_provider.decrypt_dek(
211
+ &secret.key_reference,
212
+ &secret.wrapped_dek,
213
+ &scope,
214
+ )?;
215
+ let (reference, wrapped) = self.key_provider.encrypt_dek(&scope, &dek)?;
216
+ dek.fill(0);
217
+ secret.key_reference = reference;
218
+ secret.wrapped_dek = wrapped;
219
+ count += 1;
220
+ }
221
+ self.persist(&records)?;
222
+ Ok(count)
223
+ }
224
+ pub fn revoke_secret(
225
+ &self,
226
+ actor: &str,
227
+ tenancy: &TenancyStore,
228
+ secret_id: &str,
229
+ ) -> Result<SecretMetadata, String> {
230
+ let mut records = self
231
+ .records
232
+ .write()
233
+ .map_err(|_| "connection store lock poisoned")?;
234
+ let secret = records
235
+ .secrets
236
+ .iter_mut()
237
+ .filter(|secret| secret.id == secret_id)
238
+ .max_by_key(|secret| secret.version)
239
+ .ok_or("secret not found")?;
240
+ if !Self::allowed(actor, tenancy, &secret.application_id, "applications:write") {
241
+ return Err("forbidden".into());
242
+ }
243
+ secret.status = SecretStatus::Revoked;
244
+ secret.updated_at = now();
245
+ let metadata = (&*secret).into();
246
+ self.persist(&records)?;
247
+ Ok(metadata)
248
+ }
249
+ pub fn inspect_secret(
250
+ &self,
251
+ actor: &str,
252
+ tenancy: &TenancyStore,
253
+ secret_id: &str,
254
+ ) -> Result<SecretMetadata, String> {
255
+ let records = self
256
+ .records
257
+ .read()
258
+ .map_err(|_| "connection store lock poisoned")?;
259
+ let secret = records
260
+ .secrets
261
+ .iter()
262
+ .filter(|secret| secret.id == secret_id)
263
+ .max_by_key(|secret| secret.version)
264
+ .ok_or("secret not found")?;
265
+ if !Self::allowed(actor, tenancy, &secret.application_id, "applications:read") {
266
+ return Err("forbidden".into());
267
+ }
268
+ Ok(secret.into())
269
+ }
270
+ pub fn load(path: impl Into<PathBuf>, key: [u8; 32]) -> Result<Self, String> {
271
+ let path = path.into();
272
+ let provider = Arc::new(LocalKeyProvider::load(
273
+ path.with_extension("local-keys.json"),
274
+ key,
275
+ )?);
276
+ Self::load_with_provider(path, provider)
277
+ }
278
+ pub fn load_with_provider(
279
+ path: impl Into<PathBuf>,
280
+ key_provider: Arc<dyn KeyProvider>,
281
+ ) -> Result<Self, String> {
282
+ let path = path.into();
283
+ let records = if path.exists() {
284
+ serde_json::from_slice(&fs::read(&path).map_err(|error| error.to_string())?)
285
+ .map_err(|error| error.to_string())?
286
+ } else {
287
+ ConnectionRecords::default()
288
+ };
289
+ Ok(Self {
290
+ path,
291
+ key_provider,
292
+ records: Arc::new(RwLock::new(records)),
293
+ })
294
+ }
295
+ fn persist(&self, records: &ConnectionRecords) -> Result<(), String> {
296
+ if let Some(parent) = self.path.parent() {
297
+ fs::create_dir_all(parent).map_err(|error| error.to_string())?;
298
+ }
299
+ let temporary = self.path.with_extension("tmp");
300
+ fs::write(
301
+ &temporary,
302
+ serde_json::to_vec_pretty(records).map_err(|error| error.to_string())?,
303
+ )
304
+ .map_err(|error| error.to_string())?;
305
+ fs::rename(temporary, &self.path).map_err(|error| error.to_string())
306
+ }
307
+ fn secret_aad(secret: &EncryptedSecret) -> Vec<u8> {
308
+ format!(
309
+ "feltdb-secret-v1|{}|{}|{}|{}|{}",
310
+ secret.tenant_id, secret.application_id, secret.id, secret.version, secret.purpose
311
+ )
312
+ .into_bytes()
313
+ }
314
+ fn seal(
315
+ &self,
316
+ mut secret: EncryptedSecret,
317
+ plaintext: &[u8],
318
+ ) -> Result<EncryptedSecret, String> {
319
+ let mut dek = [0u8; 32];
320
+ rand::thread_rng().fill_bytes(&mut dek);
321
+ let scope = KeyScope {
322
+ tenant_id: secret.tenant_id.clone(),
323
+ application_id: Some(secret.application_id.clone()),
324
+ purpose: secret.purpose.clone(),
325
+ };
326
+ let (key_reference, wrapped_dek) = self.key_provider.encrypt_dek(&scope, &dek)?;
327
+ secret.key_reference = key_reference;
328
+ secret.wrapped_dek = wrapped_dek;
329
+ let key = LessSafeKey::new(
330
+ UnboundKey::new(&AES_256_GCM, &dek).map_err(|_| "invalid encryption key")?,
331
+ );
332
+ let mut nonce = [0u8; 12];
333
+ rand::thread_rng().fill_bytes(&mut nonce);
334
+ let mut value = plaintext.to_vec();
335
+ key.seal_in_place_append_tag(
336
+ Nonce::assume_unique_for_key(nonce),
337
+ Aad::from(Self::secret_aad(&secret)),
338
+ &mut value,
339
+ )
340
+ .map_err(|_| "secret encryption failed")?;
341
+ dek.fill(0);
342
+ secret.nonce = STANDARD.encode(nonce);
343
+ secret.ciphertext = STANDARD.encode(value);
344
+ Ok(secret)
345
+ }
346
+ fn open(&self, secret: &EncryptedSecret) -> Result<Vec<u8>, String> {
347
+ let nonce: [u8; 12] = STANDARD
348
+ .decode(&secret.nonce)
349
+ .map_err(|_| "invalid secret nonce")?
350
+ .try_into()
351
+ .map_err(|_| "invalid secret nonce")?;
352
+ let mut value = STANDARD
353
+ .decode(&secret.ciphertext)
354
+ .map_err(|_| "invalid secret ciphertext")?;
355
+ if secret.status != SecretStatus::Active {
356
+ return Err("secret version is not active".into());
357
+ }
358
+ let scope = KeyScope {
359
+ tenant_id: secret.tenant_id.clone(),
360
+ application_id: Some(secret.application_id.clone()),
361
+ purpose: secret.purpose.clone(),
362
+ };
363
+ let mut dek =
364
+ self.key_provider
365
+ .decrypt_dek(&secret.key_reference, &secret.wrapped_dek, &scope)?;
366
+ let key = LessSafeKey::new(
367
+ UnboundKey::new(&AES_256_GCM, &dek).map_err(|_| "invalid encryption key")?,
368
+ );
369
+ let length = key
370
+ .open_in_place(
371
+ Nonce::assume_unique_for_key(nonce),
372
+ Aad::from(Self::secret_aad(secret)),
373
+ &mut value,
374
+ )
375
+ .map_err(|_| "secret decryption failed")?
376
+ .len();
377
+ value.truncate(length);
378
+ dek.fill(0);
379
+ Ok(value)
380
+ }
381
+ fn allowed(
382
+ actor: &str,
383
+ tenancy: &TenancyStore,
384
+ application_id: &str,
385
+ capability: &str,
386
+ ) -> bool {
387
+ tenancy
388
+ .application_tenant(application_id)
389
+ .map(|tenant| tenancy.authorize_application(actor, &tenant, application_id, capability))
390
+ .unwrap_or(false)
391
+ }
392
+ pub fn put_secret(
393
+ &self,
394
+ actor: &str,
395
+ tenancy: &TenancyStore,
396
+ application_id: &str,
397
+ name: &str,
398
+ scope: &str,
399
+ value: &[u8],
400
+ ) -> Result<SecretMetadata, String> {
401
+ if !Self::allowed(actor, tenancy, application_id, "applications:write") {
402
+ return Err("forbidden".into());
403
+ }
404
+ let timestamp = now();
405
+ let mut records = self
406
+ .records
407
+ .write()
408
+ .map_err(|_| "connection store lock poisoned")?;
409
+ let tenant_id = tenancy
410
+ .application_tenant(application_id)
411
+ .ok_or("application not found")?;
412
+ let secret = self.seal(
413
+ EncryptedSecret {
414
+ id: id("secret"),
415
+ tenant_id,
416
+ application_id: application_id.into(),
417
+ name: name.into(),
418
+ scope: scope.into(),
419
+ nonce: String::new(),
420
+ ciphertext: String::new(),
421
+ key_reference: KeyReference {
422
+ provider: String::new(),
423
+ key_id: String::new(),
424
+ version: 0,
425
+ },
426
+ wrapped_dek: WrappedKey {
427
+ nonce: String::new(),
428
+ ciphertext: String::new(),
429
+ algorithm: String::new(),
430
+ },
431
+ algorithm: "AES-256-GCM".into(),
432
+ purpose: "connection-secret".into(),
433
+ status: SecretStatus::Active,
434
+ version: 1,
435
+ created_by: actor.into(),
436
+ created_at: timestamp,
437
+ updated_at: timestamp,
438
+ },
439
+ value,
440
+ )?;
441
+ records.secrets.push(secret.clone());
442
+ self.persist(&records)?;
443
+ Ok((&secret).into())
444
+ }
445
+ pub fn rotate_secret(
446
+ &self,
447
+ actor: &str,
448
+ tenancy: &TenancyStore,
449
+ secret_id: &str,
450
+ value: &[u8],
451
+ ) -> Result<SecretMetadata, String> {
452
+ let mut records = self
453
+ .records
454
+ .write()
455
+ .map_err(|_| "connection store lock poisoned")?;
456
+ let index = records
457
+ .secrets
458
+ .iter()
459
+ .position(|secret| secret.id == secret_id)
460
+ .ok_or("secret not found")?;
461
+ if !Self::allowed(
462
+ actor,
463
+ tenancy,
464
+ &records.secrets[index].application_id,
465
+ "applications:write",
466
+ ) {
467
+ return Err("forbidden".into());
468
+ }
469
+ records.secrets[index].status = SecretStatus::Retired;
470
+ let previous = records.secrets[index].clone();
471
+ let timestamp = now();
472
+ let next = self.seal(
473
+ EncryptedSecret {
474
+ version: previous.version + 1,
475
+ status: SecretStatus::Active,
476
+ created_by: actor.into(),
477
+ created_at: timestamp,
478
+ updated_at: timestamp,
479
+ nonce: String::new(),
480
+ ciphertext: String::new(),
481
+ key_reference: KeyReference {
482
+ provider: String::new(),
483
+ key_id: String::new(),
484
+ version: 0,
485
+ },
486
+ wrapped_dek: WrappedKey {
487
+ nonce: String::new(),
488
+ ciphertext: String::new(),
489
+ algorithm: String::new(),
490
+ },
491
+ ..previous
492
+ },
493
+ value,
494
+ )?;
495
+ let metadata = (&next).into();
496
+ records.secrets.push(next);
497
+ self.persist(&records)?;
498
+ Ok(metadata)
499
+ }
500
+ pub fn list_secrets(
501
+ &self,
502
+ actor: &str,
503
+ tenancy: &TenancyStore,
504
+ application_id: &str,
505
+ ) -> Result<Vec<SecretMetadata>, String> {
506
+ if !Self::allowed(actor, tenancy, application_id, "applications:read") {
507
+ return Err("forbidden".into());
508
+ }
509
+ Ok(self
510
+ .records
511
+ .read()
512
+ .map_err(|_| "connection store lock poisoned")?
513
+ .secrets
514
+ .iter()
515
+ .filter(|secret| secret.application_id == application_id)
516
+ .filter(|secret| secret.status == SecretStatus::Active)
517
+ .map(Into::into)
518
+ .collect())
519
+ }
520
+ pub fn create_connection(
521
+ &self,
522
+ actor: &str,
523
+ tenancy: &TenancyStore,
524
+ application_id: &str,
525
+ provider: &dyn ConnectionProvider,
526
+ name: &str,
527
+ configuration: Value,
528
+ secret_refs: Vec<SecretReference>,
529
+ ) -> Result<Connection, String> {
530
+ if !Self::allowed(actor, tenancy, application_id, "connections:write") {
531
+ return Err("forbidden".into());
532
+ }
533
+ provider.validate(&configuration)?;
534
+ let records = self
535
+ .records
536
+ .read()
537
+ .map_err(|_| "connection store lock poisoned")?;
538
+ if secret_refs.iter().any(|reference| {
539
+ !records.secrets.iter().any(|secret| {
540
+ secret.id == reference.secret_id
541
+ && secret.application_id == application_id
542
+ && secret.status == SecretStatus::Active
543
+ })
544
+ }) {
545
+ return Err(
546
+ "connection secret belongs to another application or does not exist".into(),
547
+ );
548
+ }
549
+ drop(records);
550
+ let timestamp = now();
551
+ let connection = Connection {
552
+ id: id("connection"),
553
+ application_id: application_id.into(),
554
+ provider: provider.identify().into(),
555
+ name: name.into(),
556
+ status: ConnectionStatus::Active,
557
+ configuration,
558
+ configuration_schema: provider.configuration_schema(),
559
+ secret_refs,
560
+ created_by: actor.into(),
561
+ created_at: timestamp,
562
+ updated_at: timestamp,
563
+ };
564
+ let mut records = self
565
+ .records
566
+ .write()
567
+ .map_err(|_| "connection store lock poisoned")?;
568
+ records.connections.push(connection.clone());
569
+ self.persist(&records)?;
570
+ Ok(connection)
571
+ }
572
+ pub fn list_connections(
573
+ &self,
574
+ actor: &str,
575
+ tenancy: &TenancyStore,
576
+ application_id: &str,
577
+ ) -> Result<Vec<Connection>, String> {
578
+ if !Self::allowed(actor, tenancy, application_id, "connections:read")
579
+ && !Self::allowed(actor, tenancy, application_id, "applications:read")
580
+ {
581
+ return Err("forbidden".into());
582
+ }
583
+ Ok(self
584
+ .records
585
+ .read()
586
+ .map_err(|_| "connection store lock poisoned")?
587
+ .connections
588
+ .iter()
589
+ .filter(|connection| connection.application_id == application_id)
590
+ .cloned()
591
+ .collect())
592
+ }
593
+ pub fn disable(
594
+ &self,
595
+ actor: &str,
596
+ tenancy: &TenancyStore,
597
+ connection_id: &str,
598
+ ) -> Result<(), String> {
599
+ let mut records = self
600
+ .records
601
+ .write()
602
+ .map_err(|_| "connection store lock poisoned")?;
603
+ let connection = records
604
+ .connections
605
+ .iter_mut()
606
+ .find(|connection| connection.id == connection_id)
607
+ .ok_or("connection not found")?;
608
+ if !Self::allowed(
609
+ actor,
610
+ tenancy,
611
+ &connection.application_id,
612
+ "connections:write",
613
+ ) {
614
+ return Err("forbidden".into());
615
+ }
616
+ connection.status = ConnectionStatus::Disabled;
617
+ connection.updated_at = now();
618
+ self.persist(&records)
619
+ }
620
+ pub fn delete(
621
+ &self,
622
+ actor: &str,
623
+ tenancy: &TenancyStore,
624
+ connection_id: &str,
625
+ ) -> Result<(), String> {
626
+ let mut records = self
627
+ .records
628
+ .write()
629
+ .map_err(|_| "connection store lock poisoned")?;
630
+ let index = records
631
+ .connections
632
+ .iter()
633
+ .position(|connection| connection.id == connection_id)
634
+ .ok_or("connection not found")?;
635
+ if !Self::allowed(
636
+ actor,
637
+ tenancy,
638
+ &records.connections[index].application_id,
639
+ "connections:write",
640
+ ) {
641
+ return Err("forbidden".into());
642
+ }
643
+ records.connections.remove(index);
644
+ self.persist(&records)
645
+ }
646
+ pub fn execution_context(
647
+ &self,
648
+ principals: &PrincipalStore,
649
+ worker_id: &str,
650
+ workload_id: &str,
651
+ connection_ids: &[String],
652
+ ttl: u64,
653
+ ) -> Result<ExecutionContext, String> {
654
+ let (application_id, principal_id, capabilities) = principals
655
+ .execution_snapshot(worker_id, workload_id)
656
+ .ok_or("worker is not assigned to workload")?;
657
+ if !capabilities
658
+ .iter()
659
+ .any(|capability| capability == "connection:use" || capability == "*")
660
+ {
661
+ return Err("workload lacks connection:use".into());
662
+ }
663
+ let records = self
664
+ .records
665
+ .read()
666
+ .map_err(|_| "connection store lock poisoned")?;
667
+ let selected: Vec<&Connection> = connection_ids
668
+ .iter()
669
+ .map(|id| {
670
+ records
671
+ .connections
672
+ .iter()
673
+ .find(|connection| {
674
+ connection.id == *id
675
+ && connection.application_id == application_id
676
+ && connection.status == ConnectionStatus::Active
677
+ })
678
+ .ok_or("connection unavailable")
679
+ })
680
+ .collect::<Result<_, _>>()?;
681
+ Ok(ExecutionContext {
682
+ id: id("execution"),
683
+ tenant_id: selected
684
+ .iter()
685
+ .flat_map(|connection| connection.secret_refs.iter())
686
+ .find_map(|reference| {
687
+ records
688
+ .secrets
689
+ .iter()
690
+ .find(|secret| secret.id == reference.secret_id)
691
+ .map(|secret| secret.tenant_id.clone())
692
+ })
693
+ .unwrap_or_default(),
694
+ workload_id: workload_id.into(),
695
+ principal_id,
696
+ worker_id: worker_id.into(),
697
+ application_id,
698
+ capabilities,
699
+ connections: connection_ids.to_vec(),
700
+ secret_bindings: selected
701
+ .iter()
702
+ .flat_map(|connection| {
703
+ connection
704
+ .secret_refs
705
+ .iter()
706
+ .map(|reference| reference.binding.clone())
707
+ })
708
+ .collect(),
709
+ expires_at: now() + ttl.min(900),
710
+ issued_at: now(),
711
+ used: false,
712
+ })
713
+ }
714
+ pub fn with_execution_secrets<T>(
715
+ &self,
716
+ context: &ExecutionContext,
717
+ connection_id: &str,
718
+ execute: impl FnOnce(&HashMap<String, Vec<u8>>) -> T,
719
+ ) -> Result<T, String> {
720
+ if context.expires_at <= now() || !context.connections.iter().any(|id| id == connection_id)
721
+ {
722
+ return Err("execution context expired or connection not authorized".into());
723
+ }
724
+ let mut records = self
725
+ .records
726
+ .write()
727
+ .map_err(|_| "connection store lock poisoned")?;
728
+ let connection = records
729
+ .connections
730
+ .iter()
731
+ .find(|connection| {
732
+ connection.id == connection_id
733
+ && connection.application_id == context.application_id
734
+ && connection.status == ConnectionStatus::Active
735
+ })
736
+ .cloned()
737
+ .ok_or("connection unavailable")?;
738
+ let mut secrets = HashMap::new();
739
+ for reference in &connection.secret_refs {
740
+ let secret = records
741
+ .secrets
742
+ .iter()
743
+ .find(|secret| {
744
+ secret.id == reference.secret_id
745
+ && secret.application_id == context.application_id
746
+ && secret.status == SecretStatus::Active
747
+ })
748
+ .cloned()
749
+ .ok_or("secret unavailable")?;
750
+ secrets.insert(reference.binding.clone(), self.open(&secret)?);
751
+ records.audit.push(SecretAuditEvent {
752
+ event: "secret.injected".into(),
753
+ principal_id: context.principal_id.clone(),
754
+ workload_id: context.workload_id.clone(),
755
+ worker_id: context.worker_id.clone(),
756
+ connection_id: connection.id.clone(),
757
+ secret_id: secret.id,
758
+ decision: true,
759
+ at: now(),
760
+ });
761
+ }
762
+ self.persist(&records)?;
763
+ let output = execute(&secrets);
764
+ for value in secrets.values_mut() {
765
+ value.fill(0);
766
+ }
767
+ Ok(output)
768
+ }
769
+ pub fn with_deployment_secrets<T>(
770
+ &self,
771
+ actor: &str,
772
+ tenancy: &TenancyStore,
773
+ application_id: &str,
774
+ deployment_id: &str,
775
+ provider: &str,
776
+ references: &[(String, String)],
777
+ operation: &str,
778
+ execute: impl FnOnce(&HashMap<String, Vec<u8>>, &HashMap<String, u64>) -> T,
779
+ ) -> Result<T, String> {
780
+ let tenant_id = tenancy
781
+ .application_tenant(application_id)
782
+ .ok_or("application not found")?;
783
+ if !tenancy.authorize_application(actor, &tenant_id, application_id, "deployment:execute") {
784
+ return Err("forbidden".into());
785
+ }
786
+ let mut records = self
787
+ .records
788
+ .write()
789
+ .map_err(|_| "connection store lock poisoned")?;
790
+ let mut values = HashMap::new();
791
+ let mut versions = HashMap::new();
792
+ for (binding, secret_id) in references {
793
+ let secret = records
794
+ .secrets
795
+ .iter()
796
+ .find(|secret| {
797
+ secret.id == *secret_id
798
+ && secret.tenant_id == tenant_id
799
+ && secret.application_id == application_id
800
+ && secret.status == SecretStatus::Active
801
+ })
802
+ .cloned()
803
+ .ok_or("deployment credential unavailable")?;
804
+ values.insert(binding.clone(), self.open(&secret)?);
805
+ versions.insert(binding.clone(), secret.version);
806
+ records.audit.push(SecretAuditEvent {
807
+ event: format!("deployment.{operation}"),
808
+ principal_id: actor.into(),
809
+ workload_id: deployment_id.into(),
810
+ worker_id: provider.into(),
811
+ connection_id: "deployment-provider".into(),
812
+ secret_id: secret.id,
813
+ decision: true,
814
+ at: now(),
815
+ });
816
+ }
817
+ self.persist(&records)?;
818
+ let output = execute(&values, &versions);
819
+ for value in values.values_mut() {
820
+ value.fill(0);
821
+ }
822
+ Ok(output)
823
+ }
824
+ pub fn audit(&self) -> Vec<SecretAuditEvent> {
825
+ self.records
826
+ .read()
827
+ .map(|records| records.audit.clone())
828
+ .unwrap_or_default()
829
+ }
830
+ }
831
+
832
+ #[cfg(test)]
833
+ mod tests {
834
+ use super::*;
835
+ use crate::principals::PrincipalKind;
836
+ struct TestProvider;
837
+ impl ConnectionProvider for TestProvider {
838
+ fn identify(&self) -> &str {
839
+ "test"
840
+ }
841
+ fn configuration_schema(&self) -> Value {
842
+ serde_json::json!({"type":"object"})
843
+ }
844
+ fn validate(&self, _: &Value) -> Result<(), String> {
845
+ Ok(())
846
+ }
847
+ fn health(&self, _: &Value) -> Result<(), String> {
848
+ Ok(())
849
+ }
850
+ }
851
+ fn setup() -> (TenancyStore, PrincipalStore, ConnectionStore, String) {
852
+ let suffix = id("test");
853
+ let tenancy =
854
+ TenancyStore::load(std::env::temp_dir().join(format!("{suffix}-tenant.json"))).unwrap();
855
+ let tenant = tenancy
856
+ .create_tenant("owner", &format!("Connections {suffix}"))
857
+ .unwrap();
858
+ let app = tenancy
859
+ .create_application("owner", &tenant.id, "App")
860
+ .unwrap();
861
+ let principals =
862
+ PrincipalStore::load(std::env::temp_dir().join(format!("{suffix}-principals.json")))
863
+ .unwrap();
864
+ let connections = ConnectionStore::load(
865
+ std::env::temp_dir().join(format!("{suffix}-connections.json")),
866
+ [7u8; 32],
867
+ )
868
+ .unwrap();
869
+ (tenancy, principals, connections, app.id)
870
+ }
871
+ #[test]
872
+ fn secrets_are_opaque_and_injected_only_for_assigned_execution() {
873
+ let (tenancy, principals, store, app) = setup();
874
+ let secret = store
875
+ .put_secret(
876
+ "owner",
877
+ &tenancy,
878
+ &app,
879
+ "API_TOKEN",
880
+ "connection",
881
+ b"super-secret-value",
882
+ )
883
+ .unwrap();
884
+ assert!(!serde_json::to_string(&secret)
885
+ .unwrap()
886
+ .contains("super-secret-value"));
887
+ let connection = store
888
+ .create_connection(
889
+ "owner",
890
+ &tenancy,
891
+ &app,
892
+ &TestProvider,
893
+ "External",
894
+ serde_json::json!({"url":"https://example.com"}),
895
+ vec![SecretReference {
896
+ binding: "TOKEN".into(),
897
+ secret_id: secret.id,
898
+ }],
899
+ )
900
+ .unwrap();
901
+ let agent = principals
902
+ .create("owner", &tenancy, &app, PrincipalKind::Agent, "Agent")
903
+ .unwrap();
904
+ principals
905
+ .grant("owner", &tenancy, &agent.id, "connection:use")
906
+ .unwrap();
907
+ let workload = principals
908
+ .create_workload("owner", &tenancy, &agent.id)
909
+ .unwrap();
910
+ let worker = principals
911
+ .create("owner", &tenancy, &app, PrincipalKind::Worker, "Worker")
912
+ .unwrap();
913
+ assert!(store
914
+ .execution_context(
915
+ &principals,
916
+ &worker.id,
917
+ &workload.id,
918
+ std::slice::from_ref(&connection.id),
919
+ 60
920
+ )
921
+ .is_err());
922
+ principals
923
+ .assign_worker("owner", &tenancy, &workload.id, &worker.id)
924
+ .unwrap();
925
+ let context = store
926
+ .execution_context(
927
+ &principals,
928
+ &worker.id,
929
+ &workload.id,
930
+ std::slice::from_ref(&connection.id),
931
+ 60,
932
+ )
933
+ .unwrap();
934
+ let observed = store
935
+ .with_execution_secrets(&context, &connection.id, |values| values["TOKEN"].clone())
936
+ .unwrap();
937
+ assert_eq!(observed, b"super-secret-value");
938
+ assert_eq!(store.audit().last().unwrap().event, "secret.injected");
939
+ }
940
+ #[test]
941
+ fn disabled_and_cross_application_connections_are_denied_and_rotation_versions() {
942
+ let (tenancy, principals, store, app) = setup();
943
+ let secret = store
944
+ .put_secret("owner", &tenancy, &app, "TOKEN", "connection", b"v1")
945
+ .unwrap();
946
+ let rotated = store
947
+ .rotate_secret("owner", &tenancy, &secret.id, b"v2")
948
+ .unwrap();
949
+ assert_eq!(rotated.version, 2);
950
+ {
951
+ let records = store.records.read().unwrap();
952
+ let versions: Vec<_> = records
953
+ .secrets
954
+ .iter()
955
+ .filter(|item| item.id == secret.id)
956
+ .collect();
957
+ assert_eq!(versions.len(), 2);
958
+ assert_eq!(versions[0].status, SecretStatus::Retired);
959
+ assert_ne!(versions[0].ciphertext, versions[1].ciphertext);
960
+ }
961
+ let connection = store
962
+ .create_connection(
963
+ "owner",
964
+ &tenancy,
965
+ &app,
966
+ &TestProvider,
967
+ "API",
968
+ serde_json::json!({}),
969
+ vec![SecretReference {
970
+ binding: "TOKEN".into(),
971
+ secret_id: secret.id,
972
+ }],
973
+ )
974
+ .unwrap();
975
+ let agent = principals
976
+ .create("owner", &tenancy, &app, PrincipalKind::Agent, "Agent")
977
+ .unwrap();
978
+ principals
979
+ .grant("owner", &tenancy, &agent.id, "connection:use")
980
+ .unwrap();
981
+ let workload = principals
982
+ .create_workload("owner", &tenancy, &agent.id)
983
+ .unwrap();
984
+ let worker = principals
985
+ .create("owner", &tenancy, &app, PrincipalKind::Worker, "Worker")
986
+ .unwrap();
987
+ principals
988
+ .assign_worker("owner", &tenancy, &workload.id, &worker.id)
989
+ .unwrap();
990
+ store.disable("owner", &tenancy, &connection.id).unwrap();
991
+ assert!(store
992
+ .execution_context(&principals, &worker.id, &workload.id, &[connection.id], 60)
993
+ .is_err());
994
+ }
995
+ #[test]
996
+ fn envelope_metadata_is_authenticated_and_deks_rewrap_without_ciphertext_change() {
997
+ let (tenancy, _, store, app) = setup();
998
+ let secret = store
999
+ .put_secret(
1000
+ "owner",
1001
+ &tenancy,
1002
+ &app,
1003
+ "TOKEN",
1004
+ "connection",
1005
+ b"credential",
1006
+ )
1007
+ .unwrap();
1008
+ let before = {
1009
+ store
1010
+ .records
1011
+ .read()
1012
+ .unwrap()
1013
+ .secrets
1014
+ .iter()
1015
+ .find(|item| item.id == secret.id)
1016
+ .unwrap()
1017
+ .clone()
1018
+ };
1019
+ assert_eq!(
1020
+ store.rewrap_application("owner", &tenancy, &app).unwrap(),
1021
+ 1
1022
+ );
1023
+ let after = {
1024
+ store
1025
+ .records
1026
+ .read()
1027
+ .unwrap()
1028
+ .secrets
1029
+ .iter()
1030
+ .find(|item| item.id == secret.id)
1031
+ .unwrap()
1032
+ .clone()
1033
+ };
1034
+ assert_eq!(before.ciphertext, after.ciphertext);
1035
+ assert_ne!(before.key_reference, after.key_reference);
1036
+ assert_eq!(store.open(&after).unwrap(), b"credential");
1037
+ let mut moved = after.clone();
1038
+ moved.application_id = "another-app".into();
1039
+ assert!(store.open(&moved).is_err());
1040
+ let mut tampered = after;
1041
+ tampered.version += 1;
1042
+ assert!(store.open(&tampered).is_err());
1043
+ }
1044
+ }