@feltdb/core 0.7.3 → 0.7.4

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 (95) hide show
  1. package/dist/application-development.d.ts +76 -0
  2. package/dist/application-development.d.ts.map +1 -0
  3. package/dist/application-development.js +137 -0
  4. package/dist/application-manifest.d.ts +5 -1
  5. package/dist/application-manifest.d.ts.map +1 -1
  6. package/dist/application-manifest.js +10 -3
  7. package/dist/canonical-application.d.ts +26 -0
  8. package/dist/canonical-application.d.ts.map +1 -0
  9. package/dist/canonical-application.js +45 -0
  10. package/dist/cli/ai-model-config.js +97 -0
  11. package/dist/cli/commands.js +466 -105
  12. package/dist/cli/index.js +1 -1
  13. package/dist/cli/proposal-client.js +54 -0
  14. package/dist/cli/publish-engine.js +162 -0
  15. package/dist/cli/source-sync.js +139 -0
  16. package/dist/create/cli.js +1 -1
  17. package/dist/create/create.js +30 -6
  18. package/dist/create/managed-account.js +14 -7
  19. package/dist/create/package-versions.js +1 -1
  20. package/dist/create/server-source/crates/feltdb/src/application.rs +31 -1
  21. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +84 -6
  22. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
  23. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
  24. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +46 -3
  25. package/dist/create/server-source/crates/feltdb-server/src/main.rs +315 -16
  26. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +20 -0
  27. package/dist/create/template/default-project/README.md +57 -0
  28. package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
  29. package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
  30. package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
  31. package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
  32. package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
  33. package/dist/create/template/default-project/feltdb.flow +169 -0
  34. package/dist/create/template/default-project/src/App.tsx +20 -0
  35. package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
  36. package/dist/create/template/default-project/src/feltdb.ts +87 -0
  37. package/dist/create/template/default-project/src/index.tsx +14 -0
  38. package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
  39. package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
  40. package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
  41. package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
  42. package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
  43. package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
  44. package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
  45. package/dist/create/template/default-project/src/styles-application.css +21 -0
  46. package/dist/create/template/default-project/src/styles.css +1 -0
  47. package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
  48. package/dist/create/template/default-project/workflows/invitation.ts +30 -0
  49. package/dist/create/template/default-project/workflows/project-created.ts +2 -0
  50. package/dist/db.d.ts +49 -1
  51. package/dist/db.d.ts.map +1 -1
  52. package/dist/db.js +77 -0
  53. package/dist/error-codes.d.ts +20 -1
  54. package/dist/error-codes.d.ts.map +1 -1
  55. package/dist/error-codes.js +25 -0
  56. package/dist/flowspec.d.ts.map +1 -1
  57. package/dist/flowspec.js +27 -6
  58. package/dist/http-db.d.ts +15 -2
  59. package/dist/http-db.d.ts.map +1 -1
  60. package/dist/http-db.js +113 -5
  61. package/dist/index-core.d.ts +3 -0
  62. package/dist/index-core.d.ts.map +1 -1
  63. package/dist/index-core.js +3 -0
  64. package/dist/proposal.d.ts +85 -0
  65. package/dist/proposal.d.ts.map +1 -0
  66. package/dist/proposal.js +105 -0
  67. package/dist/studio/app.d.ts +3 -1
  68. package/dist/studio/app.d.ts.map +1 -1
  69. package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
  70. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  71. package/dist/studio/components/AskFeltDB.d.ts +20 -0
  72. package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
  73. package/dist/studio/components/ProposalBrowser.d.ts +3 -0
  74. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
  75. package/dist/studio/components/index.d.ts +2 -0
  76. package/dist/studio/components/index.d.ts.map +1 -1
  77. package/dist/studio/components/index.js +4 -4
  78. package/dist/studio/{components-Dxuhrv8_.js → components-Bm4sjhcX.js} +335 -5
  79. package/dist/studio/index.js +168 -157
  80. package/dist/studio/studio.css +1 -1
  81. package/dist/studio-app/assets/dist-DA1MYSzp.js +1 -0
  82. package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-B-Ccgccb.js} +1 -1
  83. package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
  84. package/dist/studio-app/assets/{index-C1GWyazR.css → index-BRrmvaKw.css} +1 -1
  85. package/dist/studio-app/assets/index-CEIqowCh.js +42 -0
  86. package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
  87. package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
  88. package/dist/studio-app/index.html +2 -2
  89. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  90. package/dist/workspace/local-development-authority.d.ts +3 -0
  91. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  92. package/dist/workspace/local-development-authority.js +62 -1
  93. package/package.json +1 -1
  94. package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
  95. package/dist/studio-app/assets/index-B5tnmvSD.js +0 -28
package/dist/cli/index.js CHANGED
@@ -23,7 +23,7 @@ import * as path from 'path';
23
23
  import * as readline from 'readline';
24
24
  import { getClient } from './api-client.js';
25
25
  import { loadFeltDBConfig, createDefaultConfig, validateModel, } from './config.js';
26
- const VERSION = '0.7.3';
26
+ const VERSION = '0.7.4';
27
27
  function prompt(question) {
28
28
  const rl = readline.createInterface({
29
29
  input: process.stdin,
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseFlowSpec } from '@feltdb/core';
4
+ export function developmentProposalConnection(cwd = process.cwd()) {
5
+ const file = path.join(cwd, '.feltdb', 'connections.json');
6
+ const stored = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')).developmentAuthority : undefined;
7
+ const endpoint = process.env.VITE_FELTDB_URL || process.env.VITE_FELTDB_MANAGED_URL || stored?.endpoint;
8
+ if (!endpoint)
9
+ throw new Error('No running FeltDB proposal service. Start feltdb dev.');
10
+ const config = fs.existsSync(path.join(cwd, 'feltdb.config.json')) ? JSON.parse(fs.readFileSync(path.join(cwd, 'feltdb.config.json'), 'utf8')) : {};
11
+ const published = fs.existsSync(path.join(cwd, '.feltdb', 'last-published.json')) ? JSON.parse(fs.readFileSync(path.join(cwd, '.feltdb', 'last-published.json'), 'utf8')) : {};
12
+ const flowApplication = fs.existsSync(path.join(cwd, 'feltdb.flow')) ? parseFlowSpec(fs.readFileSync(path.join(cwd, 'feltdb.flow'), 'utf8')).app : undefined;
13
+ return { endpoint: endpoint.replace(/\/$/, ''), proposalAuthorityToken: stored?.proposalAuthorityToken,
14
+ applicationId: process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || published.applicationId || config.applicationId || flowApplication,
15
+ environment: process.env.VITE_FELTDB_MANAGED_ENVIRONMENT || published.environment || 'local' };
16
+ }
17
+ export class ProposalServiceClient {
18
+ constructor(connection, token = process.env.FELTDB_MANAGED_CONTROL_API_KEY || '') {
19
+ this.connection = connection;
20
+ this.token = token;
21
+ }
22
+ async request(pathname, init = {}) {
23
+ const response = await fetch(`${this.connection.endpoint}${pathname}`, { ...init, headers: {
24
+ ...(init.body ? { 'content-type': 'application/json' } : {}), ...(this.token ? { authorization: `Bearer ${this.token}` } : {}), ...init.headers,
25
+ } });
26
+ const body = await response.json().catch(() => ({}));
27
+ if (!response.ok)
28
+ throw new Error(body.error || body.message || `Proposal service failed (${response.status})`);
29
+ return body;
30
+ }
31
+ scoped(pathname) {
32
+ const query = new URLSearchParams();
33
+ if (this.connection.applicationId)
34
+ query.set('application_id', this.connection.applicationId);
35
+ if (this.connection.environment)
36
+ query.set('environment', this.connection.environment);
37
+ return `${pathname}?${query}`;
38
+ }
39
+ actor(input = {}) { return { application_id: this.connection.applicationId, environment: this.connection.environment, ...input }; }
40
+ create(input) { return this.request('/v1/proposals', { method: 'POST', body: JSON.stringify(input) }); }
41
+ list() { return this.request(this.scoped('/v1/proposals')); }
42
+ get(id) { return this.request(this.scoped(`/v1/proposals/${encodeURIComponent(id)}`)); }
43
+ history(id) { return this.request(this.scoped(`/v1/proposals/${encodeURIComponent(id)}/history`)); }
44
+ validate(id, actor = 'local-developer', proposedManifest) { return this.request(`/v1/proposals/${encodeURIComponent(id)}/validate`, { method: 'POST', body: JSON.stringify(this.actor({ actor, proposed_manifest: proposedManifest })) }); }
45
+ preview(id, actor = 'local-developer') { return this.request(`/v1/proposals/${encodeURIComponent(id)}/preview`, { method: 'POST', body: JSON.stringify(this.actor({ actor })) }); }
46
+ transition(id, status, actor = 'local-developer', repositoryAuthority = false, approveAuthorization = false) {
47
+ return this.request(`/v1/proposals/${encodeURIComponent(id)}/status`, { method: 'PATCH', headers: repositoryAuthority && this.connection.proposalAuthorityToken
48
+ ? { 'FeltDB-Proposal-Authority': this.connection.proposalAuthorityToken, 'FeltDB-Repository-Apply': '1' } : {}, body: JSON.stringify(this.actor({ status, actor, approve_authorization: approveAuthorization })) });
49
+ }
50
+ }
51
+ export function proposalService(cwd = process.cwd(), endpoint) {
52
+ const connection = developmentProposalConnection(cwd);
53
+ return new ProposalServiceClient(endpoint ? { ...connection, endpoint } : connection);
54
+ }
@@ -0,0 +1,162 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { ApplicationManifestClient, canonicalApplicationBytes, flowSpecToManifest, parseFlowSpec, validateFlowSpec } from '@feltdb/core';
6
+ function sha256(value) { return createHash('sha256').update(value).digest('hex'); }
7
+ function loadEnv(file) {
8
+ const values = {};
9
+ if (!fs.existsSync(file))
10
+ return values;
11
+ for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
12
+ const line = raw.trim();
13
+ if (!line || line.startsWith('#'))
14
+ continue;
15
+ const separator = line.indexOf('=');
16
+ if (separator < 1)
17
+ continue;
18
+ values[line.slice(0, separator).trim()] = line.slice(separator + 1).trim().replace(/^(['"])(.*)\1$/, '$2');
19
+ }
20
+ return values;
21
+ }
22
+ export function discoverPublishConfiguration(cwd = process.cwd(), environmentOverride) {
23
+ const configFile = path.join(cwd, 'feltdb.config.json');
24
+ if (!fs.existsSync(configFile))
25
+ throw new Error('feltdb.config.json is required for publish');
26
+ const project = JSON.parse(fs.readFileSync(configFile, 'utf8'));
27
+ const file = loadEnv(path.join(cwd, '.env.local'));
28
+ const value = (name) => process.env[name] || file[name];
29
+ const configuration = {
30
+ url: value('VITE_FELTDB_MANAGED_URL'), token: value('FELTDB_MANAGED_CONTROL_API_KEY'),
31
+ tenantId: value('VITE_FELTDB_MANAGED_TENANT_ID'), applicationId: value('VITE_FELTDB_MANAGED_APPLICATION_ID'),
32
+ namespace: value('VITE_FELTDB_MANAGED_NAMESPACE') || project.namespace,
33
+ environment: environmentOverride || value('VITE_FELTDB_MANAGED_ENVIRONMENT') || 'production',
34
+ };
35
+ const missing = Object.entries(configuration).filter(([, value]) => !value).map(([name]) => name);
36
+ if (missing.length)
37
+ throw new Error(`Managed publish configuration is incomplete in .env.local: ${missing.join(', ')}`);
38
+ return configuration;
39
+ }
40
+ function filesBelow(root, directory) {
41
+ const target = path.join(root, directory);
42
+ if (!fs.existsSync(target))
43
+ return [];
44
+ return fs.readdirSync(target, { withFileTypes: true }).flatMap(entry => {
45
+ const relative = path.join(directory, entry.name);
46
+ if (entry.isSymbolicLink())
47
+ throw new Error(`Publish artifacts cannot be symbolic links: ${relative}`);
48
+ return entry.isDirectory() ? filesBelow(root, relative) : entry.isFile() ? [relative] : [];
49
+ });
50
+ }
51
+ export function buildPublishBundle(cwd, configuration) {
52
+ const flowPath = path.join(cwd, 'feltdb.flow');
53
+ if (!fs.existsSync(flowPath))
54
+ throw new Error('feltdb.flow is required for publish');
55
+ const flowSource = fs.readFileSync(flowPath, 'utf8');
56
+ const spec = parseFlowSpec(flowSource);
57
+ const diagnostics = validateFlowSpec(spec).filter(value => value.severity === 'error');
58
+ if (diagnostics.length)
59
+ throw new Error(`Publish validation failed: ${diagnostics.map(value => value.message).join('; ')}`);
60
+ const deployable = ['feltdb.flow', ...['workflows', 'agents', 'capabilities'].flatMap(directory => filesBelow(cwd, directory))].sort();
61
+ const artifacts = deployable.map(relative => {
62
+ const content = Buffer.from(fs.readFileSync(path.join(cwd, relative)));
63
+ const mediaType = relative.endsWith('.flow') ? 'text/x-feltdb-flow' : relative.endsWith('.json') ? 'application/json' : relative.endsWith('.ts') ? 'text/typescript' : 'text/plain';
64
+ return { path: relative.split(path.sep).join('/'), hash: `sha256:${sha256(content)}`, bytes: content.byteLength, mediaType, content: content.toString('base64') };
65
+ });
66
+ const artifactHash = `sha256:${sha256(artifacts.map(value => `${value.path}\0${value.hash}`).join('\n'))}`;
67
+ const manifest = flowSpecToManifest(spec, configuration.tenantId, configuration.applicationId);
68
+ const contractHash = (value) => `sha256:${sha256(Buffer.from(canonicalApplicationBytes(value)))}`;
69
+ manifest.metadata.labels = {
70
+ ...(manifest.metadata.labels || {}),
71
+ 'feltdb.dsl_version': String(spec.version),
72
+ 'feltdb.model_hash': contractHash({ collections: manifest.collections, indexes: manifest.indexes, references: manifest.references }),
73
+ 'feltdb.authorization_hash': contractHash(manifest.policies),
74
+ 'feltdb.workflow_hash': contractHash({ workflows: manifest.workflows, triggers: manifest.triggers, schedules: manifest.schedules }),
75
+ 'feltdb.agent_hash': contractHash(manifest.agents),
76
+ 'feltdb.capability_hash': contractHash(manifest.capabilities),
77
+ 'feltdb.contract_hash': contractHash({ dsl_version: spec.version, manifest }),
78
+ 'feltdb.artifact_hash': artifactHash,
79
+ 'feltdb.namespace': configuration.namespace,
80
+ };
81
+ manifest.artifacts = artifacts.map(value => ({ name: value.path, uri: `data:${value.mediaType};base64,${value.content}`, media_type: value.mediaType }));
82
+ const git = spawnSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
83
+ const sourceRevision = git.status === 0 ? git.stdout.trim() : undefined;
84
+ if (sourceRevision)
85
+ manifest.metadata.labels['feltdb.source_revision'] = sourceRevision;
86
+ return { manifest, artifacts, artifactHash, sourceRevision };
87
+ }
88
+ function sameDefinition(left, right) {
89
+ return left.metadata.labels?.['feltdb.artifact_hash'] === right.metadata.labels?.['feltdb.artifact_hash'];
90
+ }
91
+ function activeRevision(revisions, pointers, environment) {
92
+ const id = pointers[environment];
93
+ return revisions.find(value => value.revision_id === id);
94
+ }
95
+ function describeChanges(current, bundle) {
96
+ if (!current)
97
+ return bundle.artifacts.map(value => `added ${value.path}`);
98
+ const old = new Map(current.manifest.artifacts.map(value => [value.name, value.uri]));
99
+ const next = new Map(bundle.manifest.artifacts.map(value => [value.name, value.uri]));
100
+ return [...new Set([...old.keys(), ...next.keys()])].sort().flatMap(name => !old.has(name) ? [`added ${name}`] : !next.has(name) ? [`removed ${name}`] : old.get(name) !== next.get(name) ? [`modified ${name}`] : []);
101
+ }
102
+ export async function publishApplication(options = {}) {
103
+ const cwd = options.cwd || process.cwd();
104
+ const log = options.log || console.log;
105
+ const configuration = discoverPublishConfiguration(cwd, options.environment);
106
+ const bundle = buildPublishBundle(cwd, configuration);
107
+ const client = new ApplicationManifestClient(configuration.url.replace(/\/$/, ''), configuration.token);
108
+ const cloud = await client.revisions(configuration.applicationId);
109
+ const current = activeRevision(cloud.revisions, cloud.environment_pointers, configuration.environment);
110
+ const changes = describeChanges(current, bundle);
111
+ log(options.dryRun ? 'FeltDB Publish Preview' : options.status ? 'FeltDB Publish Status' : 'FeltDB Publish');
112
+ log(`Application: ${bundle.manifest.metadata.name}`);
113
+ log(`Environment: ${configuration.environment}`);
114
+ log(`Namespace: ${configuration.namespace}`);
115
+ if (options.status) {
116
+ log(`Local: ${bundle.artifactHash}`);
117
+ log(`Cloud version: ${current?.revision_number ?? 'none'}`);
118
+ log(current && sameDefinition(current.manifest, bundle.manifest) ? 'Status: ✓ Synchronized' : 'Status: ↑ Changes ready to publish');
119
+ return { status: current && sameDefinition(current.manifest, bundle.manifest) ? 'synchronized' : 'changes', current, bundle };
120
+ }
121
+ if (current && sameDefinition(current.manifest, bundle.manifest)) {
122
+ log(`✓ Local application matches cloud version ${current.revision_number}.`);
123
+ log('Nothing to publish.');
124
+ return { status: 'unchanged', current, bundle };
125
+ }
126
+ log('Changes:');
127
+ changes.forEach(value => log(` ${value}`));
128
+ if (!options.noBuild) {
129
+ const packageFile = path.join(cwd, 'package.json');
130
+ const packageManifest = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
131
+ if (!packageManifest.scripts?.build)
132
+ throw new Error('package.json must define a build script for publish');
133
+ const result = spawnSync('npm', ['run', 'build'], { cwd, stdio: 'inherit' });
134
+ if (result.error)
135
+ throw result.error;
136
+ if (result.status !== 0)
137
+ throw new Error(`Application build failed with exit code ${result.status}`);
138
+ }
139
+ const draft = await client.createDraft(configuration.applicationId, current?.revision_id);
140
+ const updated = await client.updateDraft(configuration.applicationId, draft, bundle.manifest);
141
+ const validation = await client.validateDraft(configuration.applicationId, updated.draft_id);
142
+ if (!validation.valid)
143
+ throw new Error(`Publish validation failed. No cloud changes were activated. ${validation.issues.map(value => `${value.path}: ${value.message}`).join('; ')}`);
144
+ if (options.dryRun) {
145
+ log(`Validation: ✓ ${validation.compatibility}`);
146
+ log('No changes have been published.');
147
+ return { status: 'preview', current, bundle, validation };
148
+ }
149
+ if (!options.yes && !(await options.confirm?.('Publish these changes?')))
150
+ throw new Error('Publish cancelled');
151
+ const revision = await client.commitDraft(configuration.applicationId, updated.draft_id);
152
+ const diff = await client.diff(configuration.applicationId, revision.revision_id, current?.revision_id);
153
+ if ((diff.classification === 'BREAKING' || diff.classification === 'MIGRATION_REQUIRED' || diff.security_changes.length) && !options.approveDestructive) {
154
+ throw new Error(`Publish requires explicit approval before activation (${diff.classification}${diff.security_changes.length ? ', authorization boundary changed' : ''}). Immutable version ${revision.revision_number} was staged but the active cloud version was not changed.`);
155
+ }
156
+ const promoted = await client.promoteEnvironment(configuration.applicationId, configuration.environment, revision.revision_id, current?.revision_id, options.message || 'Published from feltdb CLI', Boolean(options.approveDestructive));
157
+ fs.mkdirSync(path.join(cwd, '.feltdb'), { recursive: true });
158
+ fs.writeFileSync(path.join(cwd, '.feltdb', 'last-published.json'), `${JSON.stringify({ applicationId: configuration.applicationId, tenantId: configuration.tenantId, namespace: configuration.namespace, environment: configuration.environment, version: revision.revision_number, revisionId: revision.revision_id, contractHash: bundle.manifest.metadata.labels?.['feltdb.contract_hash'], artifactHash: bundle.artifactHash, sourceRevision: bundle.sourceRevision, publishedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
159
+ log(`✓ Published immutable version ${revision.revision_number}`);
160
+ log(`✓ Activated in ${configuration.environment}`);
161
+ return { status: 'published', current, bundle, revision, diff, promoted };
162
+ }
@@ -0,0 +1,139 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { compileApplicationContract, createContractSnapshot, flowSourceHash, validateFlowProposal, } from '@feltdb/core';
6
+ import { parseFlowSpec } from '@feltdb/core';
7
+ const FLOW_FILE = 'feltdb.flow';
8
+ const GENERATED_FILE = 'src/feltdb/generated-contract.ts';
9
+ const SYNC_STATE = '.feltdb/source-sync.json';
10
+ function digest(value) {
11
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
12
+ }
13
+ function readJson(file) {
14
+ return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : undefined;
15
+ }
16
+ function gitDirty(cwd, files) {
17
+ const result = spawnSync('git', ['status', '--porcelain', '--', ...files], { cwd, encoding: 'utf8' });
18
+ if (result.status !== 0)
19
+ return [];
20
+ return result.stdout.split(/\r?\n/).filter(Boolean).map(line => line.slice(3));
21
+ }
22
+ export async function loadLocalDevelopmentContract(cwd = process.cwd()) {
23
+ const flowPath = path.join(cwd, FLOW_FILE);
24
+ if (!fs.existsSync(flowPath))
25
+ throw new Error(`${FLOW_FILE} is required`);
26
+ const source = fs.readFileSync(flowPath, 'utf8');
27
+ const project = readJson(path.join(cwd, 'feltdb.config.json')) ?? {};
28
+ const published = readJson(path.join(cwd, '.feltdb', 'last-published.json'));
29
+ 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';
33
+ const contract = await compileApplicationContract(parseFlowSpec(source), tenantId, applicationId);
34
+ return {
35
+ source,
36
+ flowHash: await flowSourceHash(source),
37
+ snapshot: createContractSnapshot(contract, {
38
+ environment,
39
+ contractVersion: published?.version ?? 1,
40
+ }),
41
+ };
42
+ }
43
+ export async function reviewFlowProposal(proposal, cwd = process.cwd()) {
44
+ const current = await loadLocalDevelopmentContract(cwd);
45
+ return validateFlowProposal({ proposal, currentFlow: current.source, snapshot: current.snapshot });
46
+ }
47
+ function generatedContractSource(review) {
48
+ const snapshot = createContractSnapshot(review.proposedContract, {
49
+ environment: 'local',
50
+ contractVersion: review.proposal.baseContractHash === review.proposedContract.hashes.application ? 1 : 2,
51
+ });
52
+ const collections = review.proposedSpec.collections.map(collection => ({
53
+ name: collection.name,
54
+ fields: collection.fields.map(field => ({ name: field.name, type: field.type, optional: field.optional })),
55
+ }));
56
+ const typeName = (value) => {
57
+ const cleaned = value.replace(/[^A-Za-z0-9_$]/g, '_');
58
+ return /^[A-Za-z_$]/.test(cleaned) ? cleaned : `_${cleaned}`;
59
+ };
60
+ const valueType = (value) => {
61
+ const normalized = value.toLowerCase().replace(/\?$/, '');
62
+ if (['int', 'integer', 'float', 'number'].includes(normalized))
63
+ return 'number';
64
+ if (['bool', 'boolean'].includes(normalized))
65
+ return 'boolean';
66
+ if (['json', 'object'].includes(normalized))
67
+ return 'unknown';
68
+ if (normalized.endsWith('[]'))
69
+ return `${valueType(normalized.slice(0, -2))}[]`;
70
+ return 'string';
71
+ };
72
+ const types = review.proposedSpec.collections.map(collection => {
73
+ const fields = collection.fields.map(field => ` ${JSON.stringify(field.name)}${field.optional ? '?' : ''}: ${valueType(field.type)};`);
74
+ return `export interface ${typeName(collection.name)}Record {\n id: string;\n${fields.join('\n')}\n}`;
75
+ }).join('\n\n');
76
+ const bindings = review.proposedSpec.collections.map(collection => ` ${JSON.stringify(collection.name)}: database.collection<${typeName(collection.name)}Record>(${JSON.stringify(collection.name)}),`).join('\n');
77
+ return `/* Generated by feltdb sync from feltdb.flow. Do not edit this file directly. */\n`
78
+ + `export const FELTDB_CONTRACT = ${JSON.stringify(snapshot, null, 2)} as const;\n\n`
79
+ + `export const FELTDB_COLLECTIONS = ${JSON.stringify(collections, null, 2)} as const;\n\n`
80
+ + `${types}\n\n`
81
+ + `export interface FeltDBCollectionBinder {\n collection<T>(name: string): unknown;\n}\n\n`
82
+ + `/** Bind the generated, typed collection map to the project's FeltDB instance. */\n`
83
+ + `export function bindGeneratedCollections(database: FeltDBCollectionBinder) {\n return {\n${bindings}\n };\n}\n`;
84
+ }
85
+ export function syncGeneratedSource(review, cwd = process.cwd(), options = {}) {
86
+ const target = path.join(cwd, GENERATED_FILE);
87
+ const statePath = path.join(cwd, SYNC_STATE);
88
+ const state = readJson(statePath) ?? { files: {} };
89
+ if (fs.existsSync(target)) {
90
+ const actual = digest(fs.readFileSync(target));
91
+ const recorded = state.files?.[GENERATED_FILE];
92
+ if ((!recorded || recorded !== actual) && !options.replace) {
93
+ throw new Error(`SOURCE_SYNC_CONFLICT: ${GENERATED_FILE} has developer changes; review it or rerun with --replace-generated`);
94
+ }
95
+ }
96
+ const source = generatedContractSource(review);
97
+ fs.mkdirSync(path.dirname(target), { recursive: true });
98
+ fs.writeFileSync(target, source);
99
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
100
+ fs.writeFileSync(statePath, `${JSON.stringify({ version: 1, contractHash: review.proposedContract.hashes.application, files: { [GENERATED_FILE]: digest(source) } }, null, 2)}\n`, { mode: 0o600 });
101
+ return { written: [GENERATED_FILE] };
102
+ }
103
+ function assertGeneratedSourceSafe(cwd, replace = false) {
104
+ const target = path.join(cwd, GENERATED_FILE);
105
+ if (!fs.existsSync(target) || replace)
106
+ return;
107
+ const state = readJson(path.join(cwd, SYNC_STATE));
108
+ const recorded = state?.files?.[GENERATED_FILE];
109
+ if (!recorded || recorded !== digest(fs.readFileSync(target))) {
110
+ throw new Error(`SOURCE_SYNC_CONFLICT: ${GENERATED_FILE} has developer changes; review it or rerun with --replace-generated`);
111
+ }
112
+ }
113
+ export async function applyFlowProposal(proposal, cwd = process.cwd(), options = {}) {
114
+ const review = await reviewFlowProposal(proposal, cwd);
115
+ const dirty = gitDirty(cwd, [FLOW_FILE, GENERATED_FILE]);
116
+ if (dirty.length && !options.allowDirty)
117
+ throw new Error(`DIRTY_SOURCE: uncommitted changes affect ${dirty.join(', ')}`);
118
+ if (review.authorizationChanges.length && !options.approveAuthorization) {
119
+ throw new Error(`AUTHORIZATION_APPROVAL_REQUIRED: ${review.authorizationChanges.map(change => change.policy).join(', ')}`);
120
+ }
121
+ if (options.syncSource !== false)
122
+ assertGeneratedSourceSafe(cwd, options.replaceGenerated);
123
+ fs.writeFileSync(path.join(cwd, FLOW_FILE), proposal.proposedFlow.replace(/\r\n/g, '\n'));
124
+ const synchronized = options.syncSource === false ? { written: [] } : syncGeneratedSource(review, cwd, { replace: options.replaceGenerated });
125
+ return { ...review, written: [FLOW_FILE, ...synchronized.written] };
126
+ }
127
+ export function describeProposal(review) {
128
+ return [
129
+ ...review.diff.added.map(value => `+ ${value}`),
130
+ ...review.diff.changed.map(value => `~ ${value}`),
131
+ ...review.diff.removed.map(value => `- ${value}`),
132
+ ...review.authorizationChanges.flatMap(value => [
133
+ `AUTHORIZATION CHANGE${value.widening ? ' (possible widening)' : ''}: ${value.policy}`,
134
+ ` before: ${(value.before ?? ['<absent>']).join(' | ')}`,
135
+ ` after: ${(value.after ?? ['<removed>']).join(' | ')}`,
136
+ ]),
137
+ ...review.migration.filter(value => value.safety !== 'safe').map(value => `${value.safety.toUpperCase()}: ${value.target} — ${value.detail}`),
138
+ ];
139
+ }
@@ -308,7 +308,7 @@ Learn more: https://github.com/rkendel1/feltdb`);
308
308
  await createProject({
309
309
  projectName,
310
310
  autoYes: shouldAutoYes,
311
- templatesDir: path.join(__dirname, '../template'),
311
+ templatesDir: path.join(__dirname, 'template'),
312
312
  runtime: options.runtime,
313
313
  framework: options.framework,
314
314
  distributed: options.distributed,
@@ -49,9 +49,12 @@ export async function createProject(options) {
49
49
  test: 'node --test',
50
50
  feltdb: 'feltdb',
51
51
  'feltdb:server': 'feltdb server',
52
- 'feltdb:connect': 'feltdb connect http://localhost:7700',
52
+ 'feltdb:connect': 'feltdb connect',
53
53
  'feltdb:status': 'feltdb status',
54
54
  'feltdb:studio': 'feltdb studio',
55
+ 'feltdb:publish': 'feltdb publish',
56
+ 'feltdb:ai': 'feltdb ai',
57
+ 'feltdb:sync': 'feltdb sync',
55
58
  'feltdb:validate': 'feltdb validate',
56
59
  'feltdb:diff': 'feltdb diff',
57
60
  'feltdb:deploy': 'feltdb deploy',
@@ -176,25 +179,26 @@ ${hasAgents ? ` agent WorkspaceAssistant {
176
179
  lib: ['ES2020', 'DOM'],
177
180
  declaration: true,
178
181
  outDir: './dist',
179
- rootDir: './src',
182
+ rootDir: '.',
180
183
  strict: true,
181
184
  esModuleInterop: true,
182
185
  skipLibCheck: true,
183
186
  forceConsistentCasingInFileNames: true,
184
- moduleResolution: 'node',
187
+ moduleResolution: 'bundler',
185
188
  },
186
- include: ['src/**/*'],
189
+ include: framework === 'react' ? ['src/**/*', 'agents/**/*', 'capabilities/**/*', 'workflows/**/*'] : ['src/**/*'],
187
190
  exclude: ['node_modules'],
188
191
  };
189
192
  if (framework === 'react') {
190
193
  tsconfig.compilerOptions.jsx = 'react-jsx';
194
+ tsconfig.compilerOptions.types = ['vite/client'];
191
195
  }
192
196
  fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
193
197
  // Create main application files
194
198
  const runtimeOptions = runtime === 'browser'
195
199
  ? "{ namespace: import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', browser: true }"
196
200
  : runtime === 'managed'
197
- ? "{ 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 || '' } }"
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' } }"
198
202
  : runtime === 'self-hosted'
199
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 || '' } }"
200
204
  : "{ namespace: import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', memory: true }";
@@ -365,7 +369,7 @@ async function logActivity(event: Omit<ActivityEvent, 'id' | 'timestamp'>): Prom
365
369
  `;
366
370
  fs.writeFileSync(path.join(srcDir, 'feltdb.ts'), feltdbTs);
367
371
  // Create a real local-inference agent for browser projects.
368
- if (hasAgents && hasWebLLM) {
372
+ if (hasAgents && hasWebLLM && framework !== 'react') {
369
373
  const agentTs = `import { WebLLMProvider } from '@feltdb/webllm';
370
374
  import { db, reports } from '../../src/feltdb';
371
375
 
@@ -1185,6 +1189,17 @@ root.render(
1185
1189
  );
1186
1190
  `;
1187
1191
  fs.writeFileSync(path.join(srcDir, 'index.tsx'), indexTsx);
1192
+ const canonicalTemplate = path.join(templatesDir, 'default-project');
1193
+ if (fs.existsSync(canonicalTemplate)) {
1194
+ fs.cpSync(path.join(canonicalTemplate, 'src'), srcDir, { recursive: true });
1195
+ for (const directory of ['agents', 'capabilities', 'workflows']) {
1196
+ fs.cpSync(path.join(canonicalTemplate, directory), path.join(projectDir, directory), { recursive: true });
1197
+ fs.rmSync(path.join(feltdbDir, directory), { recursive: true, force: true });
1198
+ }
1199
+ fs.writeFileSync(path.join(projectDir, 'feltdb.flow'), fs.readFileSync(path.join(canonicalTemplate, 'feltdb.flow'), 'utf8').replace('app FeltDBStarter', `app ${appName}`));
1200
+ const databasePath = path.join(srcDir, 'feltdb.ts');
1201
+ fs.writeFileSync(databasePath, fs.readFileSync(databasePath, 'utf8').replace(/export const managedRuntime[\s\S]*?\n\} : \{ namespace: import\.meta\.env\.VITE_FELTDB_NAMESPACE \|\| 'feltdb-starter', browser: true \}\);/, `export const managedRuntime = ${runtime === 'managed'};\nexport const db = createFeltDB(${runtimeOptions});`));
1202
+ }
1188
1203
  }
1189
1204
  else {
1190
1205
  // Create vanilla JS app
@@ -1229,6 +1244,8 @@ VITE_FELTDB_URL=http://localhost:7700
1229
1244
  # Managed example: https://runtime.your-app.feltdb.com
1230
1245
  VITE_FELTDB_MANAGED_URL=
1231
1246
  VITE_FELTDB_MANAGED_API_KEY=
1247
+ # Server/control-plane only. Never rename with a VITE_ prefix.
1248
+ FELTDB_MANAGED_CONTROL_API_KEY=
1232
1249
  VITE_FELTDB_MANAGED_TENANT_ID=
1233
1250
  VITE_FELTDB_MANAGED_APPLICATION_ID=
1234
1251
  VITE_FELTDB_MANAGED_NAMESPACE=
@@ -1524,6 +1541,7 @@ build/
1524
1541
  *.log
1525
1542
  .DS_Store
1526
1543
  .feltdb/
1544
+ .feltdb-data/
1527
1545
  `;
1528
1546
  fs.writeFileSync(path.join(projectDir, '.gitignore'), gitignore);
1529
1547
  // Create README
@@ -1795,6 +1813,12 @@ If port 5173 is in use:
1795
1813
  MIT
1796
1814
  `;
1797
1815
  fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
1816
+ if (framework === 'react') {
1817
+ const canonicalReadme = path.join(templatesDir, 'default-project', 'README.md');
1818
+ if (fs.existsSync(canonicalReadme)) {
1819
+ fs.writeFileSync(path.join(projectDir, 'README.md'), fs.readFileSync(canonicalReadme, 'utf8').replace('# My FeltDB App', `# ${applicationName}`));
1820
+ }
1821
+ }
1798
1822
  // Initialize Development Workspace
1799
1823
  // This creates .feltdb/workspace.json which enables all FeltDB-aware tools
1800
1824
  // (CLI, IDE, agents, browser extensions) to discover and connect to the
@@ -43,20 +43,27 @@ export async function configureManagedAccount(options) {
43
43
  // Provision through the account origin that issued the human session. The
44
44
  // resulting key is still used against apiUrl, but setup must not require a
45
45
  // browser session to be trusted by every managed data-plane hostname.
46
- const key = await responseJson(await request(`${sessionOrigin}/api/keys`, {
46
+ const controlKey = await responseJson(await request(`${sessionOrigin}/api/keys`, {
47
47
  method: 'POST', headers,
48
48
  body: JSON.stringify({
49
- id: `${options.applicationName}-cli`,
49
+ id: `${options.applicationName}-control`,
50
50
  namespace: options.namespace,
51
- scopes: ['state:read', 'state:write', 'events:read'],
51
+ scopes: [
52
+ 'application:read', 'application:write', 'application:revision:create',
53
+ 'application:revision:read', 'application:revision:promote',
54
+ 'application:environment:read', 'application:environment:write',
55
+ ],
52
56
  }),
53
- }), 'Could not create the managed application key');
54
- if (!key.secret)
55
- throw new Error('Managed key service did not return an application secret');
57
+ }), 'Could not create the managed control-plane key');
58
+ if (!controlKey.secret)
59
+ throw new Error('Managed key service did not return the control-plane credential');
56
60
  const environment = [
57
61
  '# Generated by create-feltdb managed setup. Do not commit this file.',
58
62
  `VITE_FELTDB_MANAGED_URL=${apiUrl}`,
59
- `VITE_FELTDB_MANAGED_API_KEY=${key.secret}`,
63
+ '# Browser authentication uses short-lived actor sessions; no long-lived data key is embedded.',
64
+ 'VITE_FELTDB_MANAGED_API_KEY=',
65
+ '# Control-plane credential. Never prefix this with VITE_ or expose it to browser code.',
66
+ `FELTDB_MANAGED_CONTROL_API_KEY=${controlKey.secret}`,
60
67
  `VITE_FELTDB_MANAGED_TENANT_ID=${tenant.id}`,
61
68
  `VITE_FELTDB_MANAGED_APPLICATION_ID=${application.id}`,
62
69
  `VITE_FELTDB_MANAGED_NAMESPACE=${application.namespace || options.namespace}`,
@@ -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.3';
3
+ export const FELTDB_PACKAGE_VERSION = '0.7.4';
4
4
  export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
@@ -132,7 +132,10 @@ pub struct CapabilityBinding {
132
132
  pub name: String,
133
133
  #[serde(default)]
134
134
  pub provider: Option<String>,
135
+ #[serde(default = "public_capability_visibility")]
136
+ pub visibility: String,
135
137
  }
138
+ fn public_capability_visibility() -> String { "public".into() }
136
139
  #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
137
140
  pub struct SecretReference {
138
141
  pub binding: String,
@@ -368,7 +371,14 @@ pub struct ValidationReport {
368
371
  }
369
372
 
370
373
  fn is_valid_policy_subject(subject: &str) -> bool {
371
- matches!(subject, "authenticated" | "owner" | "member")
374
+ subject.split('|').map(str::trim).all(|term| {
375
+ matches!(term, "authenticated" | "owner" | "member")
376
+ || term.strip_prefix("self(").and_then(|value| value.strip_suffix(')')).is_some_and(|field| {
377
+ let mut characters = field.chars();
378
+ characters.next().is_some_and(|value| value == '_' || value.is_ascii_alphabetic())
379
+ && characters.all(|value| value == '_' || value.is_ascii_alphanumeric())
380
+ })
381
+ })
372
382
  }
373
383
 
374
384
  fn unique(
@@ -540,6 +550,11 @@ pub fn validate_manifest(
540
550
  .iter()
541
551
  .map(|v| v.name.as_str())
542
552
  .collect();
553
+ for capability in &manifest.capabilities {
554
+ if !matches!(capability.visibility.as_str(), "public" | "agent" | "workflow" | "internal") {
555
+ issues.push(issue("semantic", format!("capabilities.{}.visibility", capability.name), "visibility must be public, agent, workflow, or internal"));
556
+ }
557
+ }
543
558
  let workflows: BTreeSet<_> = manifest.workflows.iter().map(|v| v.name.as_str()).collect();
544
559
  let queries: BTreeSet<_> = manifest.queries.iter().map(|v| v.name.as_str()).collect();
545
560
  let bindings: BTreeSet<_> = manifest
@@ -2468,6 +2483,21 @@ mod tests {
2468
2483
  );
2469
2484
  }
2470
2485
 
2486
+ #[test]
2487
+ fn policy_with_identity_and_membership_composition_is_valid() {
2488
+ let mut manifest = ApplicationManifest::empty("tenant", "app", "test");
2489
+ manifest.policies.push(PolicyDefinition {
2490
+ name: "InvitationPolicy".into(),
2491
+ resource: "Invitation".into(),
2492
+ read: Some("self(user) | member".into()),
2493
+ write: Some("member".into()),
2494
+ capabilities: vec![],
2495
+ });
2496
+
2497
+ let report = validate_manifest(&manifest, "tenant", "app", None);
2498
+ assert!(report.valid, "composed identity policy should be valid: {:?}", report.issues);
2499
+ }
2500
+
2471
2501
  #[test]
2472
2502
  fn policy_with_invalid_subject_rejected() {
2473
2503
  let mut manifest = ApplicationManifest::empty("tenant", "app", "test");