@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.
- package/README.md +25 -0
- package/dist/application-development.d.ts +3 -1
- package/dist/application-development.d.ts.map +1 -1
- package/dist/application-development.js +16 -0
- package/dist/application-manifest.d.ts +1 -0
- package/dist/application-manifest.d.ts.map +1 -1
- package/dist/canonical-application.d.ts +1 -0
- package/dist/canonical-application.d.ts.map +1 -1
- package/dist/canonical-application.js +3 -2
- package/dist/cli/cli.js +1 -1
- package/dist/cli/commands.js +225 -42
- package/dist/cli/index.js +1 -1
- package/dist/cli/managed-environment.js +6 -0
- package/dist/cli/managed-project.js +6 -0
- package/dist/cli/proposal-client.js +12 -3
- package/dist/cli/publish-engine.js +7 -4
- package/dist/cli/source-sync.js +47 -3
- package/dist/create/cli.js +10 -1
- package/dist/create/create.js +10 -12
- package/dist/create/managed-account.js +71 -7
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +10 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +20 -2
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +12 -24
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
- package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
- package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
- package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
- package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +79 -14
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +28 -7
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +5 -5
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +1777 -259
- package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
- package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
- package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
- package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +183 -21
- package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
- package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
- package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
- package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
- package/dist/create/template/default-project/feltdb.flow +5 -0
- package/dist/flowspec.d.ts +1 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +46 -12
- package/dist/http-client.d.ts +135 -14
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +79 -31
- package/dist/index-core.d.ts +2 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +2 -0
- package/dist/module-intent.d.ts +37 -0
- package/dist/module-intent.d.ts.map +1 -0
- package/dist/module-intent.js +81 -0
- package/dist/module.d.ts +76 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +61 -0
- package/dist/proposal.d.ts +80 -4
- package/dist/proposal.d.ts.map +1 -1
- package/dist/proposal.js +132 -5
- package/dist/studio/app.d.ts +3 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/AskFeltDB.d.ts +2 -1
- package/dist/studio/components/AskFeltDB.d.ts.map +1 -1
- package/dist/studio/components/ContractInspector.d.ts +7 -0
- package/dist/studio/components/ContractInspector.d.ts.map +1 -0
- package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -1
- package/dist/studio/components/ManagedInstancePanel.js +1 -5
- package/dist/studio/components/ModuleExplorer.d.ts +6 -0
- package/dist/studio/components/ModuleExplorer.d.ts.map +1 -0
- package/dist/studio/components/ProposalBrowser.d.ts +10 -1
- package/dist/studio/components/ProposalBrowser.d.ts.map +1 -1
- package/dist/studio/components/StateExplorer.d.ts +6 -7
- package/dist/studio/components/StateExplorer.d.ts.map +1 -1
- package/dist/studio/components/index.d.ts +2 -0
- package/dist/studio/components/index.d.ts.map +1 -1
- package/dist/studio/components/index.js +4 -4
- package/dist/studio/components-CNSNrmA9.js +2787 -0
- package/dist/studio/index.js +193 -147
- package/dist/studio/studio.css +1 -1
- package/dist/studio/utils/index.d.ts +2 -0
- package/dist/studio/utils/index.d.ts.map +1 -1
- package/dist/studio/utils/index.js +14 -12
- package/dist/studio/utils/managed-instance.d.ts +0 -4
- package/dist/studio/utils/managed-instance.d.ts.map +1 -1
- package/dist/studio/utils/managed-instance.js +0 -4
- package/dist/studio/utils/proposal-api.d.ts +53 -0
- package/dist/studio/utils/proposal-api.d.ts.map +1 -0
- package/dist/studio/utils/proposal-api.js +91 -0
- package/dist/studio/utils/service-api.d.ts +81 -0
- package/dist/studio/utils/service-api.d.ts.map +1 -0
- package/dist/studio/utils/service-api.js +51 -0
- package/dist/studio-app/assets/{dist-DA1MYSzp.js → dist-CCOk39Uc.js} +1 -1
- package/dist/studio-app/assets/{feltdb_wasm-B-Ccgccb.js → feltdb_wasm-CSB6KVgw.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
- package/dist/studio-app/assets/index-Bncji3aN.js +43 -0
- package/dist/studio-app/assets/{index-BRrmvaKw.css → index-yazaWMUN.css} +1 -1
- package/dist/studio-app/index.html +2 -2
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -1
- package/dist/workspace/local-development-authority.js +64 -3
- package/package.json +1 -1
- package/dist/studio/components-Bm4sjhcX.js +0 -2162
- package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
- package/dist/studio-app/assets/index-CEIqowCh.js +0 -42
package/dist/cli/source-sync.js
CHANGED
|
@@ -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 =
|
|
32
|
-
const environment =
|
|
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}`),
|
package/dist/create/cli.js
CHANGED
|
@@ -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) {
|
package/dist/create/create.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 \`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
`
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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" =>
|
|
287
|
-
|
|
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" =>
|
|
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![],
|
|
@@ -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
|
-
|
|
444
|
-
|
|
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!(
|
|
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 =
|
|
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(
|
|
516
|
-
|
|
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(
|
|
519
|
-
|
|
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(
|