@feltdb/core 0.7.4 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +25 -0
  2. package/dist/application-development.d.ts +3 -1
  3. package/dist/application-development.d.ts.map +1 -1
  4. package/dist/application-development.js +16 -0
  5. package/dist/application-manifest.d.ts +1 -0
  6. package/dist/application-manifest.d.ts.map +1 -1
  7. package/dist/canonical-application.d.ts +1 -0
  8. package/dist/canonical-application.d.ts.map +1 -1
  9. package/dist/canonical-application.js +3 -2
  10. package/dist/cli/cli.js +1 -1
  11. package/dist/cli/commands.js +225 -42
  12. package/dist/cli/index.js +1 -1
  13. package/dist/cli/managed-environment.js +6 -0
  14. package/dist/cli/managed-project.js +6 -0
  15. package/dist/cli/proposal-client.js +12 -3
  16. package/dist/cli/publish-engine.js +7 -4
  17. package/dist/cli/source-sync.js +47 -3
  18. package/dist/create/cli.js +10 -1
  19. package/dist/create/create.js +10 -12
  20. package/dist/create/managed-account.js +71 -7
  21. package/dist/create/package-versions.js +1 -1
  22. package/dist/create/server-source/crates/feltdb/src/application.rs +10 -0
  23. package/dist/create/server-source/crates/feltdb/src/lib.rs +20 -2
  24. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +12 -24
  25. package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
  26. package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
  27. package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
  28. package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
  29. package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
  30. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +79 -14
  31. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +28 -7
  32. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +5 -5
  33. package/dist/create/server-source/crates/feltdb-server/src/main.rs +1777 -259
  34. package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
  35. package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
  36. package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
  37. package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
  38. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +183 -21
  39. package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
  40. package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
  41. package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
  42. package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
  43. package/dist/create/template/default-project/feltdb.flow +5 -0
  44. package/dist/flowspec.d.ts +1 -0
  45. package/dist/flowspec.d.ts.map +1 -1
  46. package/dist/flowspec.js +46 -12
  47. package/dist/http-client.d.ts +135 -14
  48. package/dist/http-client.d.ts.map +1 -1
  49. package/dist/http-client.js +79 -31
  50. package/dist/index-core.d.ts +2 -0
  51. package/dist/index-core.d.ts.map +1 -1
  52. package/dist/index-core.js +2 -0
  53. package/dist/module-intent.d.ts +37 -0
  54. package/dist/module-intent.d.ts.map +1 -0
  55. package/dist/module-intent.js +81 -0
  56. package/dist/module.d.ts +76 -0
  57. package/dist/module.d.ts.map +1 -0
  58. package/dist/module.js +61 -0
  59. package/dist/proposal.d.ts +80 -4
  60. package/dist/proposal.d.ts.map +1 -1
  61. package/dist/proposal.js +132 -5
  62. package/dist/studio/app.d.ts +3 -1
  63. package/dist/studio/app.d.ts.map +1 -1
  64. package/dist/studio/components/AskFeltDB.d.ts +2 -1
  65. package/dist/studio/components/AskFeltDB.d.ts.map +1 -1
  66. package/dist/studio/components/ContractInspector.d.ts +7 -0
  67. package/dist/studio/components/ContractInspector.d.ts.map +1 -0
  68. package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -1
  69. package/dist/studio/components/ManagedInstancePanel.js +1 -5
  70. package/dist/studio/components/ModuleExplorer.d.ts +6 -0
  71. package/dist/studio/components/ModuleExplorer.d.ts.map +1 -0
  72. package/dist/studio/components/ProposalBrowser.d.ts +10 -1
  73. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -1
  74. package/dist/studio/components/StateExplorer.d.ts +6 -7
  75. package/dist/studio/components/StateExplorer.d.ts.map +1 -1
  76. package/dist/studio/components/index.d.ts +2 -0
  77. package/dist/studio/components/index.d.ts.map +1 -1
  78. package/dist/studio/components/index.js +4 -4
  79. package/dist/studio/components-CNSNrmA9.js +2787 -0
  80. package/dist/studio/index.js +193 -147
  81. package/dist/studio/studio.css +1 -1
  82. package/dist/studio/utils/index.d.ts +2 -0
  83. package/dist/studio/utils/index.d.ts.map +1 -1
  84. package/dist/studio/utils/index.js +14 -12
  85. package/dist/studio/utils/managed-instance.d.ts +0 -4
  86. package/dist/studio/utils/managed-instance.d.ts.map +1 -1
  87. package/dist/studio/utils/managed-instance.js +0 -4
  88. package/dist/studio/utils/proposal-api.d.ts +53 -0
  89. package/dist/studio/utils/proposal-api.d.ts.map +1 -0
  90. package/dist/studio/utils/proposal-api.js +91 -0
  91. package/dist/studio/utils/service-api.d.ts +81 -0
  92. package/dist/studio/utils/service-api.d.ts.map +1 -0
  93. package/dist/studio/utils/service-api.js +51 -0
  94. package/dist/studio-app/assets/{dist-DA1MYSzp.js → dist-CCOk39Uc.js} +1 -1
  95. package/dist/studio-app/assets/{feltdb_wasm-B-Ccgccb.js → feltdb_wasm-CSB6KVgw.js} +1 -1
  96. package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
  97. package/dist/studio-app/assets/index-Bncji3aN.js +43 -0
  98. package/dist/studio-app/assets/{index-BRrmvaKw.css → index-yazaWMUN.css} +1 -1
  99. package/dist/studio-app/index.html +2 -2
  100. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  101. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  102. package/dist/workspace/local-development-authority.js +64 -3
  103. package/package.json +1 -1
  104. package/dist/studio/components-Bm4sjhcX.js +0 -2162
  105. package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
  106. package/dist/studio-app/assets/index-CEIqowCh.js +0 -42
@@ -19,17 +19,40 @@ function gitDirty(cwd, files) {
19
19
  return [];
20
20
  return result.stdout.split(/\r?\n/).filter(Boolean).map(line => line.slice(3));
21
21
  }
22
+ export async function repositoryContext(cwd = process.cwd(), plannedFiles = [FLOW_FILE, GENERATED_FILE]) {
23
+ const git = (args) => spawnSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
24
+ const rootResult = git(['rev-parse', '--show-toplevel']);
25
+ const root = rootResult.status === 0 ? rootResult.stdout.trim() : path.resolve(cwd);
26
+ const branchResult = git(['branch', '--show-current']);
27
+ const commitResult = git(['rev-parse', 'HEAD']);
28
+ const context = await loadLocalDevelopmentContract(root);
29
+ return { root, branch: branchResult.status === 0 ? branchResult.stdout.trim() || undefined : undefined,
30
+ commit: commitResult.status === 0 ? commitResult.stdout.trim() : undefined, dirty: gitDirty(root, plannedFiles),
31
+ flowPath: path.join(root, FLOW_FILE), flowHash: context.flowHash };
32
+ }
33
+ export function assertProposalSourcePlan(sourcePlan) {
34
+ const allowed = new Set([FLOW_FILE, GENERATED_FILE]);
35
+ if (!sourcePlan?.files?.length)
36
+ throw new Error('SOURCE_PLAN_BLOCKED: Proposal has no source operations');
37
+ for (const operation of sourcePlan.files) {
38
+ if (!allowed.has(operation.path) || !['add', 'modify'].includes(operation.operation))
39
+ throw new Error(`SOURCE_PLAN_BLOCKED: unsupported ${operation.operation} operation for ${operation.path}`);
40
+ }
41
+ if (!sourcePlan.files.some(value => value.path === FLOW_FILE))
42
+ throw new Error(`SOURCE_PLAN_BLOCKED: ${FLOW_FILE} synchronization is required`);
43
+ }
22
44
  export async function loadLocalDevelopmentContract(cwd = process.cwd()) {
23
45
  const flowPath = path.join(cwd, FLOW_FILE);
24
46
  if (!fs.existsSync(flowPath))
25
47
  throw new Error(`${FLOW_FILE} is required`);
26
48
  const source = fs.readFileSync(flowPath, 'utf8');
27
49
  const project = readJson(path.join(cwd, 'feltdb.config.json')) ?? {};
50
+ const managed = readJson(path.join(cwd, '.feltdb', 'managed.json')) ?? {};
28
51
  const published = readJson(path.join(cwd, '.feltdb', 'last-published.json'));
29
52
  const applicationId = process.env.VITE_FELTDB_MANAGED_APPLICATION_ID
30
- || published?.applicationId || project.applicationId || parseFlowSpec(source).app;
31
- const tenantId = process.env.VITE_FELTDB_MANAGED_TENANT_ID || published?.tenantId || 'local';
32
- const environment = process.env.VITE_FELTDB_MANAGED_ENVIRONMENT || published?.environment || 'local';
53
+ || managed.applicationId || published?.applicationId || project.applicationId || parseFlowSpec(source).app;
54
+ const tenantId = managed.tenantId || published?.tenantId || 'local';
55
+ const environment = managed.environment || published?.environment || 'local';
33
56
  const contract = await compileApplicationContract(parseFlowSpec(source), tenantId, applicationId);
34
57
  return {
35
58
  source,
@@ -124,6 +147,27 @@ export async function applyFlowProposal(proposal, cwd = process.cwd(), options =
124
147
  const synchronized = options.syncSource === false ? { written: [] } : syncGeneratedSource(review, cwd, { replace: options.replaceGenerated });
125
148
  return { ...review, written: [FLOW_FILE, ...synchronized.written] };
126
149
  }
150
+ /** Apply and verify as one developer-visible transaction, restoring exact prior bytes on failure. */
151
+ export async function applyFlowProposalAtomic(proposal, cwd = process.cwd(), options = {}) {
152
+ const files = [FLOW_FILE, GENERATED_FILE, SYNC_STATE].map(relative => ({ relative, absolute: path.join(cwd, relative) }));
153
+ const before = files.map(file => ({ ...file, existed: fs.existsSync(file.absolute), bytes: fs.existsSync(file.absolute) ? fs.readFileSync(file.absolute) : undefined }));
154
+ try {
155
+ const result = await applyFlowProposal(proposal, cwd, options);
156
+ options.verify?.();
157
+ return result;
158
+ }
159
+ catch (error) {
160
+ for (const file of before) {
161
+ if (file.existed && file.bytes) {
162
+ fs.mkdirSync(path.dirname(file.absolute), { recursive: true });
163
+ fs.writeFileSync(file.absolute, file.bytes);
164
+ }
165
+ else if (fs.existsSync(file.absolute))
166
+ fs.rmSync(file.absolute, { force: true });
167
+ }
168
+ throw error;
169
+ }
170
+ }
127
171
  export function describeProposal(review) {
128
172
  return [
129
173
  ...review.diff.added.map(value => `+ ${value}`),
@@ -10,7 +10,7 @@ import readline from 'readline';
10
10
  import { spawn, spawnSync } from 'child_process';
11
11
  import { createProject } from './create.js';
12
12
  import { FELTDB_PACKAGE_VERSION } from './package-versions.js';
13
- import { configureManagedAccount } from './managed-account.js';
13
+ import { configureManagedAccount, finalizeManagedProvisioning } from './managed-account.js';
14
14
  import { localFeltdbExecutable } from './development-handoff.js';
15
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
16
  function parseArgs(args) {
@@ -331,9 +331,18 @@ Learn more: https://github.com/rkendel1/feltdb`);
331
331
  }
332
332
  console.log('\n✅ FeltDB application created successfully!\n');
333
333
  const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
334
+ if (options.runtime === 'managed' && !shouldInstall) {
335
+ throw new Error('Managed provisioning requires dependency installation so the initial revision can be built, published, promoted, and verified. Remove --no-install.');
336
+ }
334
337
  if (shouldInstall) {
335
338
  console.log(`📦 Installing application, Studio${options.webllm ? ', and local WebLLM app builder' : ''} dependencies...\n`);
336
339
  await run(npm, ['install'], projectDir);
340
+ if (options.runtime === 'managed') {
341
+ console.log('\n☁️ Publishing and verifying the initial managed revision...\n');
342
+ await run(localFeltdbExecutable(projectDir), ['publish', '--yes', '--approve-destructive-changes'], projectDir);
343
+ const finalized = await finalizeManagedProvisioning({ projectDir });
344
+ console.log(`✓ Managed runtime verified at revision ${finalized.revisionId}`);
345
+ }
337
346
  }
338
347
  if (shouldStart) {
339
348
  if (!shouldInstall) {
@@ -198,7 +198,7 @@ ${hasAgents ? ` agent WorkspaceAssistant {
198
198
  const runtimeOptions = runtime === 'browser'
199
199
  ? "{ namespace: import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', browser: true }"
200
200
  : runtime === 'managed'
201
- ? "{ namespace: import.meta.env.VITE_FELTDB_MANAGED_NAMESPACE || import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_MANAGED_URL || import.meta.env.VITE_FELTDB_URL || '', token: import.meta.env.VITE_FELTDB_MANAGED_API_KEY || import.meta.env.VITE_FELTDB_API_KEY || '', applicationId: import.meta.env.VITE_FELTDB_MANAGED_APPLICATION_ID, environment: import.meta.env.VITE_FELTDB_MANAGED_ENVIRONMENT || 'production' } }"
201
+ ? "{ namespace: '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_MANAGED_URL || '', applicationId: import.meta.env.VITE_FELTDB_MANAGED_APPLICATION_ID, environment: 'production' } }"
202
202
  : runtime === 'self-hosted'
203
203
  ? "{ namespace: import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_URL || 'http://localhost:7700', token: import.meta.env.VITE_FELTDB_API_KEY || '' } }"
204
204
  : "{ namespace: import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', memory: true }";
@@ -1234,8 +1234,7 @@ main().catch(console.error);
1234
1234
  const envExample = `# FeltDB Configuration
1235
1235
  # Copy this file to .env.local and update the values
1236
1236
 
1237
- # API Key for authenticating with FeltDB servers
1238
- # Leave empty for browser runtime; required for authenticated self-hosted and managed runtimes
1237
+ # API key for authenticated self-hosted browser access
1239
1238
  VITE_FELTDB_API_KEY=
1240
1239
 
1241
1240
  # FeltDB Server URL (self-hosted or managed runtime)
@@ -1243,13 +1242,10 @@ VITE_FELTDB_URL=http://localhost:7700
1243
1242
 
1244
1243
  # Managed example: https://runtime.your-app.feltdb.com
1245
1244
  VITE_FELTDB_MANAGED_URL=
1246
- VITE_FELTDB_MANAGED_API_KEY=
1245
+ VITE_FELTDB_MANAGED_APPLICATION_ID=
1247
1246
  # Server/control-plane only. Never rename with a VITE_ prefix.
1248
1247
  FELTDB_MANAGED_CONTROL_API_KEY=
1249
- VITE_FELTDB_MANAGED_TENANT_ID=
1250
- VITE_FELTDB_MANAGED_APPLICATION_ID=
1251
- VITE_FELTDB_MANAGED_NAMESPACE=
1252
- VITE_FELTDB_MANAGED_ENVIRONMENT=production
1248
+ FELTDB_TOKEN=
1253
1249
 
1254
1250
  # Override the default self-hosted container image
1255
1251
  # By default the server image is built locally from the bundled source.
@@ -1388,7 +1384,7 @@ The self-hosted instance runs on \`http://localhost:7700\` by default.
1388
1384
  }
1389
1385
  \`\`\`
1390
1386
 
1391
- Managed mode uses the same application API with a FeltDB-hosted endpoint. The CLI creates \`.env.local\` with \`VITE_FELTDB_MANAGED_URL\` and \`VITE_FELTDB_MANAGED_API_KEY\` after account setup. Studio uses that same connection for state, health, operations, and API-key administration.
1387
+ Managed mode uses the same application API with a FeltDB-hosted endpoint. The CLI writes only the public URL and application ID plus separate server-only publish and runtime credentials. Tenant, namespace, environment, and active revision are discovered deployment metadata under \`.feltdb/managed.json\`.
1392
1388
 
1393
1389
  ## Vector Search Status
1394
1390
 
@@ -1657,7 +1653,7 @@ FeltDB runs through a dedicated server with Docker Compose and persistent data v
1657
1653
 
1658
1654
  ### Managed Runtime
1659
1655
 
1660
- FeltDB provides the runtime endpoint, durable storage, synchronization, and background workload infrastructure. The CLI configures \`VITE_FELTDB_MANAGED_URL\` and \`VITE_FELTDB_MANAGED_API_KEY\` in \`.env.local\`.
1656
+ FeltDB provides the runtime endpoint, durable storage, synchronization, and background workload infrastructure. The CLI configures the public \`VITE_FELTDB_MANAGED_*\` identity, separate server-only control and data-plane credentials, and the promoted revision in \`.env.local\`. The browser API-key value stays empty because browser users receive short-lived actor sessions.
1661
1657
 
1662
1658
  ## Configuration
1663
1659
 
@@ -1687,12 +1683,14 @@ Create a \`.env.local\` file (copy from \`.env.example\`):
1687
1683
  \`\`\`
1688
1684
  VITE_FELTDB_API_KEY=your_api_key_here
1689
1685
  VITE_FELTDB_URL=http://localhost:7700
1690
- VITE_FELTDB_MANAGED_API_KEY=your_managed_api_key_here
1691
1686
  VITE_FELTDB_MANAGED_URL=https://api.feltdb.com
1687
+ VITE_FELTDB_MANAGED_APPLICATION_ID=your_application_id
1688
+ FELTDB_MANAGED_CONTROL_API_KEY=your_server_only_control_key
1689
+ FELTDB_TOKEN=your_server_only_application_data_key
1692
1690
  VITE_FELTDB_WEBSOCKET_URL=ws://localhost:7700
1693
1691
  \`\`\`
1694
1692
 
1695
- These are required when connecting to an authenticated self-hosted or managed FeltDB instance.
1693
+ Never expose \`FELTDB_MANAGED_CONTROL_API_KEY\` or \`FELTDB_TOKEN\` through a \`VITE_\` variable. Managed browser access uses \`db.auth.signUp()\` and \`db.auth.signIn()\`; the active revision is discovered rather than configured.
1696
1694
 
1697
1695
  ## Development
1698
1696
 
@@ -1,5 +1,20 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ function loadEnvironment(file) {
4
+ const values = {};
5
+ for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
6
+ const line = raw.trim();
7
+ if (!line || line.startsWith('#'))
8
+ continue;
9
+ const separator = line.indexOf('=');
10
+ if (separator > 0)
11
+ values[line.slice(0, separator)] = line.slice(separator + 1);
12
+ }
13
+ return values;
14
+ }
15
+ function managedStateFile(projectDir) {
16
+ return path.join(projectDir, '.feltdb', 'managed.json');
17
+ }
3
18
  async function responseJson(response, fallback) {
4
19
  const body = await response.json().catch(() => ({}));
5
20
  if (!response.ok)
@@ -47,7 +62,8 @@ export async function configureManagedAccount(options) {
47
62
  method: 'POST', headers,
48
63
  body: JSON.stringify({
49
64
  id: `${options.applicationName}-control`,
50
- namespace: options.namespace,
65
+ namespace: application.namespace || options.namespace,
66
+ application_id: application.id,
51
67
  scopes: [
52
68
  'application:read', 'application:write', 'application:revision:create',
53
69
  'application:revision:read', 'application:revision:promote',
@@ -57,19 +73,67 @@ export async function configureManagedAccount(options) {
57
73
  }), 'Could not create the managed control-plane key');
58
74
  if (!controlKey.secret)
59
75
  throw new Error('Managed key service did not return the control-plane credential');
76
+ const dataKey = await responseJson(await request(`${sessionOrigin}/api/keys`, {
77
+ method: 'POST', headers,
78
+ body: JSON.stringify({
79
+ id: `${options.applicationName}-runtime`,
80
+ namespace: application.namespace || options.namespace,
81
+ application_id: application.id,
82
+ scopes: [
83
+ 'application:read', 'application:revision:read',
84
+ 'state:read', 'state:write', 'events:read',
85
+ 'workflows:run', 'agents:run', 'capabilities:run',
86
+ ],
87
+ }),
88
+ }), 'Could not create the managed application data key');
89
+ if (!dataKey.secret)
90
+ throw new Error('Managed key service did not return the application data credential');
91
+ try {
92
+ await responseJson(await request(`${apiUrl}/api/applications/${encodeURIComponent(application.id)}/revisions`, {
93
+ headers: { Authorization: `Bearer ${controlKey.secret}` },
94
+ }), 'Managed control-plane credential was created but is not accepted by the managed API');
95
+ }
96
+ catch (error) {
97
+ await Promise.allSettled([controlKey.id, dataKey.id].filter(Boolean).map(id => request(`${sessionOrigin}/api/keys/${encodeURIComponent(id)}`, { method: 'DELETE', headers })));
98
+ throw error;
99
+ }
60
100
  const environment = [
61
101
  '# Generated by create-feltdb managed setup. Do not commit this file.',
62
102
  `VITE_FELTDB_MANAGED_URL=${apiUrl}`,
63
- '# Browser authentication uses short-lived actor sessions; no long-lived data key is embedded.',
64
- 'VITE_FELTDB_MANAGED_API_KEY=',
103
+ `VITE_FELTDB_MANAGED_APPLICATION_ID=${application.id}`,
65
104
  '# Control-plane credential. Never prefix this with VITE_ or expose it to browser code.',
66
105
  `FELTDB_MANAGED_CONTROL_API_KEY=${controlKey.secret}`,
67
- `VITE_FELTDB_MANAGED_TENANT_ID=${tenant.id}`,
68
- `VITE_FELTDB_MANAGED_APPLICATION_ID=${application.id}`,
69
- `VITE_FELTDB_MANAGED_NAMESPACE=${application.namespace || options.namespace}`,
70
- 'VITE_FELTDB_MANAGED_ENVIRONMENT=production',
106
+ '# Server/deployment data-plane credential. Never prefix this with VITE_.',
107
+ `FELTDB_TOKEN=${dataKey.secret}`,
71
108
  '',
72
109
  ].join('\n');
73
110
  fs.writeFileSync(path.join(options.projectDir, '.env.local'), environment, { mode: 0o600 });
111
+ fs.mkdirSync(path.dirname(managedStateFile(options.projectDir)), { recursive: true });
112
+ fs.writeFileSync(managedStateFile(options.projectDir), `${JSON.stringify({
113
+ url: apiUrl,
114
+ tenantId: tenant.id,
115
+ applicationId: application.id,
116
+ namespace: application.namespace || options.namespace,
117
+ environment: 'production',
118
+ }, null, 2)}\n`, { mode: 0o600 });
74
119
  return { tenant, application, apiUrl };
75
120
  }
121
+ export async function finalizeManagedProvisioning(options) {
122
+ const publishedFile = path.join(options.projectDir, '.feltdb', 'last-published.json');
123
+ if (!fs.existsSync(publishedFile))
124
+ throw new Error('Managed provisioning did not produce .feltdb/last-published.json');
125
+ const published = JSON.parse(fs.readFileSync(publishedFile, 'utf8'));
126
+ if (!published.revisionId)
127
+ throw new Error('Managed provisioning did not promote an application revision');
128
+ const environmentFile = path.join(options.projectDir, '.env.local');
129
+ const environment = loadEnvironment(environmentFile);
130
+ const managedFile = managedStateFile(options.projectDir);
131
+ const managed = JSON.parse(fs.readFileSync(managedFile, 'utf8'));
132
+ const request = options.fetchImpl || fetch;
133
+ const discovery = await responseJson(await request(`${managed.url}/v1/application?application_id=${encodeURIComponent(managed.applicationId)}&environment=${encodeURIComponent(managed.environment)}`, { headers: { Authorization: `Bearer ${environment.FELTDB_TOKEN}` } }), 'Managed application data credential is not accepted by the promoted runtime');
134
+ if (discovery.revision_id !== published.revisionId) {
135
+ throw new Error('Managed runtime revision does not match the revision promoted during provisioning');
136
+ }
137
+ fs.writeFileSync(managedFile, `${JSON.stringify({ ...managed, revisionId: published.revisionId }, null, 2)}\n`, { mode: 0o600 });
138
+ return { revisionId: published.revisionId };
139
+ }
@@ -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.7.4';
3
+ export const FELTDB_PACKAGE_VERSION = '0.8.0';
4
4
  export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
@@ -272,6 +272,9 @@ pub struct ApplicationManifest {
272
272
  pub agents: Vec<AgentDefinition>,
273
273
  #[serde(default)]
274
274
  pub capabilities: Vec<CapabilityBinding>,
275
+ /// Resolved, provider-neutral module semantic contracts. Secret values are forbidden.
276
+ #[serde(default)]
277
+ pub modules: Vec<serde_json::Value>,
275
278
  #[serde(default)]
276
279
  pub connections: Vec<ConnectionBinding>,
277
280
  #[serde(default)]
@@ -313,6 +316,7 @@ impl ApplicationManifest {
313
316
  schedules: vec![],
314
317
  agents: vec![],
315
318
  capabilities: vec![],
319
+ modules: vec![],
316
320
  connections: vec![],
317
321
  policies: vec![],
318
322
  ui: UiApplicationModel::default(),
@@ -733,6 +737,11 @@ pub fn validate_manifest(
733
737
  }
734
738
  }
735
739
  }
740
+ for (index, module) in manifest.modules.iter().enumerate() {
741
+ if contains_plaintext_secret(module) || module.get("value").is_some() {
742
+ issues.push(issue("security", format!("modules.{index}"), "module contracts may declare secret requirements but never secret values"));
743
+ }
744
+ }
736
745
  for environment in &manifest.environments {
737
746
  if environment.configuration.iter().any(|(key, value)| {
738
747
  matches!(
@@ -868,6 +877,7 @@ fn sorted(mut manifest: ApplicationManifest) -> ApplicationManifest {
868
877
  agent.capabilities.sort();
869
878
  }
870
879
  sort_name!(manifest.capabilities);
880
+ manifest.modules.sort_by(|a, b| a.get("id").and_then(Value::as_str).cmp(&b.get("id").and_then(Value::as_str)));
871
881
  sort_name!(manifest.connections);
872
882
  sort_name!(manifest.policies);
873
883
  sort_name!(manifest.ui.bindings);
@@ -246,7 +246,8 @@ use serde_json::Value;
246
246
  use sha2::{Digest, Sha256};
247
247
  use std::any::type_name;
248
248
  use std::collections::hash_map::DefaultHasher;
249
- use std::collections::{HashMap, HashSet};
249
+ use std::collections::{BTreeMap, HashMap, HashSet};
250
+ use std::ops::Bound::{Excluded, Unbounded};
250
251
  use std::fmt::{Display, Formatter};
251
252
  use std::fs::{self, OpenOptions};
252
253
  use std::hash::{Hash, Hasher};
@@ -315,7 +316,7 @@ pub struct FeltDb {
315
316
  #[allow(dead_code)] // Reserved runtime subsystems are initialized before their public APIs land.
316
317
  struct Inner {
317
318
  path: PathBuf,
318
- rows: HashMap<String, HashMap<String, StoredRow>>,
319
+ rows: HashMap<String, BTreeMap<String, StoredRow>>,
319
320
  key_counters: HashMap<String, u64>,
320
321
  query_count_by_type: HashMap<String, usize>,
321
322
  adaptive_indexes: HashSet<String>,
@@ -1721,6 +1722,23 @@ impl FeltDb {
1721
1722
  .unwrap_or_default())
1722
1723
  }
1723
1724
 
1725
+ /// Traverse one collection in immutable record-key order without materializing it.
1726
+ pub fn list_collection_page(&self, capability: &str, after: Option<&str>, limit: usize) -> Result<Vec<StoredRow>> {
1727
+ let inner = self.inner.lock().expect("lock poisoned");
1728
+ let Some(rows) = inner.rows.get(capability) else { return Ok(vec![]) };
1729
+ let values: Box<dyn Iterator<Item = &StoredRow>> = match after {
1730
+ Some(key) => Box::new(rows.range::<str, _>((Excluded(key), Unbounded)).map(|(_, row)| row)),
1731
+ None => Box::new(rows.values()),
1732
+ };
1733
+ Ok(values.filter(|row| !row.deleted).take(limit).cloned().collect())
1734
+ }
1735
+
1736
+ /// Fetch one live collection record without materializing collection state.
1737
+ pub fn get_collection_record(&self, capability: &str, key: &str) -> Result<Option<StoredRow>> {
1738
+ let inner = self.inner.lock().expect("lock poisoned");
1739
+ Ok(inner.rows.get(capability).and_then(|rows| rows.get(key)).filter(|row| !row.deleted).cloned())
1740
+ }
1741
+
1724
1742
  /// Subscribe to canonical mutation events.
1725
1743
  pub fn subscribe_changes(&self) -> broadcast::Receiver<ChangeEvent> {
1726
1744
  self.event_tx.subscribe()
@@ -1,10 +1,10 @@
1
+ use serde_json::Value;
1
2
  use std::path::PathBuf;
2
3
  use std::{
3
4
  collections::HashMap,
4
5
  sync::{atomic::AtomicU64, Arc},
5
6
  time::Instant,
6
7
  };
7
- use serde_json::Value;
8
8
 
9
9
  #[derive(Clone)]
10
10
  pub struct BoundedQueryCursor {
@@ -16,34 +16,22 @@ pub struct BoundedQueryCursor {
16
16
  pub created_at: u64,
17
17
  }
18
18
 
19
+ use crate::{
20
+ application_contract::ApplicationContractStore, artifacts::ArtifactStore, audit::AuditLog,
21
+ auth::KeyStore, causal::CausalStore, clock::LeaseClock, cluster::ClusterStore,
22
+ connections::ConnectionStore, content::ContentStore, identity::IdentityStore,
23
+ leases::LeaseStore, metrics::Metrics, portable_bundle::BundleStore, principals::PrincipalStore,
24
+ providers::ProviderStore, releases::ReleaseStore, sessions::SessionVerifier,
25
+ tenancy::TenancyStore,
26
+ };
19
27
  use feltdb::{
20
- FeltDb,
21
28
  application::ApplicationStore,
29
+ application_runtime::RuntimeStore,
22
30
  authorization::{GrantSigner, GrantStore},
23
31
  sync_contract::SyncStore,
24
- workload::WorkloadStore,
25
32
  worker_mesh::WorkerMeshStore,
26
- application_runtime::RuntimeStore,
27
- };
28
- use crate::{
29
- artifacts::ArtifactStore,
30
- audit::AuditLog,
31
- auth::KeyStore,
32
- causal::CausalStore,
33
- cluster::ClusterStore,
34
- connections::ConnectionStore,
35
- content::ContentStore,
36
- identity::IdentityStore,
37
- leases::LeaseStore,
38
- metrics::Metrics,
39
- portable_bundle::BundleStore,
40
- principals::PrincipalStore,
41
- providers::ProviderStore,
42
- releases::ReleaseStore,
43
- sessions::SessionVerifier,
44
- tenancy::TenancyStore,
45
- application_contract::ApplicationContractStore,
46
- clock::LeaseClock,
33
+ workload::WorkloadStore,
34
+ FeltDb,
47
35
  };
48
36
 
49
37
  #[derive(Clone)]
@@ -283,10 +283,18 @@ impl ApplicationContractStore {
283
283
  confidence: 0.0,
284
284
  evidence_required: vec!["contract evidence".into()],
285
285
  suggested_questions: vec![match p {
286
- "actors" => "Who will use this application, and what roles do they have?".into(),
287
- "entities" => "What information or records should the application keep track of?".into(),
286
+ "actors" => {
287
+ "Who will use this application, and what roles do they have?".into()
288
+ }
289
+ "entities" => {
290
+ "What information or records should the application keep track of?"
291
+ .into()
292
+ }
288
293
  "actions" => "What should people be able to do with those records?".into(),
289
- "permissions" => "Who can view, create, edit, delete, or administer this information?".into(),
294
+ "permissions" => {
295
+ "Who can view, create, edit, delete, or administer this information?"
296
+ .into()
297
+ }
290
298
  _ => format!("What should FeltDB understand about {p}?"),
291
299
  }],
292
300
  candidate_resolutions: vec![],
@@ -154,7 +154,6 @@ fn now() -> u64 {
154
154
  .as_secs()
155
155
  }
156
156
 
157
-
158
157
  #[cfg(test)]
159
158
  mod tests {
160
159
  use super::*;
@@ -224,10 +224,7 @@ impl CertificationHarness {
224
224
 
225
225
  /// Gets the count of tests in a category.
226
226
  pub fn test_count(&self, category: TestCategory) -> usize {
227
- self.results
228
- .get(&category)
229
- .map(|v| v.len())
230
- .unwrap_or(0)
227
+ self.results.get(&category).map(|v| v.len()).unwrap_or(0)
231
228
  }
232
229
 
233
230
  /// Gets all failed tests.
@@ -439,13 +436,10 @@ mod tests {
439
436
 
440
437
  #[test]
441
438
  fn test_execution_plan_builder() {
442
- let plan = TestExecutionPlan::new(
443
- TestSuiteDefinition::standard(),
444
- TestEnvironment::Local,
445
- )
446
- .with_parallel()
447
- .with_timeout(600)
448
- .with_retry(3);
439
+ let plan = TestExecutionPlan::new(TestSuiteDefinition::standard(), TestEnvironment::Local)
440
+ .with_parallel()
441
+ .with_timeout(600)
442
+ .with_retry(3);
449
443
 
450
444
  assert!(plan.parallel_execution);
451
445
  assert_eq!(plan.timeout_secs, 600);
@@ -480,19 +474,18 @@ mod tests {
480
474
  fn test_environment_strings() {
481
475
  assert_eq!(TestEnvironment::Local.as_str(), "local");
482
476
  assert_eq!(TestEnvironment::ManagedStaging.as_str(), "managed_staging");
483
- assert_eq!(TestEnvironment::ManagedProduction.as_str(), "managed_production");
477
+ assert_eq!(
478
+ TestEnvironment::ManagedProduction.as_str(),
479
+ "managed_production"
480
+ );
484
481
  }
485
482
 
486
483
  #[test]
487
484
  fn certification_report_serialization() {
488
485
  let mut harness = CertificationHarness::new(TestEnvironment::Local);
489
486
 
490
- let result = TestResult::new(
491
- "test_1".into(),
492
- TestCategory::Operations,
493
- "test_1".into(),
494
- )
495
- .passed(50);
487
+ let result =
488
+ TestResult::new("test_1".into(), TestCategory::Operations, "test_1".into()).passed(50);
496
489
 
497
490
  harness.record_result(result);
498
491
 
@@ -512,11 +505,19 @@ mod tests {
512
505
 
513
506
  for i in 0..3 {
514
507
  let result = if i == 1 {
515
- TestResult::new(format!("test_{}", i), TestCategory::Authorization, format!("test_{}", i))
516
- .failed("test failed".into(), 50)
508
+ TestResult::new(
509
+ format!("test_{}", i),
510
+ TestCategory::Authorization,
511
+ format!("test_{}", i),
512
+ )
513
+ .failed("test failed".into(), 50)
517
514
  } else {
518
- TestResult::new(format!("test_{}", i), TestCategory::Authorization, format!("test_{}", i))
519
- .passed(50)
515
+ TestResult::new(
516
+ format!("test_{}", i),
517
+ TestCategory::Authorization,
518
+ format!("test_{}", i),
519
+ )
520
+ .passed(50)
520
521
  };
521
522
  harness.record_result(result);
522
523
  }
@@ -132,10 +132,7 @@ impl DelegationTokenIssuer {
132
132
  let payload = self.canonicalize_for_signing(&token)?;
133
133
  let signature = self.sign(&payload)?;
134
134
 
135
- Ok(DelegationToken {
136
- signature,
137
- ..token
138
- })
135
+ Ok(DelegationToken { signature, ..token })
139
136
  }
140
137
 
141
138
  /// Canonicalizes token for consistent signing/verification.
@@ -180,11 +177,7 @@ impl DelegationTokenIssuer {
180
177
 
181
178
  impl DelegationTokenValidator {
182
179
  /// Creates a new token validator.
183
- pub fn new(
184
- expected_issuer: String,
185
- expected_audience: String,
186
- shared_secret: Vec<u8>,
187
- ) -> Self {
180
+ pub fn new(expected_issuer: String, expected_audience: String, shared_secret: Vec<u8>) -> Self {
188
181
  Self {
189
182
  expected_issuer,
190
183
  expected_audience,
@@ -316,8 +309,7 @@ mod tests {
316
309
  fn issue_and_validate_delegation_token() {
317
310
  let secret = test_shared_secret();
318
311
  let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
319
- let validator =
320
- DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
312
+ let validator = DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
321
313
 
322
314
  let token = issuer
323
315
  .issue(
@@ -337,8 +329,7 @@ mod tests {
337
329
  fn reject_expired_token() {
338
330
  let secret = test_shared_secret();
339
331
  let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
340
- let validator =
341
- DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
332
+ let validator = DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
342
333
 
343
334
  let mut token = issuer
344
335
  .issue(
@@ -450,8 +441,7 @@ mod tests {
450
441
  fn tampering_invalidates_signature() {
451
442
  let secret = test_shared_secret();
452
443
  let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
453
- let validator =
454
- DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
444
+ let validator = DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
455
445
 
456
446
  let mut token = issuer
457
447
  .issue(