create-feltdb 0.5.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/create.js +21 -0
- package/dist/package-versions.js +1 -1
- package/dist/server-source/Cargo.lock +10 -0
- package/dist/server-source/crates/feltdb-server/Cargo.toml +1 -0
- package/dist/server-source/crates/feltdb-server/src/app_state.rs +5 -1
- package/dist/server-source/crates/feltdb-server/src/authenticated_principal.rs +273 -0
- package/dist/server-source/crates/feltdb-server/src/certification_harness.rs +528 -0
- package/dist/server-source/crates/feltdb-server/src/delegation_token.rs +472 -0
- package/dist/server-source/crates/feltdb-server/src/durable_operations.rs +992 -0
- package/dist/server-source/crates/feltdb-server/src/lib.rs +9 -0
- package/dist/server-source/crates/feltdb-server/src/main.rs +157 -9
- package/dist/server-source/crates/feltdb-server/src/managed_diagnostics.rs +413 -0
- package/dist/server-source/crates/feltdb-server/src/membership_policy.rs +488 -0
- package/dist/server-source/crates/feltdb-server/src/snapshot_cursor.rs +378 -0
- package/dist/server-source/crates/feltdb-server/src/tenancy.rs +49 -0
- package/dist/server-source/crates/feltdb-server/src/tenant_policies.rs +525 -0
- package/dist/server-source/crates/feltdb-server/src/transaction_recovery.rs +461 -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.0';
|
|
4
4
|
export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
|
|
@@ -513,6 +513,7 @@ dependencies = [
|
|
|
513
513
|
"base64",
|
|
514
514
|
"feltdb",
|
|
515
515
|
"futures-core",
|
|
516
|
+
"hmac",
|
|
516
517
|
"password-hash",
|
|
517
518
|
"rand 0.8.7",
|
|
518
519
|
"reqwest",
|
|
@@ -668,6 +669,15 @@ version = "0.5.2"
|
|
|
668
669
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
669
670
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
|
670
671
|
|
|
672
|
+
[[package]]
|
|
673
|
+
name = "hmac"
|
|
674
|
+
version = "0.12.1"
|
|
675
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
676
|
+
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
|
677
|
+
dependencies = [
|
|
678
|
+
"digest",
|
|
679
|
+
]
|
|
680
|
+
|
|
671
681
|
[[package]]
|
|
672
682
|
name = "http"
|
|
673
683
|
version = "1.5.0"
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
use std::sync::{atomic::AtomicU64, Arc};
|
|
2
1
|
use std::path::PathBuf;
|
|
2
|
+
use std::{
|
|
3
|
+
sync::{atomic::AtomicU64, Arc},
|
|
4
|
+
time::Instant,
|
|
5
|
+
};
|
|
3
6
|
|
|
4
7
|
use feltdb::{
|
|
5
8
|
FeltDb,
|
|
@@ -33,6 +36,7 @@ use crate::{
|
|
|
33
36
|
|
|
34
37
|
#[derive(Clone)]
|
|
35
38
|
pub struct AppState {
|
|
39
|
+
pub started_at: Instant,
|
|
36
40
|
pub db: FeltDb,
|
|
37
41
|
pub namespace: Arc<str>,
|
|
38
42
|
pub ids: Arc<AtomicU64>,
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
3
|
+
|
|
4
|
+
/// Authenticated principal derived exclusively from verified credentials.
|
|
5
|
+
/// Never derived from request body or untrusted sources.
|
|
6
|
+
/// Available to authorization evaluation and included in request context.
|
|
7
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
8
|
+
pub struct AuthenticatedPrincipal {
|
|
9
|
+
/// ID of the service key used for authentication.
|
|
10
|
+
/// Preserved separately from actorId for auditing.
|
|
11
|
+
pub service_key_id: String,
|
|
12
|
+
|
|
13
|
+
/// ID of the actor (user, service, workload, etc.) performing the operation.
|
|
14
|
+
pub actor_id: String,
|
|
15
|
+
|
|
16
|
+
/// Tenant ID from authenticated credentials.
|
|
17
|
+
pub tenant_id: String,
|
|
18
|
+
|
|
19
|
+
/// Roles granted to this principal.
|
|
20
|
+
pub roles: Vec<String>,
|
|
21
|
+
|
|
22
|
+
/// Authentication method used.
|
|
23
|
+
pub auth_method: AuthMethod,
|
|
24
|
+
|
|
25
|
+
/// When this principal was authenticated.
|
|
26
|
+
pub authenticated_at: u64,
|
|
27
|
+
|
|
28
|
+
/// Provenance tracking for audit.
|
|
29
|
+
pub provenance: AuthenticationProvenance,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
33
|
+
#[serde(rename_all = "lowercase")]
|
|
34
|
+
pub enum AuthMethod {
|
|
35
|
+
ApiKey,
|
|
36
|
+
Session,
|
|
37
|
+
Delegation,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
41
|
+
pub struct AuthenticationProvenance {
|
|
42
|
+
/// Source of the credential (e.g., "bearer_token", "session_cookie", "delegation_token").
|
|
43
|
+
pub credential_source: String,
|
|
44
|
+
|
|
45
|
+
/// Hash of the credential for audit purposes (never the credential itself).
|
|
46
|
+
pub credential_hash: Option<String>,
|
|
47
|
+
|
|
48
|
+
/// Additional context about the authentication.
|
|
49
|
+
pub context: String,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl AuthenticatedPrincipal {
|
|
53
|
+
/// Creates a new AuthenticatedPrincipal from API key authentication.
|
|
54
|
+
pub fn from_api_key(
|
|
55
|
+
service_key_id: String,
|
|
56
|
+
actor_id: String,
|
|
57
|
+
tenant_id: String,
|
|
58
|
+
roles: Vec<String>,
|
|
59
|
+
) -> Self {
|
|
60
|
+
Self {
|
|
61
|
+
service_key_id,
|
|
62
|
+
actor_id,
|
|
63
|
+
tenant_id,
|
|
64
|
+
roles,
|
|
65
|
+
auth_method: AuthMethod::ApiKey,
|
|
66
|
+
authenticated_at: now(),
|
|
67
|
+
provenance: AuthenticationProvenance {
|
|
68
|
+
credential_source: "bearer_token".into(),
|
|
69
|
+
credential_hash: None,
|
|
70
|
+
context: "API key authentication".into(),
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Creates a new AuthenticatedPrincipal from session authentication.
|
|
76
|
+
pub fn from_session(
|
|
77
|
+
service_key_id: String,
|
|
78
|
+
actor_id: String,
|
|
79
|
+
tenant_id: String,
|
|
80
|
+
roles: Vec<String>,
|
|
81
|
+
) -> Self {
|
|
82
|
+
Self {
|
|
83
|
+
service_key_id,
|
|
84
|
+
actor_id,
|
|
85
|
+
tenant_id,
|
|
86
|
+
roles,
|
|
87
|
+
auth_method: AuthMethod::Session,
|
|
88
|
+
authenticated_at: now(),
|
|
89
|
+
provenance: AuthenticationProvenance {
|
|
90
|
+
credential_source: "session_cookie".into(),
|
|
91
|
+
credential_hash: None,
|
|
92
|
+
context: "Session authentication".into(),
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Creates a new AuthenticatedPrincipal from delegation token.
|
|
98
|
+
pub fn from_delegation(
|
|
99
|
+
service_key_id: String,
|
|
100
|
+
actor_id: String,
|
|
101
|
+
tenant_id: String,
|
|
102
|
+
roles: Vec<String>,
|
|
103
|
+
delegation_context: String,
|
|
104
|
+
) -> Self {
|
|
105
|
+
Self {
|
|
106
|
+
service_key_id,
|
|
107
|
+
actor_id,
|
|
108
|
+
tenant_id,
|
|
109
|
+
roles,
|
|
110
|
+
auth_method: AuthMethod::Delegation,
|
|
111
|
+
authenticated_at: now(),
|
|
112
|
+
provenance: AuthenticationProvenance {
|
|
113
|
+
credential_source: "delegation_token".into(),
|
|
114
|
+
credential_hash: None,
|
|
115
|
+
context: delegation_context,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Checks if this principal has a specific role.
|
|
121
|
+
pub fn has_role(&self, role: &str) -> bool {
|
|
122
|
+
self.roles.iter().any(|r| r == role || r == "*")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Checks if actor_id matches expected value, rejecting mismatches.
|
|
126
|
+
pub fn verify_actor_id(&self, expected: &str) -> Result<(), String> {
|
|
127
|
+
if self.actor_id != expected {
|
|
128
|
+
Err(format!(
|
|
129
|
+
"actor_id mismatch: authenticated as {} but operation requested {}",
|
|
130
|
+
self.actor_id, expected
|
|
131
|
+
))
|
|
132
|
+
} else {
|
|
133
|
+
Ok(())
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Checks if tenant_id matches expected value, rejecting mismatches.
|
|
138
|
+
pub fn verify_tenant_id(&self, expected: &str) -> Result<(), String> {
|
|
139
|
+
if self.tenant_id != expected {
|
|
140
|
+
Err(format!(
|
|
141
|
+
"tenant_id mismatch: authenticated for {} but operation requested {}",
|
|
142
|
+
self.tenant_id, expected
|
|
143
|
+
))
|
|
144
|
+
} else {
|
|
145
|
+
Ok(())
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn now() -> u64 {
|
|
151
|
+
SystemTime::now()
|
|
152
|
+
.duration_since(UNIX_EPOCH)
|
|
153
|
+
.unwrap_or_default()
|
|
154
|
+
.as_secs()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
#[cfg(test)]
|
|
159
|
+
mod tests {
|
|
160
|
+
use super::*;
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn authenticated_principal_preserves_separation() {
|
|
164
|
+
let principal = AuthenticatedPrincipal::from_api_key(
|
|
165
|
+
"key_abc123".into(),
|
|
166
|
+
"user_xyz".into(),
|
|
167
|
+
"tenant_prod".into(),
|
|
168
|
+
vec!["read".into(), "write".into()],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
assert_eq!(principal.service_key_id, "key_abc123");
|
|
172
|
+
assert_eq!(principal.actor_id, "user_xyz");
|
|
173
|
+
assert_eq!(principal.tenant_id, "tenant_prod");
|
|
174
|
+
assert_eq!(principal.auth_method, AuthMethod::ApiKey);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn verify_actor_id_rejects_mismatches() {
|
|
179
|
+
let principal = AuthenticatedPrincipal::from_api_key(
|
|
180
|
+
"key_abc123".into(),
|
|
181
|
+
"user_xyz".into(),
|
|
182
|
+
"tenant_prod".into(),
|
|
183
|
+
vec![],
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
assert!(principal.verify_actor_id("user_xyz").is_ok());
|
|
187
|
+
assert!(principal.verify_actor_id("user_other").is_err());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[test]
|
|
191
|
+
fn verify_tenant_id_rejects_mismatches() {
|
|
192
|
+
let principal = AuthenticatedPrincipal::from_api_key(
|
|
193
|
+
"key_abc123".into(),
|
|
194
|
+
"user_xyz".into(),
|
|
195
|
+
"tenant_prod".into(),
|
|
196
|
+
vec![],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
assert!(principal.verify_tenant_id("tenant_prod").is_ok());
|
|
200
|
+
assert!(principal.verify_tenant_id("tenant_other").is_err());
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[test]
|
|
204
|
+
fn has_role_works_correctly() {
|
|
205
|
+
let principal = AuthenticatedPrincipal::from_api_key(
|
|
206
|
+
"key_abc123".into(),
|
|
207
|
+
"user_xyz".into(),
|
|
208
|
+
"tenant_prod".into(),
|
|
209
|
+
vec!["admin".into(), "viewer".into()],
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
assert!(principal.has_role("admin"));
|
|
213
|
+
assert!(principal.has_role("viewer"));
|
|
214
|
+
assert!(!principal.has_role("editor"));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn wildcard_role_matches_all() {
|
|
219
|
+
let principal = AuthenticatedPrincipal::from_api_key(
|
|
220
|
+
"key_abc123".into(),
|
|
221
|
+
"user_xyz".into(),
|
|
222
|
+
"tenant_prod".into(),
|
|
223
|
+
vec!["*".into()],
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
assert!(principal.has_role("any_role"));
|
|
227
|
+
assert!(principal.has_role("admin"));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[test]
|
|
231
|
+
fn from_session_creates_correct_principal() {
|
|
232
|
+
let principal = AuthenticatedPrincipal::from_session(
|
|
233
|
+
"key_session".into(),
|
|
234
|
+
"user_abc".into(),
|
|
235
|
+
"tenant_staging".into(),
|
|
236
|
+
vec!["viewer".into()],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
assert_eq!(principal.auth_method, AuthMethod::Session);
|
|
240
|
+
assert_eq!(principal.provenance.credential_source, "session_cookie");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[test]
|
|
244
|
+
fn from_delegation_creates_correct_principal() {
|
|
245
|
+
let principal = AuthenticatedPrincipal::from_delegation(
|
|
246
|
+
"key_delegated".into(),
|
|
247
|
+
"service_xyz".into(),
|
|
248
|
+
"tenant_prod".into(),
|
|
249
|
+
vec!["execute".into()],
|
|
250
|
+
"delegated from sherpa".into(),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
assert_eq!(principal.auth_method, AuthMethod::Delegation);
|
|
254
|
+
assert_eq!(principal.provenance.credential_source, "delegation_token");
|
|
255
|
+
assert_eq!(principal.provenance.context, "delegated from sherpa");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#[test]
|
|
259
|
+
fn multiple_calls_to_verify_preserve_state() {
|
|
260
|
+
let principal = AuthenticatedPrincipal::from_api_key(
|
|
261
|
+
"key_abc123".into(),
|
|
262
|
+
"user_xyz".into(),
|
|
263
|
+
"tenant_prod".into(),
|
|
264
|
+
vec![],
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Verify multiple times to ensure no state mutation
|
|
268
|
+
assert!(principal.verify_actor_id("user_xyz").is_ok());
|
|
269
|
+
assert!(principal.verify_actor_id("user_xyz").is_ok());
|
|
270
|
+
assert!(principal.verify_tenant_id("tenant_prod").is_ok());
|
|
271
|
+
assert!(principal.verify_tenant_id("tenant_prod").is_ok());
|
|
272
|
+
}
|
|
273
|
+
}
|