@feltdb/core 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.
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,488 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use serde_json::Value;
3
+ use std::collections::HashMap;
4
+
5
+ /// Explicit membership policy configuration.
6
+ /// Replaces name discovery with manifest configuration.
7
+ /// No magic User/Organization/Membership relationships.
8
+ #[derive(Debug, Clone, Serialize, Deserialize)]
9
+ pub struct MembershipPolicy {
10
+ /// Name of this membership relationship (e.g., "organization_members", "team_members").
11
+ pub name: String,
12
+
13
+ /// Collection where membership records are stored.
14
+ pub collection: String,
15
+
16
+ /// Field in membership record that identifies the actor (e.g., "user_id").
17
+ pub actor_field: String,
18
+
19
+ /// Field in membership record that identifies the tenant (e.g., "organization_id").
20
+ pub tenant_field: String,
21
+
22
+ /// Optional role field in membership record (e.g., "role").
23
+ pub role_field: Option<String>,
24
+
25
+ /// Optional status field (e.g., "status" with values "active", "suspended").
26
+ pub status_field: Option<String>,
27
+
28
+ /// Statuses considered "active" for membership verification.
29
+ #[serde(default = "default_active_statuses")]
30
+ pub active_statuses: Vec<String>,
31
+ }
32
+
33
+ fn default_active_statuses() -> Vec<String> {
34
+ vec!["active".into(), "verified".into()]
35
+ }
36
+
37
+ /// Store for membership policies.
38
+ #[derive(Debug, Clone)]
39
+ pub struct MembershipPolicyStore {
40
+ policies: HashMap<String, MembershipPolicy>,
41
+ }
42
+
43
+ impl MembershipPolicyStore {
44
+ /// Creates a new membership policy store.
45
+ pub fn new() -> Self {
46
+ Self {
47
+ policies: HashMap::new(),
48
+ }
49
+ }
50
+
51
+ /// Registers a new membership policy.
52
+ pub fn register(&mut self, policy: MembershipPolicy) {
53
+ self.policies.insert(policy.name.clone(), policy);
54
+ }
55
+
56
+ /// Gets a membership policy by name.
57
+ pub fn get(&self, name: &str) -> Option<&MembershipPolicy> {
58
+ self.policies.get(name)
59
+ }
60
+
61
+ /// Gets a policy by collection name.
62
+ pub fn get_by_collection(&self, collection: &str) -> Option<&MembershipPolicy> {
63
+ self.policies.values().find(|p| p.collection == collection)
64
+ }
65
+
66
+ /// Loads default policies.
67
+ pub fn with_defaults() -> Self {
68
+ let mut store = Self::new();
69
+
70
+ // Organization members policy
71
+ store.register(MembershipPolicy {
72
+ name: "organization_members".into(),
73
+ collection: "organization_members".into(),
74
+ actor_field: "user_id".into(),
75
+ tenant_field: "organization_id".into(),
76
+ role_field: Some("role".into()),
77
+ status_field: Some("status".into()),
78
+ active_statuses: vec!["active".into(), "verified".into()],
79
+ });
80
+
81
+ // Team members policy
82
+ store.register(MembershipPolicy {
83
+ name: "team_members".into(),
84
+ collection: "team_members".into(),
85
+ actor_field: "user_id".into(),
86
+ tenant_field: "team_id".into(),
87
+ role_field: Some("role".into()),
88
+ status_field: Some("status".into()),
89
+ active_statuses: vec!["active".into()],
90
+ });
91
+
92
+ store
93
+ }
94
+ }
95
+
96
+ impl Default for MembershipPolicyStore {
97
+ fn default() -> Self {
98
+ Self::new()
99
+ }
100
+ }
101
+
102
+ /// Membership verification context.
103
+ #[derive(Debug, Clone)]
104
+ pub struct MembershipVerificationContext {
105
+ pub actor_id: String,
106
+ pub tenant_id: String,
107
+ pub required_role: Option<String>,
108
+ }
109
+
110
+ /// Membership verification result.
111
+ #[derive(Debug, Clone, PartialEq, Eq)]
112
+ pub enum MembershipDecision {
113
+ Verified { role: Option<String> },
114
+ Denied(String),
115
+ }
116
+
117
+ impl MembershipDecision {
118
+ pub fn is_verified(&self) -> bool {
119
+ matches!(self, MembershipDecision::Verified { .. })
120
+ }
121
+
122
+ pub fn role(&self) -> Option<&str> {
123
+ match self {
124
+ MembershipDecision::Verified { role } => role.as_deref(),
125
+ MembershipDecision::Denied(_) => None,
126
+ }
127
+ }
128
+
129
+ pub fn error_message(&self) -> Option<&str> {
130
+ match self {
131
+ MembershipDecision::Verified { .. } => None,
132
+ MembershipDecision::Denied(msg) => Some(msg),
133
+ }
134
+ }
135
+ }
136
+
137
+ /// Verifies membership relationships.
138
+ pub struct MembershipVerifier {
139
+ store: MembershipPolicyStore,
140
+ }
141
+
142
+ impl MembershipVerifier {
143
+ pub fn new(store: MembershipPolicyStore) -> Self {
144
+ Self { store }
145
+ }
146
+
147
+ /// Verifies membership using a policy.
148
+ pub fn verify(
149
+ &self,
150
+ context: &MembershipVerificationContext,
151
+ policy_name: &str,
152
+ membership_record: &Value,
153
+ ) -> MembershipDecision {
154
+ let policy = match self.store.get(policy_name) {
155
+ Some(p) => p,
156
+ None => return MembershipDecision::Denied(format!("policy not found: {}", policy_name)),
157
+ };
158
+
159
+ // Verify actor field
160
+ let record_actor = self.extract_string(&policy.actor_field, membership_record);
161
+ if record_actor != Some(context.actor_id.clone()) {
162
+ return MembershipDecision::Denied(format!(
163
+ "actor mismatch: expected {} but got {:?}",
164
+ context.actor_id, record_actor
165
+ ));
166
+ }
167
+
168
+ // Verify tenant field
169
+ let record_tenant = self.extract_string(&policy.tenant_field, membership_record);
170
+ if record_tenant != Some(context.tenant_id.clone()) {
171
+ return MembershipDecision::Denied(format!(
172
+ "tenant mismatch: expected {} but got {:?}",
173
+ context.tenant_id, record_tenant
174
+ ));
175
+ }
176
+
177
+ // Check status if configured
178
+ if let Some(status_field) = &policy.status_field {
179
+ let status = self.extract_string(status_field, membership_record);
180
+ if let Some(status_val) = status {
181
+ if !policy.active_statuses.contains(&status_val) {
182
+ return MembershipDecision::Denied(format!(
183
+ "membership status not active: {}",
184
+ status_val
185
+ ));
186
+ }
187
+ }
188
+ }
189
+
190
+ // Extract role
191
+ let role = policy
192
+ .role_field
193
+ .as_ref()
194
+ .and_then(|f| self.extract_string(f, membership_record));
195
+
196
+ // Verify required role if specified
197
+ if let Some(required) = &context.required_role {
198
+ if let Some(actual) = &role {
199
+ if actual != required {
200
+ return MembershipDecision::Denied(format!(
201
+ "role mismatch: required {} but got {}",
202
+ required, actual
203
+ ));
204
+ }
205
+ } else {
206
+ return MembershipDecision::Denied(format!("required role not found: {}", required));
207
+ }
208
+ }
209
+
210
+ MembershipDecision::Verified { role }
211
+ }
212
+
213
+ /// Verifies membership from a collection of records.
214
+ /// Returns Some(role) if verified, None if not found or denied.
215
+ pub fn verify_from_records(
216
+ &self,
217
+ context: &MembershipVerificationContext,
218
+ policy_name: &str,
219
+ records: &[Value],
220
+ ) -> MembershipDecision {
221
+ // Find matching membership record
222
+ let policy = match self.store.get(policy_name) {
223
+ Some(p) => p,
224
+ None => return MembershipDecision::Denied(format!("policy not found: {}", policy_name)),
225
+ };
226
+
227
+ for record in records {
228
+ let record_actor = self.extract_string(&policy.actor_field, record);
229
+ let record_tenant = self.extract_string(&policy.tenant_field, record);
230
+
231
+ if record_actor == Some(context.actor_id.clone())
232
+ && record_tenant == Some(context.tenant_id.clone())
233
+ {
234
+ return self.verify(context, policy_name, record);
235
+ }
236
+ }
237
+
238
+ MembershipDecision::Denied(format!(
239
+ "no membership record found for actor {} in tenant {}",
240
+ context.actor_id, context.tenant_id
241
+ ))
242
+ }
243
+
244
+ /// Gets all roles an actor has in a tenant.
245
+ pub fn get_roles(
246
+ &self,
247
+ actor_id: &str,
248
+ tenant_id: &str,
249
+ policy_name: &str,
250
+ records: &[Value],
251
+ ) -> Vec<String> {
252
+ let policy = match self.store.get(policy_name) {
253
+ Some(p) => p,
254
+ None => return vec![],
255
+ };
256
+
257
+ let mut roles = vec![];
258
+
259
+ for record in records {
260
+ let record_actor = self.extract_string(&policy.actor_field, record);
261
+ let record_tenant = self.extract_string(&policy.tenant_field, record);
262
+
263
+ if record_actor.as_deref() == Some(actor_id) && record_tenant.as_deref() == Some(tenant_id)
264
+ {
265
+ // Check status
266
+ if let Some(status_field) = &policy.status_field {
267
+ let status = self.extract_string(status_field, record);
268
+ if let Some(status_val) = status {
269
+ if !policy.active_statuses.contains(&status_val) {
270
+ continue; // Skip inactive records
271
+ }
272
+ }
273
+ }
274
+
275
+ // Extract role if configured
276
+ if let Some(role_field) = &policy.role_field {
277
+ if let Some(role) = self.extract_string(role_field, record) {
278
+ if !roles.contains(&role) {
279
+ roles.push(role);
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ roles
287
+ }
288
+
289
+ /// Extracts a string value from a record.
290
+ fn extract_string(&self, field: &str, record: &Value) -> Option<String> {
291
+ record
292
+ .get(field)
293
+ .and_then(|v| v.as_str())
294
+ .map(|s| s.to_string())
295
+ }
296
+ }
297
+
298
+ #[cfg(test)]
299
+ mod tests {
300
+ use super::*;
301
+ use serde_json::json;
302
+
303
+ fn test_context() -> MembershipVerificationContext {
304
+ MembershipVerificationContext {
305
+ actor_id: "user_123".into(),
306
+ tenant_id: "org_acme".into(),
307
+ required_role: None,
308
+ }
309
+ }
310
+
311
+ fn test_membership(user_id: &str, org_id: &str, role: &str, status: &str) -> Value {
312
+ json!({
313
+ "user_id": user_id,
314
+ "organization_id": org_id,
315
+ "role": role,
316
+ "status": status
317
+ })
318
+ }
319
+
320
+ fn test_verifier() -> MembershipVerifier {
321
+ MembershipVerifier::new(MembershipPolicyStore::with_defaults())
322
+ }
323
+
324
+ #[test]
325
+ fn verify_active_membership() {
326
+ let verifier = test_verifier();
327
+ let context = test_context();
328
+ let record = test_membership("user_123", "org_acme", "admin", "active");
329
+
330
+ let decision = verifier.verify(&context, "organization_members", &record);
331
+ assert_eq!(
332
+ decision,
333
+ MembershipDecision::Verified {
334
+ role: Some("admin".into())
335
+ }
336
+ );
337
+ }
338
+
339
+ #[test]
340
+ fn deny_actor_mismatch() {
341
+ let verifier = test_verifier();
342
+ let context = test_context();
343
+ let record = test_membership("user_other", "org_acme", "admin", "active");
344
+
345
+ let decision = verifier.verify(&context, "organization_members", &record);
346
+ assert!(matches!(decision, MembershipDecision::Denied(_)));
347
+ }
348
+
349
+ #[test]
350
+ fn deny_tenant_mismatch() {
351
+ let verifier = test_verifier();
352
+ let context = test_context();
353
+ let record = test_membership("user_123", "org_other", "admin", "active");
354
+
355
+ let decision = verifier.verify(&context, "organization_members", &record);
356
+ assert!(matches!(decision, MembershipDecision::Denied(_)));
357
+ }
358
+
359
+ #[test]
360
+ fn deny_inactive_status() {
361
+ let verifier = test_verifier();
362
+ let context = test_context();
363
+ let record = test_membership("user_123", "org_acme", "admin", "suspended");
364
+
365
+ let decision = verifier.verify(&context, "organization_members", &record);
366
+ assert!(matches!(decision, MembershipDecision::Denied(_)));
367
+ }
368
+
369
+ #[test]
370
+ fn verify_with_required_role() {
371
+ let verifier = test_verifier();
372
+ let mut context = test_context();
373
+ context.required_role = Some("admin".into());
374
+ let record = test_membership("user_123", "org_acme", "admin", "active");
375
+
376
+ let decision = verifier.verify(&context, "organization_members", &record);
377
+ assert_eq!(
378
+ decision,
379
+ MembershipDecision::Verified {
380
+ role: Some("admin".into())
381
+ }
382
+ );
383
+ }
384
+
385
+ #[test]
386
+ fn deny_wrong_required_role() {
387
+ let verifier = test_verifier();
388
+ let mut context = test_context();
389
+ context.required_role = Some("owner".into());
390
+ let record = test_membership("user_123", "org_acme", "admin", "active");
391
+
392
+ let decision = verifier.verify(&context, "organization_members", &record);
393
+ assert!(matches!(decision, MembershipDecision::Denied(_)));
394
+ }
395
+
396
+ #[test]
397
+ fn get_all_roles() {
398
+ let verifier = test_verifier();
399
+ let records = vec![
400
+ test_membership("user_123", "org_acme", "admin", "active"),
401
+ test_membership("user_123", "org_acme", "reviewer", "active"),
402
+ test_membership("user_123", "org_other", "admin", "active"),
403
+ ];
404
+
405
+ let roles = verifier.get_roles("user_123", "org_acme", "organization_members", &records);
406
+ assert_eq!(roles.len(), 2);
407
+ assert!(roles.contains(&"admin".into()));
408
+ assert!(roles.contains(&"reviewer".into()));
409
+ }
410
+
411
+ #[test]
412
+ fn verify_from_records_finds_matching() {
413
+ let verifier = test_verifier();
414
+ let context = test_context();
415
+ let records = vec![
416
+ test_membership("user_other", "org_acme", "admin", "active"),
417
+ test_membership("user_123", "org_acme", "viewer", "active"),
418
+ ];
419
+
420
+ let decision = verifier.verify_from_records(&context, "organization_members", &records);
421
+ assert_eq!(
422
+ decision,
423
+ MembershipDecision::Verified {
424
+ role: Some("viewer".into())
425
+ }
426
+ );
427
+ }
428
+
429
+ #[test]
430
+ fn deny_when_no_matching_record() {
431
+ let verifier = test_verifier();
432
+ let context = test_context();
433
+ let records = vec![test_membership("user_other", "org_acme", "admin", "active")];
434
+
435
+ let decision = verifier.verify_from_records(&context, "organization_members", &records);
436
+ assert!(matches!(decision, MembershipDecision::Denied(_)));
437
+ }
438
+
439
+ #[test]
440
+ fn skip_inactive_when_getting_roles() {
441
+ let verifier = test_verifier();
442
+ let records = vec![
443
+ test_membership("user_123", "org_acme", "admin", "active"),
444
+ test_membership("user_123", "org_acme", "suspended_role", "suspended"),
445
+ ];
446
+
447
+ let roles = verifier.get_roles("user_123", "org_acme", "organization_members", &records);
448
+ assert_eq!(roles.len(), 1);
449
+ assert_eq!(roles[0], "admin");
450
+ }
451
+
452
+ #[test]
453
+ fn handle_missing_optional_fields() {
454
+ let verifier = test_verifier();
455
+ let context = test_context();
456
+ let record = json!({
457
+ "user_id": "user_123",
458
+ "organization_id": "org_acme"
459
+ // No role or status fields
460
+ });
461
+
462
+ let decision = verifier.verify(&context, "organization_members", &record);
463
+ // Should still verify even without optional fields
464
+ assert!(matches!(decision, MembershipDecision::Verified { .. }));
465
+ }
466
+
467
+ #[test]
468
+ fn no_policy_denied() {
469
+ let verifier = test_verifier();
470
+ let context = test_context();
471
+ let record = test_membership("user_123", "org_acme", "admin", "active");
472
+
473
+ let decision = verifier.verify(&context, "nonexistent_policy", &record);
474
+ assert!(matches!(decision, MembershipDecision::Denied(_)));
475
+ }
476
+
477
+ #[test]
478
+ fn duplicate_roles_not_added() {
479
+ let verifier = test_verifier();
480
+ let records = vec![
481
+ test_membership("user_123", "org_acme", "admin", "active"),
482
+ test_membership("user_123", "org_acme", "admin", "active"),
483
+ ];
484
+
485
+ let roles = verifier.get_roles("user_123", "org_acme", "organization_members", &records);
486
+ assert_eq!(roles.len(), 1);
487
+ }
488
+ }