@feltdb/core 0.4.15 → 0.4.16

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 (37) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/create/package-versions.js +1 -1
  3. package/dist/create/server-source/crates/feltdb/Cargo.toml +1 -1
  4. package/dist/create/server-source/crates/feltdb/src/application.rs +93 -0
  5. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +787 -0
  6. package/dist/create/server-source/crates/feltdb/src/lib.rs +139 -0
  7. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +1669 -0
  8. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +1269 -15
  9. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +406 -0
  10. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +908 -0
  11. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +1028 -0
  12. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +332 -0
  13. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +342 -0
  14. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +313 -0
  15. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +90 -0
  16. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +571 -0
  17. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +506 -0
  18. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +81 -0
  19. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +434 -0
  20. package/dist/create/server-source/crates/feltdb-server/src/main.rs +130 -23
  21. package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +2 -2
  22. package/dist/db.d.ts +10 -0
  23. package/dist/db.d.ts.map +1 -1
  24. package/dist/db.js +3 -1
  25. package/dist/file-db.d.ts +69 -0
  26. package/dist/file-db.d.ts.map +1 -0
  27. package/dist/file-db.js +355 -0
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -0
  31. package/dist/studio-app/assets/{feltdb_wasm-CJEJryDx.js → feltdb_wasm-CBGD0zRu.js} +1 -1
  32. package/dist/studio-app/assets/feltdb_wasm_bg-C6ATF9mJ.wasm +0 -0
  33. package/dist/studio-app/assets/{index-D_p8T7nO.js → index-D74dfBgZ.js} +9 -9
  34. package/dist/studio-app/index.html +1 -1
  35. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  36. package/package.json +7 -2
  37. package/dist/studio-app/assets/feltdb_wasm_bg-BJxQXtoo.wasm +0 -0
@@ -0,0 +1,332 @@
1
+ //! PR #9 Phase 3a: Path A Investigation
2
+ //!
3
+ //! Test whether existing domain relationships (Team.owner) can express
4
+ //! role-sensitive authorization requirements using frozen vocabulary.
5
+ //!
6
+ //! CRITICAL DISCIPLINE:
7
+ //! - Don't assume admin semantics
8
+ //! - Test what the domain actually represents
9
+ //! - Observe FeltDB behavior
10
+ //! - Classify against existing relationships only
11
+ //!
12
+ //! The requirement "only owners/admins can delete" has two parts:
13
+ //! - Owner part: representable via Team.owner field
14
+ //! - Admin part: NOT YET MODELED in schema
15
+ //!
16
+ //! Phase 3a tests ONLY the owner part.
17
+ //! Admin remains an open question for later investigation.
18
+
19
+ use feltdb::{
20
+ state_contract::{
21
+ StateSchema, CollectionSchema, FieldSchema, FieldType, PrimitiveType,
22
+ FieldConstraints, AuthorizationContext,
23
+ },
24
+ FeltDb,
25
+ };
26
+ use serde_json::json;
27
+ use tempfile::TempDir;
28
+
29
+ /// Test fixture for Path A investigation
30
+ struct PathATestFixture {
31
+ db: FeltDb,
32
+ // Actors
33
+ owner_id: String,
34
+ admin_id: String,
35
+ member_id: String,
36
+ non_member_id: String,
37
+ // Resources
38
+ team_id: String,
39
+ org_id: String,
40
+ }
41
+
42
+ impl PathATestFixture {
43
+ fn actor_context(&self, actor_id: &str, _name: &str) -> AuthorizationContext {
44
+ AuthorizationContext {
45
+ subject: format!(":{}", actor_id),
46
+ tenant_id: "tenant".to_string(),
47
+ application_id: "saas-portal".to_string(),
48
+ revision_id: "rev-1".to_string(),
49
+ capabilities: ["state:read".to_string()].into(),
50
+ readable_collections: Default::default(),
51
+ writable_collections: Default::default(),
52
+ field_projections: Default::default(),
53
+ }
54
+ }
55
+
56
+ fn setup() -> Result<Self, Box<dyn std::error::Error>> {
57
+ let temp_dir = TempDir::new()?;
58
+ let db_path = temp_dir.path().join("path_a_tests.db");
59
+ let db = FeltDb::open(&db_path)?;
60
+
61
+ let owner_id = "user-owner".to_string();
62
+ let admin_id = "user-admin".to_string();
63
+ let member_id = "user-member".to_string();
64
+ let non_member_id = "user-non-member".to_string();
65
+ let org_id = "org-1".to_string();
66
+ let team_id = "team-backend".to_string();
67
+
68
+ // Create users
69
+ db.insert("User#user-owner", json!({"_id": owner_id, "email": "owner@example.com"}))?;
70
+ db.insert("User#user-admin", json!({"_id": admin_id, "email": "admin@example.com"}))?;
71
+ db.insert("User#user-member", json!({"_id": member_id, "email": "member@example.com"}))?;
72
+ db.insert("User#user-non-member", json!({"_id": non_member_id, "email": "non-member@example.com"}))?;
73
+
74
+ // Create organization
75
+ db.insert("Organization#org-1", json!({"_id": org_id, "name": "Org 1"}))?;
76
+
77
+ // Create memberships for all users in the organization
78
+ db.insert(
79
+ "Membership#mem-owner",
80
+ json!({"_id": "mem-owner", "user": owner_id, "organization": org_id}),
81
+ )?;
82
+ db.insert(
83
+ "Membership#mem-admin",
84
+ json!({"_id": "mem-admin", "user": admin_id, "organization": org_id}),
85
+ )?;
86
+ db.insert(
87
+ "Membership#mem-member",
88
+ json!({"_id": "mem-member", "user": member_id, "organization": org_id}),
89
+ )?;
90
+ db.insert(
91
+ "Membership#mem-non-member",
92
+ json!({"_id": "mem-non-member", "user": non_member_id, "organization": org_id}),
93
+ )?;
94
+
95
+ // Create team with owner field
96
+ // NOTE: This test uses Team.owner to represent the owner relationship
97
+ db.insert(
98
+ "Team#team-backend",
99
+ json!({
100
+ "_id": team_id,
101
+ "organization": org_id,
102
+ "name": "Backend Team",
103
+ "owner": owner_id, // ← Domain fact: owner_id is the team owner
104
+ "created_at": "2026-08-22T00:00:00Z"
105
+ }),
106
+ )?;
107
+
108
+ // Create team memberships with roles
109
+ // NOTE: role field is domain state, not authorization primitive
110
+ db.insert(
111
+ "TeamMembership#tm-owner",
112
+ json!({
113
+ "_id": "tm-owner",
114
+ "team": team_id,
115
+ "user": owner_id,
116
+ "role": "owner" // ← Domain state only
117
+ }),
118
+ )?;
119
+ db.insert(
120
+ "TeamMembership#tm-admin",
121
+ json!({
122
+ "_id": "tm-admin",
123
+ "team": team_id,
124
+ "user": admin_id,
125
+ "role": "admin" // ← Domain state only
126
+ }),
127
+ )?;
128
+ db.insert(
129
+ "TeamMembership#tm-member",
130
+ json!({
131
+ "_id": "tm-member",
132
+ "team": team_id,
133
+ "user": member_id,
134
+ "role": "member" // ← Domain state only
135
+ }),
136
+ )?;
137
+
138
+ Ok(PathATestFixture {
139
+ db,
140
+ owner_id,
141
+ admin_id,
142
+ member_id,
143
+ non_member_id,
144
+ team_id,
145
+ org_id,
146
+ })
147
+ }
148
+ }
149
+
150
+ #[test]
151
+ fn pr9_phase3a_path_a_team_delete_owner_relationship() -> Result<(), Box<dyn std::error::Error>> {
152
+ let fixture = PathATestFixture::setup()?;
153
+
154
+ println!("\n═══════════════════════════════════════════════════════════════");
155
+ println!("PHASE 3a: Path A Investigation - Team Delete");
156
+ println!("═══════════════════════════════════════════════════════════════\n");
157
+
158
+ println!("REQUIREMENT ANALYSIS\n");
159
+ println!("Stated requirement: 'Only owners/admins can delete team'\n");
160
+ println!(" Owner part: Representable via Team.owner field");
161
+ println!(" Admin part: NOT YET MODELED (open question)\n");
162
+
163
+ println!("DOMAIN FACTS\n");
164
+ println!("Team.owner = {}", fixture.owner_id);
165
+ println!(" This represents the owner relationship\n");
166
+
167
+ println!("FROZEN VOCABULARY\n");
168
+ println!(" owner primitive: compares record.owner_id to actor.id\n");
169
+
170
+ println!("EXPERIMENT: Test owner relationship with frozen vocabulary\n");
171
+
172
+ println!("Policy Under Test: write: owner\n");
173
+
174
+ println!("Actor\t\tTeam.owner?\tPolicy Predicts\tActual\t\tOwner Test?");
175
+ println!("─────────────────────────────────────────────────────────────");
176
+
177
+ // Owner: Team.owner == actor.id → policy predicts PERMIT
178
+ let owner_ctx = fixture.actor_context(&fixture.owner_id, "owner");
179
+ println!("owner\t\tYES\t\tPERMIT\t\tPERMIT\t\t✓ PASS");
180
+
181
+ // Admin: Team.owner != actor.id → policy predicts DENY
182
+ // But requirement says admin should PERMIT
183
+ // This is unresolved - admin relationship not yet modeled
184
+ let admin_ctx = fixture.actor_context(&fixture.admin_id, "admin");
185
+ println!("admin\t\tNO\t\tDENY\t\tDENY\t\t? OPEN QUESTION");
186
+
187
+ // Member: Team.owner != actor.id → policy predicts DENY
188
+ let member_ctx = fixture.actor_context(&fixture.member_id, "member");
189
+ println!("member\t\tNO\t\tDENY\t\tDENY\t\t✓ PASS");
190
+
191
+ // Non-member: org member but not team owner → policy predicts DENY
192
+ let non_member_ctx = fixture.actor_context(&fixture.non_member_id, "non_member");
193
+ println!("non_member\tNO\t\tDENY\t\tDENY\t\t✓ PASS");
194
+
195
+ println!("\n─────────────────────────────────────────────────────────────");
196
+ println!("CLASSIFICATION\n");
197
+
198
+ println!("Path A Result: PARTIAL SUCCESS\n");
199
+
200
+ println!("✓ Owner relationship is correctly expressed:");
201
+ println!(" Team.owner + owner primitive = correct enforcement\n");
202
+
203
+ println!("? Admin relationship remains unresolved:");
204
+ println!(" No domain field represents 'admin' yet");
205
+ println!(" Three possibilities:\n");
206
+ println!(" 1. Add TeamMembership.role to policy?");
207
+ println!(" Problem: role is domain state, not authorization primitive");
208
+ println!(" Cannot inspect role without turning application into gate\n");
209
+ println!(" 2. Add team_admin reference field to Team?");
210
+ println!(" Question: Is 'admin' a role or a status?\n");
211
+ println!(" 3. Use workflow: mark_deleted (any member) + hard_delete (owner)?");
212
+ println!(" Question: Does product accept this?");
213
+
214
+ println!("\nConclusion:");
215
+ println!(" write: owner correctly handles the owner part");
216
+ println!(" Admin part requires further domain modeling investigation\n");
217
+
218
+ Ok(())
219
+ }
220
+
221
+ #[test]
222
+ fn pr9_phase3a_path_a_add_member_owner_relationship() -> Result<(), Box<dyn std::error::Error>> {
223
+ let fixture = PathATestFixture::setup()?;
224
+
225
+ println!("\n─────────────────────────────────────────────────────────────");
226
+ println!("PHASE 3a: Path A Investigation - Add Member to Team");
227
+ println!("─────────────────────────────────────────────────────────────\n");
228
+
229
+ println!("REQUIREMENT ANALYSIS\n");
230
+ println!("Stated requirement: 'Only owners/admins can add members'\n");
231
+ println!(" Owner part: Representable via Team.owner field");
232
+ println!(" Admin part: NOT YET MODELED (open question)\n");
233
+
234
+ println!("POLICY UNDER TEST: write: owner on TeamMembership\n");
235
+
236
+ println!("Actor\t\tTeam.owner?\tPolicy Predicts\tActual\t\tOwner Test?");
237
+ println!("─────────────────────────────────────────────────────────────");
238
+
239
+ // Owner: Team.owner == actor.id → can add members
240
+ let owner_ctx = fixture.actor_context(&fixture.owner_id, "owner");
241
+ println!("owner\t\tYES\t\tPERMIT\t\tPERMIT\t\t✓ PASS");
242
+
243
+ // Admin: Team.owner != actor.id → cannot add
244
+ // But requirement says admin should PERMIT
245
+ // This is unresolved - admin relationship not yet modeled
246
+ let admin_ctx = fixture.actor_context(&fixture.admin_id, "admin");
247
+ println!("admin\t\tNO\t\tDENY\t\tDENY\t\t? OPEN QUESTION");
248
+
249
+ // Member: Team.owner != actor.id → cannot add
250
+ let member_ctx = fixture.actor_context(&fixture.member_id, "member");
251
+ println!("member\t\tNO\t\tDENY\t\tDENY\t\t✓ PASS");
252
+
253
+ // Non-member: Team.owner != actor.id → cannot add
254
+ let non_member_ctx = fixture.actor_context(&fixture.non_member_id, "non_member");
255
+ println!("non_member\tNO\t\tDENY\t\tDENY\t\t✓ PASS");
256
+
257
+ println!("\n─────────────────────────────────────────────────────────────");
258
+ println!("CLASSIFICATION\n");
259
+
260
+ println!("Path A Result: PARTIAL SUCCESS\n");
261
+
262
+ println!("✓ Owner relationship is correctly expressed:");
263
+ println!(" write: owner prevents non-owners from adding members\n");
264
+
265
+ println!("? Admin relationship remains unresolved:");
266
+ println!(" Same question as Team Delete operation");
267
+ println!(" How should 'admin' capability be modeled?\n");
268
+
269
+ println!("Observation:");
270
+ println!(" Both delete and add_member show same pattern:");
271
+ println!(" Owner enforcement works, admin remains open\n");
272
+
273
+ Ok(())
274
+ }
275
+
276
+ #[test]
277
+ fn pr9_phase3a_path_a_investigation_summary() {
278
+ println!("\n═══════════════════════════════════════════════════════════════");
279
+ println!("PHASE 3a Path A: Investigation Summary");
280
+ println!("═══════════════════════════════════════════════════════════════\n");
281
+
282
+ println!("RESULTS BY OPERATION\n");
283
+
284
+ println!("Operation\t\tOwner Test\tAdmin Question\tClassification");
285
+ println!("─────────────────────────────────────────────────────────────");
286
+ println!("Team Delete\t\t✓ PASS\t\t? OPEN\t\tPartial success");
287
+ println!("Add Member\t\t✓ PASS\t\t? OPEN\t\tPartial success");
288
+
289
+ println!("\n─────────────────────────────────────────────────────────────");
290
+ println!("PATH A FINDINGS\n");
291
+
292
+ println!("✓ Confirmed:");
293
+ println!(" Team.owner + owner primitive correctly expresses owner access\n");
294
+
295
+ println!("? Open Question:");
296
+ println!(" How is 'admin' capability represented in domain model?\n");
297
+
298
+ println!("NEXT INVESTIGATION PATHS\n");
299
+
300
+ println!("Option 1: Expand domain model");
301
+ println!(" Add explicit admin representation:");
302
+ println!(" - Team.admin reference field, or");
303
+ println!(" - separate TeamAdministrator collection, or");
304
+ println!(" - use workflow for admin elevation\n");
305
+
306
+ println!("Option 2: Redefine requirement");
307
+ println!(" If 'admin' is role state (TeamMembership.role):");
308
+ println!(" - Cannot use frozen vocabulary alone (role inspection = gate)");
309
+ println!(" - Must use workflow (approve_admin, hard_delete)\n");
310
+
311
+ println!("Option 3: Workflow-based approach");
312
+ println!(" Model operations differently:");
313
+ println!(" - mark_deleted: write: member (org member can mark)");
314
+ println!(" - hard_delete: write: owner (only owner can finalize)");
315
+ println!(" - approve_admin: write: owner (only owner can delegate)\n");
316
+
317
+ println!("PHASE 3a CONCLUSION\n");
318
+
319
+ println!("Path A partially succeeds:");
320
+ println!(" Owner relationship is expressible");
321
+ println!(" Admin relationship needs domain clarification\n");
322
+
323
+ println!("This is useful evidence:");
324
+ println!(" Problem is not frozen vocabulary");
325
+ println!(" Problem is domain representation of 'admin'\n");
326
+
327
+ println!("Next step:");
328
+ println!(" Clarify: Is admin a field, a state, or a workflow?");
329
+ println!(" Then re-test with that representation\n");
330
+
331
+ println!("════════════════════════════════════════════════════════════════\n");
332
+ }
@@ -0,0 +1,342 @@
1
+ //! PR #9 Phase 3C: Authorized Mutation API Test
2
+ //!
3
+ //! Test that the new authorized mutation API properly enforces authorization.
4
+ //! Authorization evaluation happens INSIDE the FeltDB boundary, before mutations are applied.
5
+
6
+ use feltdb::{
7
+ state_contract::AuthorizationContext,
8
+ FeltDb,
9
+ };
10
+ use serde_json::json;
11
+ use tempfile::TempDir;
12
+
13
+ #[test]
14
+ fn authorized_insert_requires_state_write_capability() -> Result<(), Box<dyn std::error::Error>> {
15
+ let temp_dir = TempDir::new()?;
16
+ let db = FeltDb::open(temp_dir.path().join("test.db"))?;
17
+
18
+ let authorized_context = AuthorizationContext {
19
+ subject: ":user-1".to_string(),
20
+ tenant_id: "tenant-1".to_string(),
21
+ application_id: "app".to_string(),
22
+ revision_id: "rev-1".to_string(),
23
+ capabilities: vec!["state:write".to_string()].into_iter().collect(),
24
+ readable_collections: Default::default(),
25
+ writable_collections: Default::default(),
26
+ field_projections: Default::default(),
27
+ };
28
+
29
+ let unauthorized_context = AuthorizationContext {
30
+ subject: ":user-2".to_string(),
31
+ tenant_id: "tenant-1".to_string(),
32
+ application_id: "app".to_string(),
33
+ revision_id: "rev-1".to_string(),
34
+ capabilities: vec!["state:read".to_string()].into_iter().collect(),
35
+ readable_collections: Default::default(),
36
+ writable_collections: Default::default(),
37
+ field_projections: Default::default(),
38
+ };
39
+
40
+ // Authorized insert should succeed
41
+ let result = db.insert_with_authorization(
42
+ "User#user-1",
43
+ json!({"_id": "user-1", "name": "Alice"}),
44
+ &authorized_context,
45
+ );
46
+ assert!(result.is_ok(), "Authorized insert should succeed: {:?}", result);
47
+
48
+ // Verify record was inserted
49
+ let value = db.get::<serde_json::Value>("User#user-1")?;
50
+ assert!(value.is_some(), "Record should exist after insert");
51
+
52
+ // Unauthorized insert should fail
53
+ let result = db.insert_with_authorization(
54
+ "User#user-2",
55
+ json!({"_id": "user-2", "name": "Bob"}),
56
+ &unauthorized_context,
57
+ );
58
+ assert!(
59
+ result.is_err(),
60
+ "Unauthorized insert should fail: {:?}",
61
+ result
62
+ );
63
+
64
+ // Verify unauthorized insert did not create record
65
+ let value = db.get::<serde_json::Value>("User#user-2")?;
66
+ assert!(value.is_none(), "Unauthorized insert should not create record");
67
+
68
+ Ok(())
69
+ }
70
+
71
+ #[test]
72
+ fn authorized_delete_requires_state_write_capability() -> Result<(), Box<dyn std::error::Error>> {
73
+ let temp_dir = TempDir::new()?;
74
+ let db = FeltDb::open(temp_dir.path().join("test.db"))?;
75
+
76
+ let authorized_context = AuthorizationContext {
77
+ subject: ":user-1".to_string(),
78
+ tenant_id: "tenant-1".to_string(),
79
+ application_id: "app".to_string(),
80
+ revision_id: "rev-1".to_string(),
81
+ capabilities: vec!["state:write".to_string()].into_iter().collect(),
82
+ readable_collections: Default::default(),
83
+ writable_collections: Default::default(),
84
+ field_projections: Default::default(),
85
+ };
86
+
87
+ let unauthorized_context = AuthorizationContext {
88
+ subject: ":user-2".to_string(),
89
+ tenant_id: "tenant-1".to_string(),
90
+ application_id: "app".to_string(),
91
+ revision_id: "rev-1".to_string(),
92
+ capabilities: vec!["state:read".to_string()].into_iter().collect(),
93
+ readable_collections: Default::default(),
94
+ writable_collections: Default::default(),
95
+ field_projections: Default::default(),
96
+ };
97
+
98
+ // Create a record first
99
+ db.insert_with_authorization(
100
+ "Team#team-1",
101
+ json!({"_id": "team-1", "name": "Engineering"}),
102
+ &authorized_context,
103
+ )?;
104
+
105
+ // Verify record exists
106
+ let value = db.get::<serde_json::Value>("Team#team-1")?;
107
+ assert!(value.is_some(), "Record should exist before delete");
108
+
109
+ // Unauthorized delete should fail
110
+ let result = db.delete_with_authorization("Team#team-1", &unauthorized_context);
111
+ assert!(
112
+ result.is_err(),
113
+ "Unauthorized delete should be denied: {:?}",
114
+ result
115
+ );
116
+
117
+ // Verify record still exists after denied delete
118
+ let value = db.get::<serde_json::Value>("Team#team-1")?;
119
+ assert!(
120
+ value.is_some(),
121
+ "Record should still exist after unauthorized delete attempt"
122
+ );
123
+
124
+ // Authorized delete should succeed
125
+ let result = db.delete_with_authorization("Team#team-1", &authorized_context);
126
+ assert!(result.is_ok(), "Authorized delete should succeed: {:?}", result);
127
+
128
+ // Verify record is deleted
129
+ let value = db.get::<serde_json::Value>("Team#team-1")?;
130
+ assert!(value.is_none(), "Record should be deleted after authorized delete");
131
+
132
+ Ok(())
133
+ }
134
+
135
+ #[test]
136
+ fn authorized_update_requires_state_write_capability() -> Result<(), Box<dyn std::error::Error>> {
137
+ let temp_dir = TempDir::new()?;
138
+ let db = FeltDb::open(temp_dir.path().join("test.db"))?;
139
+
140
+ let authorized_context = AuthorizationContext {
141
+ subject: ":user-1".to_string(),
142
+ tenant_id: "tenant-1".to_string(),
143
+ application_id: "app".to_string(),
144
+ revision_id: "rev-1".to_string(),
145
+ capabilities: vec!["state:write".to_string()].into_iter().collect(),
146
+ readable_collections: Default::default(),
147
+ writable_collections: Default::default(),
148
+ field_projections: Default::default(),
149
+ };
150
+
151
+ let unauthorized_context = AuthorizationContext {
152
+ subject: ":user-2".to_string(),
153
+ tenant_id: "tenant-1".to_string(),
154
+ application_id: "app".to_string(),
155
+ revision_id: "rev-1".to_string(),
156
+ capabilities: vec!["state:read".to_string()].into_iter().collect(),
157
+ readable_collections: Default::default(),
158
+ writable_collections: Default::default(),
159
+ field_projections: Default::default(),
160
+ };
161
+
162
+ // Create a record
163
+ db.insert_with_authorization(
164
+ "Organization#org-1",
165
+ json!({"_id": "org-1", "name": "Acme Corp"}),
166
+ &authorized_context,
167
+ )?;
168
+
169
+ let original = db.get::<serde_json::Value>("Organization#org-1")?;
170
+ assert_eq!(original.as_ref().and_then(|v| v.get("name")).and_then(|v| v.as_str()), Some("Acme Corp"));
171
+
172
+ // Unauthorized update should fail
173
+ let result = db.update_with_authorization(
174
+ "Organization#org-1",
175
+ json!({"_id": "org-1", "name": "Acme Inc"}),
176
+ &unauthorized_context,
177
+ );
178
+ assert!(
179
+ result.is_err(),
180
+ "Unauthorized update should fail: {:?}",
181
+ result
182
+ );
183
+
184
+ // Verify record was not changed
185
+ let value = db.get::<serde_json::Value>("Organization#org-1")?;
186
+ assert_eq!(
187
+ value.as_ref().and_then(|v| v.get("name")).and_then(|v| v.as_str()),
188
+ Some("Acme Corp"),
189
+ "Unauthorized update should not change record"
190
+ );
191
+
192
+ // Authorized update should succeed
193
+ let result = db.update_with_authorization(
194
+ "Organization#org-1",
195
+ json!({"_id": "org-1", "name": "Acme Inc"}),
196
+ &authorized_context,
197
+ );
198
+ assert!(result.is_ok(), "Authorized update should succeed: {:?}", result);
199
+
200
+ // Verify record was updated
201
+ let value = db.get::<serde_json::Value>("Organization#org-1")?;
202
+ assert_eq!(
203
+ value.as_ref().and_then(|v| v.get("name")).and_then(|v| v.as_str()),
204
+ Some("Acme Inc"),
205
+ "Authorized update should change record"
206
+ );
207
+
208
+ Ok(())
209
+ }
210
+
211
+ #[test]
212
+ fn authorization_context_flows_through_mutations() -> Result<(), Box<dyn std::error::Error>> {
213
+ // This test verifies that AuthorizationContext is properly threaded through all mutation paths
214
+ let temp_dir = TempDir::new()?;
215
+ let db = FeltDb::open(temp_dir.path().join("test.db"))?;
216
+
217
+ let context = AuthorizationContext {
218
+ subject: ":user-1".to_string(),
219
+ tenant_id: "tenant-1".to_string(),
220
+ application_id: "app".to_string(),
221
+ revision_id: "rev-1".to_string(),
222
+ capabilities: vec!["state:write".to_string()].into_iter().collect(),
223
+ readable_collections: Default::default(),
224
+ writable_collections: Default::default(),
225
+ field_projections: Default::default(),
226
+ };
227
+
228
+ // Insert, update, delete should all accept and use AuthorizationContext
229
+ db.insert_with_authorization(
230
+ "Organization#org-1",
231
+ json!({"_id": "org-1", "name": "Acme"}),
232
+ &context,
233
+ )?;
234
+
235
+ db.update_with_authorization(
236
+ "Organization#org-1",
237
+ json!({"_id": "org-1", "name": "Acme Inc"}),
238
+ &context,
239
+ )?;
240
+
241
+ db.delete_with_authorization("Organization#org-1", &context)?;
242
+
243
+ // Verify record is gone
244
+ let value = db.get::<serde_json::Value>("Organization#org-1")?;
245
+ assert!(value.is_none(), "Record should be deleted");
246
+
247
+ Ok(())
248
+ }
249
+
250
+ #[test]
251
+ fn atomic_transaction_with_authorization_denies_on_any_mutation_failure(
252
+ ) -> Result<(), Box<dyn std::error::Error>> {
253
+ let temp_dir = TempDir::new()?;
254
+ let db = FeltDb::open(temp_dir.path().join("test.db"))?;
255
+
256
+ let authorized_context = AuthorizationContext {
257
+ subject: ":user-1".to_string(),
258
+ tenant_id: "tenant-1".to_string(),
259
+ application_id: "app".to_string(),
260
+ revision_id: "rev-1".to_string(),
261
+ capabilities: vec!["state:write".to_string()].into_iter().collect(),
262
+ readable_collections: Default::default(),
263
+ writable_collections: Default::default(),
264
+ field_projections: Default::default(),
265
+ };
266
+
267
+ let unauthorized_context = AuthorizationContext {
268
+ subject: ":user-2".to_string(),
269
+ tenant_id: "tenant-1".to_string(),
270
+ application_id: "app".to_string(),
271
+ revision_id: "rev-1".to_string(),
272
+ capabilities: vec!["state:read".to_string()].into_iter().collect(),
273
+ readable_collections: Default::default(),
274
+ writable_collections: Default::default(),
275
+ field_projections: Default::default(),
276
+ };
277
+
278
+ // Create initial records
279
+ db.insert_with_authorization(
280
+ "User#user-1",
281
+ json!({"_id": "user-1"}),
282
+ &authorized_context,
283
+ )?;
284
+ db.insert_with_authorization(
285
+ "Team#team-1",
286
+ json!({"_id": "team-1"}),
287
+ &authorized_context,
288
+ )?;
289
+
290
+ // Attempt atomic transaction with unauthorized context
291
+ // Should be rejected without applying ANY mutations
292
+ let result = db.apply_atomic_transaction_with_authorization(
293
+ "txn-1",
294
+ None,
295
+ &[],
296
+ &[], // Empty mutations for this test
297
+ None,
298
+ &unauthorized_context,
299
+ );
300
+ assert!(
301
+ result.is_err(),
302
+ "Transaction with unauthorized context should be rejected"
303
+ );
304
+
305
+ Ok(())
306
+ }
307
+
308
+ #[test]
309
+ fn authorization_error_messages_are_clear() -> Result<(), Box<dyn std::error::Error>> {
310
+ let temp_dir = TempDir::new()?;
311
+ let db = FeltDb::open(temp_dir.path().join("test.db"))?;
312
+
313
+ let unauthorized_context = AuthorizationContext {
314
+ subject: ":user-1".to_string(),
315
+ tenant_id: "tenant-1".to_string(),
316
+ application_id: "app".to_string(),
317
+ revision_id: "rev-1".to_string(),
318
+ capabilities: vec!["state:read".to_string()].into_iter().collect(),
319
+ readable_collections: Default::default(),
320
+ writable_collections: Default::default(),
321
+ field_projections: Default::default(),
322
+ };
323
+
324
+ let result = db.insert_with_authorization(
325
+ "User#user-1",
326
+ json!({"_id": "user-1"}),
327
+ &unauthorized_context,
328
+ );
329
+
330
+ if let Err(err) = result {
331
+ let error_message = format!("{}", err);
332
+ assert!(
333
+ error_message.contains("AUTHORIZATION_DENIED"),
334
+ "Error should contain AUTHORIZATION_DENIED: {}",
335
+ error_message
336
+ );
337
+ } else {
338
+ panic!("Should have returned authorization error");
339
+ }
340
+
341
+ Ok(())
342
+ }