Bläddra i källkod

refactor(auth): consolidate authorisation into core module and simplify API

- Move Authorisation and Authentication sources into main src/meson.build
- Remove separate meson.build files for auth submodules
- Simplify AuthorisationContext to a minimal wrapper around token
- Merge AuthorisationTokenService functionality into AuthorisationService
- Add AuthorisationPipelineComponent for request processing
- Update AuthorisationToken to use PropertyMapper for serialization
- Change permissions from string[] to ImmutableLot<string>
- Change data from Variant to Properties for better type safety
- Remove unused utility methods from PermissionMatcher
- Delete ARCHITECTURE.md documentation file

BREAKING CHANGE: Authorisation API has been significantly simplified.
AuthorisationContext, AuthorisationService, and AuthorisationToken
have reduced surface area. Identity.permissions now returns
ImmutableLot<string> instead of string[], and Identity.data returns
Properties instead of Variant.
Billy Barrow 1 månad sedan
förälder
incheckning
eba5947472

+ 4 - 5
meson.build

@@ -24,8 +24,7 @@ sodium_deps = declare_dependency(sources: sodium_vapi, dependencies: sodium_c_li
 add_project_arguments(['--vapidir', vapi_dir], language: 'vala')
 
 subdir('src')
-subdir('src/Authentication')
-subdir('examples')
-subdir('tools')
-subdir('website')
-subdir('demo')
+# subdir('examples')
+# subdir('tools')
+# subdir('website')
+# subdir('demo')

+ 0 - 1878
src/Authentication/ARCHITECTURE.md

@@ -1,1878 +0,0 @@
-# Spry Users System - Architecture Document
-
-## 1. Overview
-
-The Spry Users System provides a comprehensive user management and authentication solution for Spry web applications. It offers cookie-based authentication with signed and encrypted session tokens, granular string-keyed permissions, and optional UI components for login and user management.
-
-### Key Features
-
-- **Cookie-based Authentication**: Secure session tokens using signed+encrypted cookies
-- **User Management**: CRUD operations for users with configurable password hashing
-- **Granular Permissions**: String-keyed permissions system (e.g., "admin", "user-management", "content.edit")
-- **Application Data**: Custom JSON-serializable data field per user
-- **Optional UI Components**: Pre-built LoginFormComponent and UserManagementInterface
-- **Implexus Storage**: All data stored in nested container at `/spry/users`
-
-### Design Principles
-
-1. **Minimal Dependencies**: Uses Invercargill.DataStructures instead of Libgee
-2. **Security First**: Leverages existing CryptographyProvider for token security
-3. **Optional UI**: Applications can use custom UI or provided components
-4. **Configurable**: Session expiry, permission defaults, and storage paths are configurable
-
----
-
-## 2. File Structure
-
-```
-src/Users/
-├── ARCHITECTURE.md           # This document
-├── UsersModule.vala          # Module registration and configuration
-├── UserService.vala          # Core user management service
-├── SessionService.vala       # Session management and cookie handling
-├── PermissionService.vala    # Permission checking and management
-├── Models/
-│   ├── User.vala             # User data model
-│   ├── Session.vala          # Session data model
-│   └── Permission.vala       # Permission constants and helpers
-├── Components/
-│   ├── LoginFormComponent.vala       # Optional login form
-│   ├── UserListComponent.vala        # User list with search/filter
-│   ├── UserListItemComponent.vala    # Individual user row with actions
-│   ├── UserFormComponent.vala        # Create/edit user form
-│   └── PermissionEditorComponent.vala # Permission management widget
-├── Pages/
-│   └── UserManagementPage.vala       # PageComponent orchestrating user management
-└── meson.build               # Build configuration
-```
-
-### File Descriptions
-
-| File | Purpose |
-|------|---------|
-| `UsersModule.vala` | IoC registration, configuration, and module setup |
-| `UserService.vala` | User CRUD, password hashing, user queries |
-| `SessionService.vala` | Session creation, validation, cookie management, expiry |
-| `PermissionService.vala` | has_permission, set_permission, clear_permission operations |
-| `Models/User.vala` | User data structure with PropertyMapper for serialization |
-| `Models/Session.vala` | Session data structure with expiry tracking |
-| `Models/Permission.vala` | Permission constants and helper methods |
-| `Components/LoginFormComponent.vala` | HTMX-based login form with error handling |
-| `Components/UserListComponent.vala` | Displays user list with search/filter capabilities |
-| `Components/UserListItemComponent.vala` | Individual user row with inline action buttons |
-| `Components/UserFormComponent.vala` | Modal or inline form for create/edit user |
-| `Components/PermissionEditorComponent.vala` | Permission management widget with checkboxes |
-| `Pages/UserManagementPage.vala` | PageComponent orchestrating all user management components |
-
----
-
-## 3. Data Models
-
-### 3.1 User Model
-
-```vala
-namespace Spry.Users.Models {
-
-    public class User : Object {
-        // Identity
-        public string id { get; set; }              // UUID, also used as document name
-        public string username { get; set; }        // Unique username
-        public string email { get; set; }           // Unique email address
-        public string password_hash { get; set; }   // Argon2id hashed password
-        
-        // Metadata
-        public DateTime created_at { get; set; }
-        public DateTime? last_login_at { get; set; }
-        public DateTime? updated_at { get; set; }
-        public bool is_active { get; set; default = true; }
-        public bool is_verified { get; set; default = false; }
-        
-        // Permissions - stored as JSON array of strings
-        public Series<string> permissions { get; set; default = new Series<string>(); }
-        
-        // Application-specific data - stored as JSON object
-        public Properties? application_data { get; set; }
-        
-        // PropertyMapper for Implexus serialization
-        public static PropertyMapper<User> get_mapper() {
-            return PropertyMapper.build_for<User>(cfg => {
-                cfg.map<string>("id", u => u.id, (u, v) => u.id = v);
-                cfg.map<string>("username", u => u.username, (u, v) => u.username = v);
-                cfg.map<string>("email", u => u.email, (u, v) => u.email = v);
-                cfg.map<string>("password_hash", u => u.password_hash, (u, v) => u.password_hash = v);
-                cfg.map<string>("created", u => u.created_at.format_iso8601(), 
-                    (u, v) => u.created_at = new DateTime.from_iso8601(v, new TimeZone.utc()));
-                cfg.map<string?>("last_login", 
-                    u => u.last_login_at != null ? ((!)u.last_login_at).format_iso8601() : null,
-                    (u, v) => u.last_login_at = v != null ? new DateTime.from_iso8601((!)v, new TimeZone.utc()) : null);
-                cfg.map<string?>("updated",
-                    u => u.updated_at != null ? ((!)u.updated_at).format_iso8601() : null,
-                    (u, v) => u.updated_at = v != null ? new DateTime.from_iso8601((!)v, new TimeZone.utc()) : null);
-                cfg.map<bool>("active", u => u.is_active, (u, v) => u.is_active = v);
-                cfg.map<bool>("verified", u => u.is_verified, (u, v) => u.is_verified = v);
-                cfg.map<Series<string>>("permissions", u => u.permissions, (u, v) => u.permissions = v);
-                cfg.map<Properties?>("app_data", u => u.application_data, (u, v) => u.application_data = v);
-                cfg.set_constructor(() => new User());
-            });
-        }
-    }
-}
-```
-
-### 3.2 Session Model
-
-```vala
-namespace Spry.Users.Models {
-
-    public class Session : Object {
-        public string id { get; set; }              // UUID for session
-        public string user_id { get; set; }         // Reference to User.id
-        public DateTime created_at { get; set; }
-        public DateTime expires_at { get; set; }
-        public string? ip_address { get; set; }     // Optional IP binding
-        public string? user_agent { get; set; }     // Optional UA tracking
-        
-        public bool is_expired {
-            get { return expires_at.compare(new DateTime.now_utc()) <= 0; }
-        }
-        
-        public static PropertyMapper<Session> get_mapper() {
-            return PropertyMapper.build_for<Session>(cfg => {
-                cfg.map<string>("id", s => s.id, (s, v) => s.id = v);
-                cfg.map<string>("user", s => s.user_id, (s, v) => s.user_id = v);
-                cfg.map<string>("created", s => s.created_at.format_iso8601(),
-                    (s, v) => s.created_at = new DateTime.from_iso8601(v, new TimeZone.utc()));
-                cfg.map<string>("expires", s => s.expires_at.format_iso8601(),
-                    (s, v) => s.expires_at = new DateTime.from_iso8601(v, new TimeZone.utc()));
-                cfg.map<string?>("ip", s => s.ip_address, (s, v) => s.ip_address = v);
-                cfg.map<string?>("ua", s => s.user_agent, (s, v) => s.user_agent = v);
-                cfg.set_constructor(() => new Session());
-            });
-        }
-    }
-}
-```
-
-### 3.3 Permission Constants
-
-```vala
-namespace Spry.Users.Models {
-
-    public class Permission : Object {
-        // Built-in permissions
-        public const string USER_MANAGEMENT = "user-management";
-        public const string ADMIN = "admin";
-        
-        // Helper to check if a permission implies another
-        public static bool implies(string permission, string required) {
-            // "admin" implies all permissions
-            if (permission == ADMIN) return true;
-            
-            // Exact match
-            if (permission == required) return true;
-            
-            // Prefix match: "content.*" implies "content.edit", "content.delete"
-            if (permission.has_suffix(".*")) {
-                string prefix = permission.substring(0, permission.length - 1);
-                return required.has_prefix(prefix);
-            }
-            
-            return false;
-        }
-        
-        // Validate permission string format
-        public static bool is_valid(string permission) {
-            // Must be non-empty, alphanumeric with dots, dashes, underscores
-            return /^[a-zA-Z0-9._-]+$/.match(permission);
-        }
-    }
-}
-```
-
----
-
-## 4. Class Diagrams
-
-### 4.1 Core Services
-
-```
-┌─────────────────────────────────────────────────────────────────┐
-│                        UsersModule                               │
-│  - Configures UserService, SessionService, PermissionService    │
-│  - Registers components and pages                               │
-│  - Provides UsersConfiguration for customization                │
-└─────────────────────────────────────────────────────────────────┘
-                                │
-                                │ registers
-                                ▼
-┌──────────────────────┐  ┌──────────────────────┐  ┌──────────────────────┐
-│     UserService      │  │    SessionService    │  │  PermissionService   │
-├──────────────────────┤  ├──────────────────────┤  ├──────────────────────┤
-│ + create_user        │  │ + create_session     │  │ + has_permission     │
-│ + get_user_by_id     │  │ + validate_session   │  │ + set_permission     │
-│ + get_user_by_name   │  │ + destroy_session    │  │ + clear_permission   │
-│ + get_user_by_email  │  │ + get_current_user   │  │ + get_permissions    │
-│ + update_user        │  │ + set_session_cookie │  │ + check_any          │
-│ + delete_user        │  │ + clear_session_cookie│ │ + check_all          │
-│ + authenticate       │  │ + get_session_from   │  │                      │
-│ + list_users         │  │   cookie             │  │                      │
-│ + hash_password      │  │ + cleanup_expired    │  │                      │
-│ + verify_password    │  │                      │  │                      │
-└──────────────────────┘  └──────────────────────┘  └──────────────────────┘
-         │                          │                         │
-         │ uses                     │ uses                    │ uses
-         ▼                          ▼                         ▼
-┌─────────────────────────────────────────────────────────────────┐
-│                    Implexus.Core.Engine                          │
-│  Storage path: /spry/users                                       │
-│  - /spry/users/users/{user_id}     - User documents              │
-│  - /spry/users/sessions/{session_id} - Session documents         │
-│  - /spry/users/by_username         - Category for username lookup│
-│  - /spry/users/by_email            - Category for email lookup   │
-└─────────────────────────────────────────────────────────────────┘
-```
-
-### 4.2 Component Hierarchy
-
-```
-┌─────────────────────────────────────────────────────────────────┐
-│                       Component                                  │
-│  (from Spry namespace)                                           │
-└─────────────────────────────────────────────────────────────────┘
-                                ▲
-                                │ extends
-        ┌───────────────────────┼───────────────────────┐
-        │                       │                       │
-┌───────┴───────┐     ┌────────┴────────┐     ┌───────┴───────┐
-│ LoginFormComp │     │ UserListComp    │     │ PageComponent │
-├───────────────┤     ├─────────────────┤     └───────┬───────┘
-│ - username    │     │ - users: Series │             ▲
-│ - error_msg   │     │ - search_query  │             │ extends
-│ - redirect_url│     │ - current_user  │     ┌───────┴───────┐
-├───────────────┤     ├─────────────────┤     │UserMgmtPage   │
-│ + prepare()   │     │ + prepare()     │     ├───────────────┤
-│ + handle_     │     │ + handle_action │     │ - list: inject│
-│   action()    │     │   - Search      │     │ - form: inject│
-│   - Login     │     │   - Refresh     │     │ + prepare()   │
-│   - Logout    │     └────────┬────────┘     │ + handle_     │
-└───────────────┘              │ creates      │   action()    │
-                               ▼              │   - CreateUser│
-                    ┌──────────────────┐      │   - EditUser  │
-                    │UserListItemComp  │      │   - DeleteUser│
-                    ├──────────────────┤      └───────────────┘
-                    │ - user: User     │              │
-                    │ - parent_list    │              │ contains
-                    ├──────────────────┤              ▼
-                    │ + prepare()      │      ┌──────────────────┐
-                    │ + handle_action  │      │UserFormComponent │
-                    │   - Edit         │      ├──────────────────┤
-                    │   - Delete       │      │ - editing_user   │
-                    │   - ToggleActive │      │ - is_modal       │
-                    └──────────────────┘      │ - visible        │
-                                              ├──────────────────┤
-                                              │ + prepare()      │
-                                              │ + handle_action  │
-                                              │   - Save         │
-                                              │   - Cancel       │
-                                              │ + show()         │
-                                              │ + hide()         │
-                                              └────────┬─────────┘
-                                                       │ contains
-                                                       ▼
-                                       ┌───────────────────────────┐
-                                       │PermissionEditorComponent  │
-                                       ├───────────────────────────┤
-                                       │ - user: User              │
-                                       │ - available_perms: Series │
-                                       │ - selected_perms: Series  │
-                                       ├───────────────────────────┤
-                                       │ + prepare()               │
-                                       │ + get_selected()          │
-                                       │ + set_permissions()       │
-                                       └───────────────────────────┘
-```
-
-### 4.3 Component Communication Flow
-
-```
-┌─────────────────────────────────────────────────────────────────┐
-│                    UserManagementPage                            │
-│  - Orchestrates all user management components                   │
-│  - Handles cross-component actions                               │
-│  - Manages modal state for UserFormComponent                     │
-└─────────────────────────────────────────────────────────────────┘
-         │                           │                      │
-         │ contains                  │ contains             │ contains
-         ▼                           ▼                      ▼
-┌─────────────────┐     ┌──────────────────────┐  ┌─────────────────┐
-│ UserListComponent│     │ UserFormComponent    │  │ Status messages │
-│                 │     │ - modal overlay      │  │ - success/error │
-│ - Search input  │     │ - create/edit form   │  │   alerts        │
-│ - User table    │     │ - permission editor  │  │                 │
-│ - Pagination    │     │ - password fields    │  │                 │
-└────────┬────────┘     └──────────┬───────────┘  └─────────────────┘
-         │                         │
-         │ creates per user        │ contains
-         ▼                         ▼
-┌──────────────────────┐  ┌───────────────────────────┐
-│ UserListItemComponent│  │ PermissionEditorComponent │
-│ - Username/email     │  │ - Checkbox list           │
-│ - Status badges      │  │ - Common permissions      │
-│ - Action buttons     │  │ - Custom permission input │
-│   - Edit → triggers  │  └───────────────────────────┘
-│     form modal       │
-│   - Delete           │
-│   - Toggle active    │
-└──────────────────────┘
-
-Communication Pattern:
-1. UserListItemComponent.Edit → UserManagementPage.handle_action("EditUser")
-2. UserManagementPage shows UserFormComponent with user data
-3. UserFormComponent.Save → UserService.update_user()
-4. UserManagementPage refreshes UserListComponent via add_globals_from()
-```
-
-### 4.3 Data Relationships
-
-```
-┌───────────────────┐       ┌───────────────────┐
-│       User        │       │      Session      │
-├───────────────────┤       ├───────────────────┤
-│ id: string        │◄──────│ user_id: string   │
-│ username: string  │  1:N  │ id: string        │
-│ email: string     │       │ expires_at: Date  │
-│ password_hash     │       │ created_at: Date  │
-│ permissions[]     │       │ ip_address: ?     │
-│ application_data  │       │ user_agent: ?     │
-│ created_at        │       └───────────────────┘
-│ last_login_at     │
-│ is_active         │
-│ is_verified       │
-└───────────────────┘
-         │
-         │ stored in
-         ▼
-┌───────────────────────────────────────────────────┐
-│              Implexus Storage                     │
-├───────────────────────────────────────────────────┤
-│ /spry/users/                                       │
-│   ├── users/                    [Container]       │
-│   │   ├── {user_id_1}           [Document:User]   │
-│   │   ├── {user_id_2}           [Document:User]   │
-│   │   └── ...                                      │
-│   ├── sessions/                 [Container]       │
-│   │   ├── {session_id_1}        [Document:Session]│
-│   │   └── ...                                      │
-│   ├── by_username               [Category]        │
-│   │   └── expression: "type=='User'"              │
-│   │   └── indexed on: username                    │
-│   └── by_email                  [Category]        │
-│       └── expression: "type=='User'"              │
-│       └── indexed on: email                       │
-└───────────────────────────────────────────────────┘
-```
-
----
-
-## 5. API Design
-
-### 5.1 UserService API
-
-```vala
-namespace Spry.Users {
-
-    public errordomain UserError {
-        USER_NOT_FOUND,
-        DUPLICATE_USERNAME,
-        DUPLICATE_EMAIL,
-        INVALID_PASSWORD,
-        INVALID_CREDENTIALS,
-        USER_INACTIVE,
-        PERMISSION_DENIED
-    }
-
-    public class UserService : Object {
-        private Implexus.Core.Engine engine = inject<Implexus.Core.Engine>();
-        private UsersConfiguration config = inject<UsersConfiguration>();
-        
-        // User CRUD
-        public User create_user(string username, string email, string password, 
-            Series<string>? permissions = null, Properties? app_data = null) throws Error;
-        
-        public User? get_user_by_id(string id);
-        public User? get_user_by_username(string username);
-        public User? get_user_by_email(string email);
-        
-        public void update_user(User user) throws Error;
-        public void delete_user(string user_id) throws Error;
-        
-        public Series<User> list_users(int offset = 0, int limit = 100);
-        public Series<User> search_users(string query, int limit = 50);
-        
-        // Authentication
-        public User authenticate(string username_or_email, string password) throws Error;
-        public void update_password(string user_id, string new_password) throws Error;
-        public void record_login(string user_id);
-        
-        // Password hashing (using Argon2id via libsodium)
-        public string hash_password(string password);
-        public bool verify_password(string password, string hash);
-        
-        // Utility
-        public bool username_exists(string username);
-        public bool email_exists(string email);
-        public int user_count();
-    }
-}
-```
-
-### 5.2 SessionService API
-
-```vala
-namespace Spry.Users {
-
-    public errordomain SessionError {
-        SESSION_NOT_FOUND,
-        SESSION_EXPIRED,
-        INVALID_SESSION_TOKEN,
-        COOKIE_NOT_FOUND
-    }
-
-    public class SessionService : Object {
-        private Implexus.Core.Engine engine = inject<Implexus.Core.Engine>();
-        private CryptographyProvider crypto = inject<CryptographyProvider>();
-        private UsersConfiguration config = inject<UsersConfiguration>();
-        private HttpContext http_context = inject<HttpContext>();
-        
-        // Session Management
-        public Session create_session(string user_id, 
-            string? ip_address = null, string? user_agent = null) throws Error;
-        
-        public Session? validate_session(string session_id) throws Error;
-        public void destroy_session(string session_id) throws Error;
-        public void destroy_all_user_sessions(string user_id) throws Error;
-        
-        // Cookie Operations
-        public void set_session_cookie(Session session);
-        public void clear_session_cookie();
-        public Session? get_session_from_cookie() throws Error;
-        
-        // Current User Context
-        public User? get_current_user() throws Error;
-        public string? get_current_user_id() throws Error;
-        public bool is_authenticated() throws Error;
-        
-        // Maintenance
-        public void cleanup_expired_sessions();
-        public int active_session_count(string? user_id = null);
-        
-        // Session Token (signed + encrypted)
-        public string create_session_token(Session session) throws Error;
-        public Session? validate_session_token(string token) throws Error;
-    }
-}
-```
-
-### 5.3 PermissionService API
-
-```vala
-namespace Spry.Users {
-
-    public class PermissionService : Object {
-        private UserService user_service = inject<UserService>();
-        private SessionService session_service = inject<SessionService>();
-        
-        // Permission Checking
-        public bool has_permission(string user_id, string permission) throws Error;
-        public bool has_any_permission(string user_id, Series<string> permissions) throws Error;
-        public bool has_all_permissions(string user_id, Series<string> permissions) throws Error;
-        
-        // Current User Shortcuts
-        public bool current_user_has(string permission) throws Error;
-        public bool current_user_has_any(Series<string> permissions) throws Error;
-        public bool current_user_has_all(Series<string> permissions) throws Error;
-        
-        // Permission Management
-        public void set_permission(string user_id, string permission) throws Error;
-        public void clear_permission(string user_id, string permission) throws Error;
-        public void set_permissions(string user_id, Series<string> permissions) throws Error;
-        public void clear_all_permissions(string user_id) throws Error;
-        
-        // Querying
-        public Series<string> get_permissions(string user_id) throws Error;
-        public Series<User> get_users_with_permission(string permission) throws Error;
-        
-        // Validation
-        public void require_permission(string permission) throws Error;
-        public void require_any(Series<string> permissions) throws Error;
-        public void require_all(Series<string> permissions) throws Error;
-    }
-}
-```
-
-### 5.4 UsersConfiguration API
-
-```vala
-namespace Spry.Users {
-
-    public class UsersConfiguration : Object {
-        // Session Settings
-        public TimeSpan session_duration { get; set; default = TimeSpan.HOUR * 24 * 7; } // 7 days
-        public bool bind_to_ip { get; set; default = false; }
-        public bool track_user_agent { get; set; default = true; }
-        public string cookie_name { get; set; default = "spry_session"; }
-        public bool cookie_secure { get; set; default = true; }
-        public bool cookie_http_only { get; set; default = true; }
-        public string? cookie_domain { get; set; default = null; }
-        public string? cookie_path { get; set; default = "/"; }
-        
-        // Password Settings
-        public int min_password_length { get; set; default = 8; }
-        public bool require_uppercase { get; set; default = true; }
-        public bool require_lowercase { get; set; default = true; }
-        public bool require_digit { get; set; default = true; }
-        public bool require_special { get; set; default = false; }
-        
-        // Storage Paths
-        public string storage_base_path { get; set; default = "/spry/users"; }
-        
-        // Default Permissions for New Users
-        public Series<string> default_permissions { get; set; default = new Series<string>(); }
-        
-        // Validation
-        public bool is_valid_password(string password);
-        public string? password_validation_message(string password);
-    }
-}
-```
-
----
-
-## 6. Storage Schema
-
-### 6.1 Implexus Container Structure
-
-All user data is stored under the `/spry/users` container:
-
-```
-/spry/users/                           [Container] - Root for all user data
-├── users/                             [Container] - User documents
-│   ├── {uuid-v4-user-id-1}            [Document:type=User]
-│   ├── {uuid-v4-user-id-2}            [Document:type=User]
-│   └── {uuid-v4-user-id-3}            [Document:type=User]
-├── sessions/                          [Container] - Session documents
-│   ├── {uuid-v4-session-id-1}         [Document:type=Session]
-│   └── {uuid-v4-session-id-2}         [Document:type=Session]
-├── by_username/                       [Category] - Username lookup index
-│   └── Config: type=User, expr=username
-└── by_email/                          [Category] - Email lookup index
-    └── Config: type=User, expr=email
-```
-
-### 6.2 User Document Schema
-
-```json
-{
-    "id": "550e8400-e29b-41d4-a716-446655440000",
-    "username": "johndoe",
-    "email": "john@example.com",
-    "password_hash": "$argon2id$v=19$m=65536,t=3,p=4$...",
-    "created": "2026-01-15T10:30:00Z",
-    "last_login": "2026-03-13T15:45:00Z",
-    "updated": null,
-    "active": true,
-    "verified": true,
-    "permissions": ["user-management", "content.edit"],
-    "app_data": {
-        "display_name": "John Doe",
-        "preferences": {
-            "theme": "dark",
-            "language": "en"
-        }
-    }
-}
-```
-
-### 6.3 Session Document Schema
-
-```json
-{
-    "id": "660e8400-e29b-41d4-a716-446655440001",
-    "user": "550e8400-e29b-41d4-a716-446655440000",
-    "created": "2026-03-13T10:00:00Z",
-    "expires": "2026-03-20T10:00:00Z",
-    "ip": "192.168.1.100",
-    "ua": "Mozilla/5.0 ..."
-}
-```
-
-### 6.4 Storage Initialization
-
-```vala
-public class UsersModule : Object {
-    
-    public void initialize_storage() throws Error {
-        var engine = inject<Implexus.Core.Engine>();
-        var config = inject<UsersConfiguration>();
-        
-        var root = engine.get_root();
-        
-        // Create base container: /spry/users
-        var spry = root.create_container("spry");
-        var users_container = spry.create_container("users");
-        
-        // Create users document container
-        users_container.create_container("users");
-        
-        // Create sessions document container
-        users_container.create_container("sessions");
-        
-        // Create username lookup category
-        users_container.create_category("by_username", "User", "username");
-        
-        // Create email lookup category
-        users_container.create_category("by_email", "User", "email");
-    }
-}
-```
-
----
-
-## 7. Cryptography Integration
-
-### 7.1 Session Token Structure
-
-Session tokens are created using the existing `CryptographyProvider` pattern:
-
-```vala
-public class SessionToken {
-    public string session_id { get; set; }
-    public string user_id { get; set; }
-    public DateTime created_at { get; set; }
-    public DateTime expires_at { get; set; }
-    
-    public static PropertyMapper<SessionToken> get_mapper() {
-        return PropertyMapper.build_for<SessionToken>(cfg => {
-            cfg.map<string>("sid", t => t.session_id, (t, v) => t.session_id = v);
-            cfg.map<string>("uid", t => t.user_id, (t, v) => t.user_id = v);
-            cfg.map<string>("cat", t => t.created_at.format_iso8601(),
-                (t, v) => t.created_at = new DateTime.from_iso8601(v, new TimeZone.utc()));
-            cfg.map<string>("exp", t => t.expires_at.format_iso8601(),
-                (t, v) => t.expires_at = new DateTime.from_iso8601(v, new TimeZone.utc()));
-            cfg.set_constructor(() => new SessionToken());
-        });
-    }
-}
-```
-
-### 7.2 Token Creation and Validation
-
-```vala
-public class SessionService {
-    
-    private CryptographyProvider crypto = inject<CryptographyProvider>();
-    
-    public string create_session_token(Session session) throws Error {
-        var token = new SessionToken() {
-            session_id = session.id,
-            user_id = session.user_id,
-            created_at = session.created_at,
-            expires_at = session.expires_at
-        };
-        
-        var mapper = SessionToken.get_mapper();
-        var properties = mapper.map_from(token);
-        var json = new JsonElement.from_properties(properties);
-        var blob = json.stringify(false);
-        
-        // Sign then seal (same pattern as ComponentContext)
-        var signed = Sodium.Asymmetric.Signing.sign(blob.data, crypto.signing_secret_key);
-        var @sealed = Sodium.Asymmetric.Sealing.seal(signed, crypto.sealing_public_key);
-        
-        // URL-safe Base64 encoding
-        return Base64.encode(@sealed).replace("+", "-").replace("/", "_");
-    }
-    
-    public SessionToken? validate_session_token(string token) throws Error {
-        try {
-            // URL-safe Base64 decode
-            var decoded = Base64.decode(token.replace("-", "+").replace("_", "/"));
-            
-            // Unseal then verify signature
-            var signed = Sodium.Asymmetric.Sealing.unseal(
-                decoded, 
-                crypto.sealing_public_key, 
-                crypto.signing_secret_key
-            );
-            if (signed == null) {
-                throw new SessionError.INVALID_SESSION_TOKEN("Could not unseal token");
-            }
-            
-            var cleartext = Sodium.Asymmetric.Signing.verify(
-                signed, 
-                crypto.signing_public_key
-            );
-            if (cleartext == null) {
-                throw new SessionError.INVALID_SESSION_TOKEN("Invalid token signature");
-            }
-            
-            // Deserialize
-            var json = new JsonElement.from_string(
-                Wrap.byte_array(cleartext).to_raw_string()
-            );
-            var mapper = SessionToken.get_mapper();
-            var session_token = mapper.materialise(json.as<JsonObject>());
-            
-            // Check expiry
-            if (session_token.expires_at.compare(new DateTime.now_utc()) <= 0) {
-                throw new SessionError.SESSION_EXPIRED("Session has expired");
-            }
-            
-            return session_token;
-        } catch (Error e) {
-            return null;
-        }
-    }
-}
-```
-
-### 7.3 Password Hashing
-
-Uses Argon2id via libsodium (already available through existing dependencies):
-
-```vala
-public class UserService {
-    
-    public string hash_password(string password) {
-        return Sodium.PasswordHashing.hash(
-            password,
-            Sodium.PasswordHashing.OPSLIMIT_MODERATE,
-            Sodium.PasswordHashing.MEMLIMIT_MODERATE
-        );
-    }
-    
-    public bool verify_password(string password, string hash) {
-        return Sodium.PasswordHashing.verify(hash, password);
-    }
-}
-```
-
----
-
-## 8. Component Design
-
-The user management interface is built from multiple focused components that work together through the UserManagementPage orchestrator. This separation provides better maintainability, reusability, and cleaner code organization.
-
-### 8.1 LoginFormComponent
-
-A self-contained login form component with HTMX-based submission:
-
-```vala
-namespace Spry.Users.Components {
-
-    public class LoginFormComponent : Component {
-        private SessionService session_service = inject<SessionService>();
-        private UserService user_service = inject<UserService>();
-        private HttpContext http_context = inject<HttpContext>();
-        
-        // Properties for configuration
-        public string redirect_url { get; set; default = "/"; }
-        public string? error_message { get; private set; }
-        
-        public override string markup { get {
-            return """
-            <div class="spry-login-form" sid="login-form">
-                <script spry-res="htmx.js"></script>
-                
-                <form sid="form" spry-action=":Login" spry-target="login-form" hx-swap="outerHTML">
-                    <spry-context property="redirect_url"/>
-                    
-                    <div class="form-group">
-                        <label for="username">Username or Email</label>
-                        <input type="text" name="username" sid="username" required 
-                               autocomplete="username"/>
-                    </div>
-                    
-                    <div class="form-group">
-                        <label for="password">Password</label>
-                        <input type="password" name="password" sid="password" required
-                               autocomplete="current-password"/>
-                    </div>
-                    
-                    <div spry-if="this.error_message != null" class="error-message" sid="error">
-                        <span content-expr="this.error_message"></span>
-                    </div>
-                    
-                    <button type="submit" sid="submit-btn">Log In</button>
-                </form>
-            </div>
-            """;
-        }}
-        
-        public override async void prepare() throws Error {
-            this["form"].set_attribute("hx-vals", @"{\"redirect_url\":\"$redirect_url\"}");
-            if (error_message != null) {
-                this["error"].text_content = error_message;
-            }
-        }
-        
-        public async override void handle_action(string action) throws Error {
-            var query = http_context.request.query_params;
-            
-            if (action == "Login") {
-                var username = query.get_any_or_default("username", "");
-                var password = query.get_any_or_default("password", "");
-                
-                try {
-                    var user = user_service.authenticate(username, password);
-                    var ip = http_context.request.remote_address;
-                    var ua = http_context.request.headers.get_any_or_default("User-Agent", null);
-                    var session = session_service.create_session(user.id, ip, ua);
-                    
-                    this["form"].set_attribute("hx-refresh", "true");
-                    session_service.set_session_cookie(session);
-                } catch (UserError e) {
-                    error_message = "Invalid username or password";
-                }
-            }
-        }
-    }
-}
-```
-
-### 8.2 UserListComponent
-
-Displays the list of users with search and filter capabilities. Creates UserListItemComponent instances for each user:
-
-```vala
-namespace Spry.Users.Components {
-
-    public class UserListComponent : Component {
-        private UserService user_service = inject<UserService>();
-        private ComponentFactory factory = inject<ComponentFactory>();
-        private HttpContext http_context = inject<HttpContext>();
-        
-        // State
-        private string _search_query = "";
-        private int _page = 0;
-        private int _page_size = 20;
-        private Series<User> _users = new Series<User>();
-        private int _total_count = 0;
-        
-        public override string markup { get {
-            return """
-            <div class="spry-user-list" sid="user-list" spry-global="user-list">
-                <script spry-res="htmx.js"></script>
-                
-                <!-- Search Bar -->
-                <div class="search-bar" sid="search-bar">
-                    <input type="text" name="search" sid="search-input" 
-                           placeholder="Search users..."
-                           spry-action=":Search" spry-target="user-list" hx-swap="outerHTML"/>
-                    <button sid="clear-btn" spry-action=":ClearSearch" 
-                            spry-target="user-list" hx-swap="outerHTML"
-                            spry-if="this._search_query.length > 0">Clear</button>
-                </div>
-                
-                <!-- User Table -->
-                <table class="user-table" sid="table">
-                    <thead>
-                        <tr>
-                            <th>Username</th>
-                            <th>Email</th>
-                            <th>Status</th>
-                            <th>Created</th>
-                            <th>Last Login</th>
-                            <th>Actions</th>
-                        </tr>
-                    </thead>
-                    <tbody sid="table-body">
-                        <spry-outlet sid="users"/>
-                    </tbody>
-                </table>
-                
-                <!-- Pagination -->
-                <div class="pagination" sid="pagination" spry-if="this._total_count > this._page_size">
-                    <button sid="prev-btn" spry-action=":PrevPage" 
-                            spry-target="user-list" hx-swap="outerHTML"
-                            disabled-expr="this._page == 0 ? 'disabled' : null">
-                        Previous
-                    </button>
-                    <span content-expr="format('Page %d of %d', this._page + 1, 
-                        (this._total_count + this._page_size - 1) / this._page_size)"></span>
-                    <button sid="next-btn" spry-action=":NextPage"
-                            spry-target="user-list" hx-swap="outerHTML"
-                            disabled-expr="(this._page + 1) * this._page_size >= this._total_count ? 'disabled' : null">
-                        Next
-                    </button>
-                </div>
-                
-                <!-- Empty State -->
-                <div spry-if="this._users.length == 0" class="empty-state" sid="empty">
-                    <p>No users found</p>
-                </div>
-            </div>
-            """;
-        }}
-        
-        public override async void prepare() throws Error {
-            // Load users based on current search and page
-            if (_search_query.length > 0) {
-                _users = user_service.search_users(_search_query, _page_size);
-                _total_count = _users.length;
-            } else {
-                _users = user_service.list_users(_page * _page_size, _page_size);
-                _total_count = user_service.user_count();
-            }
-            
-            // Create user item components
-            var items = new Series<Renderable>();
-            foreach (var user in _users) {
-                var item = factory.create<UserListItemComponent>();
-                item.set_user(user);
-                items.add(item);
-            }
-            set_outlet_children("users", items);
-            
-            this["search-input"].set_attribute("value", _search_query);
-        }
-        
-        public async override void handle_action(string action) throws Error {
-            var query = http_context.request.query_params;
-            
-            switch (action) {
-                case "Search":
-                    _search_query = query.get_any_or_default("search", "");
-                    _page = 0;
-                    break;
-                case "ClearSearch":
-                    _search_query = "";
-                    _page = 0;
-                    break;
-                case "PrevPage":
-                    if (_page > 0) _page--;
-                    break;
-                case "NextPage":
-                    if ((_page + 1) * _page_size < _total_count) _page++;
-                    break;
-            }
-        }
-    }
-}
-```
-
-### 8.3 UserListItemComponent
-
-Individual user row with inline action buttons. Actions are delegated to the parent UserManagementPage:
-
-```vala
-namespace Spry.Users.Components {
-
-    public class UserListItemComponent : Component {
-        private User _user;
-        
-        public void set_user(User user) {
-            _user = user;
-        }
-        
-        public override string markup { get {
-            return """
-            <tr class="user-row" sid="user-row">
-                <td class="username" sid="username"></td>
-                <td class="email" sid="email"></td>
-                <td class="status" sid="status">
-                    <span sid="active-badge" class="badge badge-active">Active</span>
-                    <span sid="inactive-badge" class="badge badge-inactive">Inactive</span>
-                    <span sid="verified-badge" class="badge badge-verified">Verified</span>
-                </td>
-                <td class="created" sid="created"></td>
-                <td class="last-login" sid="last-login"></td>
-                <td class="actions" sid="actions">
-                    <button sid="edit-btn" spry-action="UserManagementPage:EditUser"
-                            hx-target="#user-form-container" hx-swap="innerHTML">
-                        Edit
-                    </button>
-                    <button sid="toggle-btn" spry-action="UserManagementPage:ToggleActive"
-                            hx-target="#user-list" hx-swap="outerHTML">
-                        <span spry-if="this._user.is_active">Deactivate</span>
-                        <span spry-else>Activate</span>
-                    </button>
-                    <button sid="delete-btn" spry-action="UserManagementPage:DeleteUser"
-                            hx-target="#user-list" hx-swap="outerHTML"
-                            hx-confirm="Are you sure you want to delete this user?">
-                        Delete
-                    </button>
-                </td>
-            </tr>
-            """;
-        }}
-        
-        public override async void prepare() throws Error {
-            if (_user == null) return;
-            
-            this["username"].text_content = _user.username;
-            this["email"].text_content = _user.email;
-            this["created"].text_content = _user.created_at.format("%Y-%m-%d");
-            
-            if (_user.last_login_at != null) {
-                this["last-login"].text_content = ((!)_user.last_login_at).format("%Y-%m-%d %H:%M");
-            } else {
-                this["last-login"].text_content = "Never";
-            }
-            
-            // Set user ID on action buttons for cross-component actions
-            var user_id_json = @"{\"user_id\":\"$(_user.id)\"}";
-            this["edit-btn"].set_attribute("hx-vals", user_id_json);
-            this["toggle-btn"].set_attribute("hx-vals", user_id_json);
-            this["delete-btn"].set_attribute("hx-vals", user_id_json);
-        }
-    }
-}
-```
-
-### 8.4 UserFormComponent
-
-Modal form for creating and editing users. Contains PermissionEditorComponent:
-
-```vala
-namespace Spry.Users.Components {
-
-    public class UserFormComponent : Component {
-        private UserService user_service = inject<UserService>();
-        private ComponentFactory factory = inject<ComponentFactory>();
-        private HttpContext http_context = inject<HttpContext>();
-        
-        // State
-        private User? _editing_user = null;
-        private bool _is_visible = false;
-        private string? _error_message = null;
-        
-        public void set_user(User? user) {
-            _editing_user = user;
-            _is_visible = true;
-        }
-        
-        public void show_create() {
-            _editing_user = null;
-            _is_visible = true;
-        }
-        
-        public void hide() {
-            _is_visible = false;
-            _editing_user = null;
-            _error_message = null;
-        }
-        
-        public bool is_creating { get { return _editing_user == null; } }
-        
-        public override string markup { get {
-            return """
-            <div class="spry-user-form-container" sid="form-container" spry-global="user-form">
-                <script spry-res="htmx.js"></script>
-                
-                <!-- Modal Overlay (shown when visible) -->
-                <div spry-if="this._is_visible" class="modal-overlay" sid="modal">
-                    <div class="modal-content" sid="modal-content">
-                        <div class="modal-header">
-                            <h3 content-expr="this.is_creating ? 'Create User' : 'Edit User'"></h3>
-                            <button sid="close-btn" spry-action=":Cancel" 
-                                    spry-target="form-container" hx-swap="outerHTML"
-                                    class="close-btn">&times;</button>
-                        </div>
-                        
-                        <div spry-if="this._error_message != null" class="error" sid="error">
-                            <span content-expr="this._error_message"></span>
-                        </div>
-                        
-                        <form sid="form" spry-action=":Save" 
-                              spry-target="form-container" hx-swap="outerHTML">
-                            <input type="hidden" name="user_id" sid="user-id"/>
-                            
-                            <div class="form-group">
-                                <label for="username">Username *</label>
-                                <input type="text" name="username" sid="form-username" 
-                                       required minlength="3" pattern="[a-zA-Z0-9_]+"/>
-                                <small>Alphanumeric characters and underscores only</small>
-                            </div>
-                            
-                            <div class="form-group">
-                                <label for="email">Email *</label>
-                                <input type="email" name="email" sid="form-email" required/>
-                            </div>
-                            
-                            <div class="form-group" spry-if="this.is_creating">
-                                <label for="password">Password *</label>
-                                <input type="password" name="password" sid="form-password"
-                                       required minlength="8"/>
-                                <small>Minimum 8 characters</small>
-                            </div>
-                            
-                            <div class="form-group" spry-if="!this.is_creating">
-                                <label for="new_password">New Password (leave blank to keep current)</label>
-                                <input type="password" name="new_password" sid="form-new-password"
-                                       minlength="8"/>
-                            </div>
-                            
-                            <div class="form-group">
-                                <label>
-                                    <input type="checkbox" name="is_active" sid="form-active" checked/>
-                                    Active
-                                </label>
-                            </div>
-                            
-                            <div class="form-group">
-                                <label>
-                                    <input type="checkbox" name="is_verified" sid="form-verified"/>
-                                    Email Verified
-                                </label>
-                            </div>
-                            
-                            <!-- Permission Editor -->
-                            <div class="form-group">
-                                <label>Permissions</label>
-                                <spry-component name="PermissionEditorComponent" sid="permission-editor"/>
-                            </div>
-                            
-                            <div class="form-actions">
-                                <button type="submit" sid="save-btn">
-                                    <span spry-if="this.is_creating">Create User</span>
-                                    <span spry-else>Save Changes</span>
-                                </button>
-                                <button type="button" sid="cancel-btn" 
-                                        spry-action=":Cancel" spry-target="form-container" 
-                                        hx-swap="outerHTML">Cancel</button>
-                            </div>
-                        </form>
-                    </div>
-                </div>
-            </div>
-            """;
-        }}
-        
-        public override async void prepare() throws Error {
-            if (!_is_visible) return;
-            
-            if (_editing_user != null) {
-                this["user-id"].set_attribute("value", _editing_user.id);
-                this["form-username"].set_attribute("value", _editing_user.username);
-                this["form-email"].set_attribute("value", _editing_user.email);
-                
-                if (_editing_user.is_active) {
-                    this["form-active"].set_attribute("checked", "checked");
-                }
-                if (_editing_user.is_verified) {
-                    this["form-verified"].set_attribute("checked", "checked");
-                }
-                
-                var perm_editor = get_component_child<PermissionEditorComponent>("permission-editor");
-                perm_editor.set_permissions(_editing_user.permissions);
-            } else {
-                var perm_editor = get_component_child<PermissionEditorComponent>("permission-editor");
-                perm_editor.set_permissions(new Series<string>());
-            }
-        }
-        
-        public async override void handle_action(string action) throws Error {
-            switch (action) {
-                case "Save":
-                    yield save_user();
-                    break;
-                case "Cancel":
-                    hide();
-                    break;
-            }
-        }
-        
-        private async void save_user() throws Error {
-            var query = http_context.request.query_params;
-            var user_id = query.get_any_or_default("user_id", "");
-            var username = query.get_any_or_default("username", "").strip();
-            var email = query.get_any_or_default("email", "").strip();
-            
-            if (username.length < 3) {
-                _error_message = "Username must be at least 3 characters";
-                return;
-            }
-            
-            var perm_editor = get_component_child<PermissionEditorComponent>("permission-editor");
-            var permissions = perm_editor.get_selected();
-            
-            try {
-                if (user_id == "") {
-                    var password = query.get_any_or_default("password", "");
-                    var is_active = query.get_any_or_default("is_active", "") == "on";
-                    var is_verified = query.get_any_or_default("is_verified", "") == "on";
-                    
-                    var user = user_service.create_user(username, email, password, permissions, null);
-                    user.is_active = is_active;
-                    user.is_verified = is_verified;
-                    user_service.update_user(user);
-                } else {
-                    var user = user_service.get_user_by_id(user_id);
-                    if (user == null) {
-                        _error_message = "User not found";
-                        return;
-                    }
-                    
-                    user.username = username;
-                    user.email = email;
-                    user.is_active = query.get_any_or_default("is_active", "") == "on";
-                    user.is_verified = query.get_any_or_default("is_verified", "") == "on";
-                    user.permissions = permissions;
-                    user.updated_at = new DateTime.now_utc();
-                    
-                    var new_password = query.get_any_or_default("new_password", "");
-                    if (new_password.length > 0) {
-                        user_service.update_password(user_id, new_password);
-                    }
-                    
-                    user_service.update_user(user);
-                }
-                
-                hide();
-            } catch (UserError e) {
-                _error_message = e.message;
-            }
-        }
-    }
-}
-```
-
-### 8.5 PermissionEditorComponent
-
-Widget for managing user permissions with checkboxes for common permissions and input for custom ones:
-
-```vala
-namespace Spry.Users.Components {
-
-    public class PermissionEditorComponent : Component {
-        private static Series<string> COMMON_PERMISSIONS = new Series<string>.from({
-            Permission.USER_MANAGEMENT,
-            Permission.ADMIN,
-            "content.read",
-            "content.edit",
-            "content.delete"
-        });
-        
-        private Series<string> _selected_permissions = new Series<string>();
-        private Series<string> _custom_permissions = new Series<string>();
-        
-        public void set_permissions(Series<string> permissions) {
-            _selected_permissions = new Series<string>();
-            _custom_permissions = new Series<string>();
-            
-            foreach (var perm in permissions) {
-                if (COMMON_PERMISSIONS.contains(perm)) {
-                    _selected_permissions.add(perm);
-                } else {
-                    _custom_permissions.add(perm);
-                }
-            }
-        }
-        
-        public Series<string> get_selected() {
-            var result = new Series<string>();
-            result.add_all(_selected_permissions);
-            result.add_all(_custom_permissions);
-            return result;
-        }
-        
-        public override string markup { get {
-            return """
-            <div class="spry-permission-editor" sid="perm-editor">
-                <script spry-res="htmx.js"></script>
-                
-                <!-- Common Permissions -->
-                <div class="permission-group" sid="common-group">
-                    <h4>Common Permissions</h4>
-                    <div class="permission-list" sid="common-list">
-                        <spry-outlet sid="common-items"/>
-                    </div>
-                </div>
-                
-                <!-- Custom Permissions -->
-                <div class="permission-group" sid="custom-group">
-                    <h4>Custom Permissions</h4>
-                    <div class="custom-permissions" sid="custom-list">
-                        <spry-outlet sid="custom-items"/>
-                    </div>
-                    
-                    <!-- Add Custom Permission -->
-                    <div class="add-permission" sid="add-perm">
-                        <input type="text" name="new_permission" sid="new-perm-input"
-                               placeholder="e.g., reports.view"/>
-                        <button sid="add-btn" spry-action=":AddPermission"
-                                spry-target="perm-editor" hx-swap="outerHTML">
-                            Add
-                        </button>
-                    </div>
-                </div>
-            </div>
-            """;
-        }}
-        
-        public override async void prepare() throws Error {
-            var common_items = new Series<Renderable>();
-            foreach (var perm in COMMON_PERMISSIONS) {
-                var item = create_permission_checkbox(perm, _selected_permissions.contains(perm));
-                common_items.add(item);
-            }
-            set_outlet_children("common-items", common_items);
-            
-            var custom_items = new Series<Renderable>();
-            foreach (var perm in _custom_permissions) {
-                var item = create_custom_permission_tag(perm);
-                custom_items.add(item);
-            }
-            set_outlet_children("custom-items", custom_items);
-        }
-        
-        private Renderable create_permission_checkbox(string permission, bool is_checked) {
-            var safe_perm = permission.replace(".", "-");
-            var checked_attr = is_checked ? "checked" : "";
-            
-            var doc = new MarkupDocument();
-            doc.body.inner_html = @"<label class=\"permission-checkbox\">
-                <input type=\"checkbox\" name=\"perm_$safe_perm\" value=\"$permission\" $checked_attr/>
-                <span>$permission</span>
-            </label>";
-            
-            return new InlineRenderable(doc);
-        }
-        
-        private Renderable create_custom_permission_tag(string permission) {
-            var doc = new MarkupDocument();
-            doc.body.inner_html = @"<span class=\"permission-tag\">
-                <span>$permission</span>
-                <button type=\"button\" spry-action=\":RemovePermission:$permission\"
-                        spry-target=\"perm-editor\" hx-swap=\"outerHTML\" 
-                        class=\"remove-tag\">&times;</button>
-            </span>";
-            
-            return new InlineRenderable(doc);
-        }
-        
-        public async override void handle_action(string action) throws Error {
-            var query = http_context.request.query_params;
-            
-            if (action == "AddPermission") {
-                var new_perm = query.get_any_or_default("new_permission", "").strip();
-                if (new_perm.length > 0 && Permission.is_valid(new_perm)) {
-                    if (!_custom_permissions.contains(new_perm) && 
-                        !_selected_permissions.contains(new_perm)) {
-                        _custom_permissions.add(new_perm);
-                    }
-                }
-            } else if (action.has_prefix("RemovePermission:")) {
-                var perm_to_remove = action.substring(17);
-                _custom_permissions.remove(perm_to_remove);
-            }
-            
-            // Update selected from checkboxes
-            _selected_permissions.clear();
-            foreach (var perm in COMMON_PERMISSIONS) {
-                var safe_perm = perm.replace(".", "-");
-                if (query.get_any_or_default(@"perm_$safe_perm", "") == perm) {
-                    _selected_permissions.add(perm);
-                }
-            }
-        }
-    }
-    
-    // Helper class for inline HTML rendering
-    internal class InlineRenderable : Object, Renderable {
-        private MarkupDocument _doc;
-        
-        public InlineRenderable(MarkupDocument doc) {
-            _doc = doc;
-        }
-        
-        public async MarkupDocument to_document() throws Error {
-            return _doc;
-        }
-    }
-}
-```
-
-### 8.6 UserManagementPage
-
-Orchestrates all user management components. Handles cross-component actions:
-
-```vala
-namespace Spry.Users.Pages {
-
-    public class UserManagementPage : PageComponent {
-        private PermissionService permission_service = inject<PermissionService>();
-        private UserService user_service = inject<UserService>();
-        private SessionService session_service = inject<SessionService>();
-        private ComponentFactory factory = inject<ComponentFactory>();
-        private HttpContext http_context = inject<HttpContext>();
-        
-        // State
-        private string? _success_message = null;
-        private string? _error_message = null;
-        
-        public override string markup { get {
-            return """
-            <!DOCTYPE html>
-            <html>
-            <head>
-                <title>User Management</title>
-                <link rel="stylesheet" href="/static/admin.css"/>
-                <style>
-                    .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; 
-                                    background: rgba(0,0,0,0.5); display: flex; 
-                                    align-items: center; justify-content: center; }
-                    .modal-content { background: white; padding: 2rem; border-radius: 8px; 
-                                    max-width: 500px; width: 100%; max-height: 90vh; overflow-y: auto; }
-                    .badge { padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; margin-right: 0.25rem; }
-                    .badge-active { background: #d4edda; color: #155724; }
-                    .badge-inactive { background: #f8d7da; color: #721c24; }
-                    .badge-verified { background: #cce5ff; color: #004085; }
-                    .permission-tag { display: inline-flex; align-items: center; 
-                                     background: #e9ecef; padding: 0.25rem 0.5rem; 
-                                     border-radius: 4px; margin: 0.25rem; }
-                </style>
-            </head>
-            <body>
-                <div class="admin-container" sid="admin-container">
-                    <header class="admin-header">
-                        <h1>User Management</h1>
-                        <div class="header-actions">
-                            <button sid="create-btn" spry-action=":CreateUser"
-                                    hx-target="#user-form-container" hx-swap="innerHTML"
-                                    class="btn btn-primary">
-                                Create User
-                            </button>
-                        </div>
-                    </header>
-                    
-                    <!-- Status Messages -->
-                    <div spry-if="this._success_message != null" class="alert alert-success" 
-                         sid="success-alert" spry-global="success-alert">
-                        <span content-expr="this._success_message"></span>
-                    </div>
-                    
-                    <div spry-if="this._error_message != null" class="alert alert-error"
-                         sid="error-alert" spry-global="error-alert">
-                        <span content-expr="this._error_message"></span>
-                    </div>
-                    
-                    <!-- User List Component -->
-                    <spry-component name="UserListComponent" sid="user-list"/>
-                    
-                    <!-- User Form Container (for modal) -->
-                    <div id="user-form-container" sid="user-form-container">
-                        <spry-component name="UserFormComponent" sid="user-form"/>
-                    </div>
-                </div>
-            </body>
-            </html>
-            """;
-        }}
-        
-        public override async void prepare() throws Error {
-            permission_service.require_permission(Permission.USER_MANAGEMENT);
-            
-            var user_form = get_component_child<UserFormComponent>("user-form");
-            user_form.hide();
-        }
-        
-        public async override void handle_action(string action) throws Error {
-            var query = http_context.request.query_params;
-            
-            switch (action) {
-                case "CreateUser":
-                    var user_form = get_component_child<UserFormComponent>("user-form");
-                    user_form.show_create();
-                    break;
-                    
-                case "EditUser":
-                    var user_id = query.get_any_or_default("user_id", "");
-                    var user = user_service.get_user_by_id(user_id);
-                    if (user != null) {
-                        var user_form = get_component_child<UserFormComponent>("user-form");
-                        user_form.set_user(user);
-                    }
-                    break;
-                    
-                case "ToggleActive":
-                    var toggle_id = query.get_any_or_default("user_id", "");
-                    var toggle_user = user_service.get_user_by_id(toggle_id);
-                    if (toggle_user != null) {
-                        toggle_user.is_active = !toggle_user.is_active;
-                        user_service.update_user(toggle_user);
-                        _success_message = toggle_user.is_active ? 
-                            "User activated" : "User deactivated";
-                        
-                        var user_list = get_component_child<UserListComponent>("user-list");
-                        add_globals_from(user_list);
-                    }
-                    break;
-                    
-                case "DeleteUser":
-                    var delete_id = query.get_any_or_default("user_id", "");
-                    
-                    var current_user_id = session_service.get_current_user_id();
-                    if (current_user_id == delete_id) {
-                        _error_message = "Cannot delete your own account";
-                        break;
-                    }
-                    
-                    user_service.delete_user(delete_id);
-                    _success_message = "User deleted successfully";
-                    
-                    var user_list = get_component_child<UserListComponent>("user-list");
-                    add_globals_from(user_list);
-                    break;
-            }
-        }
-    }
-}
-```
-
----
-
-## 9. Integration Points
-
-### 9.1 Module Registration
-
-Applications integrate the Users system by registering the module:
-
-```vala
-// In application startup
-var application = new Application();
-application.add_module<SpryModule>();
-application.add_module<Spry.Users.UsersModule>();
-
-// Optional: Configure settings
-var config = application.resolve<Spry.Users.UsersConfiguration>();
-config.session_duration = TimeSpan.HOUR * 24; // 1 day
-config.min_password_length = 12;
-config.default_permissions.add("user");
-```
-
-### 9.2 UsersModule Implementation
-
-```vala
-namespace Spry.Users {
-
-    public class UsersModule : Object, Inversion.Module {
-        
-        public void register(Inversion.Application application) {
-            // Register configuration (singleton)
-            application.add_singleton<UsersConfiguration>();
-            
-            // Register services (scoped for per-request isolation)
-            application.add_scoped<UserService>();
-            application.add_scoped<SessionService>();
-            application.add_scoped<PermissionService>();
-            
-            // Register components (transient)
-            var spry_cfg = application.configure_with<SpryConfigurator>();
-            spry_cfg.add_component<Components.LoginFormComponent>();
-            spry_cfg.add_component<Components.UserListComponent>();
-            spry_cfg.add_component<Components.UserListItemComponent>();
-            spry_cfg.add_component<Components.UserFormComponent>();
-            spry_cfg.add_component<Components.PermissionEditorComponent>();
-            
-            // Register pages (scoped)
-            spry_cfg.add_page<Pages.UserManagementPage>(
-                new EndpointRoute("/admin/users"));
-        }
-        
-        public void initialize(Inversion.Application application) {
-            // Initialize storage structure
-            try {
-                var engine = application.resolve<Implexus.Core.Engine>();
-                initialize_storage(engine);
-            } catch (Error e) {
-                error("Failed to initialize Users storage: %s", e.message);
-            }
-        }
-        
-        private void initialize_storage(Implexus.Core.Engine engine) throws Error {
-            var root = engine.get_root();
-            
-            // Create /spry/users container hierarchy
-            var spry = get_or_create_container(root, "spry");
-            var users_base = get_or_create_container(spry, "users");
-            
-            get_or_create_container(users_base, "users");
-            get_or_create_container(users_base, "sessions");
-            get_or_create_category(users_base, "by_username", "User", "username");
-            get_or_create_category(users_base, "by_email", "User", "email");
-        }
-        
-        private Implexus.Core.Entity get_or_create_container(
-            Implexus.Core.Entity parent, string name) throws Error {
-            var child = parent.get_child(name);
-            if (child != null) return (!)child;
-            return (!)parent.create_container(name);
-        }
-        
-        private void get_or_create_category(
-            Implexus.Core.Entity parent, string name, 
-            string type_label, string expression) throws Error {
-            var child = parent.get_child(name);
-            if (child != null) return;
-            parent.create_category(name, type_label, expression);
-        }
-    }
-}
-```
-
-### 9.3 Protecting Routes
-
-Applications can protect routes using the PermissionService:
-
-```vala
-public class AdminPage : PageComponent {
-    private PermissionService permissions = inject<PermissionService>();
-    
-    public override async void prepare() throws Error {
-        // Will throw PermissionDenied if not authorized
-        permissions.require_permission("admin");
-    }
-}
-```
-
-### 9.4 Accessing Current User
-
-```vala
-public class DashboardPage : PageComponent {
-    private SessionService sessions = inject<SessionService>();
-    
-    public override async void prepare() throws Error {
-        var user = sessions.get_current_user();
-        if (user == null) {
-            // Redirect to login
-            return;
-        }
-        
-        // Use user data
-        this["welcome"].text_content = @"Welcome, $(user.username)!";
-    }
-}
-```
-
-### 9.5 Custom Application Data
-
-```vala
-// Store custom data
-var user = user_service.get_user_by_id(user_id);
-user.application_data = new Properties();
-user.application_data["theme"] = new NativeElement<string>("dark");
-user.application_data["notifications"] = new NativeElement<bool?>(true);
-user_service.update_user(user);
-
-// Retrieve custom data
-var theme = user.application_data?.get_any_or_default("theme", "light");
-```
-
----
-
-## 10. Security Considerations
-
-### 10.1 Password Security
-
-- **Hashing**: Argon2id with moderate ops/mem limits via libsodium
-- **Never store plaintext**: Password field only accepts hashes
-- **Verification timing-safe**: Uses libsodium's constant-time comparison
-
-### 10.2 Session Token Security
-
-- **Signed**: Ed25519 signatures prevent token tampering
-- **Encrypted**: X25519-Seal prevents token inspection
-- **URL-safe encoding**: Base64 with `+` → `-` and `/` → `_` substitution
-- **Expiry validation**: Server-side expiry check in addition to token expiry
-
-### 10.3 Cookie Security
-
-- **HttpOnly**: Prevents JavaScript access to session cookie
-- **Secure**: Only transmitted over HTTPS (configurable for development)
-- **SameSite**: Strict by default to prevent CSRF
-- **Path-limited**: Cookie scoped to application path
-
-### 10.4 Permission Checks
-
-- **Deny by default**: No permissions unless explicitly granted
-- **Admin override**: Users with "admin" permission bypass all checks
-- **Wildcard support**: "content.*" implies "content.edit", "content.delete", etc.
-- **Server-side enforcement**: All permission checks happen server-side
-
-### 10.5 Input Validation
-
-- **Username**: Alphanumeric, underscores, minimum 3 characters
-- **Email**: Basic email format validation
-- **Password**: Configurable complexity requirements
-- **Permission strings**: Validated against regex `^[a-zA-Z0-9._-]+$`
-
-### 10.6 Session Management
-
-- **Session invalidation**: On password change, user deletion, or explicit logout
-- **IP binding**: Optional binding to client IP (disabled by default)
-- **User agent tracking**: Optional for anomaly detection
-- **Automatic cleanup**: Expired sessions cleaned up periodically
-
-### 10.7 Known Limitations
-
-- **No rate limiting**: Applications should implement their own rate limiting on login endpoints
-- **No account lockout**: No automatic lockout after failed attempts (application responsibility)
-- **No password reset**: Built-in password reset flow not included (application responsibility)
-- **No two-factor authentication**: Not included in this implementation
-
----
-
-## 11. Mermaid Diagrams
-
-### 11.1 Authentication Flow
-
-```mermaid
-sequenceDiagram
-    participant Client
-    participant LoginFormComponent
-    participant UserService
-    participant SessionService
-    participant Implexus
-    
-    Client->>LoginFormComponent: Submit login form
-    LoginFormComponent->>UserService: authenticate username/password
-    UserService->>Implexus: Query user by username
-    Implexus-->>UserService: User document
-    UserService->>UserService: verify_password
-    UserService-->>LoginFormComponent: User authenticated
-    LoginFormComponent->>SessionService: create_session user_id
-    SessionService->>Implexus: Store session document
-    SessionService->>SessionService: create_session_token
-    SessionService->>Client: Set session cookie
-    LoginFormComponent-->>Client: Redirect to dashboard
-```
-
-### 11.2 Permission Check Flow
-
-```mermaid
-flowchart TD
-    A[Request to protected resource] --> B{Is authenticated?}
-    B -->|No| C[Redirect to login]
-    B -->|Yes| D{Has admin permission?}
-    D -->|Yes| E[Allow access]
-    D -->|No| F{Has required permission?}
-    F -->|Yes| E
-    F -->|No| G[Permission denied error]
-    
-    E --> H[Process request]
-```
-
-### 11.3 Storage Hierarchy
-
-```mermaid
-graph TD
-    Root[Root Container /] --> Spry[spry/]
-    Spry --> Users[users/]
-    Users --> UserDocs[users/ - User Documents]
-    Users --> SessionDocs[sessions/ - Session Documents]
-    Users --> ByUsername[by_username - Category]
-    Users --> ByEmail[by_email - Category]
-    
-    UserDocs --> User1[uuid-1 - type: User]
-    UserDocs --> User2[uuid-2 - type: User]
-    
-    SessionDocs --> Session1[uuid-s1 - type: Session]
-    SessionDocs --> Session2[uuid-s2 - type: Session]
-```
-
----
-
-## 12. Dependencies
-
-### 12.1 Meson Build Configuration
-
-```meson
-# src/Users/meson.build
-
-users_sources = files(
-    'UsersModule.vala',
-    'UserService.vala',
-    'SessionService.vala',
-    'PermissionService.vala',
-    'Models/User.vala',
-    'Models/Session.vala',
-    'Models/Permission.vala',
-    'Components/LoginFormComponent.vala',
-    'Components/UserListComponent.vala',
-    'Components/UserListItemComponent.vala',
-    'Components/UserFormComponent.vala',
-    'Components/PermissionEditorComponent.vala',
-    'Pages/UserManagementPage.vala'
-)
-
-# Users library
-libspry_users = static_library('spry-users',
-    users_sources,
-    dependencies: [spry_dep, implexus_dep, sodium_deps],
-    include_directories: include_directories('..')
-)
-
-spry_users_dep = declare_dependency(
-    link_with: libspry_users,
-    dependencies: [spry_dep, implexus_dep]
-)
-```
-
-### 12.2 Required Dependencies
-
-| Dependency | Purpose |
-|------------|---------|
-| `spry-0.1` | Core framework (Component, PageComponent, CryptographyProvider) |
-| `implexus-0.1` | Document storage |
-| `invercargill-1` | Data structures (Series, Dictionary, HashSet) |
-| `invercargill-json` | JSON serialization |
-| `inversion-0.1` | IoC container |
-| `libsodium` | Cryptography (password hashing, signing, encryption) |
-| `astralis-0.1` | HTTP handling |
-
-### 12.3 Root meson.build Update
-
-Add to root `meson.build`:
-
-```meson
-implexus_dep = dependency('implexus-0.1')
-subdir('src/Users')
-```
-
----
-
-## 13. Future Considerations
-
-### Potential Enhancements
-
-1. **OAuth Integration**: Add support for external OAuth providers
-2. **Two-Factor Authentication**: TOTP or WebAuthn support
-3. **Password Reset Flow**: Email-based password recovery
-4. **Account Verification**: Email verification workflow
-5. **Audit Logging**: Track user management actions
-6. **Rate Limiting**: Built-in login attempt limiting
-7. **API Token Support**: For API-only authentication
-8. **Role-Based Access**: Group permissions into roles
-
----
-
-## 14. Implementation Order
-
-Recommended implementation sequence:
-
-1. **Models**: User, Session, Permission data classes with PropertyMappers
-2. **UsersConfiguration**: Configuration class with defaults
-3. **UserService**: Core user CRUD and password hashing
-4. **SessionService**: Session management and cookie handling
-5. **PermissionService**: Permission checking and management
-6. **UsersModule**: IoC registration and storage initialization
-7. **LoginFormComponent**: Basic login form
-8. **UserListItemComponent**: Individual user row component
-9. **UserListComponent**: User list with search and pagination
-10. **PermissionEditorComponent**: Permission management widget
-11. **UserFormComponent**: Create/edit user modal form
-12. **UserManagementPage**: Page orchestrating all components
-13. **Testing**: Unit tests and integration tests
-14. **Documentation**: Usage examples and API docs

+ 0 - 30
src/Authentication/meson.build

@@ -1,30 +0,0 @@
-authentication_sources = files(
-    'User.vala',
-    'Session.vala',
-    'UserService.vala',
-    'SessionService.vala',
-    'PermissionService.vala',
-    'UserIdentityProvider.vala',
-    'UserRepository.vala',
-    'SessionRepository.vala',
-    'SqlUserRepository.vala',
-    'SqlSessionRepository.vala',
-    'CreateAuthTables.vala',
-    'Components/LoginFormComponent.vala',
-    'Components/UserManagementComponent.vala',
-    'Components/UserDetailsComponent.vala',
-    'Components/NewUserComponent.vala'
-)
-
-libspry_authentication = static_library('spry-authentication',
-    authentication_sources,
-    dependencies: [spry_dep, spry_authorisation_dep, invercargill_sql_dep, sqlite_dep, sodium_deps, invercargill_dep, astralis_dep],
-    include_directories: include_directories('..')
-)
-
-spry_authentication_inc = include_directories('.')
-spry_authentication_dep = declare_dependency(
-    link_with: libspry_authentication,
-    include_directories: spry_authentication_inc,
-    dependencies: [spry_dep, spry_authorisation_dep, invercargill_sql_dep]
-)

+ 11 - 258
src/Authorisation/AuthorisationContext.vala

@@ -1,272 +1,25 @@
-using Inversion;
 
 namespace Spry.Authorisation {
 
-    /**
-     * Request-scoped authorisation context.
-     * 
-     * Provides access to the current identity's authorisation state.
-     * Automatically populated from cookie or Bearer token on each request.
-     * 
-     * Usage:
-     * ```vala
-     * var auth = inject<AuthorisationContext>();
-     * if (!auth.is_authorised) {
-     *     // Redirect to login
-     * }
-     * if (auth.has_permission("admin")) {
-     *     // Show admin content
-     * }
-     * ```
-     */
-    public class AuthorisationContext : GLib.Object {
+    public class AuthorisationContext : Object {
 
-        private AuthorisationToken? _token = null;
-        private IdentityProvider? _identity_provider = null;
-        private Identity? _cached_identity = null;
+        public AuthorisationToken? token { get; private set; }
 
-        /**
-         * Whether the request has a valid authorisation token.
-         */
-        public bool is_authorised { get { return _token != null; } }
-
-        /**
-         * The identity ID from the token.
-         * Returns null if not authorised.
-         */
-        public string? user_id { 
-            get { return _token?.user_id; } 
-        }
-
-        /**
-         * The username from the token.
-         * Returns null if not authorised.
-         */
-        public string? username { 
-            get { return _token?.username; } 
-        }
-
-        /**
-         * The permissions from the token.
-         * Returns empty array if not authorised.
-         */
-        public string[] permissions {
-            owned get {
-                if (_token == null) {
-                    return new string[0];
-                }
-                return _token.permissions;
-            }
-        }
-
-        /**
-         * Additional data from the token.
-         * Returns empty variant if not authorised.
-         */
-        public Variant data {
-            owned get {
-                if (_token == null) {
-                    return new Variant.array(VariantType.VARIANT, {});
-                }
-                return _token.data;
-            }
-        }
-
-        /**
-         * The token expiry time, or null if no expiry set.
-         */
-        public DateTime? expires_at {
-            get { return _token?.expires_at; }
-        }
-
-        /**
-         * The token issuance time.
-         */
-        public DateTime issued_at {
-            owned get { return _token?.issued_at ?? new DateTime.now_utc(); }
+        public AuthorisationContext(AuthorisationToken? token = null) {
+            this.token = token;
         }
 
-        /**
-         * The underlying token (for advanced use).
-         */
-        public AuthorisationToken? token { get { return _token; } }
-
-        /**
-         * Creates a new AuthorisationContext.
-         */
-        public AuthorisationContext() {
-        }
-
-        /**
-         * Creates an AuthorisationContext with a token.
-         */
-        public AuthorisationContext.with_token(AuthorisationToken token) {
-            _token = token;
-        }
-
-        /**
-         * Sets the token (called by AuthorisationService).
-         * 
-         * @param token The token to set, or null to clear
-         */
-        internal void set_token(AuthorisationToken? token) {
-            _token = token;
-            _cached_identity = null;
-        }
-
-        /**
-         * Sets the identity provider (called during initialization).
-         * 
-         * @param provider The identity provider to use
-         */
-        internal void set_identity_provider(IdentityProvider? provider) {
-            _identity_provider = provider;
-        }
-
-        // =========================================================================
-        // Permission Checking
-        // =========================================================================
-
-        /**
-         * Checks if the current identity has a specific permission.
-         * 
-         * Supports wildcard matching:
-         * - "admin" matches everything (super-user)
-         * - "users.*" matches "users.read", "users.write", etc.
-         * - "*" matches everything
-         * 
-         * @param permission The permission to check
-         * @return true if the identity has the permission
-         */
-        public bool has_permission(string permission) {
-            if (_token == null) {
+        public bool has_permission(string permission = "*") {
+            if(token == null){
                 return false;
             }
-
-            foreach (var user_perm in _token.permissions) {
-                if (PermissionMatcher.matches(user_perm, permission)) {
-                    return true;
-                }
-            }
-            return false;
+            return token.permissions.any(p => PermissionMatcher.matches(permission, p));
         }
 
-        /**
-         * Requires a specific permission, throws if not present.
-         * 
-         * @param permission The required permission
-         * @throws AuthorisationError.NOT_AUTHORISED if not authorised
-         * @throws AuthorisationError.PERMISSION_DENIED if permission missing
-         */
-        public void require_permission(string permission) throws Error {
-            if (!is_authorised) {
-                throw new AuthorisationError.NOT_AUTHORISED(
-                    "Authentication required"
-                );
-            }
-            if (!has_permission(permission)) {
-                throw new AuthorisationError.PERMISSION_DENIED(
-                    @"Permission '$permission' required"
-                );
-            }
-        }
-
-        /**
-         * Checks if the identity has ANY of the specified permissions.
-         * 
-         * @param permissions Array of permissions to check
-         * @return true if the identity has at least one of the permissions
-         */
-        public bool has_any_permission(string[] permissions) {
-            foreach (var perm in permissions) {
-                if (has_permission(perm)) {
-                    return true;
-                }
-            }
-            return false;
-        }
-
-        /**
-         * Checks if the identity has ALL of the specified permissions.
-         * 
-         * @param permissions Array of permissions to check
-         * @return true if the identity has all of the permissions
-         */
-        public bool has_all_permissions(string[] permissions) {
-            foreach (var perm in permissions) {
-                if (!has_permission(perm)) {
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        // =========================================================================
-        // Identity Retrieval
-        // =========================================================================
-
-        /**
-         * Retrieves the full Identity object from the provider.
-         * 
-         * This calls the registered IdentityProvider to get the complete
-         * identity object (e.g., User from Spry.Authentication).
-         * 
-         * @return The Identity, or null if not found/not authorised
-         * @throws Error on retrieval failure
-         */
-        public async Identity? get_current_identity_async() throws Error {
-            if (_token == null) {
-                return null;
-            }
-            if (_cached_identity != null) {
-                return _cached_identity;
-            }
-            if (_identity_provider == null) {
-                return null;
-            }
-
-            _cached_identity = yield _identity_provider.get_identity_by_id(_token.user_id);
-            return _cached_identity;
-        }
-
-        /**
-         * Synchronous version for cases where async is not available.
-         * 
-         * Note: This will only return a cached identity if one has been
-         * fetched previously via get_current_identity_async().
-         * 
-         * @return The cached Identity, or null if not available
-         */
-        public Identity? get_current_identity() {
-            if (_token == null) {
-                return null;
-            }
-            if (_cached_identity != null) {
-                return _cached_identity;
-            }
-            // Cannot call async synchronously - return null
-            return null;
-        }
-
-        // =========================================================================
-        // Utility Methods
-        // =========================================================================
-
-        /**
-         * Checks if the token has expired.
-         * 
-         * @return true if the token has expired
-         */
-        public bool is_expired() {
-            return _token?.is_expired() ?? true;
+        public bool is_anonymous() {
+            return token == null;
         }
 
-        /**
-         * Clears the authorisation context.
-         */
-        public void clear() {
-            _token = null;
-            _cached_identity = null;
-        }
     }
-}
+
+}

+ 40 - 0
src/Authorisation/AuthorisationPipelineComponent.vala

@@ -0,0 +1,40 @@
+using Astralis;
+using Inversion;
+
+namespace Spry.Authorisation {
+
+    public class AuthorisationPipelineComponent : Object, PipelineComponent {
+
+        private Scope scope = inject<Scope>();
+        private AuthorisationService authorisation_service = inject<AuthorisationService>();
+
+        public async Astralis.HttpResult process_request (Astralis.HttpContext http_context, Astralis.PipelineContext pipeline_context) throws Error {
+
+            var header = http_context.request.headers.get_any_or_default("Authorization");
+            AuthorisationToken token = null;
+            if(header.down().has_prefix ("bearer")) {
+                try {
+                    token = authorisation_service.read_token (header.substring(7).chug().chomp());
+                }
+                catch (Error e) {
+                    warning("Encountered error while reading bearer token: " + e.message);
+                }
+            }
+
+            if(token == null && http_context.request.cookies.has(COOKIE_NAME)) {
+                var token_string = http_context.request.cookies.get_any(COOKIE_NAME);
+                try {
+                    token = authorisation_service.read_token(token_string);
+                }
+                catch (Error e) {
+                    warning("Encountered error while reading cookie token: " + e.message);
+                }
+            }
+
+            scope.register_local_scoped<AuthorisationContext>(() => new AuthorisationContext (token));
+            return yield pipeline_context.next ();
+        }
+
+    }
+
+}

+ 26 - 322
src/Authorisation/AuthorisationService.vala

@@ -1,340 +1,44 @@
 using Inversion;
+using InvercargillJson;
+using Invercargill;
 using Astralis;
 
 namespace Spry.Authorisation {
 
-    /**
-     * Main service for authorisation request processing.
-     * 
-     * Token Sources (in order of precedence):
-     * 1. Authorization: Bearer <token> header
-     * 2. Cookie named in cookie_name property (default: spry_auth)
-     * 
-     * This service:
-     * - Extracts tokens from requests (cookie or bearer header)
-     * - Validates tokens via AuthorisationTokenService
-     * - Populates AuthorisationContext
-     * - Manages auth cookies on responses
-     */
-    public class AuthorisationService : GLib.Object {
+    public const string CRYPTOGRAPHY_NAMESPACE = "spry-authorisation";
+    public const string COOKIE_NAME = "_spry-authorisation";
 
-        private AuthorisationTokenService _token_service = inject<AuthorisationTokenService>();
-        private AuthorisationContext _context = inject<AuthorisationContext>();
-        private IdentityProvider? _identity_provider = inject<IdentityProvider>();
-        private HttpContext _http_context = inject<HttpContext>();
+    public class AuthorisationService : Object {
 
-        // Configuration
-        private string _cookie_name = "spry_auth";
-        private bool _cookie_secure = true;
-        private TimeSpan _token_duration = TimeSpan.HOUR * 24;
+        private CryptographyProvider crypto_provider = inject<CryptographyProvider>();
 
-        /**
-         * Cookie name for authorisation tokens.
-         */
-        public string cookie_name { 
-            get { return _cookie_name; } 
-            set { _cookie_name = value; }
+        public AuthorisationToken read_token(string token_string) throws Error {
+            var json_string = crypto_provider.read(CRYPTOGRAPHY_NAMESPACE, Base64.decode(token_string));
+            var json_object = new JsonElement.from_string(json_string).as<JsonObject>();
+            var token = AuthorisationToken.get_mapper().materialise(json_object);
+            token.cryptographic_token = Wrap.base64_string(token_string).to_byte_buffer();
+            return token;
         }
 
-        /**
-         * Whether cookies should be Secure (HTTPS only).
-         */
-        public bool cookie_secure { 
-            get { return _cookie_secure; } 
-            set { _cookie_secure = value; }
+        public AuthorisationToken authorise_identity(Identity identity, TimeSpan? duration = null) throws Error {
+            var token = new AuthorisationToken(identity, duration);
+            var properties = AuthorisationToken.get_mapper().map_from(token);
+            var json_object = new JsonElement.from_properties(properties);
+            var json_string = json_object.stringify();
+            token.cryptographic_token = Wrap.byte_array(crypto_provider.author(CRYPTOGRAPHY_NAMESPACE, json_string)).to_byte_buffer();
+            return token;
         }
 
-        /**
-         * Default token validity duration.
-         */
-        public TimeSpan token_duration { 
-            get { return _token_duration; } 
-            set { _token_duration = value; }
+        public static string get_bearer_token(AuthorisationToken token) {
+            return token.cryptographic_token.to_base64();
         }
 
-        /**
-         * The underlying token service.
-         */
-        public AuthorisationTokenService token_service { 
-            get { return _token_service; }
+        public static void set_authorisation_cookie(AuthorisationToken token, HttpResult result) {
+            var token_str = token.cryptographic_token.to_base64();
+            result.headers.add("Set-Cookie", @"$COOKIE_NAME=$token_str; Secure");
         }
 
-        /**
-         * The current authorisation context.
-         */
-        public AuthorisationContext context {
-            get { return _context; }
-        }
-
-        /**
-         * Creates a new AuthorisationService.
-         */
-        public AuthorisationService() {
-            // Set up identity provider in context if available
-            if (_identity_provider != null) {
-                _context.set_identity_provider(_identity_provider);
-            }
-        }
-
-        // =========================================================================
-        // Token Generation
-        // =========================================================================
-
-        /**
-         * Generates an authorisation token for an identity.
-         * 
-         * @param identity The identity to create a token for
-         * @param expires_at Optional custom expiry (defaults to token_duration from now)
-         * @return The encrypted token string
-         */
-        public string generate_token(Identity identity, DateTime? expires_at = null) {
-            return _token_service.generate_token(identity, expires_at);
-        }
-
-        /**
-         * Generates a token with a specific duration.
-         * 
-         * @param identity The identity to create a token for
-         * @param duration The token validity duration
-         * @return The encrypted token string
-         */
-        public string generate_token_with_duration(Identity identity, TimeSpan duration) {
-            var expires_at = new DateTime.now_utc().add(duration);
-            return _token_service.generate_token(identity, expires_at);
-        }
-
-        // =========================================================================
-        // Token Validation
-        // =========================================================================
-
-        /**
-         * Validates a token string.
-         * 
-         * @param token The encrypted token string
-         * @return The parsed AuthorisationToken, or null if invalid
-         */
-        public AuthorisationToken? validate_token(string token) {
-            return _token_service.parse_token(token);
-        }
-
-        // =========================================================================
-        // Request Processing
-        // =========================================================================
-
-        /**
-         * Extracts and validates token from an HTTP context's request.
-         * 
-         * Checks Authorization header first, then cookie.
-         * Populates the AuthorisationContext if valid token found.
-         * 
-         * @param http_context The HttpContext containing the request
-         * @return An AuthorisationContext populated with the token data
-         * @throws Error on processing failure
-         */
-        public async AuthorisationContext get_context_from_request(HttpContext http_context) throws Error {
-            string? token = null;
-
-            // 1. Check Authorization: Bearer header
-            var auth_header = http_context.request.headers.get_any_or_default("Authorization");
-            if (auth_header != null && ((!)auth_header).has_prefix("Bearer ")) {
-                token = ((!)auth_header).substring(7).strip();
-            }
-
-            // 2. Check cookie
-            if (token == null) {
-                token = http_context.request.get_cookie(_cookie_name);
-            }
-
-            // 3. Validate and populate context
-            if (token != null) {
-                var parsed_token = _token_service.parse_token((!)token);
-                if (parsed_token != null) {
-                    _context.set_token(parsed_token);
-                }
-            }
-
-            // Set identity provider if available
-            if (_identity_provider != null) {
-                _context.set_identity_provider(_identity_provider);
-            }
-
-            return _context;
-        }
-
-        /**
-         * Processes the current HTTP request (uses injected HttpContext).
-         *
-         * Checks Authorization header first, then cookie.
-         * Populates the AuthorisationContext if valid token found.
-         *
-         * @throws Error on processing failure
-         */
-        public async void process_request_async() throws Error {
-            string? token = null;
-
-            // 1. Check Authorization: Bearer header
-            var auth_header = _http_context.request.headers.get_any_or_default("Authorization");
-            if (auth_header != null && ((!)auth_header).has_prefix("Bearer ")) {
-                token = ((!)auth_header).substring(7).strip();
-            }
-
-            // 2. Check cookie
-            if (token == null) {
-                token = _http_context.request.get_cookie(_cookie_name);
-            }
-
-            // 3. Validate and populate context
-            if (token != null) {
-                var parsed_token = _token_service.parse_token((!)token);
-                if (parsed_token != null) {
-                    _context.set_token(parsed_token);
-                }
-            }
-
-            // Set identity provider if available
-            if (_identity_provider != null) {
-                _context.set_identity_provider(_identity_provider);
-            }
-        }
-
-        // =========================================================================
-        // Cookie Management
-        // =========================================================================
-
-        /**
-         * Sets the authorisation cookie on an HTTP result.
-         * 
-         * Cookie configuration:
-         * - HttpOnly: true
-         * - Secure: configurable (default true)
-         * - SameSite: Strict
-         * - Path: /
-         * 
-         * @param result The HttpResult to set the cookie on
-         * @param token The token to set
-         */
-        public void set_auth_cookie(HttpResult result, string token) {
-            // Build the Set-Cookie header value
-            var max_age = (int)(_token_duration / TimeSpan.SECOND);
-            var cookie_value = @"$_cookie_name=$token; Path=/; Max-Age=$max_age; HttpOnly";
 
-            if (_cookie_secure) {
-                cookie_value += "; Secure";
-            }
-
-            cookie_value += "; SameSite=Strict";
-
-            result.set_header("Set-Cookie", cookie_value);
-        }
-
-        /**
-         * Sets the authorisation cookie with custom max-age.
-         * 
-         * @param result The HttpResult to set the cookie on
-         * @param token The token to set
-         * @param max_age_seconds The cookie max-age in seconds
-         */
-        public void set_auth_cookie_with_max_age(HttpResult result, string token, int max_age_seconds) {
-            var cookie_value = @"$_cookie_name=$token; Path=/; Max-Age=$max_age_seconds; HttpOnly";
-
-            if (_cookie_secure) {
-                cookie_value += "; Secure";
-            }
-
-            cookie_value += "; SameSite=Strict";
-
-            result.set_header("Set-Cookie", cookie_value);
-        }
-
-        /**
-         * Clears the authorisation cookie.
-         * 
-         * @param result The HttpResult to clear the cookie on
-         */
-        public void clear_auth_cookie(HttpResult result) {
-            var cookie_value = @"$_cookie_name=; Path=/; Max-Age=0; HttpOnly";
-
-            if (_cookie_secure) {
-                cookie_value += "; Secure";
-            }
-
-            cookie_value += "; SameSite=Strict";
-
-            result.set_header("Set-Cookie", cookie_value);
-        }
-
-        // =========================================================================
-        // Convenience Methods
-        // =========================================================================
-
-        /**
-         * Checks if the current request is authorised.
-         * 
-         * Call get_context_from_request() first to populate the context.
-         * 
-         * @return true if the request has a valid authorisation token
-         */
-        public bool is_authorised() {
-            return _context.is_authorised;
-        }
-
-        /**
-         * Checks if the current identity has a specific permission.
-         * 
-         * Call get_context_from_request() first to populate the context.
-         * 
-         * @param permission The permission to check
-         * @return true if the identity has the permission
-         */
-        public bool has_permission(string permission) {
-            return _context.has_permission(permission);
-        }
-
-        /**
-         * Requires a specific permission, throws if not present.
-         * 
-         * Call get_context_from_request() first to populate the context.
-         * 
-         * @param permission The required permission
-         * @throws AuthorisationError.NOT_AUTHORISED if not authorised
-         * @throws AuthorisationError.PERMISSION_DENIED if permission missing
-         */
-        public void require_permission(string permission) throws Error {
-            _context.require_permission(permission);
-        }
-
-        /**
-         * Gets the current user ID from the context.
-         * 
-         * Call get_context_from_request() first to populate the context.
-         * 
-         * @return The user ID, or null if not authorised
-         */
-        public string? get_user_id() {
-            return _context.user_id;
-        }
-
-        /**
-         * Gets the current username from the context.
-         * 
-         * Call get_context_from_request() first to populate the context.
-         * 
-         * @return The username, or null if not authorised
-         */
-        public string? get_username() {
-            return _context.username;
-        }
-
-        /**
-         * Gets the current identity from the context.
-         * 
-         * Call get_context_from_request() first to populate the context.
-         * 
-         * @return The Identity, or null if not available
-         * @throws Error on retrieval failure
-         */
-        public async Identity? get_current_identity_async() throws Error {
-            return yield _context.get_current_identity_async();
-        }
     }
-}
+
+}

+ 40 - 240
src/Authorisation/AuthorisationToken.vala

@@ -1,4 +1,7 @@
-using Json;
+using Invercargill;
+using Invercargill.Mapping;
+using Invercargill.DataStructures;
+using InvercargillJson;
 
 namespace Spry.Authorisation {
 
@@ -13,17 +16,17 @@ namespace Spry.Authorisation {
      * - issued_at: Token issuance timestamp
      * - expires_at: Token expiry timestamp
      */
-    public class AuthorisationToken : GLib.Object {
+    public class AuthorisationToken {
 
         // Identity fields
-        public string user_id { get; set; default = ""; }
-        public string username { get; set; default = ""; }
-        public string[] permissions { get; set; default = {}; }
-        public Variant data { get; set; }
-
-        // Token metadata
-        public DateTime issued_at { get; set; }
-        public DateTime? expires_at { get; set; default = null; }
+        public string user_id { get; private set; }
+        public string username { get; private set; }
+        public ImmutableLot<string> permissions { get; private set; }
+        public DateTime issued_at { get; private set; }
+        public DateTime expires_at { get; private set; }
+        public string token_id { get; private set; }
+        public Properties data { get; private set; }
+        public ByteBuffer cryptographic_token { get; internal set; }
 
         /**
          * Creates a token from an Identity.
@@ -31,23 +34,14 @@ namespace Spry.Authorisation {
          * @param identity The identity to create a token for
          * @param duration Optional token duration (defaults to 24 hours)
          */
-        public AuthorisationToken.from_identity(Identity identity, TimeSpan? duration = null) {
-            GLib.Object(
-                user_id: identity.id,
-                username: identity.username,
-                permissions: identity.permissions,
-                data: identity.data,
-                issued_at: new DateTime.now_utc(),
-                expires_at: new DateTime.now_utc().add(duration ?? TimeSpan.HOUR * 24)
-            );
-        }
-
-        /**
-         * Default constructor for deserialization.
-         */
-        public AuthorisationToken() {
-            issued_at = new DateTime.now_utc();
-            data = new Variant.array(VariantType.VARIANT, {});
+        internal AuthorisationToken(Identity identity, TimeSpan? duration = null) {
+            this.user_id = identity.id;
+            this.username = identity.username;
+            this.permissions = identity.permissions;
+            this.data = identity.data;
+            this.issued_at = new DateTime.now_utc();
+            this.expires_at = new DateTime.now_utc().add(duration ?? TimeSpan.HOUR * 24);
+            this.token_id = Uuid.string_random();
         }
 
         /**
@@ -55,227 +49,33 @@ namespace Spry.Authorisation {
          * 
          * @return true if the token has expired, false otherwise
          */
-        public bool is_expired() {
-            if (expires_at == null) {
-                return false;
-            }
-            return ((!)expires_at).compare(new DateTime.now_utc()) <= 0;
-        }
-
-        /**
-         * Serializes the token to JSON for encryption.
-         * 
-         * @return A Json.Object containing all token data
-         */
-        public Json.Object to_json() {
-            var obj = new Json.Object();
-            
-            // Identity fields
-            obj.set_string_member("user_id", user_id ?? "");
-            obj.set_string_member("username", username ?? "");
-            
-            // Permissions array
-            var perms_array = new Json.Array();
-            foreach (var perm in permissions) {
-                perms_array.add_string_element(perm);
-            }
-            obj.set_array_member("permissions", perms_array);
-            
-            // Data - serialize Variant to JSON
-            var data_obj = variant_to_json_object(data);
-            obj.set_object_member("data", data_obj);
-            
-            // Metadata
-            obj.set_string_member("issued_at", issued_at.format_iso8601());
-            if (expires_at != null) {
-                obj.set_string_member("expires_at", ((!)expires_at).format_iso8601());
-            }
-            
-            return obj;
+        public bool has_expired() {
+            return expires_at.compare(new DateTime.now_utc()) <= 0;
         }
 
-        /**
-         * Deserializes a token from JSON after decryption.
-         * 
-         * @param obj The Json.Object to deserialize from
-         * @return A new AuthorisationToken instance
-         */
-        public static AuthorisationToken from_json(Json.Object obj) {
-            var token = new AuthorisationToken();
-            
-            // Identity fields
-            token.user_id = obj.get_string_member("user_id") ?? "";
-            token.username = obj.get_string_member("username") ?? "";
-            
-            // Permissions
-            if (obj.has_member("permissions")) {
-                var arr = obj.get_array_member("permissions");
-                var perms = new string[arr.get_length()];
-                uint i = 0;
-                foreach (var element in arr.get_elements()) {
-                    perms[i++] = element.get_string() ?? "";
-                }
-                token.permissions = perms;
-            }
-            
-            // Data
-            if (obj.has_member("data")) {
-                token.data = json_object_to_variant(obj.get_object_member("data"));
-            }
-            
-            // Metadata
-            if (obj.has_member("issued_at")) {
-                token.issued_at = new DateTime.from_iso8601(
-                    obj.get_string_member("issued_at"),
-                    new TimeZone.utc()
-                );
-            }
-            if (obj.has_member("expires_at")) {
-                token.expires_at = new DateTime.from_iso8601(
-                    obj.get_string_member("expires_at"),
-                    new TimeZone.utc()
-                );
-            }
+        // Blank constructor for mapper below
+        private AuthorisationToken.blank() {
             
-            return token;
         }
 
         /**
-         * Converts the token to a JSON string.
+         * Gets the property mapper for AuthorisationToken serialization.
          * 
-         * @return A JSON string representation
+         * Uses the PropertyMapper system from Invercargill.Mapping for
+         * consistent serialization with the rest of the framework.
          */
-        public string to_json_string() {
-            var node = new Json.Node(Json.NodeType.OBJECT);
-            node.set_object(to_json());
-            return Json.to_string(node, false);
-        }
-
-        /**
-         * Parses a token from a JSON string.
-         * 
-         * @param json_str The JSON string to parse
-         * @return A new AuthorisationToken instance, or null on parse error
-         */
-        public static AuthorisationToken? from_json_string(string json_str) {
-            try {
-                var parser = new Json.Parser();
-                parser.load_from_data(json_str);
-                var root = parser.get_root();
-                
-                if (root.get_node_type() != Json.NodeType.OBJECT) {
-                    return null;
-                }
-                
-                return from_json(root.get_object());
-            } catch (Error e) {
-                return null;
-            }
-        }
-
-        // Private helper to convert Variant to Json.Object
-        private static Json.Object variant_to_json_object(Variant v) {
-            var obj = new Json.Object();
-            
-            if (!v.is_of_type(VariantType.DICTIONARY)) {
-                // If not a dictionary, wrap in a single value
-                obj.set_string_member("value", v.print(false));
-                return obj;
-            }
-            
-            var iter = new VariantIter(v);
-            string key;
-            Variant value;
-            while (iter.next("{sv}", out key, out value)) {
-                add_variant_to_json_object(obj, key, value);
-            }
-            
-            return obj;
-        }
-
-        // Private helper to add a Variant value to a Json.Object
-        private static void add_variant_to_json_object(Json.Object obj, string key, Variant value) {
-            if (value.is_of_type(VariantType.STRING)) {
-                obj.set_string_member(key, value.get_string());
-            } else if (value.is_of_type(VariantType.BOOLEAN)) {
-                obj.set_boolean_member(key, value.get_boolean());
-            } else if (value.is_of_type(VariantType.INT64)) {
-                obj.set_int_member(key, value.get_int64());
-            } else if (value.is_of_type(VariantType.DOUBLE)) {
-                obj.set_double_member(key, value.get_double());
-            } else if (value.is_of_type(VariantType.ARRAY)) {
-                var arr = new Json.Array();
-                var iter = new VariantIter(value);
-                Variant item;
-                while ((item = iter.next_value()) != null) {
-                    arr.add_element(variant_to_json_node(item));
-                }
-                obj.set_array_member(key, arr);
-            } else if (value.is_of_type(VariantType.DICTIONARY)) {
-                obj.set_object_member(key, variant_to_json_object(value));
-            } else {
-                // Fallback: convert to string
-                obj.set_string_member(key, value.print(false));
-            }
-        }
-
-        // Private helper to convert a Variant to a Json.Node
-        private static Json.Node variant_to_json_node(Variant v) {
-            var node = new Json.Node(Json.NodeType.VALUE);
-            
-            if (v.is_of_type(VariantType.STRING)) {
-                node.set_string(v.get_string());
-            } else if (v.is_of_type(VariantType.BOOLEAN)) {
-                node.set_boolean(v.get_boolean());
-            } else if (v.is_of_type(VariantType.INT64)) {
-                node.set_int(v.get_int64());
-            } else if (v.is_of_type(VariantType.DOUBLE)) {
-                node.set_double(v.get_double());
-            } else {
-                node.set_string(v.print(false));
-            }
-            
-            return node;
-        }
-
-        // Private helper to convert Json.Object to Variant
-        private static Variant json_object_to_variant(Json.Object obj) {
-            var builder = new VariantBuilder(VariantType.VARDICT);
-            
-            foreach (var member in obj.get_members()) {
-                var node = obj.get_member(member);
-                builder.add("{sv}", member, json_node_to_variant(node));
-            }
-            
-            return builder.end();
-        }
-
-        // Private helper to convert Json.Node to Variant
-        private static Variant json_node_to_variant(Json.Node node) {
-            switch (node.get_node_type()) {
-                case Json.NodeType.VALUE:
-                    if (node.get_value_type() == typeof(string)) {
-                        return new Variant.string(node.get_string() ?? "");
-                    } else if (node.get_value_type() == typeof(bool)) {
-                        return new Variant.boolean(node.get_boolean());
-                    } else if (node.get_value_type() == typeof(int64)) {
-                        return new Variant.int64(node.get_int());
-                    } else if (node.get_value_type() == typeof(double)) {
-                        return new Variant.double(node.get_double());
-                    }
-                    return new Variant.string(node.get_string() ?? "");
-                case Json.NodeType.ARRAY:
-                    var arr = node.get_array();
-                    var builder = new VariantBuilder(VariantType.ARRAY);
-                    foreach (var element in arr.get_elements()) {
-                        builder.add_value(json_node_to_variant(element));
-                    }
-                    return builder.end();
-                case Json.NodeType.OBJECT:
-                    return json_object_to_variant(node.get_object());
-                default:
-                    return new Variant.string("");
-            }
+        public static PropertyMapper<AuthorisationToken> get_mapper() {
+            return PropertyMapper.build_for<AuthorisationToken>(cfg => {
+                cfg.map<string>("uid", o => o.user_id, (o, v) => o.user_id = v);
+                cfg.map<string>("unm", o => o.username, (o, v) => o.username = v);
+                cfg.map_many<string>("prm", o => o.permissions, (o, v) => o.permissions = v.to_immutable_buffer());
+                cfg.map<Properties>("dat", o => o.data, (o, v) => o.data = v);
+                cfg.map<string>("iat", o => o.issued_at.format_iso8601(), (o, v) => o.issued_at = new DateTime.from_iso8601(v, new TimeZone.utc()));
+                cfg.map<string?>("eat", 
+                    o => o.expires_at != null ? ((!)o.expires_at).format_iso8601() : null,
+                    (o, v) => o.expires_at = v != null ? new DateTime.from_iso8601((!)v, new TimeZone.utc()) : null);
+                cfg.set_constructor(() => new AuthorisationToken.blank());
+            });
         }
     }
 }

+ 0 - 225
src/Authorisation/AuthorisationTokenService.vala

@@ -1,225 +0,0 @@
-using Inversion;
-using Json;
-
-namespace Spry.Authorisation {
-
-    /**
-     * Service for generating and validating authorisation tokens.
-     * 
-     * Token Format:
-     * - JSON payload containing identity data and metadata (including expiry)
-     * - Signed with Ed25519 (server signing key)
-     * - Encrypted with X25519 (server sealing key)
-     * - Base64url encoded
-     * 
-     * This service handles expiry validation internally after decryption.
-     */
-    public class AuthorisationTokenService : GLib.Object {
-
-        private const string NAMESPACE = "auth-token";
-
-        private CryptographyProvider _crypto = inject<CryptographyProvider>();
-
-        // Configuration
-        private TimeSpan _token_duration = TimeSpan.HOUR * 24;
-
-        /**
-         * Default token validity duration.
-         */
-        public TimeSpan token_duration { 
-            get { return _token_duration; } 
-            set { _token_duration = value; }
-        }
-
-        /**
-         * Creates a new AuthorisationTokenService with default configuration.
-         */
-        public AuthorisationTokenService() {
-            // Default configuration
-        }
-
-        // =========================================================================
-        // Token Generation
-        // =========================================================================
-
-        /**
-         * Generates a signed and encrypted authorisation token for an identity.
-         * 
-         * @param identity The identity to create a token for
-         * @param expires_at Optional custom expiry (defaults to token_duration from now)
-         * @return The encrypted token string (URL-safe Base64)
-         */
-        public string generate_token(Identity identity, DateTime? expires_at = null) {
-            // Calculate expiry
-            DateTime token_expiry;
-            if (expires_at != null) {
-                token_expiry = (!)expires_at;
-            } else {
-                token_expiry = new DateTime.now_utc().add(_token_duration);
-            }
-
-            // Create token from identity
-            var duration = token_expiry.difference(new DateTime.now_utc());
-            var token = new AuthorisationToken.from_identity(identity, duration);
-
-            // Serialize to JSON
-            var json_str = token.to_json_string();
-
-            // Sign and seal using CryptographyProvider
-            try {
-                var blob = _crypto.author(NAMESPACE, json_str);
-                return Base64.encode(blob).replace("+", "-").replace("/", "_");
-            } catch (Error e) {
-                warning("Failed to generate token: %s", e.message);
-                return "";
-            }
-        }
-
-        /**
-         * Generates a token from an existing AuthorisationToken.
-         * 
-         * @param token The token to serialize and encrypt
-         * @return The encrypted token string (URL-safe Base64)
-         */
-        public string generate_token_from_token(AuthorisationToken token) {
-            var json_str = token.to_json_string();
-            
-            try {
-                var blob = _crypto.author(NAMESPACE, json_str);
-                return Base64.encode(blob).replace("+", "-").replace("/", "_");
-            } catch (Error e) {
-                warning("Failed to generate token: %s", e.message);
-                return "";
-            }
-        }
-
-        // =========================================================================
-        // Token Validation
-        // =========================================================================
-
-        /**
-         * Validates a token string and returns the parsed token.
-         * 
-         * This method:
-         * - Decrypts and verifies the token using CryptographyProvider
-         * - Checks expiry
-         * - Parses the JSON payload
-         * 
-         * @param token_string The encrypted token string (URL-safe Base64)
-         * @return The AuthorisationToken, or null if invalid/expired
-         */
-        public AuthorisationToken? parse_token(string token_string) {
-            try {
-                // Decode Base64
-                var decoded = Base64.decode(token_string.replace("-", "+").replace("_", "/"));
-                
-                // Decrypt and verify the token
-                var json_str = _crypto.read(NAMESPACE, decoded);
-
-                if (json_str == null) {
-                    return null;
-                }
-
-                // Parse the JSON
-                var token = AuthorisationToken.from_json_string((!)json_str);
-                if (token == null) {
-                    return null;
-                }
-
-                // Check expiry from the token itself
-                if (token.is_expired()) {
-                    return null;
-                }
-
-                return token;
-            } catch (Error e) {
-                return null;
-            }
-        }
-
-        /**
-         * Validates a token and returns detailed validation result.
-         * 
-         * @param token_string The encrypted token string (URL-safe Base64)
-         * @return A TokenValidationResult with status and token data
-         */
-        public TokenValidationResult validate_token(string token_string) {
-            try {
-                // Decode Base64
-                var decoded = Base64.decode(token_string.replace("-", "+").replace("_", "/"));
-                
-                // Decrypt and verify
-                var json_str = _crypto.read(NAMESPACE, decoded);
-
-                if (json_str == null) {
-                    return new TokenValidationResult.failure("Could not decrypt token");
-                }
-
-                var token = AuthorisationToken.from_json_string((!)json_str);
-                if (token == null) {
-                    return new TokenValidationResult.failure("Failed to parse token payload");
-                }
-
-                // Check expiry from the token itself
-                if (token.is_expired()) {
-                    return new TokenValidationResult.failure("Token has expired", true);
-                }
-
-                return new TokenValidationResult.success(token);
-            } catch (CryptoError e) {
-                return new TokenValidationResult.failure(e.message);
-            } catch (Error e) {
-                return new TokenValidationResult.failure("Invalid token: %s".printf(e.message));
-            }
-        }
-    }
-
-    /**
-     * Result of token validation containing the token and status information.
-     */
-    public class TokenValidationResult : GLib.Object {
-        /**
-         * Whether the token was successfully validated.
-         */
-        public bool is_valid { get; set; }
-
-        /**
-         * The parsed token, or null if validation failed.
-         */
-        public AuthorisationToken? token { get; set; }
-
-        /**
-         * Error message describing why validation failed.
-         */
-        public string? error_message { get; set; }
-
-        /**
-         * Whether the token has expired.
-         */
-        public bool is_expired { get; set; }
-
-        /**
-         * Creates a successful validation result.
-         */
-        public TokenValidationResult.success(AuthorisationToken token) {
-            GLib.Object(
-                is_valid: true,
-                token: token,
-                error_message: null,
-                is_expired: false
-            );
-        }
-
-        /**
-         * Creates a failed validation result.
-         */
-        public TokenValidationResult.failure(string error_message, bool expired = false) {
-            GLib.Object(
-                is_valid: false,
-                token: null,
-                error_message: error_message,
-                is_expired: expired
-            );
-        }
-    }
-}

+ 4 - 4
src/Authorisation/Identity.vala

@@ -1,4 +1,5 @@
 using Invercargill.DataStructures;
+using Invercargill;
 
 namespace Spry.Authorisation {
 
@@ -27,15 +28,14 @@ namespace Spry.Authorisation {
 
         /**
          * Permissions granted to this identity.
-         * Returns an array of permission strings for serialization.
+         * Returns an immutable lot of strings for serialization.
          */
-        public abstract string[] permissions { owned get; }
+        public abstract ImmutableLot<string> permissions { owned get; }
 
         /**
          * Additional data to embed in the token.
          * Implementation-specific data (e.g., roles, preferences).
-         * Returns a Variant that can be serialized to JSON.
          */
-        public abstract Variant data { owned get; }
+        public abstract Properties data { owned get; }
     }
 }

+ 2 - 81
src/Authorisation/PermissionMatcher.vala

@@ -1,3 +1,5 @@
+using Invercargill;
+
 namespace Spry.Authorisation {
 
     /**
@@ -58,86 +60,5 @@ namespace Spry.Authorisation {
             // No wildcard - exact match required
             return pattern == permission;
         }
-
-        /**
-         * Checks if any of the patterns match the permission.
-         * 
-         * @param patterns Array of patterns to check
-         * @param permission The permission to check
-         * @return true if any pattern matches
-         */
-        public static bool any_matches(string[] patterns, string permission) {
-            foreach (var pattern in patterns) {
-                if (matches(pattern, permission)) {
-                    return true;
-                }
-            }
-            return false;
-        }
-
-        /**
-         * Checks if all permissions are matched by at least one pattern.
-         * 
-         * @param patterns Array of patterns to check
-         * @param permissions Array of permissions to verify
-         * @return true if all permissions are covered by at least one pattern
-         */
-        public static bool all_matched(string[] patterns, string[] permissions) {
-            foreach (var permission in permissions) {
-                if (!any_matches(patterns, permission)) {
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        /**
-         * Finds all permissions that match a pattern.
-         * 
-         * @param pattern The pattern to match against
-         * @param permissions Array of permissions to filter
-         * @return Array of matching permissions
-         */
-        public static string[] filter_matching(string pattern, string[] permissions) {
-            var matching = new string[0];
-            foreach (var permission in permissions) {
-                if (matches(pattern, permission)) {
-                    matching += permission;
-                }
-            }
-            return matching;
-        }
-
-        /**
-         * Checks if a pattern contains a wildcard.
-         * 
-         * @param pattern The pattern to check
-         * @return true if the pattern contains a wildcard
-         */
-        public static bool has_wildcard(string pattern) {
-            return pattern.contains("*");
-        }
-
-        /**
-         * Checks if a pattern is the super-user "admin" pattern.
-         * 
-         * @param pattern The pattern to check
-         * @return true if the pattern is "admin"
-         */
-        public static bool is_admin(string pattern) {
-            return pattern == "admin";
-        }
-
-        /**
-         * Checks if a pattern matches everything.
-         * 
-         * This is true for "admin" and "*" patterns.
-         * 
-         * @param pattern The pattern to check
-         * @return true if the pattern matches everything
-         */
-        public static bool matches_everything(string pattern) {
-            return pattern == "admin" || pattern == "*";
-        }
     }
 }

+ 0 - 23
src/Authorisation/meson.build

@@ -1,23 +0,0 @@
-authorisation_sources = files(
-    'Identity.vala',
-    'IdentityProvider.vala',
-    'AuthorisationToken.vala',
-    'AuthorisationTokenService.vala',
-    'AuthorisationContext.vala',
-    'AuthorisationService.vala',
-    'PermissionMatcher.vala',
-    'AuthorisationError.vala'
-)
-
-libspry_authorisation = static_library('spry-authorisation',
-    authorisation_sources,
-    dependencies: [spry_dep, inversion_dep, astralis_dep, json_glib_dep, sodium_deps],
-    include_directories: include_directories('..')
-)
-
-spry_authorisation_inc = include_directories('.')
-spry_authorisation_dep = declare_dependency(
-    link_with: libspry_authorisation,
-    include_directories: spry_authorisation_inc,
-    dependencies: [spry_dep, inversion_dep, astralis_dep]
-)

+ 24 - 3
src/meson.build

@@ -19,7 +19,30 @@ sources = files(
     'Static/ConstantStaticResource.vala',
     'Static/StaticResourceProvider.vala',
     'Static/HtmxResource.vala',
-    'Static/HtmxSseResource.vala'
+    'Static/HtmxSseResource.vala',
+    'Authorisation/AuthorisationError.vala',
+    'Authorisation/AuthorisationToken.vala',
+    'Authorisation/Identity.vala',
+    'Authorisation/IdentityProvider.vala',
+    'Authorisation/PermissionMatcher.vala',
+    'Authorisation/AuthorisationContext.vala',
+    'Authorisation/AuthorisationPipelineComponent.vala',
+    'Authorisation/AuthorisationService.vala',
+    'Authentication/User.vala',
+    'Authentication/Session.vala',
+    'Authentication/UserService.vala',
+    'Authentication/SessionService.vala',
+    'Authentication/PermissionService.vala',
+    'Authentication/UserIdentityProvider.vala',
+    'Authentication/UserRepository.vala',
+    'Authentication/SessionRepository.vala',
+    'Authentication/SqlUserRepository.vala',
+    'Authentication/SqlSessionRepository.vala',
+    'Authentication/CreateAuthTables.vala',
+    'Authentication/Components/LoginFormComponent.vala',
+    'Authentication/Components/UserManagementComponent.vala',
+    'Authentication/Components/UserDetailsComponent.vala',
+    'Authentication/Components/NewUserComponent.vala'
 )
 
 
@@ -52,5 +75,3 @@ spry_dep = declare_dependency(
     dependencies: [glib_dep, gobject_dep, gio_dep, invercargill_dep, invercargill_json_dep, json_glib_dep, inversion_dep, libxml_dep]
 )
 
-# Authorisation submodule
-subdir('Authorisation')