@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
@@ -129,7 +129,9 @@ export class Collection {
129
129
  * Returns cached results (live-updated).
130
130
  */
131
131
  async all() {
132
- await this.refresh();
132
+ // A read the authority refused, or could not answer, is raised typed --
133
+ // never answered from whatever this handle last cached.
134
+ await this.refresh({ strict: true });
133
135
  return [...this.cache];
134
136
  }
135
137
  /** List records through the canonical application collection API. */
@@ -375,7 +377,7 @@ export class Collection {
375
377
  const key = `${this.name}:${id}`;
376
378
  const result = await this.db.get(key);
377
379
  if (!result.success) {
378
- throw new Error(result.error || `Failed to read record ${id}`);
380
+ throw runtimeFailure(result, result.error || `Failed to read record ${id}`);
379
381
  }
380
382
  if (result.data) {
381
383
  return JSON.parse(result.data);
@@ -421,7 +423,7 @@ export class Collection {
421
423
  const stored = { ...data, id: recordId, __version: 1 };
422
424
  const result = await this.db.insert(key, JSON.stringify(stored));
423
425
  if (!result.success) {
424
- throw new Error(`Insert failed: ${result.error}`);
426
+ throw runtimeFailure(result, `Insert failed: ${result.error}`);
425
427
  }
426
428
  await this.publish('insert', key, String(recordId), null, stored);
427
429
  return String(recordId);
@@ -614,9 +616,11 @@ export class Collection {
614
616
  const result = await this.db.delete(key);
615
617
  if (!result.success) {
616
618
  const message = `Delete failed: ${result.error}`;
617
- throw /not found/i.test(String(result.error))
618
- ? new FeltDBServiceError(message, FeltDBErrorCode.NOT_FOUND)
619
- : new Error(message);
619
+ throw result.cause
620
+ ? runtimeFailure(result, message)
621
+ : /not found/i.test(String(result.error))
622
+ ? new FeltDBServiceError(message, FeltDBErrorCode.NOT_FOUND)
623
+ : new Error(message);
620
624
  }
621
625
  // Update indexes - find and remove the deleted record
622
626
  const deleted = await this.get(id);
@@ -686,7 +690,7 @@ export class Collection {
686
690
  throw error;
687
691
  const existing = await this.db.get(key);
688
692
  if (!existing.success) {
689
- throw new Error(existing.error || `could not read the record that won ${key}`);
693
+ throw runtimeFailure(existing, existing.error || `could not read the record that won ${key}`);
690
694
  }
691
695
  if (!existing.data) {
692
696
  throw new Error(`${key} was created and then removed while resolving a conditional create`);
@@ -914,7 +918,7 @@ export class Collection {
914
918
  /**
915
919
  * Refresh data from the database and notify subscribers.
916
920
  */
917
- async refresh() {
921
+ async refresh(options = {}) {
918
922
  try {
919
923
  // The revision is read *before* the query, so it can only ever be older
920
924
  // than the data it ends up labelling. The reverse ordering would let a
@@ -980,14 +984,32 @@ export class Collection {
980
984
  }
981
985
  }
982
986
  else if (!result.success) {
987
+ if (options.strict)
988
+ throw runtimeFailure(result, result.error || `Failed to read ${this.name}`);
983
989
  console.error('Query failed:', result.error);
984
990
  }
985
991
  }
986
992
  catch (err) {
993
+ if (options.strict)
994
+ throw err;
987
995
  console.error('Refresh error:', err);
988
996
  }
989
997
  }
990
998
  }
999
+ /**
1000
+ * The error a failed runtime result is raised as. A remote runtime attaches
1001
+ * the authority's typed refusal (`cause`): its stable code, status, request id
1002
+ * and body travel on, so an unauthenticated, cross-application or interrupted
1003
+ * operation is never a bare `Error` with the code only in its message.
1004
+ */
1005
+ function runtimeFailure(result, message) {
1006
+ const cause = result.cause;
1007
+ if (!(cause instanceof FeltDBServiceError))
1008
+ return new Error(message);
1009
+ const error = new FeltDBServiceError(message, cause.code, cause.requestId, cause.status, cause.body);
1010
+ error.cause = cause;
1011
+ return error;
1012
+ }
991
1013
  /**
992
1014
  * Relationship helper for loading related data.
993
1015
  */
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "schema": "feltdb.build-identity.v2",
3
3
  "package": "create-feltdb",
4
- "version": "0.12.2",
4
+ "version": "0.15.0",
5
5
  "git": {
6
- "sha1": "7a632f5b3e79d800ff27359f7f1a7c081dc7cb69",
6
+ "sha1": "5ad54972a9e31a5459cadbb471428111624ec9c6",
7
7
  "dirty": false
8
8
  },
9
9
  "path_in_vcs": "tools/create-feltdb",
10
- "source_digest": "sha256:9db516ed9702dde647cd8d9723e1d997be999a39ddebfcb337dc0c1d721c8110",
10
+ "source_digest": "sha256:01efc5737aae1fef37c976e5841df3bb9b615d034f31d27b038f7fd4ca202428",
11
11
  "inputs": [
12
12
  "Cargo.lock",
13
13
  "Cargo.toml",
@@ -20,7 +20,7 @@
20
20
  "tools/cli",
21
21
  "tools/create-feltdb"
22
22
  ],
23
- "files": 770,
23
+ "files": 789,
24
24
  "build": {
25
25
  "command": "npm run build",
26
26
  "node": "v24.9.0"
@@ -11,15 +11,18 @@ import { spawn, spawnSync } from 'child_process';
11
11
  import { createProject } from './create.js';
12
12
  import { FELTDB_PACKAGE_VERSION } from './package-versions.js';
13
13
  import { configureManagedAccount, finalizeManagedProvisioning } from './managed-account.js';
14
+ import { RUNTIME_CHOICES, agentsIncluded, describeSetup } from './setup-summary.js';
14
15
  import { localFeltdbExecutable } from './development-handoff.js';
15
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
17
  function parseArgs(args) {
17
18
  const options = {
18
19
  runtime: 'browser',
19
20
  framework: 'react',
20
- distributed: true,
21
+ // Distribution and the browser AI builder add services and downloads a
22
+ // first project does not need; each is one flag away.
23
+ distributed: false,
21
24
  agents: true,
22
- webllm: true,
25
+ webllm: false,
23
26
  capabilities: 'search',
24
27
  };
25
28
  for (let i = 0; i < args.length; i++) {
@@ -42,6 +45,9 @@ function parseArgs(args) {
42
45
  case '--framework':
43
46
  options.framework = args[++i];
44
47
  break;
48
+ case '--distributed':
49
+ options.distributed = true;
50
+ break;
45
51
  case '--no-distributed':
46
52
  options.distributed = false;
47
53
  break;
@@ -164,29 +170,28 @@ async function promptForOptions(defaults) {
164
170
  { label: 'Yes', value: true },
165
171
  { label: 'No', value: false },
166
172
  ];
167
- return {
168
- runtime: await select('Where should FeltDB run?', [
169
- { label: 'Browser', value: 'browser', description: 'local-first with durable browser storage' },
170
- { label: 'Node.js', value: 'node', description: 'application server or worker' },
171
- { label: 'Self-hosted', value: 'self-hosted', description: 'dedicated FeltDB server' },
172
- { label: 'Managed', value: 'managed', description: 'FeltDB-hosted persistence, sync, and workloads' },
173
- ], defaults.runtime),
174
- framework: await select('Choose an application framework:', [
175
- { label: 'React', value: 'react' },
176
- { label: 'Vanilla TypeScript', value: 'vanilla' },
177
- ], defaults.framework),
178
- distributed: await select('Enable distributed operation?', yesNo, defaults.distributed),
179
- agents: await select('Include an autonomous agent example?', yesNo, defaults.agents),
180
- webllm: await select('Install WebLLM for private natural-language app building?', [
181
- { label: 'Yes', value: true, description: 'build FeltDB schemas, capabilities, workflows, and UI locally with AI' },
182
- { label: 'No', value: false, description: 'skip the browser model and AI builder' },
183
- ], defaults.webllm),
184
- capabilities: await select('Choose starter capabilities:', [
173
+ const runtime = await select('Where does your application\'s data live?', RUNTIME_CHOICES.map(choice => ({ label: choice.label, value: choice.value, description: choice.summary })), defaults.runtime);
174
+ const framework = await select('Choose an application framework:', [
175
+ { label: 'React', value: 'react', description: 'a complete starter: sign-up, projects, invitations, activity, assistants' },
176
+ { label: 'Vanilla TypeScript', value: 'vanilla', description: 'a minimal page over the same database' },
177
+ ], defaults.framework);
178
+ // The React starter's assistants are part of its tour and need no model, so
179
+ // only the vanilla template asks; its capabilities are chosen here too.
180
+ const agents = framework === 'react' ? true
181
+ : await select('Include an agent example?', yesNo, defaults.agents);
182
+ const capabilities = framework === 'react' ? defaults.capabilities
183
+ : await select('Choose starter capabilities:', [
185
184
  { label: 'Search', value: 'search' },
186
185
  { label: 'Vector search', value: 'vector' },
187
186
  { label: 'Search + vector search', value: 'search,vector' },
188
- ], defaults.capabilities),
189
- };
187
+ ], defaults.capabilities);
188
+ const webllm = await select('Add the private AI app builder (WebLLM)?', [
189
+ { label: 'No', value: false, description: 'add it later with npm install @feltdb/webllm' },
190
+ { label: 'Yes', value: true, description: 'builds schemas and UI with a model that runs in your browser: a download of a few hundred MB on first use, and WebGPU' },
191
+ ], defaults.webllm);
192
+ // Distribution is a deployment decision, not a first-run one: `--distributed`
193
+ // records it, and development always runs one local authority.
194
+ return { runtime, framework, distributed: defaults.distributed, agents, webllm, capabilities };
190
195
  }
191
196
  async function main() {
192
197
  const args = process.argv.slice(2);
@@ -211,9 +216,9 @@ Deployment Target (Explicit):
211
216
  Best for: Client-side apps, offline-first experiences
212
217
  No server required, full offline capability
213
218
 
214
- --server Node.js server runtime with server-side FeltDB authority
215
- Best for: Server-side applications, API servers
216
- Requires production FeltDB instance or self-host
219
+ --server Data lives in a FeltDB authority the application connects to
220
+ Best for: shared state, server-side work, a path to self-hosting
221
+ \`feltdb dev\` runs a local authority; deploy to feltdb-server
217
222
 
218
223
  --self-host Dedicated FeltDB server with Docker Compose
219
224
  Best for: Staging first, then explicit production promotion
@@ -221,7 +226,7 @@ Deployment Target (Explicit):
221
226
 
222
227
  --managed Managed FeltDB runtime
223
228
  Best for: Production without operating servers
224
- Requires VITE_FELTDB_URL and VITE_FELTDB_API_KEY
229
+ Requires VITE_FELTDB_URL; server-side FELTDB_TOKEN only
225
230
 
226
231
  Options:
227
232
  --browser Explicit: Browser runtime with IndexedDB
@@ -230,11 +235,15 @@ Options:
230
235
  --managed Explicit: Managed FeltDB service
231
236
  --runtime <runtime> Alternative: Set runtime (browser, node, self-hosted, managed)
232
237
  --framework <framework> Choose framework: react or vanilla (default: react)
233
- --no-distributed Disable distributed operation
234
- --no-agents Exclude agent examples
235
- --webllm Include the private natural-language FeltDB app builder (default)
236
- --no-webllm Do not install WebLLM or generate the AI builder
237
- --capabilities <list> Choose capabilities: search, vector, or search,vector
238
+ --distributed Record that the application will be deployed to several
239
+ feltdb-server nodes (development always runs one)
240
+ --no-distributed Do not record distributed deployment (default)
241
+ --no-agents Exclude the agent example (vanilla template; the React
242
+ starter always includes its assistants)
243
+ --webllm Include the private natural-language app builder; downloads
244
+ a model into the browser on first use and needs WebGPU
245
+ --no-webllm Do not install WebLLM (default)
246
+ --capabilities <list> Vanilla template capabilities: search, vector, or search,vector
238
247
  --no-install Skip npm install
239
248
  --no-start Skip automatic feltdb dev startup
240
249
  -y, --yes Use all defaults (non-interactive mode)
@@ -294,13 +303,13 @@ Learn more: https://github.com/rkendel1/feltdb`);
294
303
  console.log('\n═══════════════════════════════════════════════');
295
304
  console.log('📦 DEPLOYMENT TARGET');
296
305
  console.log('═══════════════════════════════════════════════');
297
- console.log(`\nRuntime: ${options.runtime.toUpperCase()}`);
298
- console.log(`Description: ${runtimeDescriptions[options.runtime]}`);
299
- console.log(`Framework: ${options.framework}`);
300
- console.log(`Distributed: ${options.distributed ? 'yes' : 'no'}`);
301
- console.log(`Agents: ${options.agents ? 'yes' : 'no'}`);
302
- console.log(`WebLLM: ${options.webllm ? 'yes — natural-language app builder' : 'no'}`);
303
- console.log(`Capabilities: ${options.capabilities}`);
306
+ if (options.framework === 'react' && !options.agents) {
307
+ console.log('\nNote: the React starter always includes its assistants; --no-agents applies to the vanilla template.');
308
+ }
309
+ options.agents = agentsIncluded(options);
310
+ console.log('');
311
+ for (const line of describeSetup(options))
312
+ console.log(line);
304
313
  console.log('\n═══════════════════════════════════════════════\n');
305
314
  const projectDir = path.resolve(process.cwd(), projectName);
306
315
  let projectCreated = false;
@@ -196,12 +196,25 @@ ${hasAgents ? ` agent WorkspaceAssistant {
196
196
  tsconfig.compilerOptions.types = ['vite/client'];
197
197
  }
198
198
  fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
199
- // Browsers do not expose VITE_* through process.env, so a local database must be handed its
199
+ // Browsers do not expose VITE_* through process.env, so the database must be handed its
200
200
  // namespace explicitly; otherwise it opens the shared `default` store and `feltdb dev` cannot
201
201
  // recognise the application. A managed (remote) runtime resolves its target from its URL.
202
+ //
203
+ // The generated application runs in the browser whatever the deployment target. For a
204
+ // browser target its state is the browser's own (IndexedDB). For a Node.js or self-hosted
205
+ // target the state lives in a FeltDB authority, so the browser application connects to it:
206
+ // the URL the developer configured, else the authority `feltdb dev` started. Opening a
207
+ // browser store there instead would leave the application writing data Studio and the
208
+ // authority never see.
209
+ const namespaceExpression = `import.meta.env.VITE_FELTDB_NAMESPACE || ${JSON.stringify(applicationName)}`;
202
210
  const createDatabaseCall = runtime === 'managed'
203
211
  ? 'createFeltDB()'
204
- : `createFeltDB({ namespace: import.meta.env.VITE_FELTDB_NAMESPACE || ${JSON.stringify(applicationName)} })`;
212
+ : runtime === 'browser'
213
+ ? `createFeltDB({ namespace: ${namespaceExpression} })`
214
+ : `createFeltDB({
215
+ namespace: ${namespaceExpression},
216
+ server: { url: import.meta.env.VITE_FELTDB_URL || import.meta.env.VITE_FELTDB_AUTHORITY_URL || 'http://127.0.0.1:7700' },
217
+ })`;
205
218
  // Create main application files
206
219
  const feltdbTs = `import { configureDevelopmentRuntimeBridge, createFeltDB } from '@feltdb/core';
207
220
 
@@ -1222,6 +1235,8 @@ main().catch(console.error);
1222
1235
  <head>
1223
1236
  <meta charset="UTF-8">
1224
1237
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1238
+ <!-- An inline icon: without one the browser requests /favicon.ico and logs a 404. -->
1239
+ <link rel="icon" href="data:,">
1225
1240
  <title>${applicationName}</title>
1226
1241
  </head>
1227
1242
  <body>
@@ -1244,10 +1259,9 @@ main().catch(console.error);
1244
1259
  # FeltDB Server URL (self-hosted or managed runtime)
1245
1260
  VITE_FELTDB_URL=http://localhost:7700
1246
1261
 
1247
- # Optional credential for authenticated remote browser access
1248
- VITE_FELTDB_TOKEN=
1249
- # Legacy alias accepted by older tooling
1250
- VITE_FELTDB_API_KEY=
1262
+ # Browser code authenticates users with db.auth.signIn() (end-user sessions).
1263
+ # Never put a FeltDB service key (fdb_live_…) in a VITE_ variable: Vite
1264
+ # inlines every VITE_* value into the browser bundle.
1251
1265
 
1252
1266
  # Managed example: https://runtime.your-app.feltdb.com
1253
1267
  VITE_FELTDB_MANAGED_URL=
@@ -1493,7 +1507,7 @@ npm run feltdb:import
1493
1507
 
1494
1508
  ### Self-Hosted
1495
1509
  - \`VITE_FELTDB_URL\`: Docker container URL (default: http://localhost:7700)
1496
- - \`VITE_FELTDB_API_KEY\`: API token for authentication
1510
+ - \`FELTDB_TOKEN\`: API key for server-side code only (never a \`VITE_\` variable; browsers use \`db.auth.signIn()\`)
1497
1511
  - \`FELTDB_IMAGE\`: Docker image to use (optional override)
1498
1512
  - \`DOCKER_NETWORK\`: Custom Docker network (optional)
1499
1513
 
@@ -1686,7 +1700,6 @@ Configuration is in \`feltdb.config.json\`:
1686
1700
  Create a \`.env.local\` file (copy from \`.env.example\`):
1687
1701
 
1688
1702
  \`\`\`
1689
- VITE_FELTDB_API_KEY=your_api_key_here
1690
1703
  VITE_FELTDB_URL=http://localhost:7700
1691
1704
  VITE_FELTDB_MANAGED_URL=https://api.feltdb.com
1692
1705
  VITE_FELTDB_MANAGED_APPLICATION_ID=your_application_id
@@ -1,4 +1,4 @@
1
1
  // One release train keeps generated applications installable. The repository
2
2
  // validation script checks these values against every workspace manifest.
3
- export const FELTDB_PACKAGE_VERSION = '0.12.2';
3
+ export const FELTDB_PACKAGE_VERSION = '0.15.0';
4
4
  export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
@@ -219,9 +219,9 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
219
219
 
220
220
  [[package]]
221
221
  name = "chacha20"
222
- version = "0.10.1"
222
+ version = "0.10.2"
223
223
  source = "registry+https://github.com/rust-lang/crates.io-index"
224
- checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
224
+ checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
225
225
  dependencies = [
226
226
  "cfg-if",
227
227
  "cpufeatures 0.3.0",
@@ -494,7 +494,7 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
494
494
 
495
495
  [[package]]
496
496
  name = "feltdb"
497
- version = "0.2.1"
497
+ version = "0.3.0"
498
498
  dependencies = [
499
499
  "async-stream",
500
500
  "async-trait",
@@ -513,7 +513,7 @@ dependencies = [
513
513
 
514
514
  [[package]]
515
515
  name = "feltdb-client"
516
- version = "0.2.1"
516
+ version = "0.3.0"
517
517
  dependencies = [
518
518
  "feltdb",
519
519
  "reqwest",
@@ -526,7 +526,7 @@ dependencies = [
526
526
 
527
527
  [[package]]
528
528
  name = "feltdb-server"
529
- version = "0.2.1"
529
+ version = "0.3.0"
530
530
  dependencies = [
531
531
  "argon2",
532
532
  "async-stream",
@@ -552,7 +552,7 @@ dependencies = [
552
552
 
553
553
  [[package]]
554
554
  name = "feltdb-wasm"
555
- version = "0.2.1"
555
+ version = "0.3.0"
556
556
  dependencies = [
557
557
  "feltdb",
558
558
  "getrandom 0.2.17",
@@ -14,5 +14,5 @@ exclude = [
14
14
  resolver = "2"
15
15
 
16
16
  [workspace.package]
17
- version = "0.2.1"
17
+ version = "0.3.0"
18
18
  edition = "2021"
@@ -1133,6 +1133,48 @@ impl PreconditionFailure {
1133
1133
  }
1134
1134
  }
1135
1135
 
1136
+ impl PreconditionFailure {
1137
+ /// The refusal as every transport serializes it: `predicate`, the
1138
+ /// application's `collection` (never a storage namespace), the record `id`,
1139
+ /// its `key` as `collection:id`, and `expected` / `actual` where the
1140
+ /// predicate has them. A fence names the first fenced collection and
1141
+ /// `key: "read-basis"`, with no `id` and no tokens.
1142
+ ///
1143
+ /// `layout` is where the authority addressed the record: the direct route
1144
+ /// keys rows `collection:id` under `collection`; an application namespace
1145
+ /// keys them by id under `namespace:collection`.
1146
+ pub fn public_json(&self, layout: &ReadLayout) -> Value {
1147
+ let mut value = serde_json::to_value(self).unwrap_or(Value::Null);
1148
+ if matches!(self, Self::Fence { .. }) {
1149
+ return value;
1150
+ }
1151
+ let (collection, id) = match layout {
1152
+ ReadLayout::Direct => {
1153
+ let collection = self.collection().to_string();
1154
+ let id = self
1155
+ .key()
1156
+ .strip_prefix(&format!("{collection}:"))
1157
+ .unwrap_or(self.key())
1158
+ .to_string();
1159
+ (collection, id)
1160
+ }
1161
+ ReadLayout::Namespace(namespace) => (
1162
+ self.collection()
1163
+ .strip_prefix(&format!("{namespace}:"))
1164
+ .unwrap_or(self.collection())
1165
+ .to_string(),
1166
+ self.key().to_string(),
1167
+ ),
1168
+ };
1169
+ if let Some(object) = value.as_object_mut() {
1170
+ object.insert("key".into(), Value::String(format!("{collection}:{id}")));
1171
+ object.insert("collection".into(), Value::String(collection));
1172
+ object.insert("id".into(), Value::String(id));
1173
+ }
1174
+ value
1175
+ }
1176
+ }
1177
+
1136
1178
  impl Display for PreconditionFailure {
1137
1179
  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1138
1180
  match self {
@@ -3225,6 +3267,20 @@ impl FeltDb {
3225
3267
  .collect()
3226
3268
  }
3227
3269
 
3270
+ /// The original commit of a transaction id, if it committed, exactly as a
3271
+ /// retry of it through `apply_atomic_transaction_*` would report it: the
3272
+ /// original revisions and positions (`duplicate: true`), or
3273
+ /// `TransactionIdPayloadMismatch` when `payload_hash` differs from the one
3274
+ /// the id was committed with. Read-only.
3275
+ pub fn committed_transaction(
3276
+ &self,
3277
+ transaction_id: &str,
3278
+ payload_hash: Option<&str>,
3279
+ ) -> Result<Option<AtomicCommit>> {
3280
+ let inner = self.state();
3281
+ committed_transaction_locked(&inner, transaction_id, payload_hash).transpose()
3282
+ }
3283
+
3228
3284
  pub fn has_applied_transaction(&self, transaction_id: &str) -> Result<bool> {
3229
3285
  Ok(self
3230
3286
  .inner
@@ -3385,41 +3441,10 @@ impl FeltDb {
3385
3441
  ) -> Result<AtomicCommit> {
3386
3442
  let (commit, events) = {
3387
3443
  let mut inner = self.state();
3388
- if inner.applied_transactions.contains(transaction_id) {
3389
- if let Some(expected_hash) = payload_hash {
3390
- if inner
3391
- .transaction_payload_hashes
3392
- .get(transaction_id)
3393
- .map(String::as_str)
3394
- != Some(expected_hash)
3395
- {
3396
- return Err(FlowError::TransactionIdPayloadMismatch {
3397
- transaction_id: transaction_id.to_string(),
3398
- });
3399
- }
3400
- }
3401
- let (base_revision, commit_revision) = inner
3402
- .transaction_revisions
3403
- .get(transaction_id)
3404
- .copied()
3405
- .unwrap_or((inner.authority_revision, inner.authority_revision));
3406
- let (state_before, state_after) = inner
3407
- .transaction_history
3408
- .iter()
3409
- .find(|attribution| attribution.transaction_id == transaction_id)
3410
- .map(|attribution| (attribution.state_before, attribution.state_after))
3411
- .unwrap_or((inner.sequence, inner.sequence));
3412
- return Ok(AtomicCommit {
3413
- transaction_id: transaction_id.into(),
3414
- base_revision,
3415
- commit_revision,
3416
- status: TransactionStatus::Committed,
3417
- state_before,
3418
- state_after,
3419
- rows: vec![],
3420
- duplicate: true,
3421
- payload_binding: payload_hash.map(|_| PayloadBinding::Verified),
3422
- });
3444
+ if let Some(duplicate) =
3445
+ committed_transaction_locked(&inner, transaction_id, payload_hash)
3446
+ {
3447
+ return duplicate;
3423
3448
  }
3424
3449
  // Record preconditions, evaluated before anything is staged and
3425
3450
  // under the same lock the writes take. A caller reads state,
@@ -4167,6 +4192,21 @@ impl FeltDb {
4167
4192
  self.get::<Value>(key)
4168
4193
  }
4169
4194
 
4195
+ /// One live row of a collection by its key, cloned under the state lock.
4196
+ ///
4197
+ /// A keyed lookup, not a scan: callers that know the canonical key of the
4198
+ /// record they need (Durable Work's projection refresh, for one) use this
4199
+ /// rather than a predicate over the whole collection.
4200
+ pub(crate) fn live_row(&self, capability: &str, key: &str) -> Option<StoredRow> {
4201
+ let inner = self.state();
4202
+ inner
4203
+ .rows
4204
+ .get(capability)
4205
+ .and_then(|rows| rows.get(key))
4206
+ .filter(|row| !row.deleted)
4207
+ .cloned()
4208
+ }
4209
+
4170
4210
  /// List the canonical values in one collection/capability namespace.
4171
4211
  ///
4172
4212
  /// This clones the whole collection, so its cost is linear in collection
@@ -6021,6 +6061,57 @@ fn append_event_with(
6021
6061
  Ok(())
6022
6062
  }
6023
6063
 
6064
+ /// The answer to a retried transaction id, read under the commit lock.
6065
+ ///
6066
+ /// `None` when the id has not committed. Otherwise the *original* commit's
6067
+ /// revisions and positions, never the store's current ones, or
6068
+ /// `TransactionIdPayloadMismatch` when a payload hash is given and differs from
6069
+ /// the one the id was committed with. Nothing is evaluated and nothing is
6070
+ /// applied: a duplicate is answered before any precondition is read.
6071
+ fn committed_transaction_locked(
6072
+ inner: &Inner,
6073
+ transaction_id: &str,
6074
+ payload_hash: Option<&str>,
6075
+ ) -> Option<Result<AtomicCommit>> {
6076
+ if !inner.applied_transactions.contains(transaction_id) {
6077
+ return None;
6078
+ }
6079
+ if let Some(expected_hash) = payload_hash {
6080
+ if inner
6081
+ .transaction_payload_hashes
6082
+ .get(transaction_id)
6083
+ .map(String::as_str)
6084
+ != Some(expected_hash)
6085
+ {
6086
+ return Some(Err(FlowError::TransactionIdPayloadMismatch {
6087
+ transaction_id: transaction_id.to_string(),
6088
+ }));
6089
+ }
6090
+ }
6091
+ let (base_revision, commit_revision) = inner
6092
+ .transaction_revisions
6093
+ .get(transaction_id)
6094
+ .copied()
6095
+ .unwrap_or((inner.authority_revision, inner.authority_revision));
6096
+ let (state_before, state_after) = inner
6097
+ .transaction_history
6098
+ .iter()
6099
+ .find(|attribution| attribution.transaction_id == transaction_id)
6100
+ .map(|attribution| (attribution.state_before, attribution.state_after))
6101
+ .unwrap_or((inner.sequence, inner.sequence));
6102
+ Some(Ok(AtomicCommit {
6103
+ transaction_id: transaction_id.into(),
6104
+ base_revision,
6105
+ commit_revision,
6106
+ status: TransactionStatus::Committed,
6107
+ state_before,
6108
+ state_after,
6109
+ rows: vec![],
6110
+ duplicate: true,
6111
+ payload_binding: payload_hash.map(|_| PayloadBinding::Verified),
6112
+ }))
6113
+ }
6114
+
6024
6115
  fn append_transaction(path: &Path, transaction: &TransactionLogRecord) -> Result<()> {
6025
6116
  // A transaction commit fsyncs. This span therefore includes the fsync, which
6026
6117
  // is the point: it is the durability cost of a commit, measured rather than