Skip to main content

dreamwell_engine/
authority.rs

1// Authorship ownership model — authority levels, permissions, content licensing,
2// GDPR export manifests, and revenue share configuration.
3// Pure Rust, no SpacetimeDB dependency.
4
5use crate::hash::fnv1a_64;
6use serde::{Deserialize, Serialize};
7
8// =============================================================================
9// AUTHORITY MODE
10// =============================================================================
11
12/// Which authority backend drives canonical state.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum AuthorityMode {
15    /// Single-player: local reducer execution, no network.
16    Local,
17    /// Multiplayer: SpacetimeDB server is authority.
18    Remote,
19}
20
21// =============================================================================
22// SIMULATION TIER
23// =============================================================================
24
25/// Fidelity tier for simulation updates.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub enum SimulationTier {
28    /// Server-authoritative truth (reducers execute here).
29    Canonical,
30    /// Client-side prediction (rolled back on mismatch).
31    Predicted,
32    /// Visual-only interpolation (no gameplay effect).
33    PresentationOnly,
34}
35
36// =============================================================================
37// AUTHORITY LEVEL
38// =============================================================================
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum AuthorityLevel {
42    Observer,
43    Delegate,
44    Controller,
45    Author,
46    Owner,
47    Steward,
48    System,
49}
50
51impl AuthorityLevel {
52    pub fn rank(&self) -> u8 {
53        match self {
54            Self::Observer => 0,
55            Self::Delegate => 1,
56            Self::Controller => 2,
57            Self::Author => 3,
58            Self::Owner => 4,
59            Self::Steward => 5,
60            Self::System => 6,
61        }
62    }
63
64    pub fn can_perform(&self, required: AuthorityLevel) -> bool {
65        self.rank() >= required.rank()
66    }
67}
68
69impl Ord for AuthorityLevel {
70    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
71        self.rank().cmp(&other.rank())
72    }
73}
74
75impl PartialOrd for AuthorityLevel {
76    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
77        Some(self.cmp(other))
78    }
79}
80
81// =============================================================================
82// AUTHOR PERMISSION
83// =============================================================================
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum AuthorPermission {
87    Export,
88    Rescind,
89    Delete,
90    TransferAuthorship,
91    ViewRevenue,
92    WithdrawRevenue,
93    ListContent,
94    EditMetadata,
95}
96
97// =============================================================================
98// CONTENT TYPE
99// =============================================================================
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102pub enum ContentType {
103    Waymark,
104    Entity,
105    Item,
106    Quest,
107    Dialogue,
108    Map,
109    Script,
110    Asset,
111    Tileset,
112    Lore,
113    Recipe,
114    Encounter,
115}
116
117impl ContentType {
118    pub fn label(&self) -> &'static str {
119        match self {
120            Self::Waymark => "waymark",
121            Self::Entity => "entity",
122            Self::Item => "item",
123            Self::Quest => "quest",
124            Self::Dialogue => "dialogue",
125            Self::Map => "map",
126            Self::Script => "script",
127            Self::Asset => "asset",
128            Self::Tileset => "tileset",
129            Self::Lore => "lore",
130            Self::Recipe => "recipe",
131            Self::Encounter => "encounter",
132        }
133    }
134}
135
136// =============================================================================
137// CONTENT LICENSE
138// =============================================================================
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141pub enum ContentLicense {
142    AllRightsReserved,
143    CreativeCommons,
144    RevenueShare,
145    OpenSource,
146    Custom,
147}
148
149impl ContentLicense {
150    pub fn label(&self) -> &'static str {
151        match self {
152            Self::AllRightsReserved => "all-rights-reserved",
153            Self::CreativeCommons => "creative-commons",
154            Self::RevenueShare => "revenue-share",
155            Self::OpenSource => "open-source",
156            Self::Custom => "custom",
157        }
158    }
159
160    pub fn allows_redistribution(&self) -> bool {
161        matches!(self, Self::CreativeCommons | Self::RevenueShare | Self::OpenSource)
162    }
163}
164
165// =============================================================================
166// PERMISSION GATES
167// =============================================================================
168
169pub fn min_level_for_permission(perm: AuthorPermission) -> AuthorityLevel {
170    match perm {
171        AuthorPermission::Export => AuthorityLevel::Author,
172        AuthorPermission::ListContent => AuthorityLevel::Author,
173        AuthorPermission::ViewRevenue => AuthorityLevel::Author,
174        AuthorPermission::Rescind => AuthorityLevel::Author,
175        AuthorPermission::Delete => AuthorityLevel::Author,
176        AuthorPermission::EditMetadata => AuthorityLevel::Author,
177        AuthorPermission::TransferAuthorship => AuthorityLevel::Owner,
178        AuthorPermission::WithdrawRevenue => AuthorityLevel::Steward,
179    }
180}
181
182pub fn validate_authority(required: AuthorityLevel, actual: AuthorityLevel) -> bool {
183    actual.can_perform(required)
184}
185
186pub fn can_author_action(perm: AuthorPermission, level: AuthorityLevel) -> bool {
187    validate_authority(min_level_for_permission(perm), level)
188}
189
190// =============================================================================
191// GDPR EXPORT MANIFEST
192// =============================================================================
193
194#[derive(Debug, Clone, PartialEq)]
195pub struct GdprExportManifest {
196    pub author_identity: String,
197    pub export_timestamp_ms: u64,
198    pub content_count: u32,
199    pub digest: String,
200}
201
202impl GdprExportManifest {
203    pub fn compute_digest(&mut self) {
204        let input = format!(
205            "gdpr:{}:{}:{}",
206            self.author_identity, self.export_timestamp_ms, self.content_count,
207        );
208        self.digest = format!("{:016x}", fnv1a_64(input.as_bytes()));
209    }
210}
211
212// =============================================================================
213// REVENUE SHARE CONFIG
214// =============================================================================
215
216#[derive(Debug, Clone, PartialEq)]
217pub struct RevenueShareConfig {
218    pub author_share_bps: u16,
219    pub steward_share_bps: u16,
220    pub platform_share_bps: u16,
221}
222
223impl RevenueShareConfig {
224    pub fn validate(&self) -> Result<(), String> {
225        if self.author_share_bps > 10000 {
226            return Err(format!(
227                "revenue_share_component_overflow:author {} bps exceeds 10000",
228                self.author_share_bps
229            ));
230        }
231        if self.steward_share_bps > 10000 {
232            return Err(format!(
233                "revenue_share_component_overflow:steward {} bps exceeds 10000",
234                self.steward_share_bps
235            ));
236        }
237        if self.platform_share_bps > 10000 {
238            return Err(format!(
239                "revenue_share_component_overflow:platform {} bps exceeds 10000",
240                self.platform_share_bps
241            ));
242        }
243        let total = self.author_share_bps as u32 + self.steward_share_bps as u32 + self.platform_share_bps as u32;
244        if total > 10000 {
245            return Err(format!("revenue_share_overflow:total {} bps exceeds 10000", total));
246        }
247        Ok(())
248    }
249
250    pub fn author_pct(&self) -> f64 {
251        self.author_share_bps as f64 / 100.0
252    }
253}
254
255// =============================================================================
256// TESTS
257// =============================================================================
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    // -- AuthorityLevel ordering ----------------------------------------------
264
265    #[test]
266    fn authority_level_rank_values() {
267        assert_eq!(AuthorityLevel::Observer.rank(), 0);
268        assert_eq!(AuthorityLevel::Delegate.rank(), 1);
269        assert_eq!(AuthorityLevel::Controller.rank(), 2);
270        assert_eq!(AuthorityLevel::Author.rank(), 3);
271        assert_eq!(AuthorityLevel::Owner.rank(), 4);
272        assert_eq!(AuthorityLevel::Steward.rank(), 5);
273        assert_eq!(AuthorityLevel::System.rank(), 6);
274    }
275
276    #[test]
277    fn authority_level_ord_ascending() {
278        let levels = [
279            AuthorityLevel::Observer,
280            AuthorityLevel::Delegate,
281            AuthorityLevel::Controller,
282            AuthorityLevel::Author,
283            AuthorityLevel::Owner,
284            AuthorityLevel::Steward,
285            AuthorityLevel::System,
286        ];
287        for i in 1..levels.len() {
288            assert!(
289                levels[i] > levels[i - 1],
290                "{:?} should be > {:?}",
291                levels[i],
292                levels[i - 1]
293            );
294        }
295    }
296
297    #[test]
298    fn authority_level_can_perform_same() {
299        assert!(AuthorityLevel::Author.can_perform(AuthorityLevel::Author));
300    }
301
302    #[test]
303    fn authority_level_can_perform_higher() {
304        assert!(AuthorityLevel::System.can_perform(AuthorityLevel::Observer));
305        assert!(AuthorityLevel::Owner.can_perform(AuthorityLevel::Author));
306    }
307
308    #[test]
309    fn authority_level_cannot_perform_lower() {
310        assert!(!AuthorityLevel::Observer.can_perform(AuthorityLevel::Author));
311        assert!(!AuthorityLevel::Delegate.can_perform(AuthorityLevel::Owner));
312    }
313
314    // -- Permission gates -----------------------------------------------------
315
316    #[test]
317    fn permission_export_requires_author() {
318        assert_eq!(
319            min_level_for_permission(AuthorPermission::Export),
320            AuthorityLevel::Author
321        );
322    }
323
324    #[test]
325    fn permission_list_content_requires_author() {
326        assert_eq!(
327            min_level_for_permission(AuthorPermission::ListContent),
328            AuthorityLevel::Author
329        );
330    }
331
332    #[test]
333    fn permission_view_revenue_requires_author() {
334        assert_eq!(
335            min_level_for_permission(AuthorPermission::ViewRevenue),
336            AuthorityLevel::Author
337        );
338    }
339
340    #[test]
341    fn permission_rescind_requires_author() {
342        assert_eq!(
343            min_level_for_permission(AuthorPermission::Rescind),
344            AuthorityLevel::Author
345        );
346    }
347
348    #[test]
349    fn permission_delete_requires_author() {
350        assert_eq!(
351            min_level_for_permission(AuthorPermission::Delete),
352            AuthorityLevel::Author
353        );
354    }
355
356    #[test]
357    fn permission_edit_metadata_requires_author() {
358        assert_eq!(
359            min_level_for_permission(AuthorPermission::EditMetadata),
360            AuthorityLevel::Author
361        );
362    }
363
364    #[test]
365    fn permission_transfer_authorship_requires_owner() {
366        assert_eq!(
367            min_level_for_permission(AuthorPermission::TransferAuthorship),
368            AuthorityLevel::Owner,
369        );
370    }
371
372    #[test]
373    fn permission_withdraw_revenue_requires_steward() {
374        assert_eq!(
375            min_level_for_permission(AuthorPermission::WithdrawRevenue),
376            AuthorityLevel::Steward,
377        );
378    }
379
380    #[test]
381    fn can_author_action_author_exports() {
382        assert!(can_author_action(AuthorPermission::Export, AuthorityLevel::Author));
383        assert!(can_author_action(AuthorPermission::Export, AuthorityLevel::Owner));
384        assert!(can_author_action(AuthorPermission::Export, AuthorityLevel::System));
385        assert!(!can_author_action(AuthorPermission::Export, AuthorityLevel::Controller));
386    }
387
388    #[test]
389    fn can_author_action_owner_transfers() {
390        assert!(can_author_action(
391            AuthorPermission::TransferAuthorship,
392            AuthorityLevel::Owner
393        ));
394        assert!(can_author_action(
395            AuthorPermission::TransferAuthorship,
396            AuthorityLevel::System
397        ));
398        assert!(!can_author_action(
399            AuthorPermission::TransferAuthorship,
400            AuthorityLevel::Author
401        ));
402    }
403
404    #[test]
405    fn can_author_action_steward_withdraws() {
406        assert!(can_author_action(
407            AuthorPermission::WithdrawRevenue,
408            AuthorityLevel::Steward
409        ));
410        assert!(can_author_action(
411            AuthorPermission::WithdrawRevenue,
412            AuthorityLevel::System
413        ));
414        assert!(!can_author_action(
415            AuthorPermission::WithdrawRevenue,
416            AuthorityLevel::Owner
417        ));
418    }
419
420    #[test]
421    fn validate_authority_function() {
422        assert!(validate_authority(AuthorityLevel::Author, AuthorityLevel::Owner));
423        assert!(validate_authority(AuthorityLevel::Observer, AuthorityLevel::Observer));
424        assert!(!validate_authority(AuthorityLevel::System, AuthorityLevel::Steward));
425    }
426
427    // -- ContentType labels ---------------------------------------------------
428
429    #[test]
430    fn content_type_labels() {
431        assert_eq!(ContentType::Waymark.label(), "waymark");
432        assert_eq!(ContentType::Entity.label(), "entity");
433        assert_eq!(ContentType::Item.label(), "item");
434        assert_eq!(ContentType::Quest.label(), "quest");
435        assert_eq!(ContentType::Dialogue.label(), "dialogue");
436        assert_eq!(ContentType::Map.label(), "map");
437        assert_eq!(ContentType::Script.label(), "script");
438        assert_eq!(ContentType::Asset.label(), "asset");
439        assert_eq!(ContentType::Tileset.label(), "tileset");
440        assert_eq!(ContentType::Lore.label(), "lore");
441        assert_eq!(ContentType::Recipe.label(), "recipe");
442        assert_eq!(ContentType::Encounter.label(), "encounter");
443    }
444
445    // -- ContentLicense -------------------------------------------------------
446
447    #[test]
448    fn content_license_labels() {
449        assert_eq!(ContentLicense::AllRightsReserved.label(), "all-rights-reserved");
450        assert_eq!(ContentLicense::CreativeCommons.label(), "creative-commons");
451        assert_eq!(ContentLicense::RevenueShare.label(), "revenue-share");
452        assert_eq!(ContentLicense::OpenSource.label(), "open-source");
453        assert_eq!(ContentLicense::Custom.label(), "custom");
454    }
455
456    #[test]
457    fn content_license_redistribution() {
458        assert!(!ContentLicense::AllRightsReserved.allows_redistribution());
459        assert!(ContentLicense::CreativeCommons.allows_redistribution());
460        assert!(ContentLicense::RevenueShare.allows_redistribution());
461        assert!(ContentLicense::OpenSource.allows_redistribution());
462        assert!(!ContentLicense::Custom.allows_redistribution());
463    }
464
465    // -- RevenueShareConfig ---------------------------------------------------
466
467    #[test]
468    fn revenue_share_valid() {
469        let cfg = RevenueShareConfig {
470            author_share_bps: 7000,
471            steward_share_bps: 2000,
472            platform_share_bps: 1000,
473        };
474        assert!(cfg.validate().is_ok());
475    }
476
477    #[test]
478    fn revenue_share_overflow() {
479        let cfg = RevenueShareConfig {
480            author_share_bps: 8000,
481            steward_share_bps: 2000,
482            platform_share_bps: 1000,
483        };
484        let err = cfg.validate().unwrap_err();
485        assert!(err.contains("revenue_share_overflow"));
486    }
487
488    #[test]
489    fn revenue_share_zero() {
490        let cfg = RevenueShareConfig {
491            author_share_bps: 0,
492            steward_share_bps: 0,
493            platform_share_bps: 0,
494        };
495        assert!(cfg.validate().is_ok());
496    }
497
498    #[test]
499    fn revenue_share_exact_cap() {
500        let cfg = RevenueShareConfig {
501            author_share_bps: 10000,
502            steward_share_bps: 0,
503            platform_share_bps: 0,
504        };
505        assert!(cfg.validate().is_ok());
506    }
507
508    #[test]
509    fn revenue_share_author_pct() {
510        let cfg = RevenueShareConfig {
511            author_share_bps: 7000,
512            steward_share_bps: 2000,
513            platform_share_bps: 1000,
514        };
515        assert!((cfg.author_pct() - 70.0).abs() < f64::EPSILON);
516    }
517
518    #[test]
519    fn revenue_share_author_pct_zero() {
520        let cfg = RevenueShareConfig {
521            author_share_bps: 0,
522            steward_share_bps: 0,
523            platform_share_bps: 0,
524        };
525        assert!((cfg.author_pct() - 0.0).abs() < f64::EPSILON);
526    }
527
528    // -- GdprExportManifest ---------------------------------------------------
529
530    #[test]
531    fn gdpr_manifest_digest_deterministic() {
532        let mut a = GdprExportManifest {
533            author_identity: "id:abc123".to_string(),
534            export_timestamp_ms: 1700000000000,
535            content_count: 42,
536            digest: String::new(),
537        };
538        let mut b = a.clone();
539        a.compute_digest();
540        b.compute_digest();
541        assert_eq!(a.digest, b.digest);
542        assert!(!a.digest.is_empty());
543    }
544
545    #[test]
546    fn gdpr_manifest_digest_is_hex() {
547        let mut m = GdprExportManifest {
548            author_identity: "id:test".to_string(),
549            export_timestamp_ms: 1000,
550            content_count: 5,
551            digest: String::new(),
552        };
553        m.compute_digest();
554        assert_eq!(m.digest.len(), 16);
555        assert!(m.digest.chars().all(|c| c.is_ascii_hexdigit()));
556    }
557
558    #[test]
559    fn gdpr_manifest_digest_changes_with_identity() {
560        let mut a = GdprExportManifest {
561            author_identity: "id:alice".to_string(),
562            export_timestamp_ms: 1000,
563            content_count: 1,
564            digest: String::new(),
565        };
566        let mut b = GdprExportManifest {
567            author_identity: "id:bob".to_string(),
568            export_timestamp_ms: 1000,
569            content_count: 1,
570            digest: String::new(),
571        };
572        a.compute_digest();
573        b.compute_digest();
574        assert_ne!(a.digest, b.digest);
575    }
576
577    // -- AuthorityMode --------------------------------------------------------
578
579    #[test]
580    fn authority_mode_eq() {
581        assert_eq!(AuthorityMode::Local, AuthorityMode::Local);
582        assert_eq!(AuthorityMode::Remote, AuthorityMode::Remote);
583        assert_ne!(AuthorityMode::Local, AuthorityMode::Remote);
584    }
585
586    #[test]
587    fn authority_mode_serde_roundtrip() {
588        let mode = AuthorityMode::Remote;
589        let json = serde_json::to_string(&mode).unwrap();
590        let restored: AuthorityMode = serde_json::from_str(&json).unwrap();
591        assert_eq!(mode, restored);
592    }
593
594    #[test]
595    fn authority_mode_debug() {
596        assert_eq!(format!("{:?}", AuthorityMode::Local), "Local");
597        assert_eq!(format!("{:?}", AuthorityMode::Remote), "Remote");
598    }
599
600    // -- SimulationTier -------------------------------------------------------
601
602    #[test]
603    fn simulation_tier_eq() {
604        assert_eq!(SimulationTier::Canonical, SimulationTier::Canonical);
605        assert_eq!(SimulationTier::Predicted, SimulationTier::Predicted);
606        assert_eq!(SimulationTier::PresentationOnly, SimulationTier::PresentationOnly);
607        assert_ne!(SimulationTier::Canonical, SimulationTier::Predicted);
608        assert_ne!(SimulationTier::Predicted, SimulationTier::PresentationOnly);
609    }
610
611    #[test]
612    fn simulation_tier_serde_roundtrip() {
613        for tier in [
614            SimulationTier::Canonical,
615            SimulationTier::Predicted,
616            SimulationTier::PresentationOnly,
617        ] {
618            let json = serde_json::to_string(&tier).unwrap();
619            let restored: SimulationTier = serde_json::from_str(&json).unwrap();
620            assert_eq!(tier, restored);
621        }
622    }
623
624    #[test]
625    fn simulation_tier_debug() {
626        assert_eq!(format!("{:?}", SimulationTier::Canonical), "Canonical");
627        assert_eq!(format!("{:?}", SimulationTier::Predicted), "Predicted");
628        assert_eq!(format!("{:?}", SimulationTier::PresentationOnly), "PresentationOnly");
629    }
630}