@feltdb/core 0.5.7 → 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.
Files changed (48) hide show
  1. package/dist/cli/commands.js +68 -1
  2. package/dist/cli/workspace-integration.js +96 -0
  3. package/dist/create/cli.js +1 -1
  4. package/dist/create/create.js +21 -0
  5. package/dist/create/package-versions.js +1 -1
  6. package/dist/create/server-source/Cargo.lock +10 -0
  7. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -0
  8. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +5 -1
  9. package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +273 -0
  10. package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +528 -0
  11. package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +472 -0
  12. package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +992 -0
  13. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +9 -0
  14. package/dist/create/server-source/crates/feltdb-server/src/main.rs +157 -9
  15. package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +413 -0
  16. package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +488 -0
  17. package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +378 -0
  18. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +49 -0
  19. package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +525 -0
  20. package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +461 -0
  21. package/dist/create/template/dot-feltdb-README.md +58 -0
  22. package/dist/create/workspace-initialization.js +77 -0
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +6 -0
  26. package/dist/studio-app/assets/{feltdb_wasm-h9mxesnH.js → feltdb_wasm-B4wq4mqp.js} +1 -1
  27. package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
  28. package/dist/studio-app/assets/{index-B_EMnTaE.js → index-LQmvJSq6.js} +2 -2
  29. package/dist/studio-app/index.html +1 -1
  30. package/dist/telemetry.js +1 -1
  31. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  32. package/dist/workspace/development-node.d.ts +42 -0
  33. package/dist/workspace/development-node.d.ts.map +1 -0
  34. package/dist/workspace/development-node.js +208 -0
  35. package/dist/workspace/index.d.ts +20 -0
  36. package/dist/workspace/index.d.ts.map +1 -0
  37. package/dist/workspace/index.js +16 -0
  38. package/dist/workspace/workspace-connection.d.ts +174 -0
  39. package/dist/workspace/workspace-connection.d.ts.map +1 -0
  40. package/dist/workspace/workspace-connection.js +290 -0
  41. package/dist/workspace/workspace-identity.d.ts +13 -0
  42. package/dist/workspace/workspace-identity.d.ts.map +1 -0
  43. package/dist/workspace/workspace-identity.js +70 -0
  44. package/dist/workspace/workspace-types.d.ts +82 -0
  45. package/dist/workspace/workspace-types.d.ts.map +1 -0
  46. package/dist/workspace/workspace-types.js +7 -0
  47. package/package.json +5 -1
  48. package/dist/studio-app/assets/feltdb_wasm_bg-B8U4A1n1.wasm +0 -0
@@ -0,0 +1,472 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use sha2::{Digest, Sha256};
3
+ use std::time::{SystemTime, UNIX_EPOCH};
4
+
5
+ /// Signed delegation token for federated identity.
6
+ /// Enables Sherpa to delegate operations to FeltDB without direct credentials.
7
+ #[derive(Debug, Clone, Serialize, Deserialize)]
8
+ pub struct DelegationToken {
9
+ /// Unique token ID for idempotency and revocation tracking.
10
+ pub token_id: String,
11
+
12
+ /// Service that issued this token (e.g., "sherpa", "apple", "stripe").
13
+ pub issuer: String,
14
+
15
+ /// Service this token is intended for (e.g., "feltdb").
16
+ pub audience: String,
17
+
18
+ /// ID of the service key used to sign this token.
19
+ pub service_key_id: String,
20
+
21
+ /// Actor ID this token delegates to.
22
+ pub actor_id: String,
23
+
24
+ /// Tenant ID this token is scoped to.
25
+ pub tenant_id: String,
26
+
27
+ /// Roles granted by this delegation.
28
+ pub roles: Vec<String>,
29
+
30
+ /// Scope constraint for this delegation (e.g., "read_only", "write_release_jobs").
31
+ pub scope: DelegationScope,
32
+
33
+ /// When this token was issued (Unix seconds).
34
+ pub issued_at: u64,
35
+
36
+ /// When this token expires (Unix seconds).
37
+ pub expires_at: u64,
38
+
39
+ /// Signature of the token (issuer must verify).
40
+ pub signature: String,
41
+
42
+ /// Hash algorithm used for signature (e.g., "sha256").
43
+ pub signature_algorithm: String,
44
+ }
45
+
46
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47
+ #[serde(rename_all = "snake_case")]
48
+ pub enum DelegationScope {
49
+ Unrestricted,
50
+ ReadOnly,
51
+ WriteReleaseJobs,
52
+ ExecuteWorkflows,
53
+ ManagePrincipals,
54
+ Custom(String),
55
+ }
56
+
57
+ impl DelegationScope {
58
+ pub fn allows_operation(&self, operation: &str) -> bool {
59
+ match self {
60
+ DelegationScope::Unrestricted => true,
61
+ DelegationScope::ReadOnly => operation == "read",
62
+ DelegationScope::WriteReleaseJobs => {
63
+ operation == "read" || operation == "write:release_jobs"
64
+ }
65
+ DelegationScope::ExecuteWorkflows => {
66
+ operation == "read" || operation == "execute:workflows"
67
+ }
68
+ DelegationScope::ManagePrincipals => {
69
+ operation == "read" || operation == "write:principals"
70
+ }
71
+ DelegationScope::Custom(constraint) => operation.starts_with(constraint),
72
+ }
73
+ }
74
+ }
75
+
76
+ #[derive(Debug, Clone)]
77
+ pub struct DelegationTokenIssuer {
78
+ /// Shared secret for signing (in production, loaded from HSM).
79
+ /// Both issuer and validator must have the same secret.
80
+ shared_secret: Vec<u8>,
81
+ /// Service name (e.g., "sherpa").
82
+ service_name: String,
83
+ }
84
+
85
+ #[derive(Debug, Clone)]
86
+ pub struct DelegationTokenValidator {
87
+ /// Shared secret for verification (same as issuer's).
88
+ shared_secret: Vec<u8>,
89
+ /// Expected issuer service name.
90
+ expected_issuer: String,
91
+ /// Audience we expect (e.g., "feltdb").
92
+ expected_audience: String,
93
+ }
94
+
95
+ impl DelegationTokenIssuer {
96
+ /// Creates a new token issuer (for testing; production uses HSM).
97
+ pub fn new(service_name: String, shared_secret: Vec<u8>) -> Self {
98
+ Self {
99
+ shared_secret,
100
+ service_name,
101
+ }
102
+ }
103
+
104
+ /// Issues a new delegation token.
105
+ pub fn issue(
106
+ &self,
107
+ service_key_id: String,
108
+ actor_id: String,
109
+ tenant_id: String,
110
+ roles: Vec<String>,
111
+ scope: DelegationScope,
112
+ ttl_seconds: u64,
113
+ ) -> Result<DelegationToken, String> {
114
+ let now = now();
115
+ let token_id = generate_token_id();
116
+
117
+ let token = DelegationToken {
118
+ token_id: token_id.clone(),
119
+ issuer: self.service_name.clone(),
120
+ audience: "feltdb".into(),
121
+ service_key_id,
122
+ actor_id,
123
+ tenant_id,
124
+ roles,
125
+ scope,
126
+ issued_at: now,
127
+ expires_at: now + ttl_seconds,
128
+ signature: String::new(),
129
+ signature_algorithm: "sha256-hmac".into(),
130
+ };
131
+
132
+ let payload = self.canonicalize_for_signing(&token)?;
133
+ let signature = self.sign(&payload)?;
134
+
135
+ Ok(DelegationToken {
136
+ signature,
137
+ ..token
138
+ })
139
+ }
140
+
141
+ /// Canonicalizes token for consistent signing/verification.
142
+ fn canonicalize_for_signing(&self, token: &DelegationToken) -> Result<String, String> {
143
+ let mut hasher = Sha256::new();
144
+
145
+ hasher.update(token.token_id.as_bytes());
146
+ hasher.update(token.issuer.as_bytes());
147
+ hasher.update(token.audience.as_bytes());
148
+ hasher.update(token.service_key_id.as_bytes());
149
+ hasher.update(token.actor_id.as_bytes());
150
+ hasher.update(token.tenant_id.as_bytes());
151
+ hasher.update(token.issued_at.to_le_bytes());
152
+ hasher.update(token.expires_at.to_le_bytes());
153
+
154
+ for role in &token.roles {
155
+ hasher.update(role.as_bytes());
156
+ }
157
+
158
+ let scope_str = serde_json::to_string(&token.scope)
159
+ .map_err(|e| format!("failed to serialize scope: {e}"))?;
160
+ hasher.update(scope_str.as_bytes());
161
+
162
+ Ok(format!("{:x}", hasher.finalize()))
163
+ }
164
+
165
+ /// Signs the canonicalized payload.
166
+ /// In production, this would use HSM/KMS.
167
+ fn sign(&self, payload: &str) -> Result<String, String> {
168
+ use hmac::{Hmac, Mac};
169
+ use sha2::Sha256;
170
+
171
+ type HmacSha256 = Hmac<Sha256>;
172
+
173
+ let mut mac = HmacSha256::new_from_slice(&self.shared_secret)
174
+ .map_err(|e| format!("invalid shared secret: {e}"))?;
175
+ mac.update(payload.as_bytes());
176
+
177
+ Ok(format!("{:x}", mac.finalize().into_bytes()))
178
+ }
179
+ }
180
+
181
+ impl DelegationTokenValidator {
182
+ /// Creates a new token validator.
183
+ pub fn new(
184
+ expected_issuer: String,
185
+ expected_audience: String,
186
+ shared_secret: Vec<u8>,
187
+ ) -> Self {
188
+ Self {
189
+ expected_issuer,
190
+ expected_audience,
191
+ shared_secret,
192
+ }
193
+ }
194
+
195
+ /// Validates a delegation token.
196
+ pub fn validate(&self, token: &DelegationToken) -> Result<(), String> {
197
+ self.check_not_expired(token)?;
198
+ self.check_issuer(token)?;
199
+ self.check_audience(token)?;
200
+ self.check_signature(token)?;
201
+
202
+ Ok(())
203
+ }
204
+
205
+ fn check_not_expired(&self, token: &DelegationToken) -> Result<(), String> {
206
+ let now = now();
207
+ if now > token.expires_at {
208
+ return Err(format!(
209
+ "token expired at {} (current time: {})",
210
+ token.expires_at, now
211
+ ));
212
+ }
213
+ Ok(())
214
+ }
215
+
216
+ fn check_issuer(&self, token: &DelegationToken) -> Result<(), String> {
217
+ if token.issuer != self.expected_issuer {
218
+ return Err(format!(
219
+ "issuer mismatch: expected {} but got {}",
220
+ self.expected_issuer, token.issuer
221
+ ));
222
+ }
223
+ Ok(())
224
+ }
225
+
226
+ fn check_audience(&self, token: &DelegationToken) -> Result<(), String> {
227
+ if token.audience != self.expected_audience {
228
+ return Err(format!(
229
+ "audience mismatch: expected {} but got {}",
230
+ self.expected_audience, token.audience
231
+ ));
232
+ }
233
+ Ok(())
234
+ }
235
+
236
+ fn check_signature(&self, token: &DelegationToken) -> Result<(), String> {
237
+ let mut token_copy = token.clone();
238
+ let original_sig = token_copy.signature.clone();
239
+ token_copy.signature = String::new();
240
+
241
+ let payload = self.canonicalize_for_signing(&token_copy)?;
242
+ self.verify_signature(&payload, &original_sig)
243
+ }
244
+
245
+ /// Canonicalizes token for verification (mirrors issuer logic).
246
+ fn canonicalize_for_signing(&self, token: &DelegationToken) -> Result<String, String> {
247
+ let mut hasher = Sha256::new();
248
+
249
+ hasher.update(token.token_id.as_bytes());
250
+ hasher.update(token.issuer.as_bytes());
251
+ hasher.update(token.audience.as_bytes());
252
+ hasher.update(token.service_key_id.as_bytes());
253
+ hasher.update(token.actor_id.as_bytes());
254
+ hasher.update(token.tenant_id.as_bytes());
255
+ hasher.update(token.issued_at.to_le_bytes());
256
+ hasher.update(token.expires_at.to_le_bytes());
257
+
258
+ for role in &token.roles {
259
+ hasher.update(role.as_bytes());
260
+ }
261
+
262
+ let scope_str = serde_json::to_string(&token.scope)
263
+ .map_err(|e| format!("failed to serialize scope: {e}"))?;
264
+ hasher.update(scope_str.as_bytes());
265
+
266
+ Ok(format!("{:x}", hasher.finalize()))
267
+ }
268
+
269
+ /// Verifies signature using the shared secret.
270
+ fn verify_signature(&self, payload: &str, signature: &str) -> Result<(), String> {
271
+ use hmac::{Hmac, Mac};
272
+ use sha2::Sha256;
273
+
274
+ type HmacSha256 = Hmac<Sha256>;
275
+
276
+ let mut mac = HmacSha256::new_from_slice(&self.shared_secret)
277
+ .map_err(|e| format!("invalid shared secret: {e}"))?;
278
+ mac.update(payload.as_bytes());
279
+
280
+ let expected_sig = format!("{:x}", mac.finalize().into_bytes());
281
+ if expected_sig == signature {
282
+ Ok(())
283
+ } else {
284
+ Err("signature verification failed".into())
285
+ }
286
+ }
287
+ }
288
+
289
+ fn now() -> u64 {
290
+ SystemTime::now()
291
+ .duration_since(UNIX_EPOCH)
292
+ .unwrap_or_default()
293
+ .as_secs()
294
+ }
295
+
296
+ fn generate_token_id() -> String {
297
+ use rand::{distributions::Alphanumeric, Rng};
298
+
299
+ let value: String = rand::thread_rng()
300
+ .sample_iter(&Alphanumeric)
301
+ .take(32)
302
+ .map(char::from)
303
+ .collect();
304
+ format!("tok_{}", value)
305
+ }
306
+
307
+ #[cfg(test)]
308
+ mod tests {
309
+ use super::*;
310
+
311
+ fn test_shared_secret() -> Vec<u8> {
312
+ b"test_shared_secret_32_bytes______".to_vec()
313
+ }
314
+
315
+ #[test]
316
+ fn issue_and_validate_delegation_token() {
317
+ let secret = test_shared_secret();
318
+ let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
319
+ let validator =
320
+ DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
321
+
322
+ let token = issuer
323
+ .issue(
324
+ "key_sherpa".into(),
325
+ "app_acme".into(),
326
+ "tenant_prod".into(),
327
+ vec!["execute".into()],
328
+ DelegationScope::ExecuteWorkflows,
329
+ 3600,
330
+ )
331
+ .expect("failed to issue token");
332
+
333
+ assert!(validator.validate(&token).is_ok());
334
+ }
335
+
336
+ #[test]
337
+ fn reject_expired_token() {
338
+ let secret = test_shared_secret();
339
+ let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
340
+ let validator =
341
+ DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
342
+
343
+ let mut token = issuer
344
+ .issue(
345
+ "key_sherpa".into(),
346
+ "app_acme".into(),
347
+ "tenant_prod".into(),
348
+ vec!["execute".into()],
349
+ DelegationScope::ExecuteWorkflows,
350
+ 1, // 1 second TTL
351
+ )
352
+ .expect("failed to issue token");
353
+
354
+ // Simulate expiration
355
+ token.expires_at = now() - 1;
356
+
357
+ assert!(validator.validate(&token).is_err());
358
+ }
359
+
360
+ #[test]
361
+ fn reject_wrong_issuer() {
362
+ let secret = test_shared_secret();
363
+ let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
364
+ let validator =
365
+ DelegationTokenValidator::new("other_service".into(), "feltdb".into(), secret);
366
+
367
+ let token = issuer
368
+ .issue(
369
+ "key_sherpa".into(),
370
+ "app_acme".into(),
371
+ "tenant_prod".into(),
372
+ vec!["execute".into()],
373
+ DelegationScope::ExecuteWorkflows,
374
+ 3600,
375
+ )
376
+ .expect("failed to issue token");
377
+
378
+ assert!(validator.validate(&token).is_err());
379
+ }
380
+
381
+ #[test]
382
+ fn reject_wrong_audience() {
383
+ let secret = test_shared_secret();
384
+ let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
385
+ let validator =
386
+ DelegationTokenValidator::new("sherpa".into(), "other_service".into(), secret);
387
+
388
+ let token = issuer
389
+ .issue(
390
+ "key_sherpa".into(),
391
+ "app_acme".into(),
392
+ "tenant_prod".into(),
393
+ vec!["execute".into()],
394
+ DelegationScope::ExecuteWorkflows,
395
+ 3600,
396
+ )
397
+ .expect("failed to issue token");
398
+
399
+ assert!(validator.validate(&token).is_err());
400
+ }
401
+
402
+ #[test]
403
+ fn scope_enforces_constraints() {
404
+ let read_only = DelegationScope::ReadOnly;
405
+ assert!(read_only.allows_operation("read"));
406
+ assert!(!read_only.allows_operation("write"));
407
+
408
+ let write_jobs = DelegationScope::WriteReleaseJobs;
409
+ assert!(write_jobs.allows_operation("read"));
410
+ assert!(write_jobs.allows_operation("write:release_jobs"));
411
+ assert!(!write_jobs.allows_operation("write:principals"));
412
+
413
+ let unrestricted = DelegationScope::Unrestricted;
414
+ assert!(unrestricted.allows_operation("read"));
415
+ assert!(unrestricted.allows_operation("write"));
416
+ assert!(unrestricted.allows_operation("delete"));
417
+ }
418
+
419
+ #[test]
420
+ fn token_id_is_unique() {
421
+ let secret = test_shared_secret();
422
+ let issuer = DelegationTokenIssuer::new("sherpa".into(), secret);
423
+
424
+ let token1 = issuer
425
+ .issue(
426
+ "key_sherpa".into(),
427
+ "app_acme".into(),
428
+ "tenant_prod".into(),
429
+ vec![],
430
+ DelegationScope::ReadOnly,
431
+ 3600,
432
+ )
433
+ .expect("failed to issue token 1");
434
+
435
+ let token2 = issuer
436
+ .issue(
437
+ "key_sherpa".into(),
438
+ "app_acme".into(),
439
+ "tenant_prod".into(),
440
+ vec![],
441
+ DelegationScope::ReadOnly,
442
+ 3600,
443
+ )
444
+ .expect("failed to issue token 2");
445
+
446
+ assert_ne!(token1.token_id, token2.token_id);
447
+ }
448
+
449
+ #[test]
450
+ fn tampering_invalidates_signature() {
451
+ let secret = test_shared_secret();
452
+ let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
453
+ let validator =
454
+ DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
455
+
456
+ let mut token = issuer
457
+ .issue(
458
+ "key_sherpa".into(),
459
+ "app_acme".into(),
460
+ "tenant_prod".into(),
461
+ vec!["execute".into()],
462
+ DelegationScope::ExecuteWorkflows,
463
+ 3600,
464
+ )
465
+ .expect("failed to issue token");
466
+
467
+ // Tamper with the token
468
+ token.actor_id = "app_other".into();
469
+
470
+ assert!(validator.validate(&token).is_err());
471
+ }
472
+ }