@feltdb/core 0.12.2 → 0.15.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 (126) hide show
  1. package/dist/agent-runtime.d.ts +64 -2
  2. package/dist/agent-runtime.d.ts.map +1 -1
  3. package/dist/agent-runtime.js +121 -11
  4. package/dist/agent-state.d.ts +48 -0
  5. package/dist/agent-state.d.ts.map +1 -1
  6. package/dist/agent-state.js +174 -12
  7. package/dist/agent.d.ts +10 -1
  8. package/dist/agent.d.ts.map +1 -1
  9. package/dist/agent.js +10 -1
  10. package/dist/build-identity.json +4 -4
  11. package/dist/cli/commands.js +34 -7
  12. package/dist/cli/workspace-integration.js +8 -0
  13. package/dist/collection.d.ts +3 -1
  14. package/dist/collection.d.ts.map +1 -1
  15. package/dist/collection.js +30 -8
  16. package/dist/create/build-identity.json +4 -4
  17. package/dist/create/cli.js +47 -38
  18. package/dist/create/create.js +21 -8
  19. package/dist/create/package-versions.js +1 -1
  20. package/dist/create/server-source/Cargo.lock +6 -6
  21. package/dist/create/server-source/Cargo.toml +1 -1
  22. package/dist/create/server-source/crates/feltdb/src/lib.rs +126 -35
  23. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +162 -53
  24. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +341 -2
  25. package/dist/create/server-source/crates/feltdb/src/workload.rs +2094 -126
  26. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +18 -12
  27. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +17 -5
  28. package/dist/create/server-source/crates/feltdb-client/Cargo.toml +1 -1
  29. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -1
  30. package/dist/create/server-source/crates/feltdb-server/README.md +6 -1
  31. package/dist/create/server-source/crates/feltdb-server/src/agent_execution.rs +511 -0
  32. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +9 -1
  33. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +113 -25
  34. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +4 -0
  35. package/dist/create/server-source/crates/feltdb-server/src/console_accounts.rs +1009 -0
  36. package/dist/create/server-source/crates/feltdb-server/src/control_authorization.rs +441 -0
  37. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +68 -6
  38. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +355 -23
  39. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +4 -0
  40. package/dist/create/server-source/crates/feltdb-server/src/main.rs +1740 -236
  41. package/dist/create/server-source/crates/feltdb-server/src/redaction.rs +208 -0
  42. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +198 -19
  43. package/dist/create/server-source/crates/feltdb-server/tests/transaction_precondition_contract.rs +403 -0
  44. package/dist/create/setup-summary.js +40 -0
  45. package/dist/db.d.ts +91 -10
  46. package/dist/db.d.ts.map +1 -1
  47. package/dist/db.js +170 -31
  48. package/dist/deployment.d.ts +19 -0
  49. package/dist/deployment.d.ts.map +1 -1
  50. package/dist/deployment.js +38 -2
  51. package/dist/development-runtime-bridge.d.ts +20 -0
  52. package/dist/development-runtime-bridge.d.ts.map +1 -1
  53. package/dist/development-runtime-bridge.js +61 -12
  54. package/dist/embedded-query.d.ts.map +1 -1
  55. package/dist/embedded-query.js +5 -4
  56. package/dist/embedded-transaction.d.ts +8 -4
  57. package/dist/embedded-transaction.d.ts.map +1 -1
  58. package/dist/embedded-transaction.js +55 -19
  59. package/dist/error-codes.d.ts +31 -2
  60. package/dist/error-codes.d.ts.map +1 -1
  61. package/dist/error-codes.js +41 -2
  62. package/dist/feltdb.d.ts +7 -0
  63. package/dist/feltdb.d.ts.map +1 -1
  64. package/dist/file-db.d.ts +27 -0
  65. package/dist/file-db.d.ts.map +1 -1
  66. package/dist/file-db.js +145 -7
  67. package/dist/http-client.d.ts +11 -3
  68. package/dist/http-client.d.ts.map +1 -1
  69. package/dist/http-client.js +27 -14
  70. package/dist/http-db.d.ts +40 -1
  71. package/dist/http-db.d.ts.map +1 -1
  72. package/dist/http-db.js +285 -58
  73. package/dist/index.js +1 -1
  74. package/dist/local-state-lock.d.ts +34 -0
  75. package/dist/local-state-lock.d.ts.map +1 -0
  76. package/dist/local-state-lock.js +225 -0
  77. package/dist/reactive-graph.d.ts +0 -3
  78. package/dist/reactive-graph.d.ts.map +1 -1
  79. package/dist/reactive-graph.js +11 -1
  80. package/dist/runtime-registry.d.ts +7 -2
  81. package/dist/runtime-registry.d.ts.map +1 -1
  82. package/dist/runtime-registry.js +2 -2
  83. package/dist/studio/{KeyManagementPanel-D5FMvvU9.js → KeyManagementPanel-skUNnKRw.js} +36 -29
  84. package/dist/studio/app.d.ts +8 -1
  85. package/dist/studio/app.d.ts.map +1 -1
  86. package/dist/studio/components/KeyManagementPanel.d.ts.map +1 -1
  87. package/dist/studio/components/KeyManagementPanel.js +1 -1
  88. package/dist/studio/components/index.js +2 -2
  89. package/dist/studio/{components-B4eKIUN5.js → components-CJmzntBi.js} +8 -7
  90. package/dist/studio/control-plane/Data.d.ts.map +1 -1
  91. package/dist/studio/control-plane/Instance.d.ts +4 -1
  92. package/dist/studio/control-plane/Instance.d.ts.map +1 -1
  93. package/dist/studio/control-plane/Operations.d.ts +4 -1
  94. package/dist/studio/control-plane/Operations.d.ts.map +1 -1
  95. package/dist/studio/control-plane/index.js +2 -2
  96. package/dist/studio/control-plane/primitives.d.ts +16 -0
  97. package/dist/studio/control-plane/primitives.d.ts.map +1 -1
  98. package/dist/studio/{control-plane-fxMcJn74.js → control-plane-CaBA7Rgv.js} +448 -424
  99. package/dist/studio/index.d.ts +1 -0
  100. package/dist/studio/index.d.ts.map +1 -1
  101. package/dist/studio/index.js +384 -247
  102. package/dist/studio/navigation.d.ts +57 -0
  103. package/dist/studio/navigation.d.ts.map +1 -0
  104. package/dist/studio/studio.css +1 -1
  105. package/dist/studio/utils/control-plane-api.d.ts +2 -0
  106. package/dist/studio/utils/control-plane-api.d.ts.map +1 -1
  107. package/dist/studio/utils/control-plane-api.js +5 -1
  108. package/dist/studio-app/assets/{feltdb_wasm-BatwwogY.js → feltdb_wasm-BjM1ylL3.js} +1 -1
  109. package/dist/studio-app/assets/feltdb_wasm_bg-TseJn2Ax.wasm +0 -0
  110. package/dist/studio-app/assets/index-1wd0uKua.js +30 -0
  111. package/dist/studio-app/assets/{index-CiC8VY-a.css → index-vi21oEQa.css} +1 -1
  112. package/dist/studio-app/index.html +2 -2
  113. package/dist/transaction.d.ts +27 -11
  114. package/dist/transaction.d.ts.map +1 -1
  115. package/dist/transaction.js +20 -14
  116. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  117. package/dist/wasm/package.json +1 -1
  118. package/dist/workload.d.ts +31 -3
  119. package/dist/workload.d.ts.map +1 -1
  120. package/dist/workload.js +28 -3
  121. package/dist/workspace/local-development-authority.d.ts +19 -0
  122. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  123. package/dist/workspace/local-development-authority.js +335 -15
  124. package/package.json +1 -1
  125. package/dist/studio-app/assets/feltdb_wasm_bg-BX0newOr.wasm +0 -0
  126. package/dist/studio-app/assets/index-DoVvbIay.js +0 -30
@@ -0,0 +1,403 @@
1
+ //! The transaction-precondition contract through the Rust client (`feltdb-client`) against this
2
+ //! crate's real server binary, over both transaction routes.
3
+ //!
4
+ //! The same contract is held for the TypeScript SDK by
5
+ //! `e2e/transaction-precondition-contract.test.mjs`. Every test stages the failure the contract
6
+ //! forbids and asserts the authority's structured answer as the Rust client decodes it.
7
+
8
+ use std::{
9
+ io::{BufRead, BufReader},
10
+ path::PathBuf,
11
+ process::{Child, Command, Stdio},
12
+ time::{Duration, SystemTime, UNIX_EPOCH},
13
+ };
14
+
15
+ use feltdb_client::{
16
+ path_segment, AppTransaction, Client, Error, Method, Operation, Precondition,
17
+ PreconditionFailure, Transaction,
18
+ };
19
+ use serde_json::{json, Value};
20
+
21
+ struct Server {
22
+ child: Child,
23
+ url: String,
24
+ directory: PathBuf,
25
+ }
26
+
27
+ impl Server {
28
+ fn start(name: &str) -> Self {
29
+ let directory = std::env::temp_dir().join(format!(
30
+ "feltdb-txp-{name}-{}-{}",
31
+ std::process::id(),
32
+ SystemTime::now()
33
+ .duration_since(UNIX_EPOCH)
34
+ .unwrap()
35
+ .as_nanos()
36
+ ));
37
+ std::fs::create_dir_all(&directory).unwrap();
38
+ let data = directory.join("feltdb.log");
39
+ let mut child = Command::new(env!("CARGO_BIN_EXE_feltdb-server"))
40
+ .args([
41
+ "--host",
42
+ "127.0.0.1",
43
+ "--port",
44
+ "0",
45
+ "--namespace",
46
+ "txp",
47
+ "--data",
48
+ data.to_str().unwrap(),
49
+ "--keys",
50
+ directory.join("keys.json").to_str().unwrap(),
51
+ "--audit",
52
+ directory.join("audit.log").to_str().unwrap(),
53
+ ])
54
+ .env("FELTDB_MASTER_KEY", "txp-development-key")
55
+ .stdout(Stdio::piped())
56
+ .stderr(Stdio::null())
57
+ .spawn()
58
+ .unwrap();
59
+ let mut lines = BufReader::new(child.stdout.take().unwrap()).lines();
60
+ let url = loop {
61
+ let line = lines
62
+ .next()
63
+ .expect("server exited before readiness")
64
+ .unwrap();
65
+ if let Some(rest) = line.split("ready at ").nth(1) {
66
+ break rest.split_whitespace().next().unwrap().to_owned();
67
+ }
68
+ };
69
+ std::thread::spawn(move || lines.for_each(drop));
70
+ Self {
71
+ child,
72
+ url,
73
+ directory,
74
+ }
75
+ }
76
+
77
+ fn client(&self) -> Client {
78
+ Client::builder(&self.url).build().unwrap()
79
+ }
80
+
81
+ fn data(&self) -> PathBuf {
82
+ self.directory.join("feltdb.log")
83
+ }
84
+ }
85
+
86
+ impl Drop for Server {
87
+ fn drop(&mut self) {
88
+ let _ = self.child.kill();
89
+ let _ = self.child.wait();
90
+ let _ = std::fs::remove_dir_all(&self.directory);
91
+ }
92
+ }
93
+
94
+ /// Provision one application with collections `A` and `B` and return its id.
95
+ async fn provision(client: &Client) -> String {
96
+ let tenant = client
97
+ .request(
98
+ Method::POST,
99
+ "/api/tenants",
100
+ Some(&json!({ "name": "txp" })),
101
+ )
102
+ .await
103
+ .unwrap();
104
+ let tenant_id = tenant["tenant_id"]
105
+ .as_str()
106
+ .or(tenant["id"].as_str())
107
+ .unwrap();
108
+ let application = client
109
+ .request(
110
+ Method::POST,
111
+ &format!("/api/tenants/{tenant_id}/applications"),
112
+ Some(&json!({ "name": "txp" })),
113
+ )
114
+ .await
115
+ .unwrap();
116
+ let application_id = application["application_id"]
117
+ .as_str()
118
+ .or(application["id"].as_str())
119
+ .unwrap()
120
+ .to_owned();
121
+ let drafts = format!("/api/applications/{application_id}/drafts");
122
+ let draft = client
123
+ .request(Method::POST, &drafts, Some(&json!({})))
124
+ .await
125
+ .unwrap();
126
+ let draft_id = draft["draft_id"].as_str().unwrap();
127
+ let mut manifest = draft["manifest"].clone();
128
+ let fields = json!([{ "name": "n", "type": "integer", "required": false }]);
129
+ manifest["collections"] = json!([
130
+ { "name": "A", "fields": fields },
131
+ { "name": "B", "fields": fields },
132
+ ]);
133
+ manifest["policies"] = json!(
134
+ ["A", "B"].map(|name| json!({ "name": name, "resource": name,
135
+ "read": "authenticated", "write": "authenticated", "capabilities": [] }))
136
+ );
137
+ let version = draft["version"].as_u64().unwrap().to_string();
138
+ client
139
+ .request_with_headers(
140
+ Method::PUT,
141
+ &format!("{drafts}/{draft_id}"),
142
+ &[("if-version", &version)],
143
+ Some(&json!({ "manifest": manifest })),
144
+ )
145
+ .await
146
+ .unwrap();
147
+ let revision = client
148
+ .request(
149
+ Method::POST,
150
+ &format!("{drafts}/{draft_id}/commit"),
151
+ Some(&json!({})),
152
+ )
153
+ .await
154
+ .unwrap();
155
+ let encoded = path_segment(revision["revision_id"].as_str().unwrap());
156
+ client
157
+ .request(
158
+ Method::POST,
159
+ &format!("/api/applications/{application_id}/revisions/{encoded}/promote"),
160
+ Some(&json!({ "environment": "production", "reason": "txp" })),
161
+ )
162
+ .await
163
+ .unwrap();
164
+ application_id
165
+ }
166
+
167
+ fn failure_body(error: &Error) -> Value {
168
+ match error {
169
+ Error::Api(api) => api.body["failure"].clone(),
170
+ other => panic!("expected an API refusal, got {other}"),
171
+ }
172
+ }
173
+
174
+ #[tokio::test]
175
+ async fn direct_route_a_stale_guard_refuses_every_staged_write_with_a_structured_failure() {
176
+ let server = Server::start("direct-stale");
177
+ let client = server.client();
178
+ client
179
+ .commit(&Transaction::generated().operation(Operation::create("A", "a", json!({}))))
180
+ .await
181
+ .unwrap();
182
+ client
183
+ .commit(&Transaction::generated().operation(Operation::create("B", "b", json!({ "n": 1 }))))
184
+ .await
185
+ .unwrap();
186
+ // Another writer advances A from 1 to 2.
187
+ client
188
+ .commit(
189
+ &Transaction::generated()
190
+ .operation(Operation::put("A", "a", json!({ "n": 2 })).expect_version(1)),
191
+ )
192
+ .await
193
+ .unwrap();
194
+ let refused = client
195
+ .commit(
196
+ &Transaction::generated()
197
+ .precondition(Precondition::version("A", "a", 1))
198
+ .operation(Operation::put("A", "a", json!({ "n": 9 })))
199
+ .operation(Operation::put("B", "b", json!({ "n": 9 }))),
200
+ )
201
+ .await
202
+ .unwrap_err();
203
+ assert_eq!(refused.code(), Some("PRECONDITION_FAILED"));
204
+ assert_eq!(refused.status(), Some(409));
205
+ assert_eq!(
206
+ refused.precondition_failure(),
207
+ Some(&PreconditionFailure::Version {
208
+ collection: "A".into(),
209
+ key: "A:a".into(),
210
+ expected: 1,
211
+ actual: 2
212
+ })
213
+ );
214
+ assert_eq!(
215
+ failure_body(&refused)["id"],
216
+ "a",
217
+ "the failure names the record id"
218
+ );
219
+ assert_eq!(
220
+ client.get("B", "b").await.unwrap().unwrap().value["n"],
221
+ 1,
222
+ "B is unchanged"
223
+ );
224
+ }
225
+
226
+ #[tokio::test]
227
+ async fn application_route_refusals_name_the_public_collection_and_record() {
228
+ let server = Server::start("app-shape");
229
+ let client = server.client();
230
+ let app = client.application(&provision(&client).await);
231
+ app.commit(
232
+ &AppTransaction::generated()
233
+ .insert("A", "a", json!({ "n": 1 }))
234
+ .insert("B", "b", json!({ "n": 1 })),
235
+ )
236
+ .await
237
+ .unwrap();
238
+ // `__version` is the document version the authority owns: 1 on create, 2 after a fenced write.
239
+ app.commit(
240
+ &AppTransaction::generated()
241
+ .expect_version("A", "a", 1)
242
+ .update("A", "a", json!({ "n": 2 })),
243
+ )
244
+ .await
245
+ .unwrap();
246
+
247
+ let stale = app
248
+ .commit(
249
+ &AppTransaction::generated()
250
+ .expect_version("A", "a", 1)
251
+ .update("A", "a", json!({ "n": 9 }))
252
+ .update("B", "b", json!({ "n": 9 })),
253
+ )
254
+ .await
255
+ .unwrap_err();
256
+ assert_eq!(stale.code(), Some("PRECONDITION_FAILED"), "{stale}");
257
+ assert_eq!(
258
+ stale.precondition_failure(),
259
+ Some(&PreconditionFailure::Version {
260
+ collection: "A".into(),
261
+ key: "A:a".into(),
262
+ expected: 1,
263
+ actual: 2
264
+ }),
265
+ "the refusal names the application's collection, not the authority's storage namespace: {}",
266
+ failure_body(&stale)
267
+ );
268
+ assert_eq!(failure_body(&stale)["id"], "a");
269
+
270
+ // A create of a record that exists is the same structured refusal on every route.
271
+ let present = app
272
+ .commit(&AppTransaction::generated().insert("A", "a", json!({ "n": 1 })))
273
+ .await
274
+ .unwrap_err();
275
+ assert_eq!(present.code(), Some("PRECONDITION_FAILED"), "{present}");
276
+ assert!(
277
+ matches!(
278
+ present.precondition_failure(),
279
+ Some(PreconditionFailure::Present { collection, key }) if collection == "A" && key == "A:a"
280
+ ),
281
+ "{present}: {}",
282
+ failure_body(&present)
283
+ );
284
+ }
285
+
286
+ #[tokio::test]
287
+ async fn application_route_concurrent_writers_from_one_version_exactly_one_commits() {
288
+ let server = Server::start("app-race");
289
+ let client = server.client();
290
+ let application_id = provision(&client).await;
291
+ let app = client.application(&application_id);
292
+ app.commit(&AppTransaction::generated().insert("A", "a", json!({ "n": 0 })))
293
+ .await
294
+ .unwrap();
295
+ for round in 0..20u64 {
296
+ let from = round + 1;
297
+ let contenders = (0..6u64).map(|index| {
298
+ let app = server.client().application(&application_id);
299
+ tokio::spawn(async move {
300
+ app.commit(
301
+ &AppTransaction::generated()
302
+ .expect_version("A", "a", from)
303
+ .update("A", "a", json!({ "n": index }))
304
+ .insert("B", format!("b-{round}-{index}"), json!({ "n": index })),
305
+ )
306
+ .await
307
+ })
308
+ });
309
+ let outcomes: Vec<_> = futures_join(contenders).await;
310
+ let winners = outcomes.iter().filter(|outcome| outcome.is_ok()).count();
311
+ assert_eq!(winners, 1, "round {round}: {winners} winners");
312
+ for loser in outcomes.iter().filter_map(|outcome| outcome.as_ref().err()) {
313
+ assert!(
314
+ matches!(
315
+ loser.precondition_failure(),
316
+ Some(PreconditionFailure::Version { expected, actual, .. }) if *expected == from && *actual == from + 1
317
+ ),
318
+ "round {round}: {loser}"
319
+ );
320
+ }
321
+ }
322
+ }
323
+
324
+ async fn futures_join<T: Send + 'static>(
325
+ handles: impl Iterator<Item = tokio::task::JoinHandle<T>>,
326
+ ) -> Vec<T> {
327
+ let mut outcomes = Vec::new();
328
+ for handle in handles.collect::<Vec<_>>() {
329
+ outcomes.push(handle.await.unwrap());
330
+ }
331
+ outcomes
332
+ }
333
+
334
+ #[tokio::test]
335
+ async fn application_route_a_duplicate_reports_the_original_commit() {
336
+ let server = Server::start("app-duplicate");
337
+ let client = server.client();
338
+ let app = client.application(&provision(&client).await);
339
+ let original = AppTransaction::new("txp-dup").insert("A", "a", json!({ "n": 1 }));
340
+ let first = app.commit(&original).await.unwrap();
341
+ app.commit(&AppTransaction::generated().insert("B", "b", json!({ "n": 1 })))
342
+ .await
343
+ .unwrap();
344
+ let replay = app.commit(&original).await.unwrap();
345
+ assert!(replay.result.duplicate);
346
+ assert_eq!(replay.result.commit_revision, first.result.commit_revision);
347
+ assert_eq!(replay.result.base_revision, first.result.base_revision);
348
+ assert_eq!(replay.result.state_after, first.result.state_after);
349
+ let different = app
350
+ .commit(&AppTransaction::new("txp-dup").insert("A", "other", json!({ "n": 1 })))
351
+ .await
352
+ .unwrap_err();
353
+ assert_eq!(different.code(), Some("IDEMPOTENCY_CONFLICT"));
354
+ }
355
+
356
+ #[tokio::test]
357
+ async fn a_storage_failure_at_commit_is_a_durability_failure_not_a_conflict() {
358
+ let server = Server::start("direct-storage");
359
+ let client = server.client();
360
+ client
361
+ .commit(&Transaction::generated().operation(Operation::create("A", "a", json!({}))))
362
+ .await
363
+ .unwrap();
364
+ // The next append opens a directory and fails before writing anything.
365
+ let aside = server.data().with_extension("aside");
366
+ std::fs::rename(server.data(), &aside).unwrap();
367
+ std::fs::create_dir(server.data()).unwrap();
368
+ let refused = client
369
+ .commit(&Transaction::generated().operation(Operation::create("A", "b", json!({}))))
370
+ .await
371
+ .unwrap_err();
372
+ std::fs::remove_dir(server.data()).unwrap();
373
+ std::fs::rename(&aside, server.data()).unwrap();
374
+ assert!(
375
+ !refused.is_conflict(),
376
+ "a storage failure was reported as a lost race: {refused}"
377
+ );
378
+ assert_eq!(refused.code(), Some("DURABILITY_FAILURE"), "{refused}");
379
+ assert_eq!(refused.status(), Some(503));
380
+ }
381
+
382
+ #[tokio::test]
383
+ async fn a_write_whose_answer_never_arrives_is_outcome_unknown() {
384
+ // A listener that accepts and never answers: the request is sent, no answer arrives.
385
+ let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
386
+ let address = listener.local_addr().unwrap();
387
+ tokio::spawn(async move {
388
+ let mut held = Vec::new();
389
+ while let Ok((socket, _)) = listener.accept().await {
390
+ held.push(socket);
391
+ }
392
+ });
393
+ let client = Client::builder(format!("http://{address}"))
394
+ .timeout(Duration::from_millis(300))
395
+ .build()
396
+ .unwrap();
397
+ let error = client
398
+ .commit(&Transaction::new("txp-unknown").operation(Operation::create("A", "a", json!({}))))
399
+ .await
400
+ .unwrap_err();
401
+ assert!(error.is_outcome_unknown(), "{error}");
402
+ assert!(!error.is_conflict());
403
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * What `create-feltdb` will generate, in the words the developer chose it in.
3
+ *
4
+ * The summary is printed before anything is written, so it must describe the
5
+ * project that is about to exist — not the menu that produced it. Each line
6
+ * states what is included and what it needs; nothing here claims a capability
7
+ * the resulting instance would not provide.
8
+ */
9
+ export const RUNTIME_CHOICES = [
10
+ { value: 'browser', label: 'Browser', summary: 'data stays in the browser (IndexedDB); nothing else to run' },
11
+ { value: 'node', label: 'Node.js authority', summary: 'data lives in a FeltDB authority the application connects to; `feltdb dev` runs one locally' },
12
+ { value: 'self-hosted', label: 'Self-hosted server', summary: 'data lives in your own feltdb-server; Docker Compose included' },
13
+ { value: 'managed', label: 'Managed', summary: 'data lives in FeltDB-hosted infrastructure; needs a FeltDB account' },
14
+ ];
15
+ /** The React starter's tour includes its assistants; only the vanilla template makes them optional. */
16
+ export function agentsIncluded(options) {
17
+ return options.framework === 'react' || options.agents;
18
+ }
19
+ export function describeSetup(options) {
20
+ const runtime = RUNTIME_CHOICES.find(choice => choice.value === options.runtime);
21
+ const lines = [
22
+ `Data: ${runtime ? `${runtime.label} — ${runtime.summary}` : options.runtime}`,
23
+ `Framework: ${options.framework === 'react' ? 'React' : 'Vanilla TypeScript'}`,
24
+ ];
25
+ if (options.framework === 'react') {
26
+ lines.push('Starter: sign-up, projects, invitations, activity, and two assistants that run bounded capabilities (no AI model needed)');
27
+ lines.push('Capabilities: declared in feltdb.flow (ProjectSearch, ProjectSummary, ActivitySummary)');
28
+ }
29
+ else {
30
+ lines.push(`Agents: ${options.agents ? 'example included' : 'not included'}`);
31
+ lines.push(`Capabilities: ${options.capabilities}`);
32
+ }
33
+ lines.push(options.webllm
34
+ ? 'AI builder: included — @feltdb/webllm; downloads a small language model (a few hundred MB) into the browser on first use and needs WebGPU'
35
+ : 'AI builder: not included (add later: npm install @feltdb/webllm)');
36
+ if (options.distributed) {
37
+ lines.push('Distributed: recorded in feltdb.config.json; it takes effect when you deploy several feltdb-server nodes. Development runs one local authority.');
38
+ }
39
+ return lines;
40
+ }
package/dist/db.d.ts CHANGED
@@ -11,7 +11,7 @@ import { type TransactionReadBasis, type TransactionReadSet, type AtomicTransact
11
11
  import { type Cell } from './cell.js';
12
12
  import type { AgentRef, AgentDefinition, AgentExecution } from './agent.js';
13
13
  import { AgentRegistry } from './agent-registry.js';
14
- import { AgentRuntime } from './agent-runtime.js';
14
+ import { AgentRuntime, type CreateAgentExecutionOptions, type FindAgentExecutionQuery } from './agent-runtime.js';
15
15
  import type { AuthorityQueryPage, AuthorityQueryRequest } from './http-db.js';
16
16
  import type { EmbeddedQueryExplanation } from './embedded-query.js';
17
17
  import { type FeltDBArchitectureInvariantService } from './architecture-invariant.js';
@@ -38,9 +38,25 @@ export interface FeltDBOptions extends FeltDBDeploymentConfig {
38
38
  applicationId?: string;
39
39
  environment?: string;
40
40
  requestTimeoutMs?: number;
41
+ /**
42
+ * Operator consoles only (FeltDB Studio): a human pastes their own service
43
+ * key into their own browser tab at runtime. Applications must never set
44
+ * this — a service key in an application's browser code is published to
45
+ * every visitor. Without it, a `fdb_live_` key in a browser runtime throws
46
+ * `FeltDBBrowserCredentialError`.
47
+ */
48
+ operatorConsole?: true;
41
49
  };
42
50
  memory?: true;
43
51
  browser?: true;
52
+ /**
53
+ * Local file runtime only. `exclusive` (default): this process owns the
54
+ * state directory; another process opening it is refused with
55
+ * `LocalStateLockedError` (`STATE_LOCKED`) until `db.close()` or exit.
56
+ * `shared`: several processes may open the directory at once (each commit
57
+ * is still atomic and serialized); use it for multi-process workers.
58
+ */
59
+ access?: 'exclusive' | 'shared';
44
60
  /** Explicit durable authority. Omitted only for legacy unscoped clients. */
45
61
  authorityScope?: AuthorityScope;
46
62
  }
@@ -77,6 +93,8 @@ export interface FeltDBRuntimeCapabilities {
77
93
  replication: boolean;
78
94
  remoteAuthority: boolean;
79
95
  offline: boolean;
96
+ /** `storeContent` / `acquireContent` are available. */
97
+ content: boolean;
80
98
  }
81
99
  export interface FeltDBCapabilitySurface {
82
100
  (): FeltDBRuntimeCapabilities;
@@ -249,10 +267,6 @@ export interface RuntimeDiagnostics {
249
267
  message: string;
250
268
  }>;
251
269
  }
252
- /**
253
- * State-first database interface.
254
- * Applications use this to treat FeltDB collections as live application state.
255
- */
256
270
  export declare class StateFirstDB {
257
271
  private jsDb;
258
272
  private collections;
@@ -287,10 +301,24 @@ export declare class StateFirstDB {
287
301
  idempotencyKey?: string;
288
302
  }) => Promise<unknown>;
289
303
  };
304
+ /**
305
+ * Start an agent run. It completes only when `completion` — criteria the
306
+ * server checks against the result it observed — holds; without it the run
307
+ * executes but ends `failed` with `COMPLETION_UNVERIFIABLE`.
308
+ */
290
309
  readonly agents: {
291
310
  run: (name: string, input?: unknown, options?: {
292
311
  idempotencyKey?: string;
293
312
  goal?: string;
313
+ completion?: {
314
+ kind: "result_count";
315
+ min: number;
316
+ max?: number;
317
+ } | {
318
+ kind: "result_contains";
319
+ field: string;
320
+ equals: unknown;
321
+ };
294
322
  }) => Promise<unknown>;
295
323
  };
296
324
  readonly capabilities: FeltDBCapabilitySurface;
@@ -431,6 +459,17 @@ export declare class StateFirstDB {
431
459
  * transactions. Runtimes that cannot refuse rather than silently degrading
432
460
  * the transaction into a sequence of independent writes.
433
461
  */
462
+ /**
463
+ * Tell this process's live collections that a transaction committed.
464
+ *
465
+ * Collection writes publish through the reactive dependency graph; a
466
+ * transaction bypasses the collection, so without this a subscriber on the
467
+ * local file or memory runtime never learns about it. The announcement is
468
+ * a notification only (subscribers re-read committed state), and it runs
469
+ * after the commit is durable: a failing subscriber cannot turn a committed
470
+ * transaction into a reported failure.
471
+ */
472
+ private announceCommittedTransaction;
434
473
  transaction(build: ((tx: AtomicTransactionScope) => void | Promise<void>) | AtomicTransactionDocument, options?: AtomicTransactionOptions): Promise<AtomicTransactionResult>;
435
474
  /**
436
475
  * Capture a read basis: what the authority holds, now, for a set of records
@@ -498,14 +537,28 @@ export declare class StateFirstDB {
498
537
  }>;
499
538
  /** Inspect the causal operation that currently materializes a remote record. */
500
539
  recordProvenance(collection: string, id: string): Promise<any>;
501
- /** Persist immutable content by hash; repeated bytes deduplicate automatically. */
540
+ /**
541
+ * Persist immutable content by its SHA-256; repeated bytes deduplicate.
542
+ * Returns `{ hash, bytes, ref: 'sha256:<hash>' }`.
543
+ *
544
+ * A server runtime stores it in the server's content store; the local file
545
+ * runtime stores it under its state directory (`content/<hash>`), durably
546
+ * before this returns. Memory and browser runtimes refuse with
547
+ * `CONTRACT_UNSUPPORTED`.
548
+ */
502
549
  storeContent(content: Uint8Array): Promise<{
503
550
  hash: string;
504
551
  bytes: number;
505
552
  ref: string;
506
553
  }>;
507
- /** Resolve verified immutable content locally or from configured peers. */
554
+ /**
555
+ * Resolve content by hash: verified against its SHA-256 before it is
556
+ * returned (`CONTENT_INTEGRITY_FAILURE` otherwise), `null` when absent. A
557
+ * server also asks its configured peers; the local runtime reads its own
558
+ * directory only.
559
+ */
508
560
  acquireContent(hash: string): Promise<Uint8Array | null>;
561
+ private contentUnsupported;
509
562
  /** Define a durable workflow whose lifecycle is ordinary replicated state. */
510
563
  defineWorkflow(name: string, steps: string[]): Promise<any>;
511
564
  startWorkflow(name: string, input?: unknown): Promise<any>;
@@ -513,7 +566,19 @@ export declare class StateFirstDB {
513
566
  completeWorkflowStep(runId: string, step: string, claimId: string, result?: unknown): Promise<any>;
514
567
  /** Define and start state-first agents; workers observe and advance these records externally. */
515
568
  defineStateAgent(name: string, capabilities?: string[], constraints?: unknown): Promise<any>;
516
- startStateAgent(name: string, goal: string, input?: unknown): Promise<any>;
569
+ /**
570
+ * `completion` is what the server must observe for the run to complete; a
571
+ * run without it never completes. See the agent-run section of the public API contract.
572
+ */
573
+ startStateAgent(name: string, goal: string, input?: unknown, completion?: {
574
+ kind: 'result_count';
575
+ min: number;
576
+ max?: number;
577
+ } | {
578
+ kind: 'result_contains';
579
+ field: string;
580
+ equals: unknown;
581
+ }): Promise<any>;
517
582
  /**
518
583
  * Define an agent with declarative configuration.
519
584
  *
@@ -546,16 +611,32 @@ export declare class StateFirstDB {
546
611
  */
547
612
  getAgentRuntime(): AgentRuntime;
548
613
  /**
549
- * Create an agent execution
614
+ * Create an agent execution. It is committed to `_flow_agent_executions`
615
+ * before this returns; pass `taskId` to find it later with
616
+ * `findAgentExecution({ taskId })`.
550
617
  */
551
- createAgentExecution(agentRef: AgentRef, goal: string, inputs: string[]): Promise<AgentExecution>;
618
+ createAgentExecution(agentRef: AgentRef, goal: string, inputs: string[], options?: CreateAgentExecutionOptions): Promise<AgentExecution>;
552
619
  private agentExecutionRecord;
620
+ private agentExecutionIndexesDeclared;
621
+ /** Declare the lookup indexes once per handle, on an embedded runtime only. */
622
+ private declareAgentExecutionIndexes;
623
+ private queryAgentExecution;
624
+ /**
625
+ * The most recently created execution for an agent-state task or an agent,
626
+ * read from durable state through a declared index (one bounded query; it
627
+ * never scans `_flow_agent_executions`). Null when there is none.
628
+ */
629
+ findAgentExecution(query: FindAgentExecutionQuery): Promise<AgentExecution | null>;
553
630
  /** Claim and durably materialize an embedded agent execution. */
554
631
  startAgentExecution(execution: AgentExecution, peerId?: string): Promise<void>;
555
632
  /** Advance an agent lifecycle and expose the transition as ordinary state. */
556
633
  transitionAgentExecution(execution: AgentExecution, status: import('./agent.js').AgentExecutionStatus): Promise<void>;
634
+ /** Resume an execution for `peerId` (status `Observing`), committed before this returns. */
635
+ resumeAgentExecution(execution: AgentExecution, peerId?: string): Promise<void>;
557
636
  completeAgentExecution(execution: AgentExecution, resultRef: string): Promise<void>;
558
637
  failAgentExecution(execution: AgentExecution, error: string): Promise<void>;
638
+ /** Cancel a non-terminal execution (terminal status `Cancelled`), committed before this returns. */
639
+ cancelAgentExecution(execution: AgentExecution, reason?: string): Promise<void>;
559
640
  /**
560
641
  * Close the database and clean up resources.
561
642
  */