create-feltdb 0.5.7 → 0.6.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 +1 -1
- package/dist/create.js +21 -0
- package/dist/package-versions.js +1 -1
- package/dist/server-source/crates/feltdb-server/src/main.rs +95 -4
- package/dist/server-source/crates/feltdb-server/src/tenancy.rs +49 -0
- package/dist/template/dot-feltdb-README.md +58 -0
- package/dist/workspace-initialization.js +77 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -307,7 +307,7 @@ Learn more: https://github.com/rkendel1/feltdb`);
|
|
|
307
307
|
await createProject({
|
|
308
308
|
projectName,
|
|
309
309
|
autoYes: shouldAutoYes,
|
|
310
|
-
templatesDir: path.join(__dirname, '../
|
|
310
|
+
templatesDir: path.join(__dirname, '../template'),
|
|
311
311
|
runtime: options.runtime,
|
|
312
312
|
framework: options.framework,
|
|
313
313
|
distributed: options.distributed,
|
package/dist/create.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import { randomBytes } from 'crypto';
|
|
7
7
|
import { feltdbPackageRange } from './package-versions.js';
|
|
8
8
|
import { generateDockerCompose, generateDockerfile, generateDockerIgnore, generateDotEnvLocal, generateFeltDBDockerfile, generateStudioDockerfile, } from './docker-compose-generator.js';
|
|
9
|
+
import { initializeWorkspace, appendWorkspaceGitignore, } from './workspace-initialization.js';
|
|
9
10
|
export async function createProject(options) {
|
|
10
11
|
const { projectName, templatesDir } = options;
|
|
11
12
|
const projectDir = path.resolve(process.cwd(), projectName);
|
|
@@ -1759,4 +1760,24 @@ If port 5173 is in use:
|
|
|
1759
1760
|
MIT
|
|
1760
1761
|
`;
|
|
1761
1762
|
fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
|
|
1763
|
+
// Initialize Development Workspace
|
|
1764
|
+
// This creates .feltdb/workspace.json which enables all FeltDB-aware tools
|
|
1765
|
+
// (CLI, IDE, agents, browser extensions) to discover and connect to the
|
|
1766
|
+
// same workspace without manual configuration.
|
|
1767
|
+
const workspaceDiscovery = initializeWorkspace(projectDir, applicationName);
|
|
1768
|
+
appendWorkspaceGitignore(projectDir);
|
|
1769
|
+
// Copy .feltdb README from template
|
|
1770
|
+
const templateReadmePath = path.join(templatesDir, 'dot-feltdb-README.md');
|
|
1771
|
+
const targetReadmePath = path.join(projectDir, '.feltdb', 'README.md');
|
|
1772
|
+
if (fs.existsSync(templateReadmePath)) {
|
|
1773
|
+
fs.copyFileSync(templateReadmePath, targetReadmePath);
|
|
1774
|
+
}
|
|
1775
|
+
console.log(`\n✓ Development Workspace initialized`);
|
|
1776
|
+
console.log(` Workspace ID: ${workspaceDiscovery.workspaceId}`);
|
|
1777
|
+
console.log(` Location: ${path.join(projectName, '.feltdb/workspace.json')}`);
|
|
1778
|
+
console.log(`\nNext steps:`);
|
|
1779
|
+
console.log(` cd ${projectName}`);
|
|
1780
|
+
console.log(` npm install`);
|
|
1781
|
+
console.log(` npm run dev`);
|
|
1782
|
+
console.log(`\nYour Development Workspace is ready for use!`);
|
|
1762
1783
|
}
|
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.6.1';
|
|
4
4
|
export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
|
|
@@ -104,12 +104,18 @@ struct ApiError(StatusCode, String);
|
|
|
104
104
|
|
|
105
105
|
impl IntoResponse for ApiError {
|
|
106
106
|
fn into_response(self) -> Response {
|
|
107
|
-
let body =
|
|
108
|
-
serde_json::from_str::<Value>(&self.1).unwrap_or_else(|_| json!({ "error": self.1 }));
|
|
107
|
+
let body = api_error_body(self.0, self.1);
|
|
109
108
|
(self.0, Json(body)).into_response()
|
|
110
109
|
}
|
|
111
110
|
}
|
|
112
111
|
|
|
112
|
+
fn api_error_body(status: StatusCode, message: String) -> Value {
|
|
113
|
+
if status == StatusCode::FORBIDDEN {
|
|
114
|
+
return json!({ "error": "AUTHORIZATION_DENIED" });
|
|
115
|
+
}
|
|
116
|
+
serde_json::from_str::<Value>(&message).unwrap_or_else(|_| json!({ "error": message }))
|
|
117
|
+
}
|
|
118
|
+
|
|
113
119
|
impl From<feltdb::FlowError> for ApiError {
|
|
114
120
|
fn from(error: feltdb::FlowError) -> Self {
|
|
115
121
|
Self(StatusCode::INTERNAL_SERVER_ERROR, error.to_string())
|
|
@@ -2199,6 +2205,38 @@ async fn delete_application_control(
|
|
|
2199
2205
|
.map_err(control_error)?;
|
|
2200
2206
|
Ok(StatusCode::NO_CONTENT)
|
|
2201
2207
|
}
|
|
2208
|
+
|
|
2209
|
+
async fn delete_certification_fixture(
|
|
2210
|
+
State(state): State<AppState>,
|
|
2211
|
+
Path(application_id): Path<String>,
|
|
2212
|
+
headers: HeaderMap,
|
|
2213
|
+
) -> Result<Json<Value>, ApiError> {
|
|
2214
|
+
let expected = std::env::var("FELTDB_CERTIFICATION_RECOVERY_TOKEN").map_err(|_| {
|
|
2215
|
+
ApiError(
|
|
2216
|
+
StatusCode::SERVICE_UNAVAILABLE,
|
|
2217
|
+
"certification recovery is not configured".into(),
|
|
2218
|
+
)
|
|
2219
|
+
})?;
|
|
2220
|
+
let provided = headers
|
|
2221
|
+
.get("x-feltdb-certification-recovery")
|
|
2222
|
+
.and_then(|value| value.to_str().ok())
|
|
2223
|
+
.unwrap_or_default();
|
|
2224
|
+
if expected.len() < 32
|
|
2225
|
+
|| ring::constant_time::verify_slices_are_equal(expected.as_bytes(), provided.as_bytes())
|
|
2226
|
+
.is_err()
|
|
2227
|
+
{
|
|
2228
|
+
return Err(ApiError(StatusCode::FORBIDDEN, "certification recovery denied".into()));
|
|
2229
|
+
}
|
|
2230
|
+
let tenant_id = state
|
|
2231
|
+
.tenancy
|
|
2232
|
+
.delete_certification_fixture(&application_id)
|
|
2233
|
+
.map_err(control_error)?;
|
|
2234
|
+
Ok(Json(json!({
|
|
2235
|
+
"deleted": true,
|
|
2236
|
+
"tenant_id": tenant_id,
|
|
2237
|
+
"application_id": application_id,
|
|
2238
|
+
})))
|
|
2239
|
+
}
|
|
2202
2240
|
async fn update_application_control(
|
|
2203
2241
|
State(state): State<AppState>,
|
|
2204
2242
|
Extension(principal): Extension<Principal>,
|
|
@@ -3814,7 +3852,36 @@ async fn commit_application_draft(
|
|
|
3814
3852
|
.applications
|
|
3815
3853
|
.commit(&tenant, &application_id, &draft_id, &principal.key_id)
|
|
3816
3854
|
.map_err(manifest_error)?;
|
|
3817
|
-
|
|
3855
|
+
let encoded_revision = percent_encode_path_segment(&revision.revision_id);
|
|
3856
|
+
let mut response = serde_json::to_value(revision).map_err(|error| {
|
|
3857
|
+
ApiError(StatusCode::INTERNAL_SERVER_ERROR, error.to_string())
|
|
3858
|
+
})?;
|
|
3859
|
+
response
|
|
3860
|
+
.as_object_mut()
|
|
3861
|
+
.expect("application revisions serialize as objects")
|
|
3862
|
+
.insert(
|
|
3863
|
+
"promotion".into(),
|
|
3864
|
+
json!({
|
|
3865
|
+
"status": "ready",
|
|
3866
|
+
"url": format!(
|
|
3867
|
+
"/api/applications/{application_id}/revisions/{encoded_revision}/promote"
|
|
3868
|
+
),
|
|
3869
|
+
}),
|
|
3870
|
+
);
|
|
3871
|
+
Ok((StatusCode::CREATED, Json(response)))
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
fn percent_encode_path_segment(value: &str) -> String {
|
|
3875
|
+
value
|
|
3876
|
+
.bytes()
|
|
3877
|
+
.flat_map(|byte| {
|
|
3878
|
+
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
|
|
3879
|
+
vec![byte as char]
|
|
3880
|
+
} else {
|
|
3881
|
+
format!("%{byte:02X}").chars().collect()
|
|
3882
|
+
}
|
|
3883
|
+
})
|
|
3884
|
+
.collect()
|
|
3818
3885
|
}
|
|
3819
3886
|
async fn list_application_revisions(
|
|
3820
3887
|
State(state): State<AppState>,
|
|
@@ -5789,6 +5856,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
5789
5856
|
axum::routing::patch(platform_update_application_member),
|
|
5790
5857
|
)
|
|
5791
5858
|
.route("/api/tenants/{tenant_id}", get(get_tenant))
|
|
5859
|
+
.route(
|
|
5860
|
+
"/api/certification/fixtures/{application_id}",
|
|
5861
|
+
axum::routing::delete(delete_certification_fixture),
|
|
5862
|
+
)
|
|
5792
5863
|
.route("/api/keys", get(list_keys).post(create_key))
|
|
5793
5864
|
.route("/api/keys/{id}", axum::routing::delete(revoke_key))
|
|
5794
5865
|
.route(
|
|
@@ -6314,6 +6385,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
6314
6385
|
.route("/ready", get(readiness))
|
|
6315
6386
|
.route("/runtime", get(runtime))
|
|
6316
6387
|
.route("/metrics", get(network_metrics))
|
|
6388
|
+
.route(
|
|
6389
|
+
"/api/certification/fixtures/{application_id}",
|
|
6390
|
+
axum::routing::delete(delete_certification_fixture),
|
|
6391
|
+
)
|
|
6317
6392
|
.merge(protected)
|
|
6318
6393
|
.layer(middleware::from_fn(protocol_version))
|
|
6319
6394
|
.layer(middleware::from_fn_with_state(state.clone(), count_request))
|
|
@@ -9171,7 +9246,8 @@ async fn shutdown_signal() {
|
|
|
9171
9246
|
|
|
9172
9247
|
#[cfg(test)]
|
|
9173
9248
|
mod authority_gate_tests {
|
|
9174
|
-
use super::{authorize_transaction_collections, state_authorization};
|
|
9249
|
+
use super::{api_error_body, authorize_transaction_collections, state_authorization};
|
|
9250
|
+
use axum::http::StatusCode;
|
|
9175
9251
|
use feltdb::{
|
|
9176
9252
|
application::{manifest_hash, ApplicationManifest, ApplicationRevision, PolicyDefinition, RevisionStatus},
|
|
9177
9253
|
application_runtime::{resolve_runtime, ApplicationRuntimeContract, RuntimeInventory},
|
|
@@ -9256,4 +9332,19 @@ mod authority_gate_tests {
|
|
|
9256
9332
|
);
|
|
9257
9333
|
assert!(!allowed);
|
|
9258
9334
|
}
|
|
9335
|
+
|
|
9336
|
+
#[test]
|
|
9337
|
+
fn every_forbidden_error_uses_the_canonical_wire_contract() {
|
|
9338
|
+
for internal_message in [
|
|
9339
|
+
"forbidden",
|
|
9340
|
+
"tenant access denied",
|
|
9341
|
+
"missing scope: state:read",
|
|
9342
|
+
r#"{"code":"AUTHORIZATION_DENIED","message":"policy denied"}"#,
|
|
9343
|
+
] {
|
|
9344
|
+
assert_eq!(
|
|
9345
|
+
api_error_body(StatusCode::FORBIDDEN, internal_message.into()),
|
|
9346
|
+
serde_json::json!({ "error": "AUTHORIZATION_DENIED" })
|
|
9347
|
+
);
|
|
9348
|
+
}
|
|
9349
|
+
}
|
|
9259
9350
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
use std::{
|
|
2
|
+
collections::BTreeSet,
|
|
2
3
|
fs,
|
|
3
4
|
path::PathBuf,
|
|
4
5
|
sync::{Arc, RwLock},
|
|
@@ -578,6 +579,54 @@ impl TenancyStore {
|
|
|
578
579
|
Ok(tenant)
|
|
579
580
|
}
|
|
580
581
|
|
|
582
|
+
/// Removes an isolated certification tenant selected by an exact application ID.
|
|
583
|
+
/// This deliberately refuses non-certification resources and avoids global inventory access.
|
|
584
|
+
pub fn delete_certification_fixture(&self, application_id: &str) -> Result<String, String> {
|
|
585
|
+
let mut records = self
|
|
586
|
+
.records
|
|
587
|
+
.write()
|
|
588
|
+
.map_err(|_| "tenancy store lock poisoned")?;
|
|
589
|
+
let application = records
|
|
590
|
+
.applications
|
|
591
|
+
.iter()
|
|
592
|
+
.find(|application| application.id == application_id)
|
|
593
|
+
.cloned()
|
|
594
|
+
.ok_or("application not found")?;
|
|
595
|
+
let tenant = records
|
|
596
|
+
.tenants
|
|
597
|
+
.iter()
|
|
598
|
+
.find(|tenant| tenant.id == application.tenant_id)
|
|
599
|
+
.cloned()
|
|
600
|
+
.ok_or("tenant not found")?;
|
|
601
|
+
if !application.name.to_lowercase().contains("certification")
|
|
602
|
+
|| !tenant.name.to_lowercase().contains("certification")
|
|
603
|
+
{
|
|
604
|
+
return Err("refusing to delete a non-certification fixture".into());
|
|
605
|
+
}
|
|
606
|
+
let tenant_id = tenant.id.clone();
|
|
607
|
+
let application_ids = records
|
|
608
|
+
.applications
|
|
609
|
+
.iter()
|
|
610
|
+
.filter(|candidate| candidate.tenant_id == tenant_id)
|
|
611
|
+
.map(|candidate| candidate.id.clone())
|
|
612
|
+
.collect::<BTreeSet<_>>();
|
|
613
|
+
records
|
|
614
|
+
.applications
|
|
615
|
+
.retain(|candidate| candidate.tenant_id != tenant_id);
|
|
616
|
+
records
|
|
617
|
+
.application_memberships
|
|
618
|
+
.retain(|membership| !application_ids.contains(&membership.application_id));
|
|
619
|
+
records
|
|
620
|
+
.tenant_memberships
|
|
621
|
+
.retain(|membership| membership.tenant_id != tenant_id);
|
|
622
|
+
records
|
|
623
|
+
.invitations
|
|
624
|
+
.retain(|invitation| invitation.tenant_id != tenant_id);
|
|
625
|
+
records.tenants.retain(|candidate| candidate.id != tenant_id);
|
|
626
|
+
self.persist(&records)?;
|
|
627
|
+
Ok(tenant_id)
|
|
628
|
+
}
|
|
629
|
+
|
|
581
630
|
pub fn create_application(
|
|
582
631
|
&self,
|
|
583
632
|
actor: &str,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# FeltDB Development Workspace
|
|
2
|
+
|
|
3
|
+
This directory contains the development workspace configuration for your FeltDB project.
|
|
4
|
+
|
|
5
|
+
## workspace.json
|
|
6
|
+
|
|
7
|
+
**Purpose:** Durable pairing identity for your project's development workspace.
|
|
8
|
+
|
|
9
|
+
**Contents:**
|
|
10
|
+
- `workspaceId` — Unique identifier for this workspace (format: `ws_*`)
|
|
11
|
+
- `projectId` — Your project name
|
|
12
|
+
- `version` — Configuration version
|
|
13
|
+
|
|
14
|
+
**Usage:**
|
|
15
|
+
- Committed to version control (this is your project's identity)
|
|
16
|
+
- Used by CLI, IDE, agents, and browser extensions to discover the workspace
|
|
17
|
+
- DO NOT edit manually unless you know what you're doing
|
|
18
|
+
|
|
19
|
+
**Discovery:**
|
|
20
|
+
When any FeltDB tool (VS Code, Claude agent, CLI, browser extension) opens your project, it reads this file to automatically connect to the correct workspace.
|
|
21
|
+
|
|
22
|
+
## Runtime Files (not committed)
|
|
23
|
+
|
|
24
|
+
- `pairing.json` — Short-lived pairing token for browser discovery (gitignored)
|
|
25
|
+
- `state.json` — Local workspace state (gitignored)
|
|
26
|
+
- `*.log` — Development logs (gitignored)
|
|
27
|
+
|
|
28
|
+
## Commands
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Check workspace status
|
|
32
|
+
npm run feltdb:status
|
|
33
|
+
|
|
34
|
+
# Or use the CLI directly
|
|
35
|
+
feltdb workspace status
|
|
36
|
+
|
|
37
|
+
# Launch development environment (includes workspace authority)
|
|
38
|
+
npm run dev
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Development Workspace Architecture
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Your Project
|
|
45
|
+
↓
|
|
46
|
+
.feltdb/workspace.json (pairing identity)
|
|
47
|
+
↓
|
|
48
|
+
feltdb dev (workspace authority)
|
|
49
|
+
↓
|
|
50
|
+
Browser, IDE, Agent clients
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
All tools automatically discover your workspace using `workspace.json` and connect to the local development environment started by `npm run dev`.
|
|
54
|
+
|
|
55
|
+
## Learn More
|
|
56
|
+
|
|
57
|
+
- [Development Workspaces Documentation](https://github.com/rkendel1/feltdb/blob/main/packages/core/docs/development-workspaces.md)
|
|
58
|
+
- [FeltDB Documentation](https://github.com/rkendel1/feltdb)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Initialization
|
|
3
|
+
*
|
|
4
|
+
* Initializes a FeltDB Development Workspace for newly created projects.
|
|
5
|
+
* This is the bootstrap mechanism that enables all FeltDB-aware tools
|
|
6
|
+
* to discover and connect to the same workspace.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
/**
|
|
11
|
+
* Generate a unique workspace ID
|
|
12
|
+
* Format: ws_<projectId>_<timestamp>_<random>
|
|
13
|
+
*/
|
|
14
|
+
export function generateWorkspaceId(projectId) {
|
|
15
|
+
const timestamp = Date.now();
|
|
16
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
17
|
+
return `ws_${projectId}_${timestamp}_${random}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Initialize development workspace for a new project
|
|
21
|
+
*
|
|
22
|
+
* Creates .feltdb/workspace.json with the workspace discovery information.
|
|
23
|
+
* This file is committed to the repository and used by all development tools
|
|
24
|
+
* (CLI, IDE, agents, browser extensions) to discover and connect to the
|
|
25
|
+
* same workspace.
|
|
26
|
+
*/
|
|
27
|
+
export function initializeWorkspace(projectDir, projectId) {
|
|
28
|
+
const feltdbDir = path.join(projectDir, '.feltdb');
|
|
29
|
+
// Ensure .feltdb directory exists
|
|
30
|
+
if (!fs.existsSync(feltdbDir)) {
|
|
31
|
+
fs.mkdirSync(feltdbDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
// Create workspace discovery
|
|
34
|
+
const workspaceId = generateWorkspaceId(projectId);
|
|
35
|
+
const discovery = {
|
|
36
|
+
workspaceId,
|
|
37
|
+
projectId,
|
|
38
|
+
version: 1,
|
|
39
|
+
};
|
|
40
|
+
// Write workspace.json
|
|
41
|
+
const workspacePath = path.join(feltdbDir, 'workspace.json');
|
|
42
|
+
fs.writeFileSync(workspacePath, JSON.stringify(discovery, null, 2) + '\n');
|
|
43
|
+
return discovery;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create .gitignore entry for workspace runtime files
|
|
47
|
+
*
|
|
48
|
+
* The .feltdb/workspace.json is committed (pairing identity).
|
|
49
|
+
* But runtime files like pairing tokens should not be committed.
|
|
50
|
+
*/
|
|
51
|
+
export function appendWorkspaceGitignore(projectDir) {
|
|
52
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
53
|
+
const workspaceGitignoreEntries = [
|
|
54
|
+
'',
|
|
55
|
+
'# FeltDB Development Workspace',
|
|
56
|
+
'.feltdb/pairing.json',
|
|
57
|
+
'.feltdb/state.json',
|
|
58
|
+
'.feltdb/*.log',
|
|
59
|
+
'',
|
|
60
|
+
].join('\n');
|
|
61
|
+
if (fs.existsSync(gitignorePath)) {
|
|
62
|
+
const existing = fs.readFileSync(gitignorePath, 'utf-8');
|
|
63
|
+
if (!existing.includes('.feltdb/pairing.json')) {
|
|
64
|
+
fs.appendFileSync(gitignorePath, workspaceGitignoreEntries);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const content = [
|
|
69
|
+
'node_modules/',
|
|
70
|
+
'dist/',
|
|
71
|
+
'build/',
|
|
72
|
+
'.env.local',
|
|
73
|
+
workspaceGitignoreEntries,
|
|
74
|
+
].join('\n');
|
|
75
|
+
fs.writeFileSync(gitignorePath, content);
|
|
76
|
+
}
|
|
77
|
+
}
|