@feltdb/core 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +19 -0
- package/dist/create/server-source/crates/feltdb/src/authority.rs +349 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +32 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +19 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +444 -189
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +244 -51
- package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +297 -0
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +41 -11
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +8 -3
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +1164 -225
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +134 -0
- package/dist/http-db.d.ts +3 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +2 -1
- package/dist/state-contract.d.ts +4 -1
- package/dist/state-contract.d.ts.map +1 -1
- package/dist/state-contract.js +1 -1
- package/dist/studio-app/assets/{feltdb_wasm-bIqcRzAr.js → feltdb_wasm-DB8cX151.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-ClhDHp0S.wasm +0 -0
- package/dist/studio-app/assets/{index-XGYdlElN.js → index-B0k4UAlI.js} +1 -1
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-BE79okwX.wasm +0 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#![cfg(unix)]
|
|
2
|
+
|
|
3
|
+
use feltdb::{
|
|
4
|
+
authority::{
|
|
5
|
+
AuthorityClient, AuthorityTransaction, EmbeddedAuthority, FeltDbAuthority, ReadKind,
|
|
6
|
+
},
|
|
7
|
+
AtomicMutation,
|
|
8
|
+
};
|
|
9
|
+
use serde_json::json;
|
|
10
|
+
use std::{
|
|
11
|
+
process::{Child, Command},
|
|
12
|
+
time::Duration,
|
|
13
|
+
};
|
|
14
|
+
use tempfile::TempDir;
|
|
15
|
+
|
|
16
|
+
static PROCESS_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
17
|
+
|
|
18
|
+
struct RunningAuthority {
|
|
19
|
+
child: Child,
|
|
20
|
+
socket: std::path::PathBuf,
|
|
21
|
+
storage: std::path::PathBuf,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl Drop for RunningAuthority {
|
|
25
|
+
fn drop(&mut self) {
|
|
26
|
+
let _ = self.child.kill();
|
|
27
|
+
let _ = self.child.wait();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn start(directory: &TempDir) -> RunningAuthority {
|
|
32
|
+
let socket = directory.path().join("authority.sock");
|
|
33
|
+
let storage = directory.path().join("authority.db");
|
|
34
|
+
let child = Command::new(env!("CARGO_BIN_EXE_feltdb-authority"))
|
|
35
|
+
.arg(&socket)
|
|
36
|
+
.arg(&storage)
|
|
37
|
+
.spawn()
|
|
38
|
+
.expect("authority starts");
|
|
39
|
+
for _ in 0..200 {
|
|
40
|
+
if socket.exists() {
|
|
41
|
+
return RunningAuthority {
|
|
42
|
+
child,
|
|
43
|
+
socket,
|
|
44
|
+
storage,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
std::thread::sleep(Duration::from_millis(5));
|
|
48
|
+
}
|
|
49
|
+
panic!("authority socket was not ready");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn writer(socket: &std::path::Path, id: usize) -> Child {
|
|
53
|
+
Command::new(env!("CARGO_BIN_EXE_feltdb-authority-client"))
|
|
54
|
+
.arg(socket)
|
|
55
|
+
.arg(id.to_string())
|
|
56
|
+
.spawn()
|
|
57
|
+
.expect("writer starts")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[tokio::test(flavor = "current_thread")]
|
|
61
|
+
async fn seed_34002_ten_process_writers_preserve_ten_records() {
|
|
62
|
+
let _process_test = PROCESS_TESTS
|
|
63
|
+
.lock()
|
|
64
|
+
.unwrap_or_else(|error| error.into_inner());
|
|
65
|
+
let directory = TempDir::new().unwrap();
|
|
66
|
+
let authority = start(&directory);
|
|
67
|
+
let mut writers: Vec<_> = (0..10).map(|id| writer(&authority.socket, id)).collect();
|
|
68
|
+
for writer in &mut writers {
|
|
69
|
+
assert!(writer.wait().unwrap().success());
|
|
70
|
+
}
|
|
71
|
+
let read = AuthorityClient::connect(&authority.socket)
|
|
72
|
+
.read_authoritative()
|
|
73
|
+
.await
|
|
74
|
+
.unwrap();
|
|
75
|
+
assert_eq!(read.authoritative_revision, 10);
|
|
76
|
+
assert_eq!(
|
|
77
|
+
read.rows
|
|
78
|
+
.iter()
|
|
79
|
+
.filter(|row| row.capability == "registrations")
|
|
80
|
+
.count(),
|
|
81
|
+
10
|
|
82
|
+
);
|
|
83
|
+
assert!(!directory.path().join("authority.tmp").exists());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[tokio::test(flavor = "current_thread")]
|
|
87
|
+
async fn same_revision_is_coherent_across_disposable_clients() {
|
|
88
|
+
let _process_test = PROCESS_TESTS
|
|
89
|
+
.lock()
|
|
90
|
+
.unwrap_or_else(|error| error.into_inner());
|
|
91
|
+
let directory = TempDir::new().unwrap();
|
|
92
|
+
let authority = start(&directory);
|
|
93
|
+
let mut writers: Vec<_> = (0..10).map(|id| writer(&authority.socket, id)).collect();
|
|
94
|
+
for writer in &mut writers {
|
|
95
|
+
assert!(writer.wait().unwrap().success());
|
|
96
|
+
}
|
|
97
|
+
let first = AuthorityClient::connect(&authority.socket)
|
|
98
|
+
.read_authoritative()
|
|
99
|
+
.await
|
|
100
|
+
.unwrap();
|
|
101
|
+
let second = AuthorityClient::connect(&authority.socket)
|
|
102
|
+
.read_authoritative()
|
|
103
|
+
.await
|
|
104
|
+
.unwrap();
|
|
105
|
+
assert_eq!(first.authoritative_revision, second.authoritative_revision);
|
|
106
|
+
assert_eq!(first.content_hash, second.content_hash);
|
|
107
|
+
assert_eq!(first.rows.len(), second.rows.len());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[tokio::test(flavor = "current_thread")]
|
|
111
|
+
async fn authoritative_reads_during_concurrent_writes_are_committed_states() {
|
|
112
|
+
let _process_test = PROCESS_TESTS
|
|
113
|
+
.lock()
|
|
114
|
+
.unwrap_or_else(|error| error.into_inner());
|
|
115
|
+
let directory = TempDir::new().unwrap();
|
|
116
|
+
let authority = start(&directory);
|
|
117
|
+
let mut writers: Vec<_> = (0..10).map(|id| writer(&authority.socket, id)).collect();
|
|
118
|
+
let client = AuthorityClient::connect(&authority.socket);
|
|
119
|
+
let mut last_revision = 0;
|
|
120
|
+
while writers
|
|
121
|
+
.iter_mut()
|
|
122
|
+
.any(|writer| writer.try_wait().unwrap().is_none())
|
|
123
|
+
{
|
|
124
|
+
let read = client.read_authoritative().await.unwrap();
|
|
125
|
+
assert!(read.authoritative_revision >= last_revision);
|
|
126
|
+
assert_eq!(read.observed_revision, read.authoritative_revision);
|
|
127
|
+
assert_eq!(read.rows.len() as u64, read.authoritative_revision);
|
|
128
|
+
last_revision = read.authoritative_revision;
|
|
129
|
+
}
|
|
130
|
+
for writer in &mut writers {
|
|
131
|
+
if writer.try_wait().unwrap().is_none() {
|
|
132
|
+
assert!(writer.wait().unwrap().success());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
assert_eq!(client.read_authoritative().await.unwrap().rows.len(), 10);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#[tokio::test(flavor = "current_thread")]
|
|
139
|
+
async fn recorded_deterministic_stress_seeds_preserve_every_commit() {
|
|
140
|
+
let _process_test = PROCESS_TESTS
|
|
141
|
+
.lock()
|
|
142
|
+
.unwrap_or_else(|error| error.into_inner());
|
|
143
|
+
for seed in [34002_u64, 1, 7, 42, 34003] {
|
|
144
|
+
let directory = TempDir::new().unwrap();
|
|
145
|
+
let authority = start(&directory);
|
|
146
|
+
let mut order: Vec<usize> = (0..10).collect();
|
|
147
|
+
order.rotate_left((seed as usize) % 10);
|
|
148
|
+
if seed & 1 == 1 {
|
|
149
|
+
order.reverse();
|
|
150
|
+
}
|
|
151
|
+
let mut writers: Vec<_> = order
|
|
152
|
+
.into_iter()
|
|
153
|
+
.map(|id| writer(&authority.socket, id))
|
|
154
|
+
.collect();
|
|
155
|
+
for writer in &mut writers {
|
|
156
|
+
assert!(writer.wait().unwrap().success(), "seed {seed}");
|
|
157
|
+
}
|
|
158
|
+
let read = AuthorityClient::connect(&authority.socket)
|
|
159
|
+
.read_authoritative()
|
|
160
|
+
.await
|
|
161
|
+
.unwrap();
|
|
162
|
+
assert_eq!(read.rows.len(), 10, "seed {seed}");
|
|
163
|
+
assert_eq!(read.authoritative_revision, 10, "seed {seed}");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[tokio::test(flavor = "current_thread")]
|
|
168
|
+
async fn stale_snapshot_is_explicit_and_stale_commit_is_rejected() {
|
|
169
|
+
let _process_test = PROCESS_TESTS
|
|
170
|
+
.lock()
|
|
171
|
+
.unwrap_or_else(|error| error.into_inner());
|
|
172
|
+
let directory = TempDir::new().unwrap();
|
|
173
|
+
let embedded = EmbeddedAuthority::open(directory.path().join("embedded.db")).unwrap();
|
|
174
|
+
let snapshot = embedded.snapshot().unwrap();
|
|
175
|
+
assert_eq!(snapshot.kind, ReadKind::Snapshot);
|
|
176
|
+
let commit = |id: &str, base_revision| AuthorityTransaction {
|
|
177
|
+
transaction_id: id.into(),
|
|
178
|
+
base_revision,
|
|
179
|
+
preconditions: vec![],
|
|
180
|
+
record_preconditions: vec![],
|
|
181
|
+
mutations: vec![AtomicMutation {
|
|
182
|
+
capability: "cells".into(),
|
|
183
|
+
key: format!("cells:{id}"),
|
|
184
|
+
value: Some(json!({"id": id})),
|
|
185
|
+
}],
|
|
186
|
+
audit: None,
|
|
187
|
+
};
|
|
188
|
+
let first = embedded
|
|
189
|
+
.commit(commit("first", Some(snapshot.observed_revision)))
|
|
190
|
+
.await
|
|
191
|
+
.unwrap();
|
|
192
|
+
assert_eq!(first.commit_revision, snapshot.observed_revision + 1);
|
|
193
|
+
let duplicate = embedded
|
|
194
|
+
.commit(commit("first", Some(snapshot.observed_revision)))
|
|
195
|
+
.await
|
|
196
|
+
.unwrap();
|
|
197
|
+
assert!(duplicate.duplicate);
|
|
198
|
+
assert_eq!(duplicate.base_revision, first.base_revision);
|
|
199
|
+
assert_eq!(duplicate.commit_revision, first.commit_revision);
|
|
200
|
+
let error = embedded
|
|
201
|
+
.commit(commit("stale", Some(snapshot.observed_revision)))
|
|
202
|
+
.await
|
|
203
|
+
.unwrap_err();
|
|
204
|
+
assert_eq!(error.code, "REVISION_CONFLICT");
|
|
205
|
+
assert_eq!(error.authoritative_revision, Some(first.commit_revision));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#[tokio::test(flavor = "current_thread")]
|
|
209
|
+
async fn authority_restart_recovers_revision_and_continues_monotonically() {
|
|
210
|
+
let _process_test = PROCESS_TESTS
|
|
211
|
+
.lock()
|
|
212
|
+
.unwrap_or_else(|error| error.into_inner());
|
|
213
|
+
let directory = TempDir::new().unwrap();
|
|
214
|
+
let mut authority = start(&directory);
|
|
215
|
+
let mut writers: Vec<_> = (0..3).map(|id| writer(&authority.socket, id)).collect();
|
|
216
|
+
for writer in &mut writers {
|
|
217
|
+
assert!(writer.wait().unwrap().success());
|
|
218
|
+
}
|
|
219
|
+
authority.child.kill().unwrap();
|
|
220
|
+
authority.child.wait().unwrap();
|
|
221
|
+
let _ = std::fs::remove_file(&authority.socket);
|
|
222
|
+
let restarted = start(&directory);
|
|
223
|
+
let client = AuthorityClient::connect(&restarted.socket);
|
|
224
|
+
assert_eq!(client.current_revision().await.unwrap(), 3);
|
|
225
|
+
let mut next = writer(&restarted.socket, 4);
|
|
226
|
+
assert!(next.wait().unwrap().success());
|
|
227
|
+
assert_eq!(client.current_revision().await.unwrap(), 4);
|
|
228
|
+
assert_eq!(client.read_authoritative().await.unwrap().rows.len(), 4);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#[tokio::test(flavor = "current_thread")]
|
|
232
|
+
async fn torn_commit_tail_recovers_only_the_previous_committed_revision() {
|
|
233
|
+
use std::io::Write;
|
|
234
|
+
let _process_test = PROCESS_TESTS
|
|
235
|
+
.lock()
|
|
236
|
+
.unwrap_or_else(|error| error.into_inner());
|
|
237
|
+
let directory = TempDir::new().unwrap();
|
|
238
|
+
let mut authority = start(&directory);
|
|
239
|
+
let mut first = writer(&authority.socket, 1);
|
|
240
|
+
assert!(first.wait().unwrap().success());
|
|
241
|
+
authority.child.kill().unwrap();
|
|
242
|
+
authority.child.wait().unwrap();
|
|
243
|
+
let mut log = std::fs::OpenOptions::new()
|
|
244
|
+
.append(true)
|
|
245
|
+
.open(&authority.storage)
|
|
246
|
+
.unwrap();
|
|
247
|
+
log.write_all(br#"{"record_type":"feltdb.transaction.v1","transaction_id":"torn""#)
|
|
248
|
+
.unwrap();
|
|
249
|
+
log.sync_all().unwrap();
|
|
250
|
+
let _ = std::fs::remove_file(&authority.socket);
|
|
251
|
+
let recovered = start(&directory);
|
|
252
|
+
let client = AuthorityClient::connect(&recovered.socket);
|
|
253
|
+
let read = client.read_authoritative().await.unwrap();
|
|
254
|
+
assert_eq!(read.authoritative_revision, 1);
|
|
255
|
+
assert_eq!(read.rows.len(), 1);
|
|
256
|
+
let mut second = writer(&recovered.socket, 2);
|
|
257
|
+
assert!(second.wait().unwrap().success());
|
|
258
|
+
assert_eq!(client.current_revision().await.unwrap(), 2);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#[tokio::test(flavor = "current_thread")]
|
|
262
|
+
async fn f2_authority_schedule_exposes_the_removal_at_a_new_revision() {
|
|
263
|
+
let _process_test = PROCESS_TESTS
|
|
264
|
+
.lock()
|
|
265
|
+
.unwrap_or_else(|error| error.into_inner());
|
|
266
|
+
let directory = TempDir::new().unwrap();
|
|
267
|
+
let authority = start(&directory);
|
|
268
|
+
let client = AuthorityClient::connect(&authority.socket);
|
|
269
|
+
let mut initial = writer(&authority.socket, 1);
|
|
270
|
+
assert!(initial.wait().unwrap().success());
|
|
271
|
+
let consumer_snapshot = client.read_authoritative().await.unwrap();
|
|
272
|
+
client
|
|
273
|
+
.commit(AuthorityTransaction {
|
|
274
|
+
transaction_id: "provider-removes-registration".into(),
|
|
275
|
+
base_revision: Some(consumer_snapshot.authoritative_revision),
|
|
276
|
+
preconditions: vec![],
|
|
277
|
+
record_preconditions: vec![],
|
|
278
|
+
mutations: vec![AtomicMutation {
|
|
279
|
+
capability: "registrations".into(),
|
|
280
|
+
key: "registrations:1".into(),
|
|
281
|
+
value: None,
|
|
282
|
+
}],
|
|
283
|
+
audit: None,
|
|
284
|
+
})
|
|
285
|
+
.await
|
|
286
|
+
.unwrap();
|
|
287
|
+
let resumed = AuthorityClient::connect(&authority.socket)
|
|
288
|
+
.read_authoritative()
|
|
289
|
+
.await
|
|
290
|
+
.unwrap();
|
|
291
|
+
assert!(resumed.authoritative_revision > consumer_snapshot.observed_revision);
|
|
292
|
+
assert!(resumed.rows.iter().all(|row| row.key != "registrations:1"));
|
|
293
|
+
assert!(consumer_snapshot
|
|
294
|
+
.rows
|
|
295
|
+
.iter()
|
|
296
|
+
.any(|row| row.key == "registrations:1"));
|
|
297
|
+
}
|
|
@@ -10,6 +10,7 @@ use argon2::{
|
|
|
10
10
|
};
|
|
11
11
|
use rand::{distributions::Alphanumeric, Rng};
|
|
12
12
|
use serde::{Deserialize, Serialize};
|
|
13
|
+
use sha2::{Digest, Sha256};
|
|
13
14
|
|
|
14
15
|
#[derive(Debug, Deserialize)]
|
|
15
16
|
struct LegacyApiKeyStore {
|
|
@@ -128,18 +129,18 @@ impl KeyStore {
|
|
|
128
129
|
let records = self.records.read().ok()?;
|
|
129
130
|
records
|
|
130
131
|
.iter()
|
|
132
|
+
// Newly issued high-entropy keys use the fast digest format and
|
|
133
|
+
// are appended to the store. Check them before legacy Argon2
|
|
134
|
+
// records so current requests never pay historical password-hash
|
|
135
|
+
// verification costs.
|
|
136
|
+
.rev()
|
|
131
137
|
.filter(|record| !record.revoked)
|
|
132
138
|
.find_map(|record| {
|
|
133
139
|
let namespace_allowed = record
|
|
134
140
|
.namespaces
|
|
135
141
|
.iter()
|
|
136
142
|
.any(|allowed| allowed == "*" || allowed == namespace);
|
|
137
|
-
|
|
138
|
-
if namespace_allowed
|
|
139
|
-
&& Argon2::default()
|
|
140
|
-
.verify_password(token.as_bytes(), &parsed)
|
|
141
|
-
.is_ok()
|
|
142
|
-
{
|
|
143
|
+
if namespace_allowed && verify_api_key(token, &record.hash) {
|
|
143
144
|
Some(Principal {
|
|
144
145
|
key_id: record.id.clone(),
|
|
145
146
|
scopes: record.scopes.clone(),
|
|
@@ -165,11 +166,10 @@ impl KeyStore {
|
|
|
165
166
|
.map(char::from)
|
|
166
167
|
.collect();
|
|
167
168
|
let token = format!("fdb_live_{secret}");
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
.to_string();
|
|
169
|
+
// Service keys are high-entropy random secrets, not user-selected
|
|
170
|
+
// passwords. A deterministic digest keeps them out of the credential
|
|
171
|
+
// store without imposing Argon2's CPU cost on every API request.
|
|
172
|
+
let hash = hash_api_key(&token);
|
|
173
173
|
let id: String = rand::thread_rng()
|
|
174
174
|
.sample_iter(&Alphanumeric)
|
|
175
175
|
.take(12)
|
|
@@ -246,6 +246,35 @@ impl KeyStore {
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
fn hash_api_key(token: &str) -> String {
|
|
250
|
+
let digest = Sha256::digest(token.as_bytes());
|
|
251
|
+
let encoded = digest
|
|
252
|
+
.iter()
|
|
253
|
+
.map(|byte| format!("{byte:02x}"))
|
|
254
|
+
.collect::<String>();
|
|
255
|
+
format!("sha256:{encoded}")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fn verify_api_key(token: &str, stored_hash: &str) -> bool {
|
|
259
|
+
if stored_hash.starts_with("sha256:") {
|
|
260
|
+
let candidate = hash_api_key(token);
|
|
261
|
+
return candidate.len() == stored_hash.len()
|
|
262
|
+
&& ring::constant_time::verify_slices_are_equal(
|
|
263
|
+
candidate.as_bytes(),
|
|
264
|
+
stored_hash.as_bytes(),
|
|
265
|
+
)
|
|
266
|
+
.is_ok();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
PasswordHash::new(stored_hash)
|
|
270
|
+
.ok()
|
|
271
|
+
.is_some_and(|parsed| {
|
|
272
|
+
Argon2::default()
|
|
273
|
+
.verify_password(token.as_bytes(), &parsed)
|
|
274
|
+
.is_ok()
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
249
278
|
#[cfg(test)]
|
|
250
279
|
mod tests {
|
|
251
280
|
use super::*;
|
|
@@ -272,6 +301,7 @@ mod tests {
|
|
|
272
301
|
|
|
273
302
|
let file = fs::read_to_string(&path).unwrap();
|
|
274
303
|
assert!(!file.contains(&token));
|
|
304
|
+
assert!(file.contains("sha256:"));
|
|
275
305
|
assert!(store
|
|
276
306
|
.authenticate(&token, "alpha")
|
|
277
307
|
.unwrap()
|
|
@@ -59,9 +59,14 @@ pub async fn create_key(
|
|
|
59
59
|
{
|
|
60
60
|
authentication_namespaces.push(state.namespace.to_string());
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Argon2 is intentionally CPU/memory expensive. Running it on the async
|
|
63
|
+
// request worker makes readiness and unrelated control-plane requests
|
|
64
|
+
// unavailable while qualification creates several scoped keys.
|
|
65
|
+
let keys = state.keys.clone();
|
|
66
|
+
let key_scopes = scopes.clone();
|
|
67
|
+
let key = tokio::task::spawn_blocking(move || keys.create(name, key_scopes, authentication_namespaces))
|
|
68
|
+
.await
|
|
69
|
+
.map_err(|error| (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()))?
|
|
65
70
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
|
66
71
|
|
|
67
72
|
if let Some(application_id) = req.application_id {
|