create-feltdb 0.7.4 ā 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +10 -1
- package/dist/create.js +10 -12
- package/dist/managed-account.js +72 -7
- package/dist/package-versions.js +1 -1
- package/dist/server-source/crates/feltdb/src/application.rs +14 -0
- package/dist/server-source/crates/feltdb/src/lib.rs +20 -2
- package/dist/server-source/crates/feltdb-server/src/app_state.rs +12 -24
- package/dist/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
- package/dist/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
- package/dist/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
- package/dist/server-source/crates/feltdb-server/src/connections.rs +14 -0
- package/dist/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
- package/dist/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
- package/dist/server-source/crates/feltdb-server/src/identity.rs +79 -14
- package/dist/server-source/crates/feltdb-server/src/key_management.rs +28 -7
- package/dist/server-source/crates/feltdb-server/src/lib.rs +5 -5
- package/dist/server-source/crates/feltdb-server/src/main.rs +1929 -261
- package/dist/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
- package/dist/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
- package/dist/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
- package/dist/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
- package/dist/server-source/crates/feltdb-server/src/tenancy.rs +191 -22
- package/dist/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
- package/dist/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
- package/dist/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
- package/dist/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
- package/dist/template/default-project/feltdb.flow +5 -0
- package/dist/template/default-project/src/context/AuthContext.tsx +9 -1
- package/dist/template/default-project/src/pages/SignUp.tsx +1 -1
- package/package.json +1 -1
package/dist/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.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
|
|
package/dist/managed-account.js
CHANGED
|
@@ -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,29 +62,79 @@ 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',
|
|
54
70
|
'application:environment:read', 'application:environment:write',
|
|
71
|
+
'connections:read',
|
|
55
72
|
],
|
|
56
73
|
}),
|
|
57
74
|
}), 'Could not create the managed control-plane key');
|
|
58
75
|
if (!controlKey.secret)
|
|
59
76
|
throw new Error('Managed key service did not return the control-plane credential');
|
|
77
|
+
const dataKey = await responseJson(await request(`${sessionOrigin}/api/keys`, {
|
|
78
|
+
method: 'POST', headers,
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
id: `${options.applicationName}-runtime`,
|
|
81
|
+
namespace: application.namespace || options.namespace,
|
|
82
|
+
application_id: application.id,
|
|
83
|
+
scopes: [
|
|
84
|
+
'application:read', 'application:revision:read',
|
|
85
|
+
'state:read', 'state:write', 'events:read',
|
|
86
|
+
'workflows:run', 'agents:run', 'capabilities:run',
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
}), 'Could not create the managed application data key');
|
|
90
|
+
if (!dataKey.secret)
|
|
91
|
+
throw new Error('Managed key service did not return the application data credential');
|
|
92
|
+
try {
|
|
93
|
+
await responseJson(await request(`${apiUrl}/api/applications/${encodeURIComponent(application.id)}/revisions`, {
|
|
94
|
+
headers: { Authorization: `Bearer ${controlKey.secret}` },
|
|
95
|
+
}), 'Managed control-plane credential was created but is not accepted by the managed API');
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
await Promise.allSettled([controlKey.id, dataKey.id].filter(Boolean).map(id => request(`${sessionOrigin}/api/keys/${encodeURIComponent(id)}`, { method: 'DELETE', headers })));
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
60
101
|
const environment = [
|
|
61
102
|
'# Generated by create-feltdb managed setup. Do not commit this file.',
|
|
62
103
|
`VITE_FELTDB_MANAGED_URL=${apiUrl}`,
|
|
63
|
-
|
|
64
|
-
'VITE_FELTDB_MANAGED_API_KEY=',
|
|
104
|
+
`VITE_FELTDB_MANAGED_APPLICATION_ID=${application.id}`,
|
|
65
105
|
'# Control-plane credential. Never prefix this with VITE_ or expose it to browser code.',
|
|
66
106
|
`FELTDB_MANAGED_CONTROL_API_KEY=${controlKey.secret}`,
|
|
67
|
-
|
|
68
|
-
`
|
|
69
|
-
`VITE_FELTDB_MANAGED_NAMESPACE=${application.namespace || options.namespace}`,
|
|
70
|
-
'VITE_FELTDB_MANAGED_ENVIRONMENT=production',
|
|
107
|
+
'# Server/deployment data-plane credential. Never prefix this with VITE_.',
|
|
108
|
+
`FELTDB_TOKEN=${dataKey.secret}`,
|
|
71
109
|
'',
|
|
72
110
|
].join('\n');
|
|
73
111
|
fs.writeFileSync(path.join(options.projectDir, '.env.local'), environment, { mode: 0o600 });
|
|
112
|
+
fs.mkdirSync(path.dirname(managedStateFile(options.projectDir)), { recursive: true });
|
|
113
|
+
fs.writeFileSync(managedStateFile(options.projectDir), `${JSON.stringify({
|
|
114
|
+
url: apiUrl,
|
|
115
|
+
tenantId: tenant.id,
|
|
116
|
+
applicationId: application.id,
|
|
117
|
+
namespace: application.namespace || options.namespace,
|
|
118
|
+
environment: 'production',
|
|
119
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
74
120
|
return { tenant, application, apiUrl };
|
|
75
121
|
}
|
|
122
|
+
export async function finalizeManagedProvisioning(options) {
|
|
123
|
+
const publishedFile = path.join(options.projectDir, '.feltdb', 'last-published.json');
|
|
124
|
+
if (!fs.existsSync(publishedFile))
|
|
125
|
+
throw new Error('Managed provisioning did not produce .feltdb/last-published.json');
|
|
126
|
+
const published = JSON.parse(fs.readFileSync(publishedFile, 'utf8'));
|
|
127
|
+
if (!published.revisionId)
|
|
128
|
+
throw new Error('Managed provisioning did not promote an application revision');
|
|
129
|
+
const environmentFile = path.join(options.projectDir, '.env.local');
|
|
130
|
+
const environment = loadEnvironment(environmentFile);
|
|
131
|
+
const managedFile = managedStateFile(options.projectDir);
|
|
132
|
+
const managed = JSON.parse(fs.readFileSync(managedFile, 'utf8'));
|
|
133
|
+
const request = options.fetchImpl || fetch;
|
|
134
|
+
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');
|
|
135
|
+
if (discovery.revision_id !== published.revisionId) {
|
|
136
|
+
throw new Error('Managed runtime revision does not match the revision promoted during provisioning');
|
|
137
|
+
}
|
|
138
|
+
fs.writeFileSync(managedFile, `${JSON.stringify({ ...managed, revisionId: published.revisionId }, null, 2)}\n`, { mode: 0o600 });
|
|
139
|
+
return { revisionId: published.revisionId };
|
|
140
|
+
}
|
package/dist/package-versions.js
CHANGED
|
@@ -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.1';
|
|
4
4
|
export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
|
|
@@ -211,6 +211,10 @@ pub struct ArtifactReference {
|
|
|
211
211
|
pub name: String,
|
|
212
212
|
pub uri: String,
|
|
213
213
|
pub media_type: String,
|
|
214
|
+
/// Immutable source bytes used by trusted control-plane services. The URI
|
|
215
|
+
/// remains the content identity; values are base64 and never interpreted by Rust.
|
|
216
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
217
|
+
pub content: Option<String>,
|
|
214
218
|
}
|
|
215
219
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
|
216
220
|
pub struct DeploymentIntent {
|
|
@@ -272,6 +276,9 @@ pub struct ApplicationManifest {
|
|
|
272
276
|
pub agents: Vec<AgentDefinition>,
|
|
273
277
|
#[serde(default)]
|
|
274
278
|
pub capabilities: Vec<CapabilityBinding>,
|
|
279
|
+
/// Resolved, provider-neutral module semantic contracts. Secret values are forbidden.
|
|
280
|
+
#[serde(default)]
|
|
281
|
+
pub modules: Vec<serde_json::Value>,
|
|
275
282
|
#[serde(default)]
|
|
276
283
|
pub connections: Vec<ConnectionBinding>,
|
|
277
284
|
#[serde(default)]
|
|
@@ -313,6 +320,7 @@ impl ApplicationManifest {
|
|
|
313
320
|
schedules: vec![],
|
|
314
321
|
agents: vec![],
|
|
315
322
|
capabilities: vec![],
|
|
323
|
+
modules: vec![],
|
|
316
324
|
connections: vec![],
|
|
317
325
|
policies: vec![],
|
|
318
326
|
ui: UiApplicationModel::default(),
|
|
@@ -733,6 +741,11 @@ pub fn validate_manifest(
|
|
|
733
741
|
}
|
|
734
742
|
}
|
|
735
743
|
}
|
|
744
|
+
for (index, module) in manifest.modules.iter().enumerate() {
|
|
745
|
+
if contains_plaintext_secret(module) || module.get("value").is_some() {
|
|
746
|
+
issues.push(issue("security", format!("modules.{index}"), "module contracts may declare secret requirements but never secret values"));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
736
749
|
for environment in &manifest.environments {
|
|
737
750
|
if environment.configuration.iter().any(|(key, value)| {
|
|
738
751
|
matches!(
|
|
@@ -868,6 +881,7 @@ fn sorted(mut manifest: ApplicationManifest) -> ApplicationManifest {
|
|
|
868
881
|
agent.capabilities.sort();
|
|
869
882
|
}
|
|
870
883
|
sort_name!(manifest.capabilities);
|
|
884
|
+
manifest.modules.sort_by(|a, b| a.get("id").and_then(Value::as_str).cmp(&b.get("id").and_then(Value::as_str)));
|
|
871
885
|
sort_name!(manifest.connections);
|
|
872
886
|
sort_name!(manifest.policies);
|
|
873
887
|
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
|
}
|
|
@@ -590,6 +590,20 @@ impl ConnectionStore {
|
|
|
590
590
|
.cloned()
|
|
591
591
|
.collect())
|
|
592
592
|
}
|
|
593
|
+
|
|
594
|
+
/// Internal inventory lookup used after the caller has already passed an
|
|
595
|
+
/// application-scoped runtime or promotion authorization check.
|
|
596
|
+
pub fn application_connections(&self, application_id: &str) -> Result<Vec<Connection>, String> {
|
|
597
|
+
Ok(self
|
|
598
|
+
.records
|
|
599
|
+
.read()
|
|
600
|
+
.map_err(|_| "connection store lock poisoned")?
|
|
601
|
+
.connections
|
|
602
|
+
.iter()
|
|
603
|
+
.filter(|connection| connection.application_id == application_id)
|
|
604
|
+
.cloned()
|
|
605
|
+
.collect())
|
|
606
|
+
}
|
|
593
607
|
pub fn disable(
|
|
594
608
|
&self,
|
|
595
609
|
actor: &str,
|
|
@@ -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(
|
|
@@ -114,10 +114,7 @@ impl DurableOperationStore {
|
|
|
114
114
|
.map_err(|_| "store lock poisoned".to_string())?;
|
|
115
115
|
|
|
116
116
|
if ops.contains_key(&operation_id) {
|
|
117
|
-
return Err(format!(
|
|
118
|
-
"operation already exists: {}",
|
|
119
|
-
operation_id
|
|
120
|
-
));
|
|
117
|
+
return Err(format!("operation already exists: {}", operation_id));
|
|
121
118
|
}
|
|
122
119
|
|
|
123
120
|
let now = now();
|
|
@@ -158,7 +155,8 @@ impl DurableOperationStore {
|
|
|
158
155
|
.ok_or(format!("operation not found: {}", operation_id))?;
|
|
159
156
|
|
|
160
157
|
// Can only claim Pending or Unknown operations
|
|
161
|
-
if operation.state != OperationState::Pending && operation.state != OperationState::Unknown
|
|
158
|
+
if operation.state != OperationState::Pending && operation.state != OperationState::Unknown
|
|
159
|
+
{
|
|
162
160
|
return Err(format!(
|
|
163
161
|
"cannot claim operation in state {:?}",
|
|
164
162
|
operation.state
|
|
@@ -339,7 +337,8 @@ impl DurableOperationStore {
|
|
|
339
337
|
}
|
|
340
338
|
|
|
341
339
|
// Must be in Claimed or Running state
|
|
342
|
-
if operation.state != OperationState::Claimed && operation.state != OperationState::Running
|
|
340
|
+
if operation.state != OperationState::Claimed && operation.state != OperationState::Running
|
|
341
|
+
{
|
|
343
342
|
return Err(format!(
|
|
344
343
|
"cannot fail operation in state {:?}",
|
|
345
344
|
operation.state
|
|
@@ -356,10 +355,7 @@ impl DurableOperationStore {
|
|
|
356
355
|
}
|
|
357
356
|
|
|
358
357
|
/// Marks an operation as Unknown (when lease expires).
|
|
359
|
-
pub fn mark_unknown(
|
|
360
|
-
&self,
|
|
361
|
-
operation_id: &str,
|
|
362
|
-
) -> Result<DurableOperation, String> {
|
|
358
|
+
pub fn mark_unknown(&self, operation_id: &str) -> Result<DurableOperation, String> {
|
|
363
359
|
let mut ops = self
|
|
364
360
|
.operations
|
|
365
361
|
.write()
|
|
@@ -372,7 +368,8 @@ impl DurableOperationStore {
|
|
|
372
368
|
let now = now();
|
|
373
369
|
|
|
374
370
|
// Can only mark Unknown if in Claimed or Running state
|
|
375
|
-
if operation.state != OperationState::Claimed && operation.state != OperationState::Running
|
|
371
|
+
if operation.state != OperationState::Claimed && operation.state != OperationState::Running
|
|
372
|
+
{
|
|
376
373
|
return Err(format!(
|
|
377
374
|
"cannot mark Unknown operation in state {:?}",
|
|
378
375
|
operation.state
|
|
@@ -484,9 +481,7 @@ impl DurableOperationStore {
|
|
|
484
481
|
.map_err(|_| "store lock poisoned".to_string())?;
|
|
485
482
|
|
|
486
483
|
let before_len = ops.len();
|
|
487
|
-
ops.retain(|_, op|
|
|
488
|
-
!(op.state == OperationState::Completed && op.updated_at < cutoff_time)
|
|
489
|
-
});
|
|
484
|
+
ops.retain(|_, op| !(op.state == OperationState::Completed && op.updated_at < cutoff_time));
|
|
490
485
|
let after_len = ops.len();
|
|
491
486
|
|
|
492
487
|
Ok(before_len - after_len)
|
|
@@ -755,9 +750,7 @@ mod tests {
|
|
|
755
750
|
.claim_operation("op_123", "worker_1".into(), 60)
|
|
756
751
|
.expect("claim failed");
|
|
757
752
|
|
|
758
|
-
let unknown = store
|
|
759
|
-
.mark_unknown("op_123")
|
|
760
|
-
.expect("mark unknown failed");
|
|
753
|
+
let unknown = store.mark_unknown("op_123").expect("mark unknown failed");
|
|
761
754
|
assert_eq!(unknown.state, OperationState::Unknown);
|
|
762
755
|
|
|
763
756
|
let reconciled = store
|
|
@@ -826,9 +819,7 @@ mod tests {
|
|
|
826
819
|
)
|
|
827
820
|
.expect("create failed");
|
|
828
821
|
|
|
829
|
-
let ops = store
|
|
830
|
-
.list_operations("tenant_prod")
|
|
831
|
-
.expect("list failed");
|
|
822
|
+
let ops = store.list_operations("tenant_prod").expect("list failed");
|
|
832
823
|
assert_eq!(ops.len(), 2);
|
|
833
824
|
}
|
|
834
825
|
|
|
@@ -915,9 +906,7 @@ mod tests {
|
|
|
915
906
|
.expect("cleanup failed");
|
|
916
907
|
assert_eq!(deleted, 1);
|
|
917
908
|
|
|
918
|
-
let ops = store
|
|
919
|
-
.list_operations("tenant_prod")
|
|
920
|
-
.expect("list failed");
|
|
909
|
+
let ops = store.list_operations("tenant_prod").expect("list failed");
|
|
921
910
|
assert_eq!(ops.len(), 1);
|
|
922
911
|
assert_eq!(ops[0].operation_id, "op_pending");
|
|
923
912
|
}
|
|
@@ -978,11 +967,7 @@ mod tests {
|
|
|
978
967
|
.expect("start failed");
|
|
979
968
|
|
|
980
969
|
store
|
|
981
|
-
.complete_operation(
|
|
982
|
-
"op_123",
|
|
983
|
-
"worker_1",
|
|
984
|
-
serde_json::json!({"result": "done"}),
|
|
985
|
-
)
|
|
970
|
+
.complete_operation("op_123", "worker_1", serde_json::json!({"result": "done"}))
|
|
986
971
|
.expect("complete failed");
|
|
987
972
|
|
|
988
973
|
let final_op = store.get_operation("op_123").expect("get failed");
|